Deep Dive: Block Fields & Root Commitments

After Phases 1–4: how commitments differ from signatures, how each *_root is computed, and how blocks chain without one global tx Merkle. Next: Timers & chain time.

⏱️ ~35–50 min read 📊 Deep dive 🎯 Hyperscale-rs

Three layers (read every table with this in mind)

Commitments vs signatures (two different jobs)

Header *_root fields are commitments in the blockchain sense: one hash that binds the header to a specific collection (tx list, wave receipts, state trie, …). They answer “What is inside this package?” — not “Who approved it?” (that is digital signatures on votes). This is narrower than a ZK commitment scheme (hide-then-reveal); here the body is usually public and peers recompute the root.

Commitment (header root)Signature (vote / QC)
JobContent fingerprint — change one leaf, root changesProof that this validator key approved a specific message
Made withHash / Merkle root / JMT root (anyone can recompute from public data)Private key → BLS signature; peers verify with registered public key
AnalogySeal on the envelope whose pattern is determined by the letter insidePersonal stamp — proves who approved; still tied to a specific signed payload, not “anything in the box”
Hyperscale exampletransaction_root, certificate_root, state_rootBlockVote on block_hash → aggregated into QC

How verify works (BlockVote): rebuild the canonical signing bytes (DOMAIN_BLOCK_VOTE + network + shard + height + round + block_hash), look up the voter’s BLS public key from ValidatorId, run verify_bls(message, pubkey, signature) → accept or reject. The block_hash already digests the header (including all commitment roots), so the stamp is on this exact proposal package, not on each tx individually.

Verify ≠ re-execute: signature check only proves quorum attested to that block_hash. Separate steps recompute Merkle roots, replay certificates for state_root, and run execution rules.

Core four (most confused)

Name Layer Where Commits to / proves Purpose
state_root Execution Header JMT root after applying this block’s body.certificatesnot a Merkle over txs Execution meaning: “applying these certs yields this ledger.” Consensus attests the claim indirectly — BlockVotes sign block_hash, which digests the header including state_root.
transaction_root Consensus (inputs) Header Merkle root over tx hashes in body.transactions “Which user txs are on this page, in what order?” Does not prove execution results.
QuorumCertificate (QC) Consensus Child header as parent_qc; CommittedBlockHeader gossip 2f+1 block votes on one block_hash “Did a quorum agree this block is valid to chain?” Enables two-chain commit.
Transaction certificate (in FinalizedWave) Execution body.certificates Per-tx / per-wave execution outcomes (success/fail, receipt hashes) “What happened when we ran txs from earlier blocks?” Feeds the next block’s state_root.

Who attests what (don’t collapse the layers)

Three different “yes” messages — each with its own signed payload and aggregation story:

Question One validator signs Quorum becomes Layer
“Is this block package OK to chain?” BlockVote on block_hash QC Consensus
“Did we all run this wave the same way?” Execution vote on receipt / outcome root EC (execution certificate) Execution
“Is cross-shard proof present & verified?” StateProvision payloads + header/QC checks Tracked in ProvisionCoordinator (not one global cert) Cross-shard
QC ≠ FinalizedWave: QC proves quorum agreed on a block hash (ordering + header commitments). FinalizedWave in body.certificates proves execution finished for a wave — stapled on a block, often later than the block that first listed those txs. body.certificates is not a pile of QCs or provision blobs.

certificate_root sharp edge: leaves are each wave’s receipt_hash (outcome + event root), not wave ids or raw tx hashes. The header root is a summary; body.certificates carries the full FinalizedWave objects used to recompute state_root.

Teaching name vs code: course copy still says transaction certificate for “execution proof in the body.” In Rust that role is FinalizedWave (wraps WaveCertificate + receipts) — wave-granularity, not one cert per tx.

How each *_root is computed

Most Merkle roots share one recipe: pad leaves to the next power of two with Hash::ZERO, pair siblings, parent = Blake3(left_bytes || right_bytes) via Hash::from_parts, repeat until one hash remains. Order matters — swap leaves, different root.

transaction_root

