如果你在 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 账户里存的一个值。
运行时强制三条规则:
- 交易的第一条指令必须是
AdvanceNonceAccount - blockhash 字段必须等于 nonce 账户当前存的值
- 交易执行时,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 的交易还在途中时,你不能用同一个账户发另一个不相关的交易,那等于给同一个位置造了两个竞争者,只有一个可能落地。
这一点最容易搞混,所以说明白:
- 同一个机会、发 N 家 → 一个 nonce 账户。这正是它的设计用途。
- N 个不同机会 → 需要 N 个 nonce 账户。
所以高频机器人都会建一个 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 之后:
- 想抢跑几家就抢几家
- 每个机会只付一次 base fee
- 代价是约 0.1% 的计算开销和零额外延迟
如果你需要一条快的路径来参与竞速,BoltTx 在法兰克福、新泽西、洛杉矶、新加坡提供直连节点。我们法兰克福节点实测中位延迟 133ms、抖动约 1ms,而且只有交易真正落块时才收 tip。