Skip to content

Querying Block Rewards⚓︎

Each block on Symbol generates a reward consisting of inflation plus the transaction fees collected in that block. This reward is then distributed among the harvester, the node beneficiary, and the network sink account.

This tutorial shows how to query the reward for any block and break down its distribution among accounts using receipts.

Prerequisites⚓︎

Before you start, set up your development environment.

This tutorial only reads data from the network. No account or XYM balance is required.

Full Code⚓︎

import json
import os
import urllib.request

from symbolchain.CryptoTypes import PublicKey
from symbolchain.symbol.Network import Address, Network

NODE_URL = os.getenv(
    'NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using node {NODE_URL}')

BLOCK_HEIGHT = os.getenv('BLOCK_HEIGHT', '3222290')

try:
    # Get the block header
    with urllib.request.urlopen(
            f'{NODE_URL}/blocks/{BLOCK_HEIGHT}') as response:
        block = json.loads(response.read())
    signer = Network.TESTNET.public_key_to_address(
        PublicKey(block['block']['signerPublicKey']))
    beneficiary = block['block']['beneficiaryAddress']
    print(f'Block height: {BLOCK_HEIGHT}')
    print(f'Signer: {signer}')
    beneficiary_b32 = Address.from_decoded_address_hex_string(
        beneficiary)
    print(f'Beneficiary: {beneficiary_b32}')

    # Get the network sink address
    with urllib.request.urlopen(
            f'{NODE_URL}/network/properties') as response:
        properties = json.loads(response.read())
    sink_b32 = properties['chain']['harvestNetworkFeeSinkAddress']
    sink = Address(sink_b32).bytes.hex().upper()
    print(f'Network sink: {sink_b32}')

    # Get the inflation reward at this height
    with urllib.request.urlopen(
            f'{NODE_URL}/network/inflation'
            f'/at/{BLOCK_HEIGHT}') as response:
        inflation = json.loads(response.read())
    reward = int(inflation['rewardAmount'])
    print(f'Inflation reward: {reward / 1e6:,.6f} XYM')

    # Get harvest fee receipts for this block
    with urllib.request.urlopen(
            f'{NODE_URL}/statements/transaction'
            f'?height={BLOCK_HEIGHT}'
            f'&receiptType=8515') as response:
        receipts = json.loads(response.read())

    # Label and display the reward distribution
    total = 0
    print('\nReward distribution:')
    for item in receipts['data']:
        for r in item['statement']['receipts']:
            if r['type'] != 8515:
                continue
            amount = int(r['amount'])
            total += amount
            target = r['targetAddress']
            if target == sink:
                label = 'Network sink (5%)'
            elif target == beneficiary:
                label = 'Beneficiary (25%)'
            else:
                label = 'Harvester'
                print(f'  {label}: {amount / 1e6:,.6f} XYM')
                harvester = Address.from_decoded_address_hex_string(
                    target)
                print(f'  Harvester: {harvester}')
                continue
            print(f'  {label}: {amount / 1e6:,.6f} XYM')

    # Summary
    fees = total - reward
    print('\nSummary:')
    print(f'  Total block reward: {total / 1e6:,.6f} XYM')
    print(f'  Inflation: {reward / 1e6:,.6f} XYM')
    print(f'  Transaction fees: {fees / 1e6:,.6f} XYM')

except Exception as error:
    print(error)

Download source

import { PublicKey } from 'symbol-sdk';
import { Address, Network } from 'symbol-sdk/symbol';

const fmt = (v) => (v / 1e6).toLocaleString(
    'en-US', { minimumFractionDigits: 6 });

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log(`Using node ${NODE_URL}`);

const BLOCK_HEIGHT = process.env.BLOCK_HEIGHT || '3222290';

// Get the block header
const block = await (await fetch(
    `${NODE_URL}/blocks/${BLOCK_HEIGHT}`)).json();
const signer = Network.TESTNET.publicKeyToAddress(
    new PublicKey(block.block.signerPublicKey));