Leaves = tx hashes in hash-ascending block order
  (sorted by TxHash value — NOT submit time)

0 txs  →  ZERO
1 tx   →  root = that tx_hash
2 txs  →  root = Blake3(hash1_bytes || hash2_bytes)
3 txs  →  pad [h1,h2,h3,ZERO] → pair → pair → root

Hash-ascending = strict increasing TxHash order so every validator builds the same leaf list. Proposer sorts before packaging; peers reject unsorted bodies.

state_root — JMT replay

state_root = JMT(parent_state_root, apply(body.certificates)). Replay each FinalizedWave’s receipts against the state trie — not a Merkle over txs. Worked example (block 40 → 41).

Other roots — same tree, different leaves

FieldLeavesEmpty
certificate_rootEach wave’s receipt_hashZERO
local_receipt_rootPer-tx consensus receipt hashesZERO
provision_rootProvisionHash per batch in bodyZERO
provision_tx_rootsPer remote shard: Merkle over tx hashes needing that shard’s proofEmpty map

state_root example — block 40 → block 41

Walkthrough on one shard: what raw material goes in, how many hashes run, and how JMT differs from transaction_root. Toy key paths use 4 bits for drawing; production JMT uses ~256 levels.

Timeline

BlockWhat happened
40 (committed)Body had TxA, TxB. transaction_root(40) = Merkle over their hashes. Execution ran after commit.
41 (proposed)Leader packages new txs TxC, TxD and staples FinalizedWave₁ (outcomes for block 40’s wave). Both roots computed at propose time, before votes.

Raw material for block 41

parent_state_root  = 0xaaa…   (from committed block 40 header)
parent_block_hash  = hash(block 40)
parent_qc          = QC₄₀

body.transactions  = [ TxC, TxD ]              ← NEW inputs (transaction_root)
body.certificates  = [ FinalizedWave₁ ]        ← execution DONE for block 40
  WaveCertificate  (quorum ECs for wave ∅)
  receipts:
    StoredReceipt(TxA) → Succeeded + database_updates
    StoredReceipt(TxB) → Failed (no writes)

Code path: build_proposal → prepare_block_commit(parent_state_root, certificates)

State deltas from certificates

Merge database_updates from each receipt in body.certificates (simplified toy keys):

TxResultWrites
TxASuccessK_alice balance 100 → 70; K_bob balance 50 → 80
TxBFailednone

k = 2 substate keys change. Real keys are (partition, sort_key) mapped into the JMT; same idea — sparse updates along key paths only.

transaction_root on block 41 (Merkle — same propose pass)

leaves (hash-ascending): [ hash(TxC), hash(TxD) ]

        ROOT_tx  (= transaction_root for block 41)
       /        \
  hash(TxC)    hash(TxD)

Blake3 hash ops: 1 pairing

JMT replay (toy 4-bit keys)

K_alice = 1010          K_bob = 1101

Before (parent_state_root = 0xaaa…):

              R₀ (0xaaa…)
             /   \
      (sparse — only occupied paths stored; rest = ∅ default hashes)

After apply FinalizedWave₁ (only Alice & Bob paths touched):

              R₁ (= state_root for block 41)
             /   \
           ...   ...
         path to 1010 → Alice = 70
         path to 1101 → Bob   = 80

You never materialize all leaf slots in a full tree. In this 4-bit toy, depth = 4 → only 2⁴ = 16 possible leaf positions — still only paths to 1010 and 1101 are stored. In production (~256-bit keys), depth ≈ 256 → 2²⁵⁶ possible paths — same sparse rule: touch only changed keys, ∅ elsewhere.

How many computations?

RootInput size (example)Hash work (order of magnitude)
transaction_root2 new txs1 Blake3 pairing
state_root (toy depth 4)2 key writes2 × 4 = 8 rehashes on touched paths
state_root (production depth ~256)2 key writes2 × 256 ≈ 512 rehashes (plus snapshot / cache I/O)
certificate_root1 FinalizedWave1 leaf → root equals that receipt_hash (or one Merkle level if padded with others)

