Blocks & consensus
This page describes the blockchain itself: what a block contains, how its hash and merkle roots are computed, how the target adjusts after every block, how much a block pays and how nodes pick the best chain. The rules are implemented in lib/src/Validation.java, PowUtil.java, BlockchainUtil.java and ShardUtil.java; the proof-of-work function itself has its own page.
Units, supply and block reward
The unit is the SNOW; the protocol counts in flakes, with 1 SNOW = 1,000,000 flakes (Globals.SNOW_VALUE). Every value in a transaction, fee or coinbase is a 64-bit integer of flakes.
The base reward is computed by PowUtil.getBlockReward (“half every four years as per SIP-1”):
blocks_per_day = 86400 / 600 = 144
blocks_four_years = 144 × 365 × 4 = 210,240
reward(height) = 50 SNOW >> (height ÷ 210,240) // integer halving
| Era | Heights | Reward | Reached |
|---|---|---|---|
| 1 | 0 – 210,239 | 50 SNOW | 22 May 2018 (launch) |
| 2 | 210,240 – 420,479 | 25 SNOW | 6 May 2022 |
| 3 | 420,480 – 630,719 | 12.5 SNOW | 12 May 2026 (current) |
| 4 | 630,720 – 840,959 | 6.25 SNOW | ≈ 2030 |
| … | Integer halving reaches 0 flakes after 26 eras; total issuance converges to just under 21,024,000 SNOW. There is no explicit supply constant in the code - the cap follows from the schedule. Circulating supply today: ≈ 15.9 M SNOW. | ||
Since sharding (header version 2) the coinbase amount is computed by ShardUtil.getBlockReward, which divides the same reward among shards; for a single shard 0 it equals the base reward exactly. The coinbase must pay exactly reward + sum of fees - not less. See Sharding → rewards.
SIP-1 matters for history: at launch the halving interval was mistakenly one year; the vote to fix it to four years passed and was implemented in 1.1.2 before the first halving (see SIP-1).
The block header
A Block is a BlockHeader, a list of Transactions (the first is the coinbase) and, since version 2, a list of ImportedBlocks. The header fields:
| # | Field | Size | Meaning |
|---|---|---|---|
| 1 | version | int32 | 1 before block 211,600, 2 from then on (mainnet). Both required and forbidden on the wrong side of the activation height. |
| 2 | block_height | int32 | Previous height + 1; 0 for the genesis block. |
| 3 | prev_block_hash | 32 B | Hash of the previous block (all zeros for genesis). |
| 4 | merkle_root_hash | 32 B | Merkle root of the transaction hashes (below). |
| 5 | utxo_root_hash | 32 B | Root of the UTXO hashed trie after applying this block. This is what lets light clients verify balances - see the UTXO trie. |
| 6 | nonce | 12 B | Exactly 12 bytes, chosen freely by the miner. |
| 7 | timestamp | int64 | Milliseconds since the epoch. Must be strictly greater than the previous block's and at most 45 s in the future. |
| 8 | target | 32 B | Big-endian unsigned integer; the block hash must be below it. Not miner-chosen: it must equal the value the difficulty algorithm derives from the previous block and this timestamp, byte for byte. |
| 9 | snow_field | int32 | Index of the snow field used. Must be ≥ the field activated on the chain; larger is allowed. |
| 10 | snow_hash | 32 B | Output of the proof of work = the block hash, used everywhere a block is referenced. |
| 11 | pow_proof | 6 × | One SnowPowProof per PoW pass: the 16-byte word read plus its merkle path to the field root. |
| 12 | shard_id | int32 | v2: which shard this block belongs to (0 in v1). |
| 13 | shard_export_root_hash | map | v2: destination shard → root hash of a trie holding this block's outputs that target that shard. |
| 14 | shard_import | map | v2: shard → (height → block hash) of the foreign blocks this block imports. |
| 15 | tx_data_size_sum | int32 | v2: Σ over transactions of inner_data.size + 32; feeds the shard-split decision. |
| 16 | tx_count | int32 | v2: number of transactions including the coinbase. |
What gets hashed
PowUtil.hashHeaderBits feeds a Skein-256-256 digest with, in this order:
nonce(12 bytes)- 20 bytes big-endian:
version(4),block_height(4),timestamp(8),snow_field(4) prev_block_hash,merkle_root_hash,utxo_root_hash,target(32 bytes each)- if
version == 2:shard_id,tx_data_size_sum,tx_count(4 bytes each); then every export entry as shard id (4) + root (32); then every import entry, sorted by shard then height, as shard (4) + height (4) + hash (32).
The result is the starting context of the Stoat proof of work; six field lookups later the final context is snow_hash. Note that snow_hash and pow_proof are not themselves hashed - they are outputs.
Hash functions
| Purpose | Algorithm | Output |
|---|---|---|
| Block hash, PoW context, transaction id, transaction merkle tree | Skein-256-256 | 32 bytes |
| Snow-field merkle tree and PoW proofs | Skein-256-128 | 16 bytes |
| Address (hash of an AddressSpec) | Skein-256-160 | 20 bytes |
| UTXO trie and chain-index trie nodes | SHA-256 | 32 bytes |
| Snow field generator seed | Skein-1024-1024 | 128 bytes |
All are provided by BouncyCastle, registered once at start-up (Globals.addCryptoProvider).
Transactions in a block and the merkle root
- A block has at least one transaction. Transaction 0 must be the coinbase; all others must not be.
- The transaction id is
Skein-256-256(inner_data); signatures are outside the hashed bytes (see Transactions). DigestUtil.getMerkleRootForTxList: take the list of transaction ids; at each level hash adjacent pairs asSkein-256-256(left ‖ right); an odd trailing element is carried up unchanged (not duplicated as in Bitcoin). Repeat until one 32-byte value remains.- Coinbase rules:
is_coinbase, no inputs, no signatures,fee = 0,remarks ≤ 100bytes;coinbase_extras.block_heightand.shard_idmust equal the header's (this guarantees every coinbase is unique). Block 0's remark must start with the network's “block zero remark” - on mainnet the hash of Bitcoin block 523,850,00000000000000000019d1562bd02674302db7ddd6ccdb77be3e6daaa8eb8a51, which is how the launch was made fair: nobody could mine before that Bitcoin block existed. - The coinbase outputs must sum to exactly
reward + Σ fees. Miner pools split this among many outputs (direct payouts). - Coinbase extras also carry
motions_approved/motions_rejected- the vote signalling - and free-formremarks(pool names, messages). No consensus rule reads the motions.
Target, difficulty and work
The target is a 256-bit number. A block is valid when its hash, read as a big-endian unsigned number, is strictly less than the target. For humans the code shows a logarithmic difficulty:
target(diff) = 2^(256 − diff) // BlockchainUtil.getTargetForDiff
diff(target) = 256 − log2(target) // PowUtil.getDiffForTarget (display only)
expected hashes per block = 2^diff
So difficulty 38 needs on average 238 ≈ 2.7 × 1011 attempts, and +1 means twice the work. Mainnet's easiest allowed target is difficulty 25 (getMaxTarget); testnet starts at 22.
- Expected hashes per block
- -
- Network hash rate at that block time
- -
- Target (hex, leading bytes)
- -
Work and chain weight
Each block adds to the chain's cumulative work_sum (BlockchainUtil.getWorkForSummary):
work = maxTarget × 1024 / target // own block
+ Σ maxTarget × 1024 / import_target // every imported foreign block (v2)
work *= 4^activated_field // SIP-2 weighting
work_sum = prev.work_sum + work
The 4field factor (SIP-2) makes a chain that has activated a larger snow field always outweigh one that has not, which closes an attack where a miner keeps a private fork on the old field during a “snow storm”.
Difficulty adjustment
There is no retarget window. Every block's target is derived from two exponentially weighted running averages carried in the previous block's BlockSummary: blocktime_average_ms and target_average. With weight = 10 per mille on mainnet (100 on the small shard test networks) and decay = 1000 − weight, PowUtil.calcNextTarget computes:
delta_t = new_block.timestamp − prev.timestamp
averaged_delta_t = (prev.blocktime_average × decay + delta_t × weight) / 1000
scale = averaged_delta_t × 1000 / 600000 − 1000 // per-mille deviation from 10 min
scale = clamp(scale / 2, −500, +500) // at most ±50 % per block
new_target = prev.target_average + prev.target_average × scale / 1000
if the previous block triggers a shard split: new_target ×= 2 // children start at half difficulty
new_target = min(new_target, maxTarget)
Because the new block's own timestamp enters the formula, the target keeps easing while no block is found - the longer the wait, the easier the next block, which makes recovery from hash-rate drops smooth. Validators recompute the target from the previous summary and demand byte-exact equality with the header. After the block is accepted its summary updates the averages:
block_time = timestamp − prev.timestamp
blocktime_average_ms = (prev_average × decay + block_time × weight) / 1000
target_average = (prev_target_average × decay + target × weight) / 1000
The target average also decides snow field activation: when it drops to or below the next field's activation target, activated_field increments, permanently.
Timestamps
- A header's timestamp may be at most
maxClockSkewMs = 45,000 msin the future relative to the validating node's clock (“Block too far into future”). - It must be strictly greater than the previous block's timestamp.
- Nodes check their clock against
https://timecheck.snowblossom.org/timeevery 5 minutes and warn when off by more than 5 s. Keep NTP running on nodes and miners.
Validation, step by step
Two layers: checkBlockBasics needs nothing but the block and the network parameters; deepBlockValidation needs the database (previous summary, UTXO trie).
Stateless checks (checkBlockBasics)
- Version 1 or 2, matching the activation height; all hash fields 32 bytes, nonce 12, target 32.
- Timestamp not more than 45 s in the future.
snow_fieldknown to the network.- Exactly six PoW proofs, each a valid merkle path to the field's root; the recomputed six-pass chain reproduces
snow_hash;snow_hash < target. - v1:
shard_id = 0, no export/import maps. v2: export keys outside the shard's own cover set, import keys likewise,tx_data_size_sum > 0,tx_count > 0. - At least one transaction; transaction 0 is a valid coinbase, the others valid non-coinbase (stateless transaction checks); the merkle root matches.
Stateful checks (deepBlockValidation)
targetequalscalcNextTarget(prev_summary, timestamp)exactly.snow_field ≥ prev_summary.activated_field.- Height = previous + 1 and timestamp strictly increasing (genesis: zero prev hash and height 0).
- Coinbase extras match height and shard; block-0 remark rule.
- v2: shard rules - forced split, import ordering, chain continuity of imports, braid completeness within 6 blocks, no (shard, height) collisions; imported outputs are added to the UTXO view; a right-hand child shard starts from an empty UTXO.
- Every transaction is applied against the UTXO view: inputs must exist, be spendable (time/height locks), match declared values (SIP-4), use signature types allowed at this height (SIP-6); inputs = outputs + fee.
- v2: the computed export tries' roots equal
shard_export_root_hash;tx_data_size_sumandtx_countmatch. - Coinbase outputs =
ShardUtil.getBlockReward + Σ fees. - The resulting UTXO root equals
utxo_root_hash(UtxoUpdateBuffer.commitIfEqual- the trie is only written if it matches).
getMaxBlockSize() (3,800,000 bytes on mainnet, 8,000,000 on testnet) is used when building templates and to size gRPC messages (max block + 1,000,000 bytes), which is the practical ceiling. A single transaction is limited to 1,000,000 bytes by consensus.Chain selection and reorganisations
BlockchainUtil.isBetter(current, candidate) decides whether a newly validated block becomes the head:
- No head yet → the candidate wins.
- If the shard is not a coordinator and both are in the same shard: the one that imports the higher coordinator block wins (see the coordinator).
- Higher
work_sumwins. - Ties: the older timestamp, then the lexicographically smaller hash.
Reorganisations are cheap by design. The UTXO set is a persistent hashed trie: every validated block - even one on a side chain - gets its own summary and its own UTXO root, nothing is ever overwritten. Switching heads is a pointer change plus a rewrite of the height → hash index back to the fork point (BlockIngestor.updateHeights), after which the mempool is re-evaluated against the new UTXO root and the new tip is gossiped to peers. There are no checkpoints; the chain is anchored only by work (and the SIP-2 field weighting).
Block summaries
Alongside every block the node stores a BlockSummary (local, not part of the protocol): work_sum, blocktime_average_ms, target_average, activated_field, total_transactions, the root of the chain-index trie (address/tx indexes), and for v2 the per-shard imported_shards heads, tx_size_average, shard_length, and a short shard_history_map. Summaries have a summary_version (currently 6); a node started with older summaries recomputes them at start-up (“Reindexing”), which is the one-time rebuild mentioned in the 2.0 upgrade notes. The summary is written last, after the block, its transactions and the UTXO nodes, so its presence is the commit marker that a block is completely stored.
Genesis
There is no hard-coded genesis hash. Block 0 is any block with a zero previous hash, height 0 and the correct remark prefix; its target is the network maximum. Mainnet's block 0 is 00000023…b664b7, mined on 22 May 2018 on field 0 (“cricket”). A fresh network (regtest) simply starts by mining a block 0.