每一条 Solana 指令都要声明它碰哪些账户、以及怎么碰。这些声明不是文档 —— 运行时用它们来决定什么能并行执行、什么必须排队。
★写错很少会报错。它产出的是一笔"能用"的交易,只是悄悄地在和比它需要的更多的交易竞争。★
两个标志
const keys = [
{ pubkey: userWallet, isSigner: true, isWritable: true }, // 付钱且会变
{ pubkey: poolAccount, isSigner: false, isWritable: true }, // ★会变★
{ pubkey: tokenMint, isSigner: false, isWritable: false }, // 只读
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
];
isSigner —— 这个账户的签名必须在场。写错会大声失败:Missing required signature。
isWritable —— 这条指令可能修改该账户。★写错这个不会报错,代价是悄悄付掉的。★
为什么 isWritable 决定你的吞吐
Solana 并行执行交易。调度器靠比较可写账户集合来判断哪些能一起跑:
交易 A 写 [pool_1] ┐ ★并行 —— 没有交集★
交易 B 写 [pool_2] ┘
交易 C 写 [pool_1] ┐ ★串行 —— 同一个可写账户★
交易 D 写 [pool_1] ┘
★把一个你只读的账户标成可写,等于让你的交易排进了一条它本不必排的队。★ 对同一个账户的两次读会并行,两次写不会。
对一个朝热门账户发大量交易的机器人来说,这不是理论问题。过度声明可写账户,是少数几个"吃掉吞吐却连一条错误信息都不产生"的错误之一。
// ★读 mint,不要写它。★
{ pubkey: tokenMint, isSigner: false, isWritable: false },
值得跑一遍的检查: 对每一个你标成可写的账户,问一句"程序真的会改它吗"。mint、program ID、sysvar、配置账户,几乎总是只读的。
如果你的指令写对了、交易还是落得晚,免费领个 BoltTx key 改一行就能测测提交路径。
顺序是接口的一部分
程序是按位置读账户的。 accounts[0]、accounts[1] 依此类推 —— IDL 里的名字是给人看的,运行时传过去的是一个数组。
// ★把这两个调换,程序就会读错账户。★
const keys = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: authority, isSigner: true, isWritable: false },
];
在这里把来源和目标反过来,校验不会失败。两个都是类型正确的可写代币账户。★它会朝错误的方向转账★,而交易报告成功。
这才是值得害怕的失败模式 —— 不是 revert,而是一笔成功的交易做了和你意图不同的事。用 Anchor 的客户端时它的 IDL 会保护你;手工拼的指令没有这层护栏。
去重与手续费支付方
Solana 会在一笔交易内对账户 key 去重,并把标志合并:
// 同一个账户出现在两条指令里。
ix1: { pubkey: X, isWritable: false }
ix2: { pubkey: X, isWritable: true }
// ★合并后:X 在整笔交易里都是可写的。★
★取并集。★ 只要有一条指令把某账户声明为可写,就调度而言它在整笔交易里都是可写的 —— 所以一条指令里的一次过度声明,会影响所有指令的争用情况。
另外两条容易踩的规则:
手续费支付方永远是索引 0、永远是签名者、永远可写。 它要付手续费,所以余额按定义会变。
签名者排在非签名者之前,每一组内可写排在只读之前。 你用 TransactionMessage 构建时 SDK 会处理这个顺序 —— 这也是手工拼账户数组比看起来更危险的原因之一。
看清你实际发出去的是什么
在调试"这笔交易行为怪怪的"之前,先看看你构建出来的东西:
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message();
message.staticAccountKeys.forEach((key, i) => {
console.log(
i,
key.toBase58(),
message.isAccountSigner(i) ? "签名者" : "",
message.isAccountWritable(i) ? "可写" : "只读",
);
});
★isAccountWritable 报告的是合并之后的结果,也就是调度器看到的东西。★ 这个输出和任何单条指令的声明不一致是很常见的,而那个差异正是你要检查的。
会出什么错,以及它长什么样
| 错误 | 症状 |
|---|---|
漏了 isSigner |
★Missing required signature —— 大声★ |
多了 isSigner |
签名校验失败 |
漏了 isWritable |
★readonly data modified —— 大声★ |
多了 isWritable |
★什么都没有。吞吐悄悄下降。★ |
| 顺序错 | ★成功,但行为是错的★ |
| 少传账户 | NotEnoughAccountKeys |
★没有大声失败的那两行,才是危险的。★ 其余每一种都会立刻告诉你。
account meta 与交易体积
每个账户 key 要占 1232 字节上限里的 32 字节,所以账户列表通常正是让一笔复杂交易超标的东西。
console.log("账户数:", message.staticAccountKeys.length);
console.log("账户字节:", message.staticAccountKeys.length * 32);
两个值得知道的削减点:
重复是免费的。 同一个账户在五条指令里被引用,因为上面说的去重,只占一次 32 字节。
没用到的账户不免费。 一个你传了、但程序从没碰过的账户,照样占它那 32 字节。★从示例里抄一份账户列表、留着自己并不需要的条目,是一种常见的"白白逼近体积上限"的方式。★
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★account meta 影响你面对多大的争用;路由影响你以什么路径去面对它。★ 一笔可写集合最小化的交易,仍然要到达出块方 —— 而那一半,和你的账户声明写得多仔细无关。
BoltTx 在这里的位置
我们负责提交。指令构建完全归你 —— 我们不修改交易内容,这也包括绝不碰你的 account meta。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。
你在本地签名。我们不托管资金、不代签。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 的 AccountMeta 是什么?
对一条指令所碰的某一个账户的声明,包含它的公钥以及 isSigner、isWritable 两个标志。运行时用这些标志做签名检查和并行调度。
isWritable 是做什么的? 它声明这条指令可能修改该账户。它同时决定调度:写同一个账户的交易会串行化,而只读它的交易可以并行。
把账户不必要地标成可写会怎样? 不报错。你的交易白白排进了那个账户的争用队列,吞吐悄悄下降。这是最常见的 account meta 错误。
为什么我的交易报 readonly data modified?
程序试图修改一个你声明为只读的账户。把那个账户改成 isWritable: true,或者检查你是不是在那个位置传错了账户。
Solana 指令里账户顺序重要吗? 重要。程序是按位置读账户的,所以顺序错了可能产出一笔"成功但做错事"的交易 —— 比如朝反方向转账。
Solana 怎么对交易里的账户去重? 重复的 key 只出现一次,标志按并集合并。只要有任何一条指令把某账户声明为可写,就调度而言它在整笔交易里都是可写的。
手续费支付方永远可写吗? 是,而且永远是签名者、永远在索引 0。付手续费会改变它的余额,所以它按定义可写。
怎么检查我的交易里哪些账户可写?
编译 message,对每个索引调 isAccountWritable。它报告的是调度器看到的合并结果,常常和任何单条指令的声明不一致。
账户排序规则是什么?
签名者排在非签名者前,每组内可写排在只读前。你用 TransactionMessage 构建时 SDK 会安排好,这也是它优于手工拼装的原因之一。
多传账户有代价吗? 有,每个占 1232 字节上限里的 32 字节,即便程序从没用过它。抄来的账户列表里留着没用的条目,是在白白逼近体积上限。
为什么我会收到 NotEnoughAccountKeys? 程序期待的账户比你传的多。每一个程序要读的账户都必须显式列出,包括那些只在 CPI 内部才被碰到的。
program ID 该标成可写吗? 不该。程序账户在执行期间是只读的,把它标成可写会让你和每一笔用到该程序的交易产生争用。
sysvar 需要可写吗? 不需要。rent、clock 这类 sysvar 是只读的,而且很多程序现在已经不再要求传它们了。
account meta 怎么影响并行执行? 调度器比较可写集合。没有共同可写账户的交易可以一起执行,所以可写集合越小,串行化越少。
Anchor 会帮我处理 account meta 吗? 用它生成的客户端时会,因为 IDL 里带着顺序和标志。手工拼的指令没有这层护栏 —— 顺序错误就出现在那里。
怎么减少一笔交易里的账户数? 去掉程序用不到的账户、减少路由跳数,并用 address lookup table 压缩引用。同一个账户重复出现只占一次,所以不必花力气去合并它们。
延伸阅读
- Solana 版本化交易
- Solana PDA 推导
- Solana 交易模拟
- Solana 交易错误码
- Solana 交易上链完全指南