Dominated by JMT replay, not tx Merkle — scales with # of writes × tree depth, not total accounts in the world.

Header package peers vote on

BlockHeader {
  height: 41
  parent_block_hash, parent_qc  (from block 40)

  transaction_root  = Merkle( hash(TxC), hash(TxD) )     ← inputs on THIS page
  state_root        = JMT(0xaaa…, apply FinalizedWave₁)  ← ledger AFTER certs
  certificate_root  = Merkle( receipt_hash(FinalizedWave₁) )
  waves             = [ … ]   ← wave ids for TxC/TxD run AFTER block 41 commits
}

Verify before BlockVote:
  1. Recompute transaction_root from body.transactions
  2. prepare_block_commit(parent_state_root, body.certificates) → state_root
  3. Match → sign block_hash

Pipeline (one picture)

Block 40 committed [TxA, TxB] → execute → FinalizedWave₁ ready

Block 41 proposal
  parent_state_root + body.certificates ──► prepare_block_commit ──► state_root
  body.transactions ──────────────────────► TransactionRoot::compute ──► transaction_root
                              │
                              ▼
                    BlockHeader → block_hash → BlockVote → QC
Same block, different roots: transaction_root(41) covers TxC, TxD (future execution). state_root(41) updates from TxA, TxB outcomes in FinalizedWave₁. Certs usually describe earlier txs; new txs get a state_root bump on a later block when their wave finalizes.

Per-block roots & lifecycle

Every *_root fingerprints this block only. History chains via parent_block_hash + parent_qc — not one global tx Merkle over mempool + proposals + finalized history.

Tx hash vs transaction_root

Three buckets (what lands in which root)

Bucket In a block yet? In transaction_root? In this block’s state_root?
Mempool — not proposed No No No
In proposed / committed block H Yes Yes — root(H) covers those tx hashes Usually no — execution runs after commit
Executed — outcomes in a later block Txs were in an earlier block That earlier block’s root Yes — via body.certificates on the block that applies them

Proposed but not committed

Leader packages body.transactions, computes roots, gossips proposal. Peers recompute roots before BlockVote. Until commit, not canonical — view change can swap tx set and transaction_root at the same height/round. Incremental packaging: when provisions or finalized waves land mid-height, the proposal latch can trigger a new proposal with updated provision_root / certs — not “one proposal forever.”

One transaction’s passport (single-shard path)

Follow one tx through layers — wall-clock submit time is irrelevant; block order is.

Mempool
  tx hash exists; no header root yet

Block H proposed & committed
  transaction_root(H) = Merkle(…, hash(tx), …)
  BlockVote → QC → commit
  state_root(H) usually unchanged for this tx (certs describe OLDER work)

After commit on H
  wave runs tx → Radix → execution votes → EC

Block H+k (later)
  body.certificates includes FinalizedWave for that wave
  certificate_root(H+k) = Merkle(receipt_hash, …)
  state_root(H+k) updates ledger — tx outcome now on the whiteboard

Cross-shard: same tx may need provisions from remote shards first; finalization can span more than one block (source commit → provisions → execute → cert on a later page). Explorer “one logical tx” ≠ always one block height on every shard.

BlockHeader fields & roots

Hover dotted terms for glossary cards. Block anatomy cards in Phases 2–4 use the same block-field-* entries.

Field Layer What it commits to One-line purpose
parent_qc Consensus QC for parent block Chains blocks: child carries proof parent was quorum-certified.
parent_block_hash Consensus Hash of parent block “This page continues that previous page” (identity link, not vote proof).
height / round Consensus Slot index / attempt at that height Page number on this shard; round bumps when leader stalls (timers).
certificate_root Execution (summary) Merkle over each FinalizedWave’s receipt_hash in body Fingerprint of which finished waves are listed — not the full proofs.
local_receipt_root Execution (detail) Per-tx receipt hashes for txs in wave certs Finer per-tx delta commitment; often ZERO if unused.
provision_root Cross-shard Merkle over ProvisionHash of each batch in body.provisions Which cross-shard proof packages are included on this page.
provision_tx_roots Cross-shard BoundedBTreeMap<ShardId, ProvisionTxRoot> — one Merkle per remote shard over tx hashes on this page that need that shard’s proof Per other team: which txs on our page still need their signed proof.
waves Execution Wave ids assigned at commit for in-flight execution Labels for grading bundles running after this page became official.
transaction_root Consensus Tx hash list in body Ordered membership of user txs on this block.
state_root Execution State after applying body certificates Whiteboard fingerprint after execution outcomes applied.

