概述

本节介绍钱包开发中管理交易 nonce 的关键工具 NonceTracker。在以太坊钱包开发中,正确分配和追踪 nonce 对于防止交易冲突、确保交易顺序和链上状态一致性至关重要。NonceTracker 正是为此而生,帮助开发者安全、高效地管理账户的 nonce。

源码:https://github.com/MetaMask/nonce-tracker/blob/main/src/NonceTracker.ts

NonceTracker 是什么

NonceTracker 是一个用于管理和分配以太坊账户 nonce 的工具,主要负责:

  • 获取最新网络 nonce:通过区块链节点获取当前账户的最新 nonce

  • 结合本地状态:结合本地已确认和待处理交易,推算下一个可用 nonce

  • 并发控制:通过锁机制防止多笔交易同时分配到相同 nonce

实现机制

源码解读

async getNonceLock(address: string): Promise<NonceDetails> {
  // 等待全局锁释放,保证全局排他性,防止全局并发冲突
  await this._globalMutexFree();
  // 等待该地址的锁释放,并加锁,防止同一地址并发分配 nonce
  const releaseLock: VoidFunction = await this._takeMutex(address);
  try {
    // 计算多种 nextNonce 策略
    // 1. 从网络获取下一个 nonce
    const networkNonceResult: NetworkNextNonce = await this._getNetworkNextNonce(address);
    // 2. 获取本地已确认的最大 nonce(+1 得到下一个可用)
    const highestLocallyConfirmed: number = this._getHighestLocallyConfirmed(address);
    // 3. 网络建议的下一个 nonce
    const nextNetworkNonce: number = networkNonceResult.nonce;
    // 4. 取网络和本地已确认的最大值,作为建议起点
    const highestSuggested: number = Math.max(
      nextNetworkNonce,
      highestLocallyConfirmed,
    );

    // 5. 获取本地 pending(待处理)交易
    const pendingTxs: Transaction[] = this.#getPendingTransactions(address);
    // 6. 检查 pending 交易占用的 nonce,找到连续未被占用的最小 nonce
    const localNonceResult: HighestContinuousFrom = this._getHighestContinuousFrom(pendingTxs, highestSuggested);

    // 7. 组装详细的 nonce 信息
    const nonceDetails: NonceDetails = {
      params: {
        highestLocallyConfirmed,
        nextNetworkNonce,
        highestSuggested,
      },
      local: localNonceResult,
      network: networkNonceResult,
    };

    // 8. 取网络和本地可用 nonce 的最大值,作为最终分配的 nextNonce
    const nextNonce: number = Math.max(
      networkNonceResult.nonce,
      localNonceResult.nonce,
    );

    // 返回 nextNonce、详细信息和释放锁的方法
    return { nextNonce, nonceDetails, releaseLock };
  } catch (err) {
    // 出错时及时释放锁,防止死锁
    releaseLock();
    throw err;
  }
}
  
  // 等待全局锁释放(只获取一次立即释放),用于全局排他控制
  async _globalMutexFree(): Promise<void> {
    const globalMutex: Mutex = this._lookupMutex('global');
    const releaseLock: VoidFunction = await globalMutex.acquire();
    releaseLock();
  }
  // 获取指定 lockId 的互斥锁,并加锁,返回释放锁的方法
  async _takeMutex(lockId: string): Promise<VoidFunction> {
    const mutex: Mutex = this._lookupMutex(lockId);
    const releaseLock: VoidFunction = await mutex.acquire();
    return releaseLock;
  }
  // 查找或创建指定 lockId 的互斥锁
  _lookupMutex(lockId: string): Mutex {
    let mutex: Mutex = this.#lockMap[lockId];
    if (!mutex) {
      mutex = new Mutex();
      this.#lockMap[lockId] = mutex;
    }
    return mutex;
  }
  
  // 获取网络中的下一个 nonce
  async _getNetworkNextNonce(address: string): Promise<NetworkNextNonce> {
    //  获取最新区块号(保证数据新鲜且一致),需要确保 base count 和 pending count 来自同一个区块
    const blockNumber = await this.#blockTracker.getLatestBlock();

    // 用 ethers.js 的 Web3Provider 查询该地址在该区块的交易计数(即 nonce)
    const baseCount: number = await new Web3Provider(
      this.#provider,
    ).getTransactionCount(address, blockNumber);

    // 返回结构体,包含来源、nonce值、区块号和原始计数
    return {
      name: 'network',
      nonce: baseCount,
      details: { blockNumber, baseCount },
    };
  }
    // 获取本地已确认的最大nonce值
    _getHighestLocallyConfirmed(address: string): number {
      const confirmedTransactions: Transaction[] =
        this.#getConfirmedTransactions(address);
      const highest: number = this._getHighestNonce(confirmedTransactions);
      // +1表示返回下一个可用的nonce值
      return Number.isInteger(highest) ? highest + 1 : 0;
    }

    // 获取交易列表中的最大 nonce
    _getHighestNonce(txList: Transaction[]): number {
      const nonces: number[] = txList.map((txMeta) => {
        const { nonce } = txMeta.txParams;
        return parseInt(nonce, 16);
      });
      const highestNonce: number = Math.max.apply(null, nonces);
      return highestNonce;
    }

    // 检查 pending 状态的交易占用的nonce值
    _getHighestContinuousFrom(
      txList: Transaction[],
      startPoint: number,
    ): HighestContinuousFrom {
      const nonces: number[] = txList.map((txMeta) => {
        const { nonce } = txMeta.txParams;
        return parseInt(nonce, 16);
      });

      let highest: number = startPoint;
      // 如果pending状态的交易已经占用了之前计算的最大的nonce值,就把最大的nonce值再+1
      while (nonces.includes(highest)) {
        highest += 1;
      }
      return { name: 'local', nonce: highest, details: { startPoint, highest } };
    }

安全机制

  • 多重来源保障:结合链上状态、本地已确认、本地 pending,确保 nonce 唯一且连续。

  • 锁机制防并发:全局锁+地址锁,防止多线程/多请求环境下的 nonce 冲突。

  • 自动跳过冲突:pending 交易占用的 nonce 会被自动跳过,保证新交易不会和本地未上链交易冲突。

流程图

开始 getNonceLock
等待全局锁释放
对地址加锁
获取网络 nonce,调用链上 eth_getTransactionCount
获取本地已确认最大 nonce,加1得到下一个
建议起点 = 网络 nonce 和本地已确认 nonce 的最大值
遍历本地 pending 交易
从建议起点开始,跳过已被 pending 占用的 nonce
找到本地可用的最小 nonce
最终 nextNonce = 网络 nonce 和本地可用 nonce 的最大值
返回 nextNonce,详细信息,releaseLock
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