Skip to content

Fix verify_proof host functions for Substrate compact proofs#2250

Closed
x3c41a wants to merge 1 commit into
paritytech:mainfrom
x3c41a:ndk/verify-proof-compact
Closed

Fix verify_proof host functions for Substrate compact proofs#2250
x3c41a wants to merge 1 commit into
paritytech:mainfrom
x3c41a:ndk/verify-proof-compact

Conversation

@x3c41a

@x3c41a x3c41a commented May 19, 2026

Copy link
Copy Markdown
Contributor

Fixes the implementation of ext_trie_blake2_256_verify_proof_version_1 and _version_2 added in #2221. That PR used proof_decode::decode_and_verify_proof, which expects regular Merkle proofs where every node is fully encoded and looked up by its blake2b hash. Substrate generates compact proofs via sp_trie::generate_trie_proof (trie-db's generate_proof): along the proven path, child references are replaced by an empty inline placeholder and the actual child nodes follow as the next proof entries; the target leaf's value is also omitted and reconstructed from the caller-supplied expected value (hashed if state version V1 and >= 32 bytes).

What this PR does

Adds verify_compact_trie_proof in trie::proof_decode and wires the two host functions to it. The verifier:

  1. SCALE-decodes the proof into a stream of node entries.
  2. Reads the first entry as the root node; decodes it.
  3. Matches the partial key prefix, then descends one nibble at a time:
    • If the child reference is an empty inline placeholder, consumes the next proof entry as that child's node and recurses.
    • If the child reference is an inline non-empty node, decodes it directly without consuming a proof entry.
    • If the child reference is a 32-byte hash, returns false (descending into a non-path child means the proof is incomplete for this key).
  4. At the target node, injects the caller-supplied value (as Unhashed or Hashed(blake2b(value)) based on state version and value length).
  5. Re-encodes the node with the reconstructed children/value and computes its merkle value.
  6. Returns true iff the final root node's hash equals the caller-supplied root.

Tests

Seven unit tests in trie::proof_decode::tests cover the verifier using proof bytes captured live from paseo-bulletin-rpc.polkadot.io block #1503870 (a 256-byte chunk-trie proof from pallet-transaction-storage's apply_block_inherents):

  • verify_compact_proof_accepts_valid_substrate_proof — positive case.
  • verify_compact_proof_rejects_wrong_root — flipping a bit in the root.
  • verify_compact_proof_rejects_wrong_value — flipping a bit in the value.
  • verify_compact_proof_rejects_wrong_key — different key.
  • verify_compact_proof_rejects_wrong_state_version — V0 instead of V1.
  • verify_compact_proof_rejects_tampered_proof_byte — corrupted byte inside the root node entry.
  • verify_compact_proof_rejects_truncated_proof — missing the leaf entry.

End-to-end, replaying five real ProofChecked-emitting blocks (1503870, 1504069, 1504256, 1504290, 1504307, 1504375) through chopsticks run-block against paseo-bulletin now completes finalize_block without panicking on the Storage proof must be checked once in the block assertion. Previously, with #2221's implementation, all of them panicked.

Ref: paritytech/polkadot-bulletin-chain#341

The implementation added in paritytech#2221 used trie::proof_decode::decode_and_verify_proof,
which expects regular Merkle proofs where every node is fully encoded and looked
up by its blake2b hash. Substrate generates compact proofs via
sp_trie::generate_trie_proof (trie-db's generate_proof): along the proven path,
child references are replaced by an empty inline placeholder and the actual child
nodes follow as the next proof entries. At the target leaf the value is also
omitted and reconstructed from the caller-supplied expected_value (hashed if
state version V1 and >= 32 bytes).

This commit adds verify_compact_trie_proof in trie::proof_decode and wires
ext_trie_blake2_256_verify_proof_version_{1,2} to it. The verifier walks proof
entries in order, descends into placeholders by consuming the next entry, and
unwinds by re-encoding each node with reconstructed children. The final root
node's blake2b hash must equal the expected root.

Verified against paseo-bulletin-rpc.polkadot.io: replaying five blocks that
emit transactionStorage.ProofChecked (1503870, 1504069, 1504256, 1504290,
1504307, 1504375) through chopsticks run-block now completes finalize_block
without panicking on the "Storage proof must be checked once in the block"
assertion.
@x3c41a

x3c41a commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

Closing — re-opening against the active upstream paritytech/smoldot, which is where new development happens. This repo (smoldot-archive) is the older fork and chopsticks pulls from it via the smol-dot/smoldot redirect, but the host function fix should land in the active fork first.

@x3c41a x3c41a closed this May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant