发送一笔几分钟前就签好的 Solana 交易

blockhash 大约一分钟就过期。当签名和提交被审批、跨链或者一个真人隔开时,该怎么办。

BoltTx Team··14 min read
solanadurable-nonceblockhash交易上链多签后端

大多数 Solana 文档都假设你在同一个函数里签名并发送。有一整类系统做不到这一点:签名来自一个地方,而提交发生在之后的某处。

投票结束后的治理执行。等第三个审批人的多签。等源链最终性的跨链桥。任何需要真人点击"批准"的后端。★在所有这些场景里,你签名时用的那个 getLatestBlockhash 值,到提交的时候早就没了。★

为什么这个窗口这么短

recentBlockhash 大约在 150 个区块内有效 —— 是一分钟的量级,不是一小时。过了以后,验证节点会直接拒绝这笔交易:

Blockhash not found

★这是一次拒绝,不是一次失败。★ 交易从没执行、什么都没花、状态也没变。这是可能结果里最干净的一种 —— 而当你手上那个签名花了二十分钟才凑齐时,它也完全没用。

更麻烦的是,签名是覆盖了 blockhash 的。你没法换一个新的进去而不作废你收集到的每一个签名。 对多签来说,这意味着要回去找每一个签名者重来。

如果你的延迟提交写对了、交易还是错过,免费领个 BoltTx key 改一行就能测测路由那一半。

Durable nonce 移除了这个时钟

nonce 账户用一个在你用掉它之前不会过期的值,替换掉 blockhash。

import { SystemProgram, NONCE_ACCOUNT_LENGTH } from "@solana/web3.js";

const tx = new Transaction();
tx.add(
  SystemProgram.createAccount({
    fromPubkey: payer.publicKey,
    newAccountPubkey: nonceAccount.publicKey,
    lamports: await connection.getMinimumBalanceForRentExemption(
      NONCE_ACCOUNT_LENGTH,
    ),
    space: NONCE_ACCOUNT_LENGTH,
    programId: SystemProgram.programId,
  }),
  SystemProgram.nonceInitialize({
    noncePubkey: nonceAccount.publicKey,
    authorizedPubkey: nonceAuthority.publicKey,
  }),
);

然后每一笔用到它的交易都要遵守同一条规则:

const nonceInfo = await connection.getNonce(nonceAccount.publicKey);

const tx = new Transaction();
// ★advanceNonce 必须是第一条指令。★
tx.add(
  SystemProgram.nonceAdvance({
    noncePubkey: nonceAccount.publicKey,
    authorizedPubkey: nonceAuthority.publicKey,
  }),
);
tx.add(...yourInstructions);

tx.recentBlockhash = nonceInfo.nonce;   // ★放 nonce,不是 blockhash★

★这两个细节都很容易搞错,而且失败方式都很让人困惑。★ 如果 nonceAdvance 不是第一条,交易无效。如果你填的是一个真实的 blockhash 而不是 nonce 值,你就回到了一分钟的窗口,而且没有任何提示告诉你保护已经丢了。

决定你整个设计的那条约束

一个 nonce 账户只持有一个 nonce 值,这意味着同一时间只能有一笔在途交易

签 交易 A  (nonce = N)
签 交易 B  (nonce = N)     ← ★同一个值★
交易 A 落块,nonce 前进到 N+1
交易 B 现在永久无效

★这是 durable nonce 最大的困惑来源。★ 它不是 bug,正是它让 nonce 成为一道防重放守卫:用过一次,那个值就没了。

对架构的后果是:每一条并发流程配一个 nonce 账户。 一个同时执行多个提案的治理系统需要多个 nonce 账户,要有追踪和分配,而不是共用一个。把它们当成一个池子来管理。

取消一笔已经签好的交易

durable nonce 的交易会无限期保持有效,而这是把双刃剑。★一笔你不再想要的已签名交易,仍然是一笔别人可以提交的已签名交易。★

取消的办法是自己把这个 nonce 烧掉:

// 用一笔空操作推进 nonce。★任何针对旧 nonce 值
// 签好的交易,都会永久失效。★
const cancel = new Transaction().add(
  SystemProgram.nonceAdvance({
    noncePubkey: nonceAccount.publicKey,
    authorizedPubkey: nonceAuthority.publicKey,
  }),
);

任何可能签出"之后又想放弃"的交易的治理、金库或跨链设计,都需要把这条通道建好并测过。nonce authority 正是让取消成为可能的东西,所以它应该和签名密钥一样被小心保管

什么时候你不需要 nonce

durable nonce 要花租金、多一个账户、多一条指令,以及实打实的运维复杂度。★对于一个你本来就能避免的延迟,它是错误的答案。★

在提交那一刻才签名。 如果你的后端持有密钥,就在发送之前取一个新 blockhash 再签。大多数"延迟提交"问题,其实是"我们签得太早了"问题。

最后才签。 在多签里,先收集意图,只在最后一个批准到达时才产出真正的已签名交易 —— 这样窗口从末尾开始,而不是从开头。