const beneficiary = block.block.beneficiaryAddress;
console.log(`Block height: ${BLOCK_HEIGHT}`);
console.log(`Signer: ${signer}`);
const beneficiaryB32 = Address.fromDecodedAddressHexString(
    beneficiary);
console.log(`Beneficiary: ${beneficiaryB32}`);

// Get the network sink address
const properties = await (await fetch(
    `${NODE_URL}/network/properties`)).json();
const sinkB32 = properties.chain.harvestNetworkFeeSinkAddress;
const sink = Array.from(new Address(sinkB32).bytes)
    .map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
console.log(`Network sink: ${sinkB32}`);

// Get the inflation reward at this height
const inflation = await (await fetch(
    `${NODE_URL}/network/inflation/at/${BLOCK_HEIGHT}`)).json();
const reward = parseInt(inflation.rewardAmount, 10);
console.log(`Inflation reward: ${fmt(reward)} XYM`);

// Get harvest fee receipts for this block
const receipts = await (await fetch(
    `${NODE_URL}/statements/transaction`
    + `?height=${BLOCK_HEIGHT}&receiptType=8515`)).json();

// Label and display the reward distribution
let total = 0;
console.log('\nReward distribution:');
for (const item of receipts.data) {
    for (const r of item.statement.receipts) {
        if (r.type !== 8515) continue;
        const amount = parseInt(r.amount, 10);
        total += amount;
        let label;
        if (r.targetAddress === sink) {
            label = 'Network sink (5%)';
        } else if (r.targetAddress === beneficiary) {
            label = 'Beneficiary (25%)';
        } else {
            label = 'Harvester';
            const harvester = Address.fromDecodedAddressHexString(
                r.targetAddress);
            console.log(`  ${label}: ${fmt(amount)} XYM`);
            console.log(`  Harvester: ${harvester}`);
            continue;
        }
        console.log(`  ${label}: ${fmt(amount)} XYM`);
    }
}

// Summary
const fees = total - reward;
console.log('\nSummary:');
console.log(`  Total block reward: ${fmt(total)} XYM`);
console.log(`  Inflation: ${fmt(reward)} XYM`);
console.log(`  Transaction fees: ${fmt(fees)} XYM`);

Download source

Code Explanation⚓︎

The code retrieves the block's signer key, beneficiary and network sink addresses, along with the inflation amount. It then queries the block's harvest receipts and labels each recipient by comparing addresses, identifying the harvester by elimination. Finally, it derives the transaction fees by subtracting inflation from the total.

Fetching Block Information⚓︎

    # Get the block header
    with urllib.request.urlopen(
            f'{NODE_URL}/blocks/{BLOCK_HEIGHT}') as response:
        block = json.loads(response.read())
    signer = Network.TESTNET.public_key_to_address(
        PublicKey(block['block']['signerPublicKey']))
    beneficiary = block['block']['beneficiaryAddress']
    print(f'Block height: {BLOCK_HEIGHT}')
    print(f'Signer: {signer}')
    beneficiary_b32 = Address.from_decoded_address_hex_string(
        beneficiary)
    print(f'Beneficiary: {beneficiary_b32}')
// Get the block header
const block = await (await fetch(
    `${NODE_URL}/blocks/${BLOCK_HEIGHT}`)).json();
const signer = Network.TESTNET.publicKeyToAddress(
    new PublicKey(block.block.signerPublicKey));
const beneficiary = block.block.beneficiaryAddress;
console.log(`Block height: ${BLOCK_HEIGHT}`);
console.log(`Signer: ${signer}`);
const beneficiaryB32 = Address.fromDecodedAddressHexString(
    beneficiary);
console.log(`Beneficiary: ${beneficiaryB32}`);

The snippet retrieves the block header using the NODE_URL and BLOCK_HEIGHT environment variables to select the API node and the target block. If not set, they default to the reference testnet node and block 3222290.

