Solana 质押交易与 epoch 边界

为什么一笔晚了一个 slot 的质押交易要花掉你两天,以及怎么围绕一个你挪不动的截止点来搭提交路径。

BoltTx Team··14 min read
solana质押epoch交易上链验证节点流动性质押

大多数 Solana 交易的紧急是相对的 —— 抢在别人前面落块。质押交易的紧急是绝对的:有一条边界,站错一边就要付出一段固定的时间代价。

一个 epoch 大约两天。错过边界的委托不会失败,它只是改到下一个 epoch 才生效 —— 而交易结果里没有任何东西会告诉你这件事发生了。

边界就是全部问题

质押操作不会立刻生效。它们在 epoch 边界生效:

操作 何时生效
委托 ★下一个 epoch 边界★
取消委托 ★下一个 epoch 边界★
拆分 / 合并 立即
提取(已失活) 立即

★一笔在边界前一个 slot 落块的委托,和一笔在边界后一个 slot 落块的委托,结果相差两天。★ 两笔都报告成功,两笔都是正确的交易。但只有一笔能拿到你瞄准的那个 epoch 的奖励。

这就是质押的提交值得投入注意力、而普通转账不值得的原因。落得晚的代价不是价格更差,而是一段固定的、无法挽回的延迟。

知道自己在哪

const info = await connection.getEpochInfo();

const slotsRemaining = info.slotsInEpoch - info.slotIndex;
const fractionElapsed = info.slotIndex / info.slotsInEpoch;

console.log(`epoch ${info.epoch},还剩 ${slotsRemaining} 个 slot`);

★用 slot 计算你的截止点,不要用墙上时钟。★ slot 时长随网络状况变化,所以按分钟估算会在网络繁忙时漂移 —— 而那恰恰是你的余量最要紧的时候。

// ★按 slot 余量提交,不是按时间余量。★
const SAFETY_SLOTS = 300;

if (slotsRemaining < SAFETY_SLOTS) {
  // 太靠近了。落块没有保证,而错过要花掉一个 epoch。
  await scheduleForNextEpoch(operation);
} else {
  await submitStakeOperation(operation);
}

合适的余量取决于错过的代价有多大以及你落块有多可靠。对一个只委托一次的金库来说,余量给得宽是免费的;对一个每个 epoch 都要再平衡的流动性质押协议来说,余量就是机会成本 —— 正确的数值要从你实测的落块分布里来。

验证的是激活,不是提交

这是质押系统最常报错东西的地方。一笔落块的交易意味着指令执行了。 ★它不意味着这份质押已经生效。★

const activation = await connection.getStakeActivation(stakeAccount);

// activation.state: "active" | "activating" | "deactivating" | "inactive"
if (activation.state === "activating") {
  // ★落块了,但还没开始产生收益。要到下一个边界才激活。★
}

给用户展示激活状态,不要展示交易状态。 一个在交易确认那一刻就显示"已质押"的面板,最长会错两天 —— 而会注意到的,恰恰是那些在查自己有没有在产生收益的用户。

反过来对取消委托也一样。一个申请解除质押的用户看到交易确认了,就期待拿回 SOL。 而它是 deactivating,并且会一直是,直到边界。

如果你的 epoch 时机算对了、交易还是落得晚,免费领个 BoltTx key 改一行就能测测提交路径。

所有人都在同一时刻提交

epoch 边界是整个网络的调度点。流动性质押协议在再平衡、验证节点在调整、自动化系统在触发 —— 全都卡在同一条边界上。

★epoch 边界附近的拥堵不是随机的,它是结构性的。★

对你的提交路径的后果:

手续费会可预测地上升。 从近期费用推导,不要用固定值,并且预期这里的数值会高于 epoch 中段。

不要瞄准最后几个 slot。 你最需要余量的地方,恰恰是你余量最少的地方。

把你自己的活儿摊开。 如果你有很多笔质押操作,把它们分散到整个 epoch 里提交、而不是全堆在边界上,可以避免在所有人之上再和自己竞争。

// ★把不需要挤同一个边界的操作错开。★
const perBatch = Math.ceil(operations.length / availableWindows);

先拆分再取消委托

一个常见的操作失误:为了提取一部分,把整个质押账户都取消委托了。

★质押账户是整体失活的。★ 想解除一部分,要先拆分,再对拆出来的那个取消委托:

const tx = new Transaction().add(
  StakeProgram.split({
    stakePubkey: existingStake,
    authorizedPubkey: authority.publicKey,
    splitStakePubkey: newStake.publicKey,
    lamports: partialAmount,
  }),
);
// 然后只对这个新账户取消委托。

拆分是立即生效的,所以它可以在 epoch 里的任何时候做。 只有取消委托受边界约束 —— 这意味着拆分永远不该是让你错过边界的那个原因。

