Solana getBlock 响应中的 rewards 数组描述的是该区块处理过程中入账的 lamports,而不是其交易支付的手续费总额。rewardType 为 Fee 的条目对应已收取交易手续费的那一部分,但它会在领导者、销毁和其他接收方之间拆分,因此不会等于逐笔交易 fee 字段之和。要正确对账,应汇总交易 fee 字段、单独提取 Fee rewardType 条目、排除 Rent、Staking 和 Voting 奖励,并将任何差额视为结构性拆分而非错误。本指南提供一个可运行的 Node.js 脚本和结果表,让你可以针对自己的端点测量这种关系。
getBlock 的 rewards 数组实际代表什么
当你调用 Solana 的 getBlock RPC 方法并启用 rewards 时,响应中会包含一个 rewards 数组。每个条目以 pubkey 和 rewardType 为键,lamports 字段是有符号的,因为有些条目代表扣款而非入账。该数组描述的是该区块处理过程中入账或扣款的 lamports,其中既包括领导者的手续费奖励,也包括其他与交易手续费完全无关的奖励事件。
Solana getBlock RPC 参考与 reward 对象记录了 rewardType 的取值,包括 Fee、Rent、Staking 和 Voting。这意味着 rewards 数组是区块级的会计视图,而不是交易手续费账本。如果你把 rewards 之和当作区块中交易支付的手续费总额,就会根据出现的奖励类型不同而多计或少计。
这一区别对区块浏览器、手续费仪表盘和会计任务都很重要。Solana RPC 提供商与端点(RPC Assistant)页面可以帮助你选择能返回完整 rewards 数据的端点,但无论使用哪家提供商,对账逻辑都必须正确。
- rewards 条目以 pubkey 和 rewardType 为键
- lamports 是有符号的:负值表示扣款
- rewardType 为 Fee 的条目对应已收取交易手续费的那一部分
- Rent、Staking 和 Voting 奖励不是交易手续费
交易手续费和优先费如何进入区块
每笔 Solana 交易都会声明一个 fee,并且可能附加优先费。Solana 交易手续费与优先费文档描述了这些费用如何计算和收取。因此,一个区块的交易隐含了一笔手续费总流入:将每笔交易的 fee 字段相加,如果你有优先费数据,也一并加上。
然而,rewards 数组并不简单地镜像这个总和。运行时会应用一种拆分:一部分手续费归领导者,一部分被销毁,其他接收方也可能获得一部分。Fee rewardType 条目反映的是领导者的那部分以及任何其他与手续费相关的入账,而不是手续费总流入。这就是 Stack Exchange 上关于“rewardType: Fee lamports”的问题存在的原因:该数字与交易手续费的简单求和并不匹配。
要深入了解交易在 getBlock 响应中如何编码和解析,请参阅 Solana 版本化交易与 getBlock 解析。该页面涵盖编码细节;本页面聚焦于会计恒等式。
你可以断言的会计恒等式
你可以断言,区块中逐笔交易 fee 字段之和应与 Fee rewardType 条目相关,任何差额都可由运行时在领导者、销毁和其他接收方之间应用的拆分来解释。这是一种结构性关系,而非精确相等。如果拆分规则一致,这种差额是预期内的,并且在不同区块之间应当保持稳定。
你必须将 Rent、Staking 和 Voting 奖励条目完全排除在手续费对账之外。它们代表不同的经济事件:租金与账户存储相关,质押奖励与质押的激活和停用相关,投票奖励与验证者投票相关。把它们包含进来会使你的对账失去意义。
正确的对账表应显示:交易手续费总额、Fee rewardType 的 lamports 总额、差额,以及在考虑已知拆分后任何残差的“未解释”行。如果未解释行始终为零,说明你的模型是完整的。如果不是,你可能遗漏了某种奖励类型或某个手续费组成部分。
- 汇总区块中所有交易的 fee 字段
- 单独提取 Fee rewardType 条目并汇总其 lamports
- 排除 Rent、Staking 和 Voting 奖励条目
- 计算差额,并将任何残差标记为未解释
破坏对账的数据质量陷阱
rewards 数组可能缺失或为空,而不是零数组。如果你假设它总是存在,脚本会在未返回 rewards 的区块上失败。始终检查 rewards 键是否存在,并优雅地处理 null 或空数组。
奖励条目的 lamports 是有符号的,因为有些条目代表扣款。如果你丢弃负的 lamports 条目,就会高估总额。金额单位是 lamports,因此转换为 SOL 时要一致地除以 1e9。此外,响应会随请求的 transaction-details 编码和 commitment 级别而变化,因此跨 commitment 级别进行比较不是同类比较。
关于获取区块时处理超时和重试的指导,请参阅 Solana RPC 超时与重试。关于历史数据方面的考虑,请参阅 通过 RPC 查询 Solana 历史数据。
- rewards 可能缺失或为空,而不是零数组
- lamports 是有符号的;负值表示扣款
- 转换为 SOL 时一致地除以 1e9
- transaction-details 编码会改变你所汇总内容的形态
- commitment 级别会改变响应;不要混用级别
可运行脚本:获取一个区块并对账 lamport 流向
以下 Node.js 脚本会获取一个包含完整交易详情的区块,提取每笔交易的 fee,按 rewardType 拆分提取 rewards 数组,并打印一张包含计算差额和标记为“未解释”行的对账表。然后它会在一小段区块范围内重复,以便你观察差额是稳定的、结构性的,还是一次性的。
将 RPC 端点替换为你自己的。该脚本使用标准 JSON-RPC 接口,不依赖任何提供商特定的 SDK。针对几个区块运行它,以构建你自己的结果表。
const https = require('https');
const RPC_URL = 'https://api.mainnet-beta.solana.com';
const START_SLOT = 250000000;
const END_SLOT = START_SLOT + 4;
function rpcCall(method, params) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
const url = new URL(RPC_URL);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
});
});
req.on('error', reject);
req.write(data);
req.end();
});
}
async function reconcileBlock(slot) {
const resp = await rpcCall('getBlock', [slot, {
encoding: 'json',
transactionDetails: 'full',
rewards: true,
maxSupportedTransactionVersion: 0
}]);
if (resp.error) {
console.log(`Slot ${slot}: RPC error`, resp.error.message);
return null;
}
const block = resp.result;
if (!block) {
console.log(`Slot ${slot}: no block`);
return null;
}
let totalTxFees = 0;
for (const tx of block.transactions || []) {
const meta = tx.meta;
if (meta && typeof meta.fee === 'number') {
totalTxFees += meta.fee;
}
}
const rewardsByType = {};
let totalRewards = 0;
for (const r of block.rewards || []) {
const type = r.rewardType || 'Unknown';
rewardsByType[type] = (rewardsByType[type] || 0) + r.lamports;
totalRewards += r.lamports;
}
const feeReward = rewardsByType['Fee'] || 0;
const rentReward = rewardsByType['Rent'] || 0;
const stakingReward = rewardsByType['Staking'] || 0;
const votingReward = rewardsByType['Voting'] || 0;
const otherReward = totalRewards - feeReward - rentReward - stakingReward - votingReward;
const difference = totalTxFees - feeReward;
return {
slot,
totalTxFees,
feeReward,
rentReward,
stakingReward,
votingReward,
otherReward,
difference,
unexplained: difference
};
}
(async () => {
const rows = [];
for (let slot = START_SLOT; slot <= END_SLOT; slot++) {
const row = await reconcileBlock(slot);
if (row) rows.push(row);
}
console.log('slot | totalTxFees | FeeReward | Rent | Staking | Voting | Other | Difference | Unexplained');
for (const r of rows) {
console.log(`${r.slot} | ${r.totalTxFees} | ${r.feeReward} | ${r.rentReward} | ${r.stakingReward} | ${r.votingReward} | ${r.otherReward} | ${r.difference} | ${r.unexplained}`);
}
})();结果表:针对你自己的端点进行测量
使用上面的脚本为你自己的端点和区块范围填充一张结果表。该表每个区块一行,列包括交易手续费总额、Fee rewardType 的 lamports、Rent rewardType 的 lamports、Staking rewardType 的 lamports、Voting rewardType 的 lamports、其他奖励的 lamports、总手续费与 Fee 奖励之间的差额,以及未解释的残差。
至少在 10 个区块上运行,以观察差额是否稳定。如果差额始终是总手续费的固定比例,那就是文档所述的拆分。如果它变化不定,你可能遗漏了某种奖励类型或某个手续费组成部分。不要断言本文中测量出的奖励数字;这些值取决于你的端点、commitment 级别和区块范围。
关于提供商特定的性能特征,请参考你的提供商文档。OnFinality 的 Solana 网络页面和 RPC 定价描述了可用的端点和套餐,但对账方法与提供商无关。
- 每个区块一行;为每种 rewardType 和差额设置列
- 在 10 个以上区块上运行以评估稳定性
- 不要跨 commitment 级别进行比较
- 记录你的端点和区块范围以便复现
常见失败及如何避免
把 rewards 之和当作手续费总额是最常见的失败。rewards 数组包含 Rent、Staking 和 Voting 条目,它们不是交易手续费。在与交易手续费比较之前,始终单独提取 Fee rewardType 条目。
丢弃负 lamports 的奖励条目是另一个陷阱。负条目是扣款,必须计入总和。汇总失败或被跳过交易的 fee 字段也会扭曲总额;只应包括实际被处理并收取了手续费的交易。
将 confirmed commitment 的区块与 finalized commitment 的区块进行比较不是同类比较,因为响应可能不同。忘记区块的交易列表编码会改变你所汇总内容的形态,会破坏你的解析器。从余额而非交易重新推导手续费容易出错且没有必要;fee 字段才是权威来源。
- 不要把 rewards 之和当作手续费总额
- 包含负 lamports 条目
- 从手续费总和中排除失败或被跳过的交易
- 不要混用 commitment 级别
- 使用交易 fee 字段,而不是余额变化量
故障排查检查清单
当你的对账结果与预期不符时,请按此检查清单逐项排查。首先,确认响应中存在 rewards。如果 rewards 键缺失或为空,你的端点可能不会为该区块返回 rewards,或者你可能需要显式请求它们。
其次,确认你在手续费比较中只汇总了 Fee rewardType 条目。第三,检查你是否包含了负 lamports。第四,确保你在比较中对所有区块使用相同的 commitment 级别。第五,验证你的 transaction-details 编码一致,并且你从响应中的正确位置读取 fee 字段。
如果未解释的残差很大,请考虑你的端点是否单独返回优先费数据。Solana 交易手续费与优先费文档描述了这些数据的结构。关于影响奖励类型的账户和租金机制,请参阅 通过 RPC 获取 Solana 账户信息、租金与代币账户。
- 检查 rewards 是否存在以及端点是否支持
- 手续费比较中只汇总 Fee rewardType
- 包含负 lamports
- 使用一致的 commitment 级别
- 验证 transaction-details 编码和 fee 字段位置
局限性与假设
这种对账方法假设 Fee rewardType 条目捕获了领导者和其他接收方的所有与手续费相关的入账。如果运行时改变了手续费的拆分方式或引入新的奖励类型,该方法可能需要调整。它还假设区块中所有交易的 fee 字段都存在且准确。
除非你的端点单独返回优先费,否则该方法不考虑优先费。它也不尝试对租金或质押奖励进行对账,这些不在手续费核算的范围内。结果会因端点、commitment 级别和区块范围而异,因此务必记录你的参数。
关于 Solana RPC 能力的更广泛概述,请参阅 OnFinality Learn 中心和 API 服务页面。这些资源可以帮助你为会计任务选择合适的端点。
后续步骤:将解析与运行时问题分开
现在你已经理解了会计恒等式,请把解析细节留在姊妹页面,把运行时问题留在账户/租金页面。关于版本化交易解析和 getBlock 编码,请继续阅读 Solana 版本化交易与 getBlock 解析。关于租金、账户存储和代币账户机制,请参阅 通过 RPC 获取 Solana 账户信息、租金与代币账户。
如果你需要历史区块数据来回测你的对账,请参阅 通过 RPC 查询 Solana 历史数据。关于生产环境可靠性,请查看 Solana RPC 超时与重试。
要选择能返回完整 rewards 数据的端点,请从 Solana RPC 提供商与端点(RPC Assistant)开始。关于定价和套餐详情,请参阅 RPC 定价。