Proof of work (Stoat)
Snowblossom's proof of work is called Stoat - “because stoats hop around a snow field doing things”. Instead of hashing a header millions of times per second, each attempt must read six 16-byte words from a huge deterministic file, each read depending on the previous one. The cost of an attempt is therefore six random storage reads, and the hash rate of any machine is bounded by the IOPS of whatever holds the field. Nodes verify a block without owning the field, using merkle proofs against a known root.
The algorithm
POW_LOOK_PASSES = 6, word size 16 bytes.In code (PowUtil, Validation.checkBlockBasics):
context = Skein-256-256( headerBits(header, nonce) ) // see Blocks → "What gets hashed"
word_count = field_length / 16
repeat 6 times:
h = Skein-256-256(context)
h[0] = 0 // non-negative 56-bit value
idx = bigEndianInt64(h[0..8)) mod word_count // getNextSnowFieldIndex
word = snowfield[idx*16 .. idx*16+16)
context = Skein-256-256(context ‖ word) // getNextContext
snow_hash = context
valid iff snow_hash < target (unsigned, strictly)
The comparison (lessThanTarget) fast-rejects on the first eight bytes, then compares all 32 bytes lexicographically. Pool miners compare against an easier share target instead.
Proofs: how a node verifies without the field
Every snow field has a merkle tree over its 16-byte words: leaves are the raw words, internal nodes are Skein-256-128(left ‖ right), the tree is a perfect binary tree (field sizes are powers of two) and its 16-byte root is fixed in the network parameters. The header carries six SnowPowProofs:
message SnowPowProof {
int64 word_idx = 1; // index of the word in the field
repeated bytes merkle_component = 2; // [0] = the 16-byte word itself, then sibling hashes leaf → root
}
A proof for a 1 GiB field (226 words) has 27 components; for “avanc” (2 TiB, 237 words) 38. Six proofs add roughly 2.6–3.6 KB to a header.
Validation.checkProof folds each path back up: starting from the word, it repeatedly doubles the span, aligns the start, and hashes the current value with the popped sibling on the correct side (idx < mid → sibling on the right), finally requiring that the span is exactly [0, word_count) - so a miner cannot pass off an inner node as the root - and that the result equals the field's published root. Then the node replays the six passes: for each proof it recomputes the expected index from the context (“POW Pass index does not match” otherwise), mixes in merkle_component[0], and at the end checks snow_hash and the target. Cost for the verifier: 13 Skein-256-256 and about 6 × (1 + log2 words) Skein-256-128 hashes - microseconds, no disk. The node never opens a snow file.
Generating proofs on the miner
Recomputing a merkle path naively would mean hashing the whole field. Miners instead keep deck files - precomputed internal nodes at every 1024k-word level. SnowMerkleProof.getInnerProof recurses over [start, end): when the target word is outside a sub-range whose size is a deck level, it reads the single precomputed hash from that deck; when inside, it descends and records the sibling hash of the half it did not take. With decks, one proof costs about 16 KiB of contiguous reads at each level (the 1024 words around the target, ~1023 deck.a entries, ~1023 deck.b entries, …). Proofs are only built when an attempt beats the (share) target, so they never slow the hot loop.
Why it is IO-bound and ASIC-resistant
- Six dependent random reads per attempt. Each index is derived from the previous word's content, so reads are serialised and uniformly spread over the whole field. Attempts per second ≈ random-read IOPS ÷ 6 whatever the arithmetic speed - and every 16-byte read costs a full device block (4 KiB or more).
- The field cannot be compressed, predicted or regenerated on demand. Generation (see SnowFall) mixes 256 MiB of generator history back into the stream and rewrites every page seven times in random order, so reproducing a single page requires the whole file.
- The field grows with the network. Each +2 bits of average difficulty activates a field twice as large (1 GiB → 2 TiB over 12 fields), keeping the working set ahead of what is cheap to put on-chip or in fast RAM. Mainnet is on field 9 (512 GiB “hippo”); the next, 1 TiB “shai-hulud”, activates at difficulty 45.
- Verification is cheap. Full nodes, wallets and pools validate PoW from proofs alone, so the cost is borne entirely by miners and the network stays easy to run.
In practice this means: hard disks mine at a few hundred attempts per second, NVMe at roughly IOPS/6, and a field held in RAM at CPU hashing speed - which is why the miners offer memfield, partial RAM caching with depth cut-offs, the distributed Arktika layers and the sequential-wave SurfMiner (see Mining). GPU mining brings nothing: there is no arithmetic to parallelise, only memory latency.
Which field a block may use
- The chain state carries
activated_field. It starts at 0 and increments when the running average target (target_average) reaches the next field's activation target - i.e. when the average difficulty reaches 25, 27, 29, … 47. It never decreases, even if difficulty later falls. The moment of activation is called a snow storm. - A block must use
snow_field ≥ activated_fieldof the previous block. Using a larger field than required is allowed (deepBlockValidation), which lets miners switch to the next field ahead of time if they lack space for both. Unknown field indexes are rejected. - Block templates from a node always specify exactly the activated field; miners substitute the smallest field they have locally that is at least that (
FieldScan.selectField). Since the field index is hashed into the header and setsword_count, the choice is committed by the proof of work. - Pools reject shares whose field is lower than the template's (“Too low of snow field”).
- SIP-2: block work is multiplied by 4activated_field, so after a snow storm a chain on the new field outweighs any chain lingering on the old one.
Constants
| Constant | Value | Meaning |
|---|---|---|
POW_LOOK_PASSES | 6 | field reads per attempt |
SNOW_MERKLE_HASH_ALGO / _LEN | Skein-256-128 / 16 | word size and merkle node size |
BLOCKCHAIN_HASH_ALGO / _LEN | Skein-256-256 / 32 | context, block and tx hashes |
NONCE_LENGTH | 12 | header nonce bytes |
TARGET_LENGTH | 32 | header target bytes |
MINE_CHUNK_SIZE | 1 GiB | chunk unit for Arktika / split fields |
Worked example
Take mainnet block 420,480: field 9 (512 GiB = 235 words), target 0000000004584… (difficulty ≈ 37.9), nonce 382cf30a7c067331237a407c. The miner hashed the header bits with that nonce, derived an index in [0, 2^35), read that word from snowblossom.9.snow, mixed, and repeated five more times; the final context starts with nine zero bytes, below the target. The header carries six proofs of 36 components each (1 word + 35 siblings) - 3,456 bytes - which is most of the block's 4,295 bytes. Anyone can verify it against root ecdb28f1912e2266a71e71de601117d2 without owning the 512 GiB file.