Snowblossom Docs
Docs › Protocol › Sharding (the Braid)

Sharding (the Braid)

Since version 2.0 (“Wee Big Shards”, SIP-5, active on mainnet since block 211,600 in May 2022) the Snowblossom chain is not one chain but a binary tree of chains called the Braid. A shard splits deterministically into two children when its blocks have been large enough for long enough; the children keep importing each other's blocks, value moves between them through committed root hashes, and the total reward per height stays the same however many shards exist. On mainnet only shard 0 is active so far; the public testnet has split into shards 1 and 2 and demonstrates the mechanism.

Shard ids

0 12 3456 7891011121314 children of p: 2p+1 (left), 2p+2 (right) · parent of c: (c−1)/2 if odd, (c−2)/2 if even highlighted: the coordinator lineage 0, 1, 3, 7, …
Ids are fixed by position. Mainnet allows ids 0–62 (getMaxShardId = 62): up to 32 leaf shards. Testnet allows 0–512, the shard test networks 0–6 or 0–30.

Helper functions in ShardUtil:

generation(s)
depth in the tree: 0 for shard 0, 1 for shards 1–2, 2 for 3–6, …
inherit set(s)
{s} plus, while the id is odd, its parent: inherit(1) = {0,1}, inherit(2) = {2}, inherit(3) = {0,1,3}, inherit(5) = {2,5}. The left (odd) child inherits everything that belonged to its parent.
cover set(s)
inherit(s) ∪ all descendants of s up to the max id. Outputs whose target_shard is in the cover set live in shard s's UTXO. cover(0) is everything; cover(1) = {0,1,3,4,7…10,15…22,31…46}; cover(2) = {2,5,6,11…14,23…30,47…62}.
coordinator
a shard whose inherit set contains 0: 0, 1, 3, 7, 15, 31, 63, … (2k−1). Exactly one coordinator is active at any time: the left-most leaf.

When a shard splits

ShardUtil.shardSplit(summary) is true - meaning “this block is the last of its shard and the next blocks must be in the two children” - when all of:

  1. the header is version 2;
  2. tx_size_average > getShardForkThreshold() - mainnet 1,900,000 bytes (half the 3.8 MB max block), testnet 4,000,000;
  3. shard_length ≥ getMinShardLength() = 144 blocks (the shard has existed for a day);
  4. the right child id ≤ max shard id (the shard is not a leaf).

tx_size_average is an exponential moving average (1 % weight per block on mainnet) of Σ(inner_data.size + 32) over the block's transactions, reset to 0 when a shard is born; shard_length counts blocks since the split. The decision depends only on the parent's last block summary, so it is deterministic and enforced: a block after a split-triggering block must carry a child id (“Must split”), and otherwise must keep the parent's id. Shards never merge - there is no code path for it.

Mainnet statusShard 0's average transaction bytes per block are currently ≈ 100 bytes against a threshold of 1,900,000 - a split is nowhere near. The explorer's shard page shows both ratios live. The testnet (“teapot”, threshold 4 MB, same 144-block minimum) was load-tested into splitting at height 170,459 and now runs shards 1 and 2.

The first blocks of the children

  • Both children continue the parent's height numbering (height = parent_last + 1) and both point at the parent's last block with prev_block_hash. Their coinbase carries the child's shard_id.
  • UTXO: the left child (odd id) inherits the parent's entire UTXO root; the right child (even id) starts from an empty trie. Coins do not move at a split - outputs targeted at an ancestor shard are now spendable in the left lineage, and the right lineage fills with coinbases and outputs explicitly targeted at it.
  • Difficulty: both children start at half the parent's difficulty (target × 2, the target average is doubled too); each then adjusts independently toward a 10-minute block time, so N leaf shards produce about N blocks per 10 minutes.
  • Averages: tx_size_average and shard_length restart at 0; block-time average carries over.

Keeping the braid together