The /blocks/{height} GET endpoint returns the block header, which includes the signerPublicKey and the beneficiaryAddress designated by the node operator. The beneficiary address is needed later to identify beneficiary receipts.

Fetching the Network Sink Address⚓︎

    # Get the network sink address
    with urllib.request.urlopen(
            f'{NODE_URL}/network/properties') as response:
        properties = json.loads(response.read())
    sink_b32 = properties['chain']['harvestNetworkFeeSinkAddress']
    sink = Address(sink_b32).bytes.hex().upper()
    print(f'Network sink: {sink_b32}')
// Get the network sink address
const properties = await (await fetch(
    `${NODE_URL}/network/properties`)).json();
const sinkB32 = properties.chain.harvestNetworkFeeSinkAddress;
const sink = Array.from(new Address(sinkB32).bytes)
    .map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
console.log(`Network sink: ${sinkB32}`);

The harvestNetworkFeeSinkAddress is fetched from /network/properties GET and needed later to identify sink receipts. Since the property is in base32 format and receipt addresses are hex-encoded, the SDK's Address class converts it to hex for comparison.

Fetching the Inflation Reward⚓︎

    # Get the inflation reward at this height
    with urllib.request.urlopen(
            f'{NODE_URL}/network/inflation'
            f'/at/{BLOCK_HEIGHT}') as response:
        inflation = json.loads(response.read())
    reward = int(inflation['rewardAmount'])
    print(f'Inflation reward: {reward / 1e6:,.6f} XYM')
// Get the inflation reward at this height
const inflation = await (await fetch(
    `${NODE_URL}/network/inflation/at/${BLOCK_HEIGHT}`)).json();
const reward = parseInt(inflation.rewardAmount, 10);
console.log(`Inflation reward: ${fmt(reward)} XYM`);

The /network/inflation/at/{height} GET endpoint returns the inflation reward amount for the given block height. This value is in atomic units, so for XYM (divisibility 6), 113474978 atomic units represent 113.474978 whole units.

The inflation schedule is defined in the network configuration and decreases over time.

Querying the Reward Distribution⚓︎

    # Get harvest fee receipts for this block
    with urllib.request.urlopen(
            f'{NODE_URL}/statements/transaction'
            f'?height={BLOCK_HEIGHT}'
            f'&receiptType=8515') as response:
        receipts = json.loads(response.read())

    # Label and display the reward distribution
    total = 0
    print('\nReward distribution:')
    for item in receipts['data']:
        for r in item['statement']['receipts']:
            if r['type'] != 8515:
                continue
            amount = int(r['amount'])
            total += amount
            target = r['targetAddress']
            if target == sink:
                label = 'Network sink (5%)'
            elif target == beneficiary:
                label = 'Beneficiary (25%)'
            else:
                label = 'Harvester'
                print(f'  {label}: {amount / 1e6:,.6f} XYM')
                harvester = Address.from_decoded_address_hex_string(
                    target)
                print(f'  Harvester: {harvester}')
                continue
            print(f'  {label}: {amount / 1e6:,.6f} XYM')
// Get harvest fee receipts for this block
const receipts = await (await fetch(
    `${NODE_URL}/statements/transaction`
    + `?height=${BLOCK_HEIGHT}&receiptType=8515`)).json();

// Label and display the reward distribution
let total = 0;
console.log('\nReward distribution:');
for (const item of receipts.data) {
    for (const r of item.statement.receipts) {
        if (r.type !== 8515) continue;
        const amount = parseInt(r.amount, 10);
        total += amount;
        let label;
        if (r.targetAddress === sink) {
            label = 'Network sink (5%)';
        } else if (r.targetAddress === beneficiary) {
            label = 'Beneficiary (25%)';
        } else {
            label = 'Harvester';
            const harvester = Address.fromDecodedAddressHexString(
                r.targetAddress);
            console.log(`  ${label}: ${fmt(amount)} XYM`);
            console.log(`  Harvester: ${harvester}`);
            continue;
        }
        console.log(`  ${label}: ${fmt(amount)} XYM`);
    }
}

