Skip to content

feat(primitives): formally verified BMT inclusion proof + CI - #89

Closed
mfw78 wants to merge 4 commits into
mainfrom
verification/bmt-fstar
Closed

feat(primitives): formally verified BMT inclusion proof + CI#89
mfw78 wants to merge 4 commits into
mainfrom
verification/bmt-fstar

Conversation

@mfw78

@mfw78 mfw78 commented Jun 15, 2026

Copy link
Copy Markdown
Member

First slice of the formal-verification effort, targeting the BMT hash — the Swarm content-address kernel that every chunk hashes through. A divergence here is catastrophic (wrong addresses, failed proof-of-custody, self-fork off the network), and the logic is pure and finite, so it's the ideal first target.

What's in here

1. Machine-checked F* proofverification/bmt/Bmt.fst

  • Models the BMT inclusion proof exactly as nectar-primitives::bmt implements it and as the Book of Swarm specifies it (§2.2.2).
  • Proves completeness (an honestly generated proof for any leaf verifies against the genuine root) and soundness (the only segment that verifies at an index against the genuine root is the real leaf — a forged proof therefore exhibits a keccak collision).
  • Zero admits, including the elementary index-arithmetic helpers. The only assumptions are the two intended cryptographic axioms: keccak is abstract and collision-resistant.

2. Differential testcrates/primitives/src/bmt/spec_equivalence.rs

  • Pins the optimised Hasher (zero-tree rollup, all-zeros fast path, rayon) to a deliberately naive brute-force reference: 512 random (data, span) cases, every boundary size, the all-zeros path, and proof round-trips for all 128 segment indices with tamper-detection.

3. CI.github/workflows/verification.yml

  • fstar-proof: proves Bmt.fst (F* from the public nix binary cache, pinning the same version as local dev).
  • bmt-differential: runs the bmt::spec_equivalence tests.
  • Path-filtered to bmt/verification changes on PRs; always on main and in merge queues.

How the guarantees compose

  • F* proves the reference satisfies proof soundness/completeness.
  • The Rust test proves the production code equals the reference — so the optimised hasher inherits the proved properties and can be optimised further (SIMD, unsafe, new parallelism) as long as the test stays green.
  • The Book of Swarm validates that the reference matches the spec, including the LSB-first proof convention and the keccak(span_le ‖ bmt_root) address wrap.

Verifying locally

make -C verification/bmt verify        # F* proof (needs nix; no global install)
cargo test -p nectar-primitives --lib bmt::spec_equivalence

Next increments (see verification/bmt/README.md)

  1. OCaml oracle extracted from the model to replace the hand-written reference, closing the last trust gap.
  2. Span/length and single-owner-chunk (SOC) address semantics.
  3. File-level BMT so inclusion proofs compose across a whole file.

mfw78 added 3 commits June 15, 2026 05:27
…l test

Add the first slice of the formal-verification effort for the BMT hash, the
Swarm content-address kernel that every chunk hashes through.

- verification/bmt/Bmt.fst: F* model of the BMT inclusion proof, mirroring
  nectar-primitives::bmt and the Book of Swarm (§2.2.2). Proves completeness
  (honest proofs verify) and soundness (a forged segment that verifies implies a
  keccak collision) with no admits; the only assumptions are abstract keccak and
  its collision-resistance.
- crates/primitives/src/bmt/spec_equivalence.rs: differential test pinning the
  optimised Hasher (zero-tree rollup, all-zeros fast path, rayon) to a naive
  brute-force reference across random inputs, boundary sizes, the all-zeros path,
  and proof round-trips with tamper-detection.
- verification/bmt/{README.md,Makefile}: methodology, spec-to-code mapping, and
  `make verify` to re-run the proof via nix.

F* proves the reference is sound; the Rust test proves the production code equals
the reference; the Book of Swarm validates the reference matches the spec.
Add a verification workflow with two gates: an F* job that proves
verification/bmt/Bmt.fst (F* from the nix binary cache), and a Rust job that runs
the bmt::spec_equivalence differential tests. Path-filtered to bmt/verification
changes on PRs; always run on main and in merge queues.
#90)

