账户布局通常被当成一个内部设计问题。它同时也是你为每一个创建账户的用户定下的价格。
★租金豁免随字节数增长。一个你"以防万一"加了三个字段的结构体,是一笔向你程序创建的每一个账户永久收取的成本。★
租金是被锁住的资本,不是手续费
这个区分决定了它该怎么算:
const rent = await connection.getMinimumBalanceForRentExemption(space);
★这些 lamport 是被锁住,不是被花掉。账户关闭时会退回。★ 这让账户大小没有手续费那么严重 —— 但它仍然是你的用户在账户存在期间用不了的资本,而对一个要创建几千个账户的机器人来说,它会累积成一个真实的数字。
对你的设计有两个后果:
一个过大的账户,是一项对采用率的永久征税,由每个用户预先支付。
★一个没有关闭指令的程序,让那份资本永远拿不回来。★ 而这是两者中更严重的错误。
把大小算出来,不要猜
Anchor 的 InitSpace 会从结构体推导它:
#[account]
#[derive(InitSpace)]
pub struct Position {
pub owner: Pubkey, // 32
pub amount: u64, // 8
pub opened_at: i64, // 8
pub is_active: bool, // 1
#[max_len(32)]
pub label: String, // ★4 + 32★
}
// space = 8(discriminator)+ Position::INIT_SPACE
★那 8 个字节的 discriminator 正是手算大小时最容易忘的★ —— 忘掉它会产出一个小了一截的账户,至少它在初始化时就失败,不会静默出错。
变长字段需要一个显式上限。 String 或 Vec 没有内在最大值,所以 max_len 就是你决定"用户被允许存多少"的地方 —— 也就是他们要为多少付钱的地方。
如果你的账户大小定得很好、交易还是会错过,免费领个 BoltTx key 你的用户改一行就能测测提交路径。
留白是一个真实的决策
"为未来字段加保留字节"是常见建议。★它是一个真实的取舍,不是一个免费选项。★
支持它: 增加字段的升级不需要迁移已有账户。
反对它: 每个用户从第一天起,就在每个账户上为没人用的字节付钱。
pub struct Position {
// ... 真实字段 ...
pub _reserved: [u8; 64], // ★64 字节 × 曾经创建过的每一个账户★
}
★老实的表述是:留白是拿一个确定的当下成本,去换一次不确定的未来迁移。★ 对账户很少的程序,这是便宜的保险;对一个"每用户每仓位一个账户"的程序,这是为一个假设按次收费。
realloc 是存在的,所以留白不是增长的唯一路径 —— 它只是要求账户可调整大小,并且届时有人来出那笔追加的租金。
少而大 vs 多而小
★这个决策影响的是你用户的交易,不只是你的租金。★
很多小账户:
- 每笔交易里更多账户引用,每个 32 字节
- ★更逼近 1232 字节的交易上限★
- 写入并行更好 —— 不同账户之间不会串行化
少数大账户:
- 引用更少、交易更小
- ★写入争用 —— 碰同一个账户的所有人都被串行化★
- 每条指令的反序列化成本更高
对任何高频场景,并行那一点通常是决定性的。 一个共享的状态账户是一个客户端优化再多也抬不高的吞吐天花板 —— 而你的用户体验到的,会是一个扩不上去的机器人。
永远要提供关闭指令
#[account(mut, close = receiver, has_one = owner)]
pub position: Account<'info, Position>,
★一个没有关闭通路的程序,把用户的资本永久锁死。★ 对一个频繁开仓平仓的交易机器人来说,这就是"租金是一笔浮存"和"租金是沉没成本"之间的差别。
两件要做对的事:
谁收到这些 lamport。 通常是最初的支付方,而且必须校验,不能任由调用方指定。
防止过早关闭。 ★一个还持有代币时被关掉的账户,会把那些代币困死★,所以指令应该先验证账户确实是空的。
字段顺序做对不花任何成本
// ★定长字段在前;变长字段放最后。★
pub struct Position {
pub owner: Pubkey, // 32,定长
pub amount: u64, // 8,定长
#[max_len(32)]
pub label: String, // ★变长 —— 它之后的一切都没有固定偏移★
}
★一旦出现变长字段,它之后的每个字段都失去了固定偏移。★ 这意味着客户端没法对那些字段做 memcmp 过滤,也没法不顺序遍历 buffer 就读到它们。
把客户端要过滤的字段放在任何变长字段之前,不花任何成本,却让 getProgramAccounts 查询成为可能。 这是少数几个纯粹免费的布局选择之一。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★账户设计影响交易体积和写入争用,而这两者决定了一笔交易能不能被构建、以及它怎么竞争。★ 两者都不是靠更快的提交路径修好的 —— 这就是布局决策值得在上线前做对的原因。
BoltTx 在这里的位置
我们为调用你程序的那些人处理提交。账户布局、租金和关闭通路都在你的程序里决定。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。
你的调用方在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从他们自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
账户大小怎么影响 Solana 上的成本? 租金豁免随字节数增长,而创建账户的人付它。 一个过大的结构体,是一笔向每一个创建它的用户永久收取的成本。
租金是手续费还是被锁住的资本? 被锁住的资本。账户关闭时会退回,这让它没有手续费那么严重,但它在账户存在期间仍然不可用。
怎么算我的账户需要多大空间?
用 Anchor 的 InitSpace 从结构体推导,再加 8 字节的 discriminator —— 那正是手算大小时最常忘的一项。
该为未来字段加留白吗? 它是拿一个确定的当下成本换一次不确定的未来迁移。 对账户少的程序很便宜,对"每用户每仓位一个账户"的程序很贵。
realloc 是干什么的? 在创建之后调整账户大小,是留白之外的另一条路。它要求账户可调整大小,并且届时有人出那笔追加的租金。
该用很多小账户还是少数大账户? 高频场景通常是很多小账户胜出,因为不同账户可以并行写入,而共享账户会把碰它的一切串行化。
账户数量怎么影响用户的交易? 每个账户引用占 1232 字节上限里的 32 字节。账户越多,交易越大,留给路由的空间越少。
我的程序为什么需要关闭指令? 没有它,用户付的租金就被永久锁死。对一个频繁开平仓的机器人来说,那把一笔可回收的浮存变成了沉没成本。
账户关闭时 lamport 该给谁? 通常是最初的支付方,而且接收方必须校验、不能任由调用方指定 —— 否则任何人都能把退款改道。
账户还持有代币时被关掉会怎样? 那些代币会被困死。 关闭指令应该在放行之前验证账户确实是空的,因为这个操作不可逆。
账户结构体里字段顺序要紧吗?
对客户端要紧。一旦出现变长字段,它之后的一切都失去固定偏移,那些字段就没法做 memcmp 过滤了。
字段偏移为什么对客户端要紧?
因为 getProgramAccounts 的过滤器匹配的是固定偏移处的字节。String 或 Vec 之后的字段没法过滤,所以把可过滤字段放前面不花成本、却让查询成为可能。
变长字段怎么影响大小?
它们需要一个显式的 max_len,而那就是你定下存储上限的地方。这个上限会变成每个用户都要付的租金,不管他们实际存了多少。
更大的账户会消耗更多计算吗? 会,间接地,因为反序列化随大小增长。这是"没用的留白不免费"的另一个理由 —— 它在租金和每条指令的计算上都要付。
之后能把账户改小吗?
realloc 既能扩也能缩,并退回差额。扩更常见,但如果重新设计移除了字段,缩也是可用的。
最常见的账户定大小的错误是什么? 忘掉 8 字节的 discriminator,其次是上线时没有关闭指令。前者在初始化时大声失败;后者悄悄把用户资本永久锁死。