Block body (full objects behind the roots)

Body fieldPurpose
transactionsFull signed user txs; transaction_root commits to this list.
certificates (FinalizedWave)Full execution proofs for completed waves; used to recompute state_root.
provisionsFull cross-shard proof batches; provision_root commits to their hashes.

Votes → certificates (who signs what)

One validator signs… Aggregated into… Layer Purpose
BlockVote (“I accept this block”) QuorumCertificate Consensus Quorum agreement on block hash before/at commit.
Execution vote / StateVoteBlock ExecutionCertificate (EC) Execution Quorum agreement on wave execution outcome (receipt root) per shard.
(bundle) FinalizedWave Execution Wave done locally + remote shard coverage → stapled on a later block.
StateProvision / Provisions Verified by ProvisionCoordinator Cross-shard Provable substate from a committed remote block so this shard can execute.

Not one central issuer: every node collects votes and can build QC/EC locally; QCs mostly propagate via parent_qc on the next block. Per-shard height: each shard group advances its own height on commit — there is no single global “blockchain height” across all shards.

Cross-shard bookkeeping (not one HashMap)

ProvisionCoordinator is a per-validator pipeline of cooperating sub-machines — not a single “provision map”:

Header provision_tx_roots is the static obligation map on a proposed page (“which txs on this block still need shard X’s proof”). provision_root is the Merkle over actual provision batches included in body.provisions once satisfied. No 2PC coordinator — each shard runs BFT + provisions independently.

End-to-end flow (one shard)

Block proposed
  transaction_root  → commits to txs IN this block
  state_root        → claims state AFTER applying certs IN this block (from PRIOR execution)

BlockVote → QC → commit
  parent_qc on next block → proof this block was quorum-agreed

After commit
  txs → waves → Radix → execution votes → EC → FinalizedWave
  later block: body.certificates + certificate_root + updated state_root

Cross-shard extra
  provision_tx_roots → which txs need remote proof
  provisions → verify → body.provisions + provision_root
  then wave can dispatch & finalize

Common mix-ups

ConfusionReality
QC vs transaction certificateQC = block agreed; cert = execution result.
transaction_root vs state_rootInputs vs outcome of running prior work.
certificate_root vs body.certificatesHeader summary vs full proof objects.
provision_root vs provision_tx_rootsPackages included vs which txs still need remote proof.
Commit block H vs certs in block HCerts in H usually describe execution from earlier txs; state_root on H updates when those certs apply.
Commitment vs signatureRoots = what is in the package; votes/QC = who agreed to that block_hash.
One global tx Merkle over history + pendingNo — each block has its own transaction_root; mempool and in-flight execution stay outside header roots until packaged.
Signature verify aloneProves who attested to block_hash — separate steps recompute roots and replay execution.
certificate_root leavesreceipt_hash per FinalizedWave — not wave id, not tx hash list.
body.certificates contentsFinalizedWave execution proofs — not QCs, not raw provisions.
EC acronymExecution certificate — quorum on a wave outcome; different message/domain from BlockVote.
Global block heightPer shard group — compare heights only within the same shard.
Cross-shard “done in one block”Often N ≥ 1 blocks across shards (commit → provision → execute → cert).
ProvisionCoordinatorMulti-stage pipeline on each node — not one central map or 2PC coordinator.
Transaction certificate vs WaveCertificateTeaching = per-tx framing; code = WaveCertificate inside FinalizedWave.
Sim livelock.rs vs live ConflictDetectorPost-run diagnostics vs runtime overlap breaking (Phase 4).

Where to go deeper