过期就重新签。 如果重新收集签名的成本很低,过期就只是不便,不是阻塞。

★当签名确实无法按需重新生成时,才用 nonce★ —— 离线签名者、放在保险库里的硬件钱包、在别的时区的审批人、你正在等待的源链。

过期不是唯一变了的东西

这是 nonce 解决不了的那部分,值得单独关注。

一笔二十分钟前签好的交易,是基于二十分钟前的状态构建的。★blockhash 问题解决了,陈旧问题没有。★

// ★提交一个陈旧签名之前,先对着当前状态校验。★
const stillValid = await revalidate(intent);
if (!stillValid) {
  await burnNonce();          // 取消而不是提交
  await requestNewApproval(); // 按当前状态重建
}

对金库转账来说这通常没问题 —— 一笔固定金额的转账不在乎过了多久。但对任何依赖价格的东西,nonce 让你能够提交一个陈旧的决策,而这比根本提交不了更糟。 要么把这类流程限制在固定金额,要么在提交前重新校验。

上链应该是什么水平

我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。

★durable nonce 移除的是截止时间,不是排队。★ 一旦提交,nonce 交易和其它任何交易一样要争夺收录 —— 所以如果这些是有人在等的金库划转或治理执行,同样的路由考量依然适用

BoltTx 在这里的位置

我们负责提交。不签名、不托管、不管你的审批流程。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。当这笔交易是一次金库划转、而它的内容提前知道是有价值的时候,这一点很重要。

你在本地签名,无论你的流程在什么时候签。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。

免费领 API key,没有月费:

const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

常见问题

Solana 交易签好之后有效多久? 从你签名用的 blockhash 起算大约 150 个区块,一分钟量级。过了以后验证节点会用 "Blockhash not found" 拒绝它,它从没执行过。

怎么发送一笔很久之前签好的交易? 用 durable nonce 代替 recent blockhash。nonce 值在被消耗之前不会过期,所以签名可以无限期保持有效。

Solana 的 durable nonce 是什么? 一个持有某个值、用来替代 recent blockhash 的账户。它按"被使用"过期,而不是按时间过期 —— 这正是签名和提交能够分离开的原因。

我的 durable nonce 交易为什么失败? 最常见的是 nonceAdvance 不是第一条指令,或者填了真实 blockhash 而不是 nonce 值。这两种失败的表现都不指向 nonce 本身。

一个 nonce 账户能同时用于多笔交易吗? 不能并发。nonce 账户只持有一个值,一旦有一笔交易消耗掉它,所有针对该值签好的其它交易都永久无效。每条并发流程配一个账户。

怎么取消一笔已经签好的交易? 自己用一笔空操作推进 nonce。这会消耗掉那个值,并永久作废所有针对它签好的东西 —— 这是 durable nonce 交易唯一可靠的取消方式。

多签需要用 durable nonce 吗? 只有当"可靠地收齐所有签名"确实比 blockhash 窗口更久时才需要。如果你能在最后一个批准到达的那一刻才产出已签名交易,普通 blockhash 更简单。

durable nonce 能让交易更快落块吗? 不能。它移除的是过期这个截止时间。一旦提交,这笔交易和其它任何交易一样争夺收录,所以手续费和路由仍然决定它什么时候落块。

一个 nonce 账户要花多少钱? 它所占空间的租金豁免额,关闭账户时可以取回,再加上追踪它的运维成本,以及每笔用到它的交易里多出的那一条指令。

提交一笔陈旧的已签名交易安全吗? 只有当这个意图不依赖已经变动的状态时才安全。固定金额的转账没问题,而任何依赖价格的东西,可能已经变成一个你不会再做的决策。

nonce authority 该由谁持有? 由被信任去执行取消的人持有,因为正是这个 authority 才能烧掉 nonce、作废已签名交易。要用和签名密钥同等的谨慎对待它。

交易落块之后 nonce 账户还能复用吗? 能。它会前进到一个新值,并且立刻可以用于下一笔交易。复用是常规做法,每次都新建账户会浪费租金和时间。

怎么查当前的 nonce 值? 对账户地址调用 getNonce,会返回当前值和 authority。构建的时候去取,因为更早一次构建时拿到的值可能已经被消耗了。

两个签名者用了同一个 nonce 值会怎样? 先落块的那笔消耗掉它,另一笔永久无效。这是防重放机制按设计工作,不是故障。

治理执行该用 durable nonce 吗? 取决于这笔交易是在投票结束之前还是之后产出的。结果已知后再签,可以完全避开 nonce;在结果之前就签,则需要它。

提交前怎么重新校验一笔陈旧交易? 重新检查它的正确性所依赖的状态 —— 余额、账户是否存在,以及价格(如果相关)。如果有任何东西发生了实质变化,就烧掉 nonce 重建,而不是提交一个基于旧状态做出的决策。

延伸阅读

返回博客列表