并发抢跑 3 家服务商,别付 3 份 Gas:Solana Durable Nonce 实战

同一笔套利同时发给 BoltTx、Helius、Jito,base fee 要付三次。Durable nonce 让没抢到的那几笔直接不进块、零费用。附完整可运行代码。

BoltTx Team··10 min read
solanadurable-nonce套利mev交易落块rpc交易机器人

如果你在 Solana 上跑套利或抢跑机器人,几乎一定会把同一个机会同时发给多家落块服务——BoltTx、Helius Sender、Jito、0slot,谁先送到 leader 谁赢。

但有一点很多人没意识到:这几份拷贝是各自独立的交易,每一笔真的上了链,你就要付一份 base fee。

这篇讲清楚为什么会这样,以及 durable nonce 如何让没抢到的那几笔变成零成本。文末有可以直接跑的完整代码。

为什么发给三家 = 三笔不同的交易

每家落块服务都要求交易里带自己的 tip 指令,收款地址各不相同。收款地址一变,指令就变;指令一变,message 就变;message 一变,签名必然不同

所以你不是"把一笔交易发给三个地方",而是发了三笔各自独立、只是做同一件事的交易:

一个套利机会
├── 交易 #1  tip → BoltTx 地址    签名 A
├── 交易 #2  tip → Helius 地址    签名 B
└── 交易 #3  tip → Jito 地址      签名 C

Solana 的 leader 完全不知道这三笔有关系,它会把三笔都执行掉

如果套利真的成立,那是灾难:同一笔交易被执行三次。如果你的程序在无利可图时会 abort(这是正确做法),那三笔全部 abort,你为一无所获付了三份 base fee

一笔失败的交易到底扣了多少

这是我们生产日志里的一笔真实交易。机器人的程序检查利润,发现为零,主动 abort:

err          : InstructionError [5, Custom(1)]
程序日志      : profit=0
tip 地址      : +0 lamports        ← tip 全额回滚
付款钱包      : -135,000 lamports  ← base fee 照扣

tip 会原子回滚,base fee 不会。★ 这个不对称就是问题的全部。一笔回滚的套利很便宜,但不是免费。当你同时抢跑三家,每个机会都要付三次。

按每次 135,000 lamports、三家并发算,一天 1,000 个机会就是 约 0.4 SOL/天白白烧掉。

Durable Nonce 如何消除这个浪费

普通交易靠 recentBlockhash 防重放,它约 90 秒过期。durable nonce 交易把这个字段换成链上 nonce 账户里存的一个值

运行时强制三条规则:

  1. 交易的第一条指令必须是 AdvanceNonceAccount
  2. blockhash 字段必须等于 nonce 账户当前存的值
  3. 交易执行时,nonce 推进到一个新值

现在看三份拷贝共用一个 nonce 账户会发生什么:

nonce = X

交易 #1 先到 → X 匹配 → 执行 → nonce 推进到 Y
交易 #2 后到 → 带的还是 X, 而账户已是 Y → 被拒绝
交易 #3 后到 → 带的还是 X, 而账户已是 Y → 被拒绝

对钱包最关键的一点在这里:

这个拒绝发生在「交易验证阶段」,早于执行阶段。交易根本不进块,不消耗计算,不扣任何费用。

这和"交易执行失败"是两回事。失败的交易(比如上面那个 profit=0)已经进块了,base fee 照扣;而 nonce 不匹配压根没走到那一步

结果 进块了吗 扣费了吗
执行成功
执行失败(profit=0)
nonce 不匹配(抢跑输了)

所以你能享受"想抢跑几家就抢几家"的全部收益,而只为真正赢的那一笔付费

代价是什么

AdvanceNonceAccount 大约消耗 150 CU。一笔典型套利交易要烧 15 万–25 万 CU,开销约 0.1%

另外需要给 nonce 账户存租金(约 0.0015 SOL,一次性,关闭账户时可取回)。

没有任何延迟增加。★ 这条指令和交易其余部分一起处理。

唯一的真实限制:一个 nonce 账户同时只能有一笔在途

nonce 账户只存一个值。当一笔用 nonce X 的交易还在途中时,你不能用同一个账户发另一个不相关的交易,那等于给同一个位置造了两个竞争者,只有一个可能落地。

这一点最容易搞混,所以说明白:

所以高频机器人都会建一个 nonce 池,随机会到来轮换使用。我们网络上交易量最大的两个套利机器人就是这么做的,从它们的链上指令数据能直接看出来。

可运行代码

先一次性创建并充值 nonce 账户:

