基于MetaMask 理解钱包工作原理

基于MetaMask 理解钱包工作原理
Photo by Ray Hennessy / Unsplash

MetaMask 是一款广受欢迎的以太坊钱包浏览器插件,它为用户提供了一种安全、便捷的方式来管理他们的数字资产并与去中心化应用程序(DApp)进行交互。本文将深入探讨 MetaMask 的源代码,分析其架构设计、关键模块和功能实现,以帮助读者更好地理解这个钱包是如何工作的,以及其背后所依赖的相关标准。

MetaMask 的架构概览

MetaMask 采用了一种模块化的架构设计,各个模块之间职责明确,相互协作,以实现钱包的各项功能。主要模块包括:

  1. background.js:负责处理后台任务,如事务签名、状态更新等。
  2. contentscript.js:负责与 Web3 网站进行交互,注入 MetaMask 提供的 Web3 实例。
  3. ui:包含用户界面相关的组件和逻辑,如界面渲染、用户输入处理等。
  4. lib:包含各种工具函数和辅助模块,如助记词管理、密钥派生等。
  5. app:整合各个模块,构建完整的 MetaMask 应用。

钱包创建流程

当用户首次安装 MetaMask 时,会触发钱包创建流程。这个流程主要包括以下步骤:

  1. 生成助记词:MetaMask 使用 bip39 库生成一组随机的助记词(通常为 12 个单词)。相关代码位于 lib/seed-phrase.js 文件中的 generateMnemonic 函数。
  2. 派生主密钥:根据助记词和可选的密码,MetaMask 使用 hdkey 库派生出一个主密钥。相关代码位于 lib/hd-keyring.js 文件中的 _deserializeVault 函数。
  3. 派生账户私钥:基于 BIP44 定义的路径规则,MetaMask 从主密钥派生出默认账户的私钥。路径为 m/44'/60'/0'/0/0。相关代码位于 lib/hd-keyring.js 文件中的 _pathFromIndex 函数。
  4. 加密存储:MetaMask 使用用户提供的密码对助记词和私钥进行加密,然后将加密后的数据存储在本地。相关代码位于 lib/keyring.js 文件中的 serialize 函数。

种子短语和私钥的安全存储

MetaMask 采用了多重安全措施来保护用户的种子短语和私钥,其中最关键的是加密存储。

加密种子短语和私钥

MetaMask 使用对称加密算法(如 AES)对种子短语和私钥进行加密,然后将加密后的密文存储在本地。加密密钥是从用户的密码中派生出来的。相关的代码位于 lib/keyring.js 文件中的 encrypt 函数:

async encrypt(password, object) {
  const salt = this._getSalt() || crypto.randomBytes(16);
  const key = await this._getKey(password, salt);
  
  const cipher = crypto.createCipheriv('aes-256-gcm', key, crypto.randomBytes(16));
  const ciphertext = Buffer.concat([cipher.update(JSON.stringify(object)), cipher.final()]);
  const tag = cipher.getAuthTag();
  
  return Buffer.concat([salt, ciphertext, tag]).toString('base64');
}

这个函数首先生成一个随机的盐值(salt),然后使用 _getKey 函数从用户密码和盐值派生出加密密钥。接着,它使用 AES-256-GCM 算法创建一个 cipher 对象,随机生成一个初始化向量(IV),并使用 cipher 对象加密种子短语或私钥的 JSON 字符串表示。最后,它将盐值、密文和认证标签(tag)连接起来,并转换为 Base64 编码的字符串。

解密种子短语和私钥

当用户解锁 MetaMask 时,需要使用用户的密码来派生出解密密钥,然后用其解密种子短语和私钥。相关的代码位于 lib/keyring.js 文件中的 decrypt 函数:

async decrypt(password, encryptedString) {
  const encryptedBuffer = Buffer.from(encryptedString, 'base64');
  const salt = encryptedBuffer.slice(0, 16);
  const ciphertext = encryptedBuffer.slice(16, -16);
  const tag = encryptedBuffer.slice(-16);
  
  const key = await this._getKey(password, salt);
  
  const decipher = crypto.createDecipheriv('aes-256-gcm', key, crypto.randomBytes(16));
  decipher.setAuthTag(tag);
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
  
  return JSON.parse(plaintext.toString());
}

这个函数首先从 Base64 编码的字符串中提取出盐值、密文和认证标签。然后,它使用 _getKey 函数和盐值从用户密码派生出解密密钥。接着,它创建一个 decipher 对象,设置认证标签,并使用 decipher 对象解密密文。最后,它将解密后的明文转换为字符串,并解析为 JavaScript 对象。

DApp 交互和交易签名

MetaMask 通过注入一个自定义的 Web3 实例,使得 DApp 能够与以太坊网络进行交互。这个自定义的 Web3 实例在 contentscript.js 文件中定义,其结构如下:

const inpageProvider = new MetamaskInpageProvider(metamaskStream);
const web3 = new Web3(inpageProvider);
web3.setProvider = () => {
  console.error('MetaMask: setProvider() is deprecated');
};