Every v2 block imports the recent blocks of every other active shard: shard_import in the header lists them (shard → height → hash) and Block.imported_blocks carries their headers and exported outputs. Validation.checkShardBasics enforces:

  • Ordering: imported_blocks must be exactly the header's import map flattened in ascending (shard, height) order, each header matching.
  • Validity: every imported header passes the full header checks including its proof of work.
  • Continuity (“shard friends”): starting from the previous summary's known head per shard, each imported block must chain from that shard's current head (or, for a shard not yet seen, from its parent shard's head - this is how a new child is first accepted), height + 1, correct prev_block_hash. A shard cannot be imported while one of its children is already known.
  • Braid completeness: recursively from shard 0, every shard must either have both children covered or be present with build_height − its_height ≤ getMaxShardSkewHeight() = 6. In words: when you build block 1000, every other active shard must be imported at least up to height 994 (“Incomplete or old braid” otherwise).
  • Collisions: across the previous block's 30-entry history map, this header, and all imported headers and their own import maps, a (shard, height) pair must map to a single hash (“Block collision”). All shards are thereby forced onto one consistent braid.
  • A shard's own cover set may appear neither in its export map nor in its import map.

The coordinator and “the Dance”

Cross-shard imports could conflict: shard 2 might import shard 5's block X while shard 1 imports a competing X′. The braid resolves this by giving one shard - the coordinator, the active shard on the 0-1-3-7 lineage - the role of ordering. Rules (in Dancer and BlockchainUtil.isBetter):

  • A coordinator block may only import blocks that are themselves compliant.
  • A non-coordinator block is compliant only if the highest coordinator block it sees is compliant and every block it imports lies in the chain of what that coordinator has already imported for that shard - non-coordinators follow the coordinator's choices.
  • Head selection in a non-coordinator shard prefers the candidate that imports the higher coordinator block before looking at work.

Compliance is a block-building heuristic; the consensus rules are the ones in the previous section. Nodes build templates through ShardBlockForge, which every 15 s explores “block concepts” - for each shard, candidate next blocks importing the newest compliant foreign blocks - and prefers concepts that advance the shortest shard (BlockTemplate.advances_shard). MrPlow asks several nodes for templates and picks the one with the best reward per hash (see MrPlow).

Cross-shard transfers

Shard 1 - block 5000 tx: output{value, recipient, target_shard = 2} → not in cover(1): goes to export trie for 2 header.shard_export_root_hash[2] = root of trie{key(recipient, tx_id, idx) → raw output} the output is NOT in shard 1's UTXO imported within ≤ 6 blocks Shard 2 - block 5003 header.shard_import[1][5000] = hash imported_blocks: header + import_outputs[2] check: recomputed root == export root → outputs inserted into shard 2's UTXO spendable here with the original tx_id/idx
An output declares where it wants to live. The source block commits to it by root hash; the destination imports the block and verifies the outputs against that root.
  • A transaction spends inputs from one shard only (they must be in the building shard's UTXO) and may send outputs anywhere via target_shard. Fees stay with the shard that mined the transaction.
  • When the destination is in the shard's own cover set the output goes straight into its UTXO; otherwise it is collected into a small in-memory trie per target shard whose root becomes shard_export_root_hash[target]. Validators recompute these tries and compare roots.
  • The importing block carries ImportedBlock{header, import_outputs{shard → [raw_output, tx_id, out_idx]}}; for every shard in its cover set for which the source header has an export root, the list must be present, must hash to that root and every output's target_shard must match. Then the outputs are added to the importer's UTXO under their original keys.
  • Target does not exist yet (e.g. target_shard = 7 while only shard 0 exists): 7 is in cover(0), so the output simply lives in shard 0 - and later in 1, then 3 - until 7 is real. Existing UTXOs are never migrated.
  • Target is an ancestor (target_shard = 0 after the split - what every ordinary wallet produces): 0 is in inherit(1) but not in cover(2), so a shard-2 block exports it and the coordinator lineage imports it. Wallets that never set a target therefore keep all their coins on the 0-1-3-7 lineage, which is why today's wallets keep working unchanged after a split.
  • Node feature gap: the wallet cannot yet assemble a payment from inputs spread over several shards (“Split outputs required but not implemented”); balances are aggregated across shards, but a single send must be covered by one shard.

Rewards across shards

ShardUtil.getBlockReward divides the base reward R(height) so that the total per height never changes (math in 128-bit fixed point, rounding once). For a block in a shard of generation g:

direct   = (3/4) · R(h) / 2^g
indirect = (1/4) · R(h) / 4^g
imports  = Σ over imported blocks (shard i at height h_i):  (1/4) · R(h_i) / 2^(g + generation(i))
coinbase = direct + indirect + imports + fees

Checks: one shard (g = 0, nothing to import) gets ¾R + ¼R = R. Two shards importing each other at the same height get ⅜R + 1/16R + 1/16R = ½R each. Four leaves get ¼R each. Unbalanced trees work out because each block of generation g carries weight 2−g (ShardUtilTest verifies the grand total equals ΣR). The quarter that is paid through imports is the incentive to import other shards promptly. Per-block work likewise includes the work of imported blocks.

Node operation with shards

  • shards=3,4 in node.conf restricts a node to those shards; its interest set is each listed shard's cover set plus all of their ancestors (needed to find the entry point). Without the setting a node follows shard 0 and everything below it - the whole network - so no configuration change was required at activation.
  • The node opens a BlockIngestor + mempool per shard it has a head for, and opens children automatically when it validates a splitting block. A shard stops being a “building” shard once both children are active.
  • NodeStatus exposes network_active_shards, net_shard_head_map (network-wide heads), shard_head_map (local heads of building shards) and interest_shards. head_summary and GetFeeEstimate.fee_per_byte still refer to shard 0; use the maps after a split. GetTransactionStatus only consults shard 0's index. GetUTXONodeMulti returns the UTXO of every building shard.
  • Peers advertise their interest set in PeerInfo.shard_id_set; the node tries to keep a few connections per shard (interest_peer_count, default 4) and gossips a tip per shard.
  • A node that does not validate some shard still needs that shard's ImportedBlocks to validate its own imports. It accepts them only if the block hash was announced in a tip signed by a configured trustnet_signers key - the trust network. A trustless replacement (UTXO proofs of foreign blocks from their headers) is sketched in shard-notes.md but not implemented; tx_data_size_sum was added to the header so header-only nodes can at least recompute split decisions.

Quick reference

ParameterMainnetTestnettestshard / demoshard / regshard
Activation height (header v2)211,600 (15 May 2022)170,00010
Max shard id62 (32 leaves)51230 / 6 / 6
Fork threshold (EMA tx bytes)1,900,0004,000,0002,000 / 2,000 / 0
Min shard length144 blocks14410
Max skew6 blocks66
Average weight (per mille)1010100