To see how the total reward (inflation + transaction fees) was distributed, the code queries the /statements/transaction GET endpoint filtered by receiptType=8515 (Harvest_Fee), which returns the exact amount each participant received for harvesting the block.

Receipt type filter

The receiptType parameter filters statements that contain at least one receipt of that type, but each statement may also include other receipt types. The code skips non-harvest receipts within the same statement.

Each receipt's targetAddress is compared against the beneficiary and sink addresses to label each recipient. The remaining address is the harvester.

If the harvester and beneficiary are the same account, the network skips the beneficiary share and the harvester receives the full remainder. In that case only two receipts are created (harvester + sink) instead of three.

Why not use the block's signerPublicKey?

Harvesters typically sign blocks with a remote key rather than their main key. In that case, the block's signerPublicKey does not correspond to the harvester's main address. The remote key can be resolved by querying /accounts/{accountId} GET with the signer key and following its supplementalPublicKeys.linked.publicKey to the main account. However, this link reflects the current account state and may have changed since the block was harvested. Receipts are the reliable source.

The sum of all Harvest_Fee receipts equals the total block reward (inflation + transaction fees).

Calculating the Fee Breakdown⚓︎

    # Summary
    fees = total - reward
    print('\nSummary:')
    print(f'  Total block reward: {total / 1e6:,.6f} XYM')
    print(f'  Inflation: {reward / 1e6:,.6f} XYM')
    print(f'  Transaction fees: {fees / 1e6:,.6f} XYM')
// Summary
const fees = total - reward;
console.log('\nSummary:');
console.log(`  Total block reward: ${fmt(total)} XYM`);
console.log(`  Inflation: ${fmt(reward)} XYM`);
console.log(`  Transaction fees: ${fmt(fees)} XYM`);

Finally, subtracting the inflation amount from the total block reward gives the transaction fees collected in the block. All values are converted from atomic units to whole units for display.

Alternatively, the fee total can be calculated by summing each transaction's effective fee, which equals its size in bytes multiplied by the block header's feeMultiplier.

Output⚓︎

The following output shows a typical run querying the rewards for block 3,222,290:

Using node https://reference.symboltest.net:3001
Block height: 3222290
Signer: TAEJLGM3HFHSWEBISOOPGHFQYENKKGTNPZR4MGQ
Beneficiary: TACLHA4QM3AHTVYMD4ON5BHRMA2E5P5EXRETKBY
Network sink: TBC3AX4TMSYWTCWR6LDHPKWQQL7KPCOMHECN2II
Inflation reward: 113.474978 XYM

Reward distribution:
  Harvester: 79.455446 XYM
  Harvester: TCOADQ4OCZYUF3SVIC6LYLSMQI2DWJIF4DU4EHA
  Network sink (5%): 5.675388 XYM
  Beneficiary (25%): 28.376944 XYM

Summary:
  Total block reward: 113.507778 XYM
  Inflation: 113.474978 XYM
  Transaction fees: 0.032800 XYM

Some highlights from the output:

  • Addresses (lines 3-5): The signer address is derived from the block's signerPublicKey. The beneficiary is set by the node operator, and the network sink is a fixed system account defined in the network configuration.

  • Reward distribution (lines 9-12): Each participant's share of the total block reward. The harvester is identified by elimination, as any receipt target that is neither the sink nor the beneficiary. Notice the signer address (line 3) differs from the harvester address (line 10) because the harvester uses a remote key to sign blocks.

  • Summary (lines 15-17): The total block reward is the sum of all Harvest_Fee receipts. The inflation portion comes from the network configuration, while the transaction fees (0.032800 XYM) are the difference between the total reward and inflation.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Fetch block information /blocks/{height} GET
Fetch network sink address /network/properties GET
Fetch inflation reward /network/inflation/at/{height} GET
Query reward distribution /statements/transaction GET