当用户在 DApp 中发起一笔交易时,MetaMask 会拦截这个请求,并向用户显示一个确认页面,列出交易的详细信息,如接收地址、交易金额、Gas 费用等。

用户确认后,MetaMask 会使用相应账户的私钥对交易进行签名。签名过程遵循以太坊的交易签名标准(如 EIP-155),并使用 ethereumjs-tx 库进行实现。相关的代码位于 lib/eth-tx-manager.js 文件中的 addUnapprovedTransaction 函数:

addUnapprovedTransaction(txParams, cb) {
  const { from } = txParams;
  this._validateTxParams(txParams);
  
  const txMeta = {
    id: createId(),
    time: Date.now(),
    status: 'unapproved',
    metamaskNetworkId: this._getCurrentChainId(),
    txParams: { ...txParams, from },
  };
  
  this._txs.push(txMeta);
  this._addTxToHistory(txMeta);
  this._updateTxsInState();
  
  this._recomputeUnapprovedTxsEvent();

  cb && cb();
}

签名完成后,MetaMask 将签名后的交易发送到以太坊网络,并返回交易哈希给 DApp。

页面注入对象

MetaMask 通过在 DApp 页面的 JavaScript 上下文中注入一个名为 window.ethereum 的对象来实现与 DApp 的交互。这个对象提供了一组标准化的 API,允许 DApp 与以太坊网络进行通信,发送交易,签名消息等。

window.ethereum 对象的结构和行为在很大程度上遵循了 EIP-1193(Ethereum Provider JavaScript API)标准。这个标准定义了 Ethereum Provider 应该如何与 DApp 进行交互,以及应该提供哪些 API。

以下是 window.ethereum 对象的一些关键属性和方法:

  1. ethereum.isMetaMask: 一个布尔值,指示当前 Provider 是否是 MetaMask。
  2. ethereum.networkVersion: 一个字符串,表示当前连接的以太坊网络 ID。
  3. ethereum.selectedAddress: 一个字符串,表示用户当前选择的以太坊地址(账户)。
  4. ethereum.request(args): 一个方法,允许 DApp 发送请求到 Ethereum Provider。这是 EIP-1193 中定义的主要方法,它取代了旧的 ethereum.send() 方法。args 参数是一个对象,包含请求的详细信息,如方法名和参数。
  5. ethereum.on(eventName, callback): 一个方法,允许 DApp 监听 Ethereum Provider 触发的事件。常见的事件包括:
    • accountsChanged: 当用户切换账户时触发。
    • networkChanged: 当用户切换网络时触发。
    • chainChanged: 当用户切换链时触发(EIP-1193 中新增的事件)。

下面是一个简化的 window.ethereum 对象示例:

window.ethereum = {
  isMetaMask: true,
  networkVersion: '1',
  selectedAddress: '0x1234567890123456789012345678901234567890',
  
  request: async (args) => {
    // 处理请求,如发送交易,签名消息等
    // ...
  },
  
  on: (eventName, callback) => {
    // 注册事件监听器
    // ...
  },
  
  // 其他属性和方法
  // ...
};

Web3 实例拦截

MetaMask 会通过其注入的自定义 Web3 实例(基于window.ethereum 创建的)拦截这个请求。具体而言,首次加载时就把MetamaskInpageProvider 注入到了window.ethereum,以便在交易发送到以太坊网络之前对其进行处理。

contentscript.js 文件中,MetaMask 定义了一个名为 MetamaskInpageProvider 的类,这个类继承自 EventEmitter,并重写了 request 方法:

class MetamaskInpageProvider extends SafeEventEmitter {
  // ...

 async request(args) {
    // 验证请求参数
    // ...

    return new Promise((resolve, reject) => {
      this._rpcRequest({ method, params }, getRpcPromiseCallback(resolve, reject));
    });
  }
  sendAsync(payload, cb) {
    this._rpcRequest(payload, cb);
  }

  // ...
}

_rpcRequest 方法是处理所有 RPC 请求的核心方法,它会根据请求的方法名进行不同的处理。对于 eth_sendTransaction 方法(用于发送交易),MetaMask 会拦截这个请求,并执行以下步骤:

  1. 对交易参数进行验证和规范化处理。
  2. 向后台页面发送一个名为 addUnapprovedTransaction 的消息,将交易添加到未确认交易列表中。
  3. 在用户界面上显示一个确认页面,列出交易的详细信息,如接收地址、交易金额、Gas 费用等。
  4. 用户确认交易后,MetaMask 会使用相应账户的私钥对交易进行签名。
  5. 将签名后的交易发送到以太坊网络,并返回交易哈希给 DApp。

相关的代码片段如下(contentscript.js):

async _rpcRequest(payload, cb) {
  // ...

  if (payload.method === 'eth_sendTransaction') {
    const txMeta = await this._addUnapprovedTransactionAndGetMeta(
      payload.params[0],
    );

    this._showConfirmationDialog(txMeta, () => {
      // 用户确认交易后的处理逻辑
      // ...
    });

    // 返回交易哈希给 DApp
    cb(null, txMeta.hash);
  } else {
    // 处理其他 RPC 请求
    // ...
  }

  // ...
}