solana-keygen new -o nonce-account.json
solana create-nonce-account nonce-account.json 0.0015
solana nonce-account nonce-account.json    # 打印当前 nonce 值

然后用它来抢跑:

import base64
import requests
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.hash import Hash
from solders.message import Message
from solders.transaction import Transaction
from solders.system_program import (
    advance_nonce_account, AdvanceNonceAccountParams,
    transfer, TransferParams,
)

payer = Keypair.from_base58_string("你的付款私钥")
nonce_account = Pubkey.from_string("你的NONCE账户地址")
nonce_authority = payer.pubkey()

# 每家服务商都需要自己的 tip 地址和 tip 金额
PROVIDERS = [
    {
        "name": "bolttx",
        "url": "https://fr.bolttx.io/v1/send",
        "tip": Pubkey.from_string("BoLt1A77XnXgmLPTWFXztQ9oZRrTPuQHV7cChFCt35g6"),
        "lamports": 1_000_000,
        "key": "你的BOLTTX密钥",
    },
    # 在这里加入其他服务商
]


def current_nonce(rpc_url: str) -> Hash:
    """读取 nonce 账户里存的值。这一步替代了 getLatestBlockhash。"""
    r = requests.post(rpc_url, json={
        "jsonrpc": "2.0", "id": 1, "method": "getAccountInfo",
        "params": [str(nonce_account), {"encoding": "base64"}],
    }, timeout=10).json()
    raw = base64.b64decode(r["result"]["value"]["data"][0])
    # 账户布局: 4字节版本 + 4字节状态 + 32字节授权者 + 32字节 nonce 值
    return Hash.from_bytes(raw[40:72])


def build(nonce: Hash, provider: dict, trade_instructions: list) -> str:
    """为某一家服务商构建这笔交易的专属拷贝。"""
    ixs = [
        # 必须是第 0 条指令,否则运行时不认
        advance_nonce_account(AdvanceNonceAccountParams(
            nonce_pubkey=nonce_account,
            authorized_pubkey=nonce_authority,
        )),
        *trade_instructions,
        transfer(TransferParams(
            from_pubkey=payer.pubkey(),
            to_pubkey=provider["tip"],
            lamports=provider["lamports"],
        )),
    ]
    msg = Message(ixs, payer.pubkey())
    # nonce 值填在原本放 blockhash 的位置
    tx = Transaction([payer], msg, nonce)
    return base64.b64encode(bytes(tx)).decode()


def race(trade_instructions: list, rpc_url: str) -> None:
    """把一个机会同时发给所有服务商。只有赢的那一笔会扣费。"""
    nonce = current_nonce(rpc_url)
    for p in PROVIDERS:
        payload = build(nonce, p, trade_instructions)
        try:
            requests.post(
                p["url"],
                json={"transaction": payload},
                headers={"Authorization": f"Bearer {p['key']}"},
                timeout=5,
            )
        except requests.RequestException:
            pass  # 某一家慢,绝不能拖住其他家

两个细节必须强调:

advance_nonce_account 必须是第 0 条指令。★ 放在别的位置,运行时就不会把它当作 durable nonce 交易处理——去重失效,你又回到给每家都付费的状态。

不要调用 getLatestBlockhash★ nonce 值就是用来替代它的。如果你取了真实 blockhash 填进去,得到的是一笔普通交易,上述所有好处全部消失。

怎么验证真的生效了

抢跑之后,把输掉的那几个签名拿去链上查。如果 durable nonce 正常工作,它们根本不存在。不是"失败",而是查无此签名。只有赢的那一笔会出现在区块里。

# 赢的那笔: 能查到交易
solana confirm -v <赢的签名>

# 输的那些: 提示 "Transaction not found" —— 这正是我们要的结果
# 说明它们在验证阶段就被拒绝了, 一分钱没花
solana confirm -v <输的签名>

如果输的签名能在链上查到并且带着错误码,说明 durable nonce 没有生效。去检查 AdvanceNonceAccount 是不是真的在第 0 条,以及你传的是 nonce 值还是 blockhash。

结论

在 Solana 上并发抢跑多家服务商是正确策略:落块本身是概率事件,通往 leader 的独立路径越多,成功率越高。错的是为每条路径都付费。

用上 durable nonce 之后:

如果你需要一条快的路径来参与竞速,BoltTx 在法兰克福、新泽西、洛杉矶、新加坡提供直连节点。我们法兰克福节点实测中位延迟 133ms、抖动约 1ms,而且只有交易真正落块时才收 tip

返回博客列表