Nonce Bitmap - 为并行区块链启用并行交易提交

本文为机器翻译
展示原文

TL;DR

想象一下,你是一位拥有多个时间敏感型交易机会的交易者。你同时发现了三个套利机会,但你每次只能提交一笔交易。当你的第三笔交易被处理时,机会就消失了。这就是顺序性问题。我们提出了一种新颖的解决方案——Nonce Bitmap,用于在区块链系统中实现并行交易提交,同时保持安全性并与现有钱包兼容。

  • 并行性。Nonce Bitmap 引入了一种基于位图的方法,允许用户利用传统 nonce 字段中未充分利用的位并行发送最多 256 个交易。
  • 最小存储开销。与其他方法相比,Nonce Bitmap 需要最小的存储开销(每个地址 32 字节)。
  • 向后兼容性。旧版钱包和普通用户的行为不会发生任何变化。
  • 重放保护。Nonce Bitmap 在消除顺序瓶颈的同时,保留了针对重放攻击的安全保障。
  • 更好的用户体验/用户体验。Nonce Bitmap 对于高频用户(如交易者、MEV 搜索者以及需要并发交易提交的协议)尤其有价值。

传统 Nonce 机制

EVM Nonce:不仅仅是一个计数器

本质上,nonce 与每个外部拥有账户 (EOA) 相关联,用于表示从该账户发送的交易数量。每个 StateAccount 维护一个最多 8 个字节的 nonce 字段。

// StateAccount is the Ethereum consensus representation of accounts. // These objects are stored in the main account trie. type StateAccount struct {Nonce uint64 Balance *uint256.IntRoot common.Hash // merkle root of the storage trie CodeHash [] byte }

对于新账户,nonce 从零开始,每当该账户发起的交易被打包到区块中时,nonce 就会加 1。这个计数器看似简单,却对 EVM 的完整性和确定性运行至关重要。

  • 安全性。Nonce 用于防止重放攻击,要求每个来自同一地址的交易都必须使用唯一的 Nonce。因此,任何尝试两次提交同一交易的行为都将失败,因为网络已经确认使用了该 Nonce。这种安全保障对于用户资产和区块链交互的安全至关重要。

  • 排序。以太坊中的 nonce 值严格递增。也就是说,每笔交易的 nonce 值必须比前一笔交易的 nonce 值大 1。

    next := opts.State.GetNonce(from) if next > tx.Nonce() { return fmt.Errorf( "%w: next nonce %v, tx nonce %v" , core.ErrNonceTooLow, next, tx.Nonce())} // Ensure the transaction doesn't produce a nonce gap in pools that do not // support arbitrary orderings if opts.FirstNonceGap != nil { if gap := opts.FirstNonceGap(from); gap < tx.Nonce() { return fmt.Errorf( "%w: tx nonce %v, gapped nonce %v" , core.ErrNonceTooHigh, tx.Nonce(), gap)}}
  • 交易计数。虽然这在以太坊黄皮书中被定义为 nonce 的定义,但它仅仅是当前 nonce 设计的一种暗示。

    随机数(Nonce) 。一个标量值,等于从该地址发送的交易数量,或者,对于具有关联代码的帐户,等于该帐户创建的合约数量。

    使用当前设计,给定地址的交易计数( eth_getTransactionCount )是通过简单地返回当前帐户的随机数来完成的。

    // GetTransactionCount returns the number of transactions the given address has sent for the given block number func (s *TransactionAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Uint64, error ) { // Ask transaction pool for the nonce which includes pending transactions if blockNr, ok := blockNrOrHash.Number(); ok && blockNr == rpc.PendingBlockNumber {nonce, err := sbGetPoolNonce(ctx, address) if err != nil { return nil , err} return (*hexutil.Uint64)(&nonce), nil } // Resolve block number and use its state to ask for the nonce state, _, err := sbStateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return nil , err}nonce := state.GetNonce(address) return (*hexutil.Uint64)(&nonce), state.Error()}

总体而言,该设计提供了一种简单而有效的方法来解决重放攻击,从而实现高效的交易验证并减少存储占用空间。

顺序性瓶颈

当前的 nonce 设计在账户层面造成了不可避免的顺序性。也就是说,即使交易n+1 n + 1n n不相关(可以并行执行),交易n+1 n + 1仍然必须等待交易n n ,因为同一发送者的交易是按照 nonce 的顺序处理的。

