Skip to main content

持久交易编号(Nonces)

问题#

为了防止重花,Solana 交易包含一个有“最近”块哈希值的非空值。 包含太久的(撰文时为 ~2分钟) 区块哈希交易被网络拒绝为无效。 很不幸,某些情况下(例如托管服务),需要更多时间来生成交易的签名。 需要一种机制来支持这些潜在的线下网络参与者。

需求#

  1. 交易签名必须包括编号值
  2. 即使是在签名密钥披露的情况下,nonce 也不可以重复使用。

基于合约的解决办法#

这里我们描述了一种基于合约的解决方法,其中客户端可以在最近一次交易的 recent_blockhash 字段中“存放”一个未来可以使用的 nonce 值。 这个方法类似于通过一些 CPU ISA 实现的比较和交换原子指令。

当使用后续的 nonce 时,客户端必须首先从账户数据查询它的值。 现在的交易是正常的,但需要满足以下附加要求:

  1. 后续的 nonce 值用于 recent_blockhash 字段
  2. AdvanceNonceAccount 指令是交易中第一次发出的

合约机制#

未完成工作:svgbob 将其变成一个流程图

Start
Create Account
state = Uninitialized
NonceInstruction
if state == Uninitialized
if account.balance < rent_exempt
error InsufficientFunds
state = Initialized
elif state != Initialized
error BadState
if sysvar.recent_blockhashes.is_empty()
error EmptyRecentBlockhashes
if !sysvar.recent_blockhashes.contains(stored_nonce)
error NotReady
stored_hash = sysvar.recent_blockhashes[0]
success
WithdrawInstruction(to, lamports)
if state == Uninitialized
if !signers.contains(owner)
error MissingRequiredSignatures
elif state == Initialized
if !sysvar.recent_blockhashes.contains(stored_nonce)
error NotReady
if lamports != account.balance && lamports + rent_exempt > account.balance
error InsufficientFunds
account.balance -= lamports
to.balance += lamports
success

客户端想要使用该功能,首先需要在系统程序下创建一个 nonce 帐户。 此帐户将处于 Uninitialized 状态,且没有存储哈希,因此无法使用。

要初始化该新创建的帐户,必须发出 InitializeNonceAccount 指令。 该指令需要一个 Pubkey参数,它位于账户 授权 中。 Nonce 帐户必须是 rent-exempt 状态,才能满足数据持续性功能的要求, 因此它要求初始化之前先存入足够的 lamports。 初始化成功后,集群最近的区块哈希与指定 nonce 授权的 Pubkey 将一同存储。

AdvanceNonceAccount 指令用于管理帐户存储的 nonce 值。 它在账户的状态数据中存储集群最新的区块哈希,如果与已存储的值相匹配,那么会提示失败。 这个检查可以防止在同一个区块内重新广播交易。

由于 nonce 帐户的 免租 要求,一个自定义提现指令用于将资产从帐户中移出。 WithdrawNonceAccount 指令需要一个单一参数,提示取款信号,强制免除租金,防止账户余额下降至低于免租金的最低值。 该检查的一个例外情况是,最终余额是零,从而让账户能够删除。 这个账户关闭详细信息还有一个额外要求,即存储的 nonce 值必须与集群最近的区块不匹配, 正如 AdvanceNonceAccount

账户的 nonce authority 可以通过 AuthorizeNonceAccount 说明进行更改。 它需要传入一个参数,新授权的 Pubkey。 执行该指令将把完全的帐户及其余额控制权转移给新的授权。

AdvanceNonceAccountWithdrawNonceAccountAuthorizeNonceAccount 都需要当前 nonce authority 才能签署交易。

运行时(Runtime)支持#

合约本身并不足以实现这个功能。 为了在交易上强制执行一个现有的recent_blockhash,并防止通过失败的交易重放来窃取费用,runtime的修改是必要的。

任何未能通过通常的check_hash_age验证的交易将被测试为持久交易Nonce。 这是由包括一个AdvanceNonceAccount指令作为交易中的第一条指令发出的信号。

如果runtime确定使用了一个持久事务Nonce,它将采取以下额外的操作来验证事务:

  1. 加载Nonce指令中指定的NonceAccount
  2. NonceAccount 的数据字段反序列化NonceState,并确认其处于Initialized状态。
  3. 存储在NonceAccount中的nonce值与交易的recent_blockhash字段中指定的nonce值进行匹配测试。

如果上述三项检查都成功,则允许交易继续验证。

由于以InstructionError失败的交易会被收取费用,并且其状态的改变会被回滚,所以如果AdvanceNonceAccount指令被回滚,则费用可能会被盗。 恶意验证者可以重放失败的交易,直到存储的nonce被成功推进。 Runtime的更改可以防止这种行为。 当一个持久的nonce事务失败时,除了AdvanceNonceAccount指令外,还有一个InstructionError,nonce账户会像往常一样被回滚到执行前的状态。 然后,runtime将其nonce值和高级nonce账户存储起来,就像已经成功了。