Snowblossom Docs
Docs › Protocol › Proof of work (Stoat)

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

header bits + nonceSkein-256-256 → context₀ index = H(context) mod wordsSkein, top byte zeroed, mod words read 16-byte wordrandom IO into the snow field context = H(context ‖ word)Skein-256-256 repeat 6 times snow_hash = context₆valid if < target Per attempt 1 header hash + 6 index hashes + 6 mix hashes = 13 Skein-256-256 (cheap) 6 dependent random reads (expensive) spread uniformly over 1 GiB … 2 TiB On a hit: build 6 merkle proofs (~16 KiB each) and submit the block / share.
Each of the six reads depends on the result of the previous one, so they cannot be issued in parallel or predicted. Constants: 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_field of 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 sets word_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

ConstantValueMeaning
POW_LOOK_PASSES6field reads per attempt
SNOW_MERKLE_HASH_ALGO / _LENSkein-256-128 / 16word size and merkle node size
BLOCKCHAIN_HASH_ALGO / _LENSkein-256-256 / 32context, block and tx hashes
NONCE_LENGTH12header nonce bytes
TARGET_LENGTH32header target bytes
MINE_CHUNK_SIZE1 GiBchunk 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.