交易卡住是另一个问题。如果用户提交的交易n n的 Gas Price 过低,无法被打包进区块,那么这笔交易就会一直停留在内存池中,直到被打包、丢弃或替换。在此期间,无论 Gas Price 是多少,该用户提交的任何后续交易都将失败。这种情况是用户不希望看到的,会给用户体验带来不好的影响。

  • 例如,用户可能有一笔手续费相对较低的 ETH 转账交易待处理,但随后急需平仓即将被清算的 DeFi 仓位。根据目前的 nonce 设计,他必须等待这笔卡住的交易,或者用另一笔使用相同 nonce 但 Gas Price 更高的交易来替换这笔卡住的交易。无论哪种情况,都会增加很多复杂性,普通用户可能无法应对。

此外,高级用户(例如高频交易者)通常需要并行发送交易。为此,他们通常必须仔细管理链下的随机数 (nonce)。然而,这并非易事。即使使用链下管理,也必须查询 1) 当前随机数或 2) 上一笔交易的收据,才能选择提交当前交易。在延迟至关重要的情况下,此查询也会导致一些延迟。尤其是在 RISE、MegaETH、Monad、Flashblocks 等交易处理速度极快( <10ms 分解时间)的区块链中,查询链上数据可能会导致数十到数百毫秒的延迟。

更糟糕的是,尝试并行发送多笔交易(即使使用不同的 nonce)可能会导致无效 nonce 间隙错误。发生这种情况的原因是,当前的 P2P 广播无法保证 nonce 为n n 的交易会在交易n+1 n + 1之前到达节点的内存池。

这些限制在低延迟链上变得尤为严重。
让我们探索一下其他系统如何尝试解决这个问题。

打破顺序性

高性能区块链可以以极低的延迟处理数万TPS。然而,如果没有更好的钱包/客户端用户体验,就无法充分发挥其潜力。这些区块链能够并发处理大量交易,但目前交易提交是顺序的,这可能是充分释放其潜力的瓶颈。

在本节中,我们将探讨一些解决这种顺序性问题的潜在替代设计。这些设计主要关注 nonce 的安全性,其中 nonce 是一个一次性使用的数字,用于防止重放攻击。

我们注意到,虽然交易排序和计数很重要,但可以通过不同的方法来实现。例如,使用eth_getTransactionCount的服务(通常是浏览器)可以通过其本地数据库提取此数据。

随机数

这种设计并非严格递增 nonce,而是允许交易携带任意唯一标识符,从而实现并行交易提交和处理。每个账户都维护所有先前使用过的 nonce 的记录,而不是单个标量计数器。例如,这可以表示为从地址到已使用 nonce 集合的映射。

UsedNonces = map [Address] map [ uint64 ] bool

提交交易时,节点会检查 nonce 是否已被该账户使用,从而验证其有效性。如果 nonce 未被使用,则接受该交易,并将 nonce 标记为已使用。

这种方法允许处理节点并行执行来自同一用户的交易(如果交易不相关),从而显著提升性能。对于客户端,钱包可以生成随机数,而无需仔细跟踪或同步随机数计数器,从而简化并行交易提交。可以实现一个额外的 RPC,例如eth_usedNonces ,来检索给定地址的已用随机数。此外, eth_getTransactionCount可以返回给定地址的UsedNonces的长度。

然而,这种方法也带来了一些不容忽视的缺点。最显著的缺点是存储开销,因为节点必须为每个账户存储并维护一组可能很大的已用 nonce,这增加了状态大小和存储复杂性。此外,根据随机 nonce 生成器算法,可能会出现 nonce 重复的情况。

Hyperliquid 的设计

Hyperliquid不会存储给定地址所有已使用的 nonce,而是仅存储每个地址使用率最高的 100 个 nonce,并且需要额外的规则来检查 nonce 的重用性。也就是说,新交易的 nonce 必须大于此集合中最小的 nonce,并且之前从未使用过(即不存在于集合中)。此外,nonce 必须在交易所在区块的 UNIX 毫秒时间戳的 1 天内。例如,对于新交易,可以将 nonce 设置为当前时间戳。

与随机 nonce 设计类似,Hyperliquid 提供了大量的并行性。用户体验也得到了改进,因为钱包可以灵活选择不同的 nonce 策略,并且支持并行提交交易。时间限制的有效性也降低了中继攻击的风险。与随机 nonce 设计相比,这种方法的存储开销更小,因为只存储 100 个已使用的 nonce。然而,它存在一个瓶颈,即无法并行发送超过 100 个交易。

然而,由于 nonce 的管理方式完全不同,某些操作可能无法与现有钱包向后兼容。事实上,这种 nonce 设计仅用于 HyperCore 层。HyperEVM 层仍然沿用传统的 nonce 设计。此外,时间限制也限制了预定交易的持续时间(例如,无法进行预定在一天以上进行的预签名交易)。

2D 随机数

RIP-7712账户抽象(AA) 交易引入了二维随机数 (2D Nonce) 机制。二维随机数允许智能合约账户处理更灵活、更可并行化的随机数系统。二维随机数是一个n位(例如,在 AA 中n = 256 的值,它被拆分为两个逻辑部分:

┌─────────────────────────────────┬──────────────────────────┐│ Upper k bits │ Lower nk bits ││ (Nonce Key) │ (Nonce Sequence) │└─────────────────────────────────┴──────────────────────────┘
  • nonceKey nonceKey作为一个 nonce 类别,将 nonce 空间划分为多个独立的类别。
    • 经典的 nonce 对应一个 1D nonce,相当于只有一个 nonce 类别(即nonceKey = 0
  • nonceSequence 。nonceSequence 是该类别中的nonceSequence ,必须按顺序增加才能排序。

具有不同nonceKeys的交易可以按任意顺序执行或包含,从而实现并行处理。另一方面,具有相同nonceKey的交易必须按顺序执行,并且nonceSequences值必须递增。

nonceKey决定了账户的并行度, k越大,可以并行发送的交易越多。理论上,这种结构允许一个账户维护最多2^{k}类别每个类别可以容纳2^{nk}连续nonce 。这意味着一个账户可以同时发送最多2^{k}交易,而无需担心 nonce 的排序问题。

然而, nonceKey主要用作一组相关交易(例如会话密钥、基于时间的交易)的标识符。如果以这种方式使用,同一组内的交易将无法并行化。例如,如果nonceKeys 1、2、3 分别用于交换、ETH 转账和 ERC-20 转账,则一个账户无法并行发送两笔交换交易、两笔 ETH 转账或两笔 ERC-20 转账。它只能同时发送一笔交换交易、一笔 ETH 转账和一笔 ERC-20 转账(相对于 nonce 管理)。此外,每个新的nonceKey都会引入另一个n n位存储开销来存储nonceKeynonceSequence

Nonce 位图提案

在本节中,我们提出了一种新颖的随机数管理机制,允许一个账户并行发送最多 256 笔交易,每笔并行交易的存储开销超过 1 位。此外,该提案完全向后兼容,并且在交易提交过程中无需引入任何额外字段。

设计

Nonce 位图设计
Nonce 位图设计1300×1416 89.8 KB

我们的解决方案扩展了标准账户状态,将传统的 nonce 与Bitmap字段相结合,以跟踪 nonce 的使用情况。对于每个Nonce值,我们最多允许 256 笔具有不同Index交易。Bitmap 字段是一个Bitmap位的字段,其中每位代表当前Nonce下 256 个可用 slot(每个 slot 对应一个Index )中的一个,用于并行交易。如果交易修改了Nonce ,则Bitmap字段将被清零,并将Index位置的位设置为 1。

// NonceBitmapStateAccount is the Ethereum consensus representation of accounts accommodating with nonce bitmap. // These objects are stored in the main account trie. type NonceBitmapStateAccount struct {Nonce uint64 // Anchor nonce: the last finalized sequential checkpoint, always have first 8 bits as 0s. Balance *uint256.IntRoot common.Hash // merkle root of the storage trie CodeHash [] byte Bitmap *uint256.Int // NEW: Tracks used slots for parallel transactions; nil for legacy accounts }

具体来说, Nonce仍然是严格递增的,与传统实现相同。但是, Nonce的前 8 位始终为 0。我们观察到当前的Nonce值是 64 位,我们预计一个账户永远不会用完。这是因为一个 64 位的 Nonce 值最多可以占用2^{64} = 18446744073709551616如果一个账户每天发送 10 亿笔交易,则该账户需要 50+M 年才能用完所有 Nonce。因此,我们认为 56 位空间对一个账户来说已经足够了( 2^{56} = 72057594037927936

Bitmap中的每个位都指示该位对应的索引是否已被使用。例如,如果索引 10 处的位图设置为 1,则表示索引 10 已用于当前Nonce 。256 位的Bitmap字段允许一个账户一次最多发送 256 笔交易,且顺序不限(无需严格排序)。使用Bitmap可以非常高效地检查索引是否已被使用。此外,这种方法每个并行交易只需要一个额外的位。

交易创建

一个关键挑战是如何在不破坏现有交易格式的情况下,发出所选并行槽的信号。用户在创建新交易时,必须能够配置Index ,以支持发送并行交易。一种简单的方法是在现有交易类型中引入一个新的 8 位Index变量。该Index字段用于定位发送者账户状态中Bitmap字段的相应位值。

// LegacyTx is the transaction data of the original Ethereum transactions. type LegacyTx struct {Nonce uint64 // nonce of sender account GasPrice *big.Int // wei per gas Gas uint64 // gas limit To *common.Address `rlp:"nil"` // nil means contract creation Value *big.Int // wei amount Data [] byte // contract invocation input data V, R, S *big.Int // signature values ~~Index uint8 ~~ // NEW: The index value for parallelism? }

幸运的是,有一种更好的方法可以将Index信息实际合并到交易中,而无需引入新的字段。我们采用了一种位打包技术来解决这个问题,该技术充分利用了 64 位Nonce字段的巨大且未被充分利用的空间。具体方法是将交易中的Nonce字段逻辑地拆分成两个不同的部分,然后通过简单的按位运算来提取:

func ExtractNonce (nonce uint64 ) (index uint8 , actualNonce uint64 ) {index = uint8 (nonce >> 56 ) // Extract first 8 bits (most significant bits) actualNonce = nonce & 0x00FFFFFFFFFFFFFF // Mask lower 56 bits return }
  • Nonce的前 8 位用作Index
  • 最后 56 位作为实际的 nonce 值。正如我们上面分析的那样,56 位的 nonce 空间对于任何账户来说都足够了。

对于普通用户来说,前 8 位始终为 0。actualNonce 是常见的顺序随机actualNonce 。从他们的角度来看,整个系统似乎没有变化。对于高级用户,他们可以将前 8 位设置为任意值,且顺序任意。这意味着高级用户可以并行发送最多 256 笔交易。

Nonce 验证

验证逻辑是系统的核心,确保安全和进度。当新交易到达时,将执行以下逻辑(请注意,此处不考虑费用替代交易):

func ValidateNonce (account *NonceBitmapStateAccount, txPackedNonce uint64 ) bool {index, actualNonce := ExtractNonce(txPackedNonce) if actualNonce == account.Nonce + 1 { // --- CASE 1: Advancing the Sequence --- // This transaction is the next in the sequential chain. // It is valid. This will cause the account's Nonce to increment. // The bitmap is reset, as we are moving to a new base state. return true } else if actualNonce == account.Nonce { // --- CASE 2: Parallel Transaction --- // This transaction operates at the current account's Nonce. // Check if the requested parallel slot is available. if account.Bitmap == nil { // Bitmap is not initialized; this is the first parallel tx at this nonce. return true } return account.Bitmap.Bit( int (index)) == 0 // True if the slot is free } else { // --- CASE 3: Invalid Nonce --- // actualNonce is either too old (less than account's Nonce) or has a gap. // This mirrors the existing Ethereum validation rule. return false }}

让我们考虑以下交易序列。它始于一个账户,其当前Nonce为 5,并且Bitmap显示 slot 0 已被占用。用户使用不同的 slot( Index 2 和 3)在Nonce 5 处成功提交了两笔并行交易(TxA 和 TxB),验证器更新位图以将这些 slot 标记为已使用。在验证器处理 TxB 期间,TxA 已完成,但未产生任何冲突。

Nonce Bitmap 实现并行交易
Nonce Bitmap 启用并行交易878×1388 90.6 KB

然而,当用户尝试提交另一笔交易 (TxC) 并试图重用已被占用的Index 2 时,验证器会正确地将其视为重复交易而拒绝。之后,当用户提交Nonce为 6 的交易 (TxD) 时,系统会继续前进,这将触发 Nonce 更新:当前Nonce递增至 6,并且Bitmap会根据交易的Index进行更新。此 Nonce 递增至关重要,因为当用户尝试提交使用现已过时的Nonce 5 的交易 (TxE) 时,验证器会拒绝该交易,因为系统已向前推进,从而防止任何旧交易的重放。

分析与思考

Nonce Bitmap 方法直接解决了困扰高频用户的顺序性问题,同时保持了 EVM 区块链必不可少的安全保障和向后兼容性。

简单

该设计实现起来非常简单。其核心原理直观易懂:为普通用户维护一个顺序的Nonce ,同时使用Bitmap跟踪每个步骤中的并行操作。检查和更新Bitmap位操作逻辑计算简单,使用处理器原生支持的高效位操作。与随机 Nonce、Hyperliquid 的方法或二维 Nonce 管理相比,整体复杂度仍然较低。

效率和性能

与 Hyperliquid 的 nonce 系统或账户抽象中使用的二维 nonce 管理相比,位图方法在相同并行度下将存储开销降低了 64 倍。这种高效性实现了高度的并行性(尽管并非无限),同时保持了最小的状态扩展(每笔交易一位)。该设计巧妙地利用了巨大的 64 位 nonce 空间,对其进行了分区,同时又不损害确保安全性的顺序级数。

向后兼容性

这种方法的一个关键优势在于其无缝的向后兼容性。对于绝大多数用户和应用程序而言,系统无需任何更改。旧版钱包将继续运行,因为它们的交易会自动使用Bitmap结构中的Index零。系统为现有应用程序保留了熟悉的顺序随机数语义,同时为升级后的钱包解锁了并行性。这种双模式操作确保了生态系统的平稳过渡,而无需所有参与者进行协调升级。

然而,值得一提的是,用户不应假设eth_getTransactionCount总是返回账户的总交易数。这个问题也存在于 2D Nonces 或 Hyperliquid 的方法中。需要注意的是, eth_getTransactionCount主要用于PendingNonceAt函数。随着PendingNonceAt函数的重新设计,高级用户可以丢弃/禁用此 RPC。对于使用此eth_getTransactionCount的其他服务(通常是浏览器),他们可以使用其本地数据库来提取此数字。

谁能从 Nonce Bitmap 中受益?

  • 高频交易者。无需复杂的 nonce 管理,即可同时提交多笔交易。无需在交易之间查询 nonce 状态,从而降低延迟。
  • MEV 搜索器。发送可并行处理的交易包,提高执行速度和成功率。
  • DeFi 高级用户/鲸鱼。同时执行涉及多种协议的复杂策略,无需担心交易排序或交易卡顿。
  • 开发人员。构建能够更高效地提交批量交易的应用程序,从而改善用户体验并降低操作复杂性。
  • 普通用户。体验没有变化,系统仍然向后兼容现有钱包。

与其他方法的比较

方法原来的随机的超液体2D 随机数随机数位图
订购是的部分的部分的部分的
验证简单的简单的更复杂(结合基于时间的) - 与普通用户的原始方法相同
- 对于高级用户来说有点复杂
- 与普通用户的原始方法相同
- 对于高级用户来说有点复杂
并行性是的,无限制是的,约 100 笔交易是的,取决于nonceKey的大小。是的,最多 256 笔交易
交易次数简单的简单的不平凡中等,需要 API 更新不平凡
钱包兼容性是的对于普通用户来说对于普通用户来说
额外存储空间跟踪所有使用的随机数8*100 字节(每个地址跟踪 100 个已使用的随机数) 8 * 活动nonceKeys每个地址 32 字节

结论

Nonce Bitmap 方法表明,经过深思熟虑的协议设计可以实现
在不影响安全性和兼容性的情况下,显著提升了用户体验。通过观察 64 位 nonce 空间的利用率,我们只需为每个帐户额外增加 32 个字节即可解锁 256 路并行。

对于低延迟链来说,交易延迟不再受区块时间的限制,而是受用户提交交易的速度限制。幸运的是,Nonce Bitmap 消除了这一瓶颈。


来源
免责声明:以上内容仅为作者观点,不代表Followin的任何立场,不构成与Followin相关的任何投资建议。
喜欢
收藏
评论