在后台页面中,addUnapprovedTransaction 方法(位于 lib/eth-tx-manager.js)会将交易添加到未确认交易列表中,并触发相应的事件和状态更新:

addUnapprovedTransaction(txParams, cb) {
  // ...

  const txMeta = {
    id: createId(),
    time: Date.now(),
    status: 'unapproved',
    metamaskNetworkId: this._getCurrentChainId(),
    txParams: { ...txParams, from },
  };

  this._txs.push(txMeta);
  this._addTxToHistory(txMeta);
  this._updateTxsInState();

  this._recomputeUnapprovedTxsEvent();

  // ...
}

Read more

ngrok本地调试原理及Telegram mini app cookie path 问题

ngrok本地调试原理及Telegram mini app cookie path 问题

在现代web开发中,本地调试是一个非常重要的环节。然而,当我们需要将本地开发的应用暴露到公网以便进行测试时,就会遇到一些挑战。本文将详细介绍如何使用ngrok实现内网穿透进行本地调试,特别是在Telegram小程序开发场景中的应用,以及可能遇到的常见问题及其解决方案。 ngrok原理 ngrok是一个反向代理工具,它可以将本地服务器安全地暴露到公网。下面是ngrok的工作原理: 1. 用户启动ngrok客户端,并指定要暴露的本地端口。 2. ngrok客户端与ngrok云服务建立安全的通道。 3. ngrok云服务生成一个公网可访问的URL。 4. 当外部请求到达这个URL时,ngrok云服务将请求通过安全通道转发到本地ngrok客户端。 5. 本地ngrok客户端将请求转发到指定的本地端口。 6. 本地服务器处理请求并返回响应,响应通过相同的路径返回给客户端。 Telegram小程序调试场景 在Telegram小程序开发中,我们经常需要使用ngrok来进行本地调试。以下是具体步骤: 1. 启动本地开发服务器(例如运行在localhost:3000)。

TypeScript:从架构分层设计到IOC和AOP

TypeScript:从架构分层设计到IOC和AOP

TypeScript作为JavaScript的超集,为开发者提供了强大的类型系统和面向对象编程能力。然而,要在大型项目中充分发挥TypeScript的优势,我们需要深入理解软件架构原则和设计模式。本文将探讨如何使用TypeScript构建一个健壮的应用架构,涵盖分层设计、常见设计模式、控制反转(IOC)和面向切面编程(AOP)等高级概念。 分层架构 分层架构是组织大型应用程序的常用方法。它有助于关注点分离,使得每一层都可以独立开发和测试。一个典型的分层架构包括: 1. 表现层(Presentation Layer) 2. 业务逻辑层(Business Logic Layer) 3. 数据访问层(Data Access Layer) 4. 数据库(Database) 让我们使用图表来可视化这个架构: 接下来,我们将探讨每一层中可能使用的设计模式,并通过TypeScript代码示例来说明如何实现这些模式。 表现层 表现层负责处理用户界面和用户交互。在这一层,我们经常使用MVC(Model-View-Controller)或MVVM(Model-View-ViewM

Jotai v2: React状态管理的新篇章

Jotai v2: React状态管理的新篇章

Jotai是一个为React应用设计的轻量级状态管理库。2023年3月,Jotai发布了v2.0版本,带来了许多新特性和改进。本文将深入探讨Jotai v2的使用方法、适用场景、设计理念、源码结构以及核心功能的实现原理。 版本信息 本文讨论的是Jotai v2.0.3版本,发布于2023年5月。你可以通过以下命令安装 npm install [email protected] 基本使用 Jotai的核心概念是"atom"。atom是最小的状态单位,可以存储任何JavaScript值。让我们看一个简单的例子: import { atom, useAtom } from 'jotai' // 创建一个atom const countAtom = atom(0) function Counter() { // 使用atom const [count, setCount] = useAtom(

加密货币交易所十二:安全性和风险控制

加密货币交易所十二:安全性和风险控制

在加密货币合约交易所中,安全性和风险控制是至关重要的。这不仅关系到交易所的声誉和用户的资产安全,也直接影响到整个加密货币生态系统的稳定性。本章将详细探讨合约交易所在安全性和风险控制方面的关键策略和实施方法。 多重签名机制 多重签名(MultiSig)是一种强大的安全机制,要求多个私钥来授权交易,大大降低了单点故障和内部欺诈的风险。 概念解释 多重签名是一种需要多个私钥来签署和授权交易的加密技术。例如,在一个 2-of-3 多重签名设置中,需要三个私钥中的任意两个来完成交易。 在合约交易所中的应用 热钱包管理: * 设置:通常采用 2-of-3 或 3-of-5 的多重签名方案。 * 应用:每次从热钱包转出大额资金时,需要多个管理员的授权。 冷钱包管理: * 设置:可能采用更严格的 3-of-5 或 4-of-7 方案。 * 应用:定期将热钱包中的多余资金转移到冷钱包时使用。 智能合约升级: * 设置:可能需要多个核心开发者和安全审计员的签名。 * 应用:在升级关键智能合约时,确保变更经过充分审核和授权。 实现考虑 密钥管理: * 使用硬件安全