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.
importjsonimportosimporturllib.requestfromsymbolchain.CryptoTypesimportPublicKeyfromsymbolchain.symbol.NetworkimportAddress,NetworkNODE_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 headerwithurllib.request.urlopen(f'{NODE_URL}/blocks/{BLOCK_HEIGHT}')asresponse: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 addresswithurllib.request.urlopen(f'{NODE_URL}/network/properties')asresponse: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 heightwithurllib.request.urlopen(f'{NODE_URL}/network/inflation'f'/at/{BLOCK_HEIGHT}')asresponse:inflation=json.loads(response.read())reward=int(inflation['rewardAmount'])print(f'Inflation reward: {reward/1e6:,.6f} XYM')# Get harvest fee receipts for this blockwithurllib.request.urlopen(f'{NODE_URL}/statements/transaction'f'?height={BLOCK_HEIGHT}'f'&receiptType=8515')asresponse:receipts=json.loads(response.read())# Label and display the reward distributiontotal=0print('\nReward distribution:')foriteminreceipts['data']:forrinitem['statement']['receipts']:ifr['type']!=8515:continueamount=int(r['amount'])total+=amounttarget=r['targetAddress']iftarget==sink:label='Network sink (5%)'eliftarget==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}')continueprint(f' {label}: {amount/1e6:,.6f} XYM')# Summaryfees=total-rewardprint('\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')exceptExceptionaserror:print(error)
import{PublicKey}from'symbol-sdk';import{Address,Network}from'symbol-sdk/symbol';constfmt=(v)=>(v/1e6).toLocaleString('en-US',{minimumFractionDigits:6});constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log(`Using node ${NODE_URL}`);constBLOCK_HEIGHT=process.env.BLOCK_HEIGHT||'3222290';// Get the block headerconstblock=await(awaitfetch(`${NODE_URL}/blocks/${BLOCK_HEIGHT}`)).json();constsigner=Network.TESTNET.publicKeyToAddress(newPublicKey(block.block.signerPublicKey));constbeneficiary=block.block.beneficiaryAddress;console.log(`Block height: ${BLOCK_HEIGHT}`);console.log(`Signer: ${signer}`);constbeneficiaryB32=Address.fromDecodedAddressHexString(beneficiary);console.log(`Beneficiary: ${beneficiaryB32}`);// Get the network sink addressconstproperties=await(awaitfetch(`${NODE_URL}/network/properties`)).json();constsinkB32=properties.chain.harvestNetworkFeeSinkAddress;constsink=Array.from(newAddress(sinkB32).bytes).map(b=>b.toString(16).padStart(2,'0')).join('').toUpperCase();console.log(`Network sink: ${sinkB32}`);// Get the inflation reward at this heightconstinflation=await(awaitfetch(`${NODE_URL}/network/inflation/at/${BLOCK_HEIGHT}`)).json();constreward=parseInt(inflation.rewardAmount,10);console.log(`Inflation reward: ${fmt(reward)} XYM`);// Get harvest fee receipts for this blockconstreceipts=await(awaitfetch(`${NODE_URL}/statements/transaction`+`?height=${BLOCK_HEIGHT}&receiptType=8515`)).json();// Label and display the reward distributionlettotal=0;console.log('\nReward distribution:');for(constitemofreceipts.data){for(constrofitem.statement.receipts){if(r.type!==8515)continue;constamount=parseInt(r.amount,10);total+=amount;letlabel;if(r.targetAddress===sink){label='Network sink (5%)';}elseif(r.targetAddress===beneficiary){label='Beneficiary (25%)';}else{label='Harvester';constharvester=Address.fromDecodedAddressHexString(r.targetAddress);console.log(` ${label}: ${fmt(amount)} XYM`);console.log(` Harvester: ${harvester}`);continue;}console.log(` ${label}: ${fmt(amount)} XYM`);}}// Summaryconstfees=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`);
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.
# Get the block headerwithurllib.request.urlopen(f'{NODE_URL}/blocks/{BLOCK_HEIGHT}')asresponse: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 headerconstblock=await(awaitfetch(`${NODE_URL}/blocks/${BLOCK_HEIGHT}`)).json();constsigner=Network.TESTNET.publicKeyToAddress(newPublicKey(block.block.signerPublicKey));constbeneficiary=block.block.beneficiaryAddress;console.log(`Block height: ${BLOCK_HEIGHT}`);console.log(`Signer: ${signer}`);constbeneficiaryB32=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.
# Get the network sink addresswithurllib.request.urlopen(f'{NODE_URL}/network/properties')asresponse: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 addressconstproperties=await(awaitfetch(`${NODE_URL}/network/properties`)).json();constsinkB32=properties.chain.harvestNetworkFeeSinkAddress;constsink=Array.from(newAddress(sinkB32).bytes).map(b=>b.toString(16).padStart(2,'0')).join('').toUpperCase();console.log(`Network sink: ${sinkB32}`);
The harvestNetworkFeeSinkAddress is fetched from /network/propertiesGET 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.
# Get the inflation reward at this heightwithurllib.request.urlopen(f'{NODE_URL}/network/inflation'f'/at/{BLOCK_HEIGHT}')asresponse:inflation=json.loads(response.read())reward=int(inflation['rewardAmount'])print(f'Inflation reward: {reward/1e6:,.6f} XYM')
// Get the inflation reward at this heightconstinflation=await(awaitfetch(`${NODE_URL}/network/inflation/at/${BLOCK_HEIGHT}`)).json();constreward=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.
// Get harvest fee receipts for this blockconstreceipts=await(awaitfetch(`${NODE_URL}/statements/transaction`+`?height=${BLOCK_HEIGHT}&receiptType=8515`)).json();// Label and display the reward distributionlettotal=0;console.log('\nReward distribution:');for(constitemofreceipts.data){for(constrofitem.statement.receipts){if(r.type!==8515)continue;constamount=parseInt(r.amount,10);total+=amount;letlabel;if(r.targetAddress===sink){label='Network sink (5%)';}elseif(r.targetAddress===beneficiary){label='Beneficiary (25%)';}else{label='Harvester';constharvester=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/transactionGET 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).
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.
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.