★新的质押账户需要租金豁免★,所以拆分之后剩下的会比你预期的略少一点。计算金额时要把它算进去,尤其是当用户指定了一个具体数字的时候。

这里的"重试"是什么意思

质押的重试算法和交易不一样。

const outcome = await resolve(sig, lastValidBlockHeight);

switch (outcome.status) {
  case "success":
    await recordActivating(stakeAccount, targetEpoch); break;
  case "expired":
    // ★重建之前先看边界。★
    if (stillEnoughSlots()) await rebuildAndResend();
    else await deferToNextEpoch();             // ★不要迟到还硬提交★
    break;
  case "reverted":
    await investigate(outcome.err); break;
}

★靠近边界时,一笔过期的质押交易有时候不该重试。★ 如果重试会落在边界之后,提交它就意味着比预期晚一个 epoch 激活 —— 而这可能比"主动推迟并告诉用户"更糟,总好过让用户自己发现有两天的延迟。

这和交易场景恰好相反 —— 在那边你会毫不犹豫地重发直到过期。在这里,重试的决定取决于边界在哪。

上链应该是什么水平

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

★对质押来说,那个数字决定了你的安全余量。★ 一条可预测的尾巴,让你能有把握地把提交时间贴得离边界更近。 而一条不可预测的尾巴会逼你留保守的余量 —— 对一个每个 epoch 都要再平衡的协议来说,那是一项反复发生的成本。

BoltTx 在这里的位置

我们负责提交。不管质押管理、不管验证节点选择、不管你的再平衡逻辑。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool。对 epoch 边界的工作来说,真正相关的属性是:在恰好发生于那些边界上的结构性拥堵中,它表现如何。

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

免费领 API key,没有月费:

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

常见问题

Solana 的质押委托什么时候生效? 在下一个 epoch 边界,不是在交易落块的时候。一笔刚好在边界前落块的委托和一笔刚好在边界后落块的委托,开始产生收益的时间大约相差两天。

一个 Solana epoch 有多长? 大约两天,但会随出块情况变化。用 getEpochInfo 按 slot 计算截止点,不要按墙上时钟,因为 slot 时长在高负载下会漂移。

交易都确认了,我的质押为什么还没有收益? 因为它处在 activating 而不是 active。查 getStakeActivation —— 交易确认意味着指令执行了,而激活发生在 epoch 边界。

怎么查一个质押账户是不是已经生效? getStakeActivation 会返回 active、activating、deactivating、inactive 之一。展示这个状态而不是交易状态,因为两者最长能差两天。

提交时能贴 epoch 边界贴多近? 取决于你实测的落块分布,以及错过的代价有多大。按 slot 而不是按分钟留余量,并且把边界当成硬截止点,而不是一个目标。

为什么 epoch 边界附近的交易更容易失败? 因为整个网络都把活儿排在那里。流动性质押的再平衡、验证节点调整、自动化系统全都卡在同一条边界上,所以这种拥堵是结构性的,不是随机的。

能只解除质押账户的一部分吗? 不能直接做。先拆分账户,再对拆出来的那部分取消委托。 拆分是立即生效的,所以可以在 epoch 里的任何时候做。

拆分之后我的质押金额为什么少了一点? 新的质押账户需要租金豁免,这笔钱从拆分出来的金额里扣。计算金额时要算进去,尤其是用户指定了具体数字的时候。

过期的质押交易该重试吗? 只有当重试仍然能落在边界之前时才该。如果落不到,主动推迟并告诉用户,好过比他们预期晚一个 epoch 才激活。

怎么知道我的质押会在哪个 epoch 激活? 在你落块那个 slot 之后的那条边界的下一个 epoch。提交时把目标 epoch 记下来,这样之后可以对账,也能回答关于时间的问题。

质押交易需要优先费吗? 在 epoch 边界附近一般需要,因为那正是争用的峰值。从近期活动推导,而不是用一个在某一端必然错的固定值。

Solana 上解除质押要多久? 失活在下一个 epoch 边界完成,之后 SOL 才能提取。一笔确认了的解除质押交易,不意味着资金立刻可用。

能取消一次失活吗? 能,在失活完成之前重新委托即可。质押会直接回到 active 而不经过 inactive,所以不会损失一个 epoch。

流动性质押协议该每个 epoch 都再平衡吗? 那是策略决策,但如果要这么做,提交路径就比一次性委托重要得多 —— 一次失误会每个 epoch 重复发生,而不是只发生一次。

怎么避免和自己的质押交易竞争? 把不需要挤同一条边界的操作分散到整个 epoch 里。全堆在边界上提交,等于在全网拥堵之上再和自己抢。

质押交易落在了错误的 epoch 会怎样? 什么都不会失败。它只是比预期晚一个 epoch 激活 —— 这正是"验证激活状态而不是交易状态"是唯一能发现它的办法的原因。

延伸阅读

← 返回博客列表