Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 14 分钟

Solana getBlock 奖励与交易手续费:Lamport 流向

了解 getBlock 的 rewards 数组实际包含什么,以及如何在不混淆交易手续费与领导者奖励的情况下,对区块级 lamport 流向进行对账。

TL;DR

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 定价

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始