Stacks on #89 — base branch is `verification/bmt-fstar`, because this
change uses the `bmt::spec_equivalence` differential test added there as
its correctness gate. Re-target to `main` after #89 merges.

## What

Performance optimization of the BMT hasher and inclusion-proof
generation. **Output hashes are byte-identical** — every change is gated
by the spec-equivalence differential test (optimised `Hasher` vs the
F\*-proven-sound brute-force reference: 512 random inputs, all boundary
sizes, the all-zeros path, all 128 proof indices + tamper detection).
26/26 bmt tests pass on both branches.

This is the synthesis of three parallel optimization explorations
(sequential / parallel / unsafe lenses); only changes that measured as a
real win were kept.

### `hasher.rs`
- `Keccak256::finalize()` already returns a `B256` — drop the redundant
`B256::from_slice(finalize().as_slice())` copy at every one of the ~127
nodes.
- Allocation-free iterative bottom-up sweep over a fixed `[B256; 64]`
stack array, collapsing levels in place with zero-subtree
short-circuiting (replaces the recursive sequential hash).
- Full-body (4096) path recurses with rayon only down to 512-byte
subtrees, then hands each off to the iterative sweep — ~8-way leaf
parallelism instead of a deep per-node `rayon::join`.
- Word-wide `is_all_zeros` (usize lanes via `align_to`).

### `proof.rs`
- `generate_proof` builds the 128 leaves into one `[B256; 128]` stack
array and collapses in place — **8 heap allocations per proof → 0**.
Fully safe: an `unsafe`/`get_unchecked` variant measured no faster
(proof time is keccak-dominated).

Discarded a flat `par_chunks` leaf strategy — it regressed the large
sizes versus divide-and-conquer `rayon::join`.

## Benchmarks

AMD Ryzen 9 7940HS (16 cores), `performance` governor, `cargo bench -p
nectar-primitives --bench bmt_bench`, 8 interleaved rounds with
alternating run-order. **Best-case (full-boost) medians** shown: on this
laptop AMD Core Performance Boost swings single-thread clocks ~1.7× with
thermal headroom, so the per-round minimum is the reproducible figure.

| Benchmark | Baseline | Optimised | Speedup |
|---|---|---|---|
| `bmt_hash/64` | 2.39 µs | 2.34 µs | 1.02× |
| `bmt_hash/128` | 2.75 µs | 2.68 µs | 1.03× |
| `bmt_hash/256` | 3.54 µs | 3.49 µs | 1.01× |
| `bmt_hash/512` | 5.61 µs | 5.53 µs | 1.01× |
| `bmt_hash/1024` | 9.89 µs | 9.70 µs | 1.02× |
| `bmt_hash/2048` | 19.07 µs | 18.57 µs | 1.03× |
| **`bmt_hash/4096`** (full chunk) | **45.15 µs** | **31.16 µs** |
**1.45×** |
| **`bmt_proof/generate`** | **118.3 µs** | **36.36 µs** | **3.25×** |
| `bmt_proof/verify` | 2.65 µs | 2.27 µs | 1.17× |

**Headlines:**
- **4096-byte hashing — every full chunk — is ~1.45× faster** (stable on
the median too, ~1.40×, since the all-core path saturates boost). Swarm
splits all data into 4096-byte chunks, so this is the dominant path.
- **Proof generation is ~3.25× faster** (zero heap allocations).
- Single-threaded partial-chunk sizes (64–2048) are at parity
(1.01–1.03×) — same keccak work, no regression at any size.

## Correctness

Output is byte-identical; the `bmt::spec_equivalence` differential test
(added in #89) is the gate — the optimised hasher is checked against the
brute-force reference that #89's F\* proof establishes is sound. `cargo
test -p nectar-primitives --lib bmt`: **26 passed, 0 failed**. clippy +
rustfmt clean.
@mfw78 mfw78 closed this Aug 21, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant