A complete, dependency-free Go implementation of the Merkle-Forest SPV design in
this folder (01_ARCHITECTURE.md, 02_MODULE_SPECS.md, 03_SCALING_MODEL.md,
04_TEST_PLAN.md, PAPER.md).
BSV only. No BTC parameter, code path, or assumption appears anywhere. The module has zero external dependencies — it builds and tests entirely offline, including the secp256k1 signer.
A five-level Merkle commitment hierarchy that reuses Teranode's existing subtree structure, ships inclusion proofs sender-pushed with the payment, and freezes the historical proof at block-sealing:
L0 field -> MTxID = TXID (Merkle tree over tx FIELDS; US 2022/0216997)
L1 TXID -> subtree root (Teranode subtree, <= 2^20 TXIDs)
L2 subtree root -> block Merkle root (built over subtree roots)
L3 block root -> 80-byte header (PoW-sealed) -- core ends here, no consensus change
L4 header -> off-header accumulator (MMR root in the generation tx) -- OPTIONAL
Double-spend is kept orthogonal to the proof: a valid inclusion proof is a fail-fast against spam/error, never double-spend protection. Acceptance at the point of sale combines inclusion (local) with a live UTXO query, a PoW-attested alert layer, and a merchant risk parameter τ.
| Package | Design ref | Role |
|---|---|---|
commitment |
§commitment / T1 | SHA-256d Merkle core; L0–L2 build & verify; depth law |
crypto |
(spend authorisation) | self-contained secp256k1 ECDSA, RFC 6979, low-S |
accumulator |
§accumulator / T2 | L4 MMR over headers; VerifyAnchor; absent-period honesty |
teranode |
§teranode_adapter / T6 | read-only ProofSource/HeaderChain/UTXOClient + in-memory MockNode |
bundle |
§bundle / T3 | the sender-held proof object; Build/Serialize/Verify/Reanchor |
payment |
§3.2 | Tx3, sighash, the Alice→Bob push message |
walletalice |
§wallet_alice / T4 | offline customer wallet (Sign/FillTemplate/Export) |
walletbob |
§wallet_bob / T4,T5 | point-of-sale till (AcceptPayment/Broadcast, RiskPolicy) |
dsalert |
§dsalert / T5 | evidence-gated, PoW-attested double-spend alert layer |
bench |
§bench / T7 | scaling simulator reproducing the derived table |
cmd/mfspv |
— | demonstration runner |
adversarial |
04_TEST_PLAN A1–A5 | consolidated red-team rejections |
go test ./... # 75 tests: T1–T7, A1–A5, RT-1..7, KAT, differential, property, Monte-Carlo, value
go test -shuffle=on ./... # order-independent (06_EVALUATION_DESIGN §3.4)
go run ./cmd/mfspv # scaling table + capacity + one full push payment
go run ./cmd/mfspv -eval # emit environment.json + claims.csv, tagged M/D/S (06 §8 reproduce)
go run ./cmd/mutate # offline mutation-testing gate: kills 17/17 security-critical mutants (06 §4.4/§10)
go run ./cmd/mfspv -fabric # 07 verification-fabric throughput benchmark (bar A>=1.5)Verification throughput (07_VERIFICATION_FABRIC.md) — COMPLETE real pipeline.
go run ./scalebench measures the whole SPV inclusion path end to end: decode a
real proof from its wire bytes, then verify it, with a zero-allocation streaming
verifier and shared-node amortisation (Lever B). Measured on a 64-core Xeon: 6.71×10⁷
verifications/second (A = 6.71), and the rate is independent of depth — 10⁶ through
10¹¹ tx/s all measure A ≈ 6.71, 4.5× the 1.5×10⁷ minimum (larger batches reach A ≈ 13).
Hashing is not the bottleneck; the per-proof cost is decoding a proof that grows only
logarithmically (960→1472 B over 10⁶→10¹¹), so throughput does not fall as tx/s rises.
Stateless and shares-nothing → aggregate scales ~linearly with cores/nodes. No consensus
change. Depth-independence holds at 10× the target (depth 460: A ≈ 3.5). Per 07 §5, the
inclusion leaf is the consensus TXID (VerifyToBlockRoot with leaf=txid, l0=nil); the
MTxID field tree is an optional secondary commitment.
Full SPV protocol is tested, both acquisition modes:
- Push (
walletbob.TestE2E_SPV_PushToNodes): Alice→Bob signed Tx + inclusion path → Bob verifies locally (path and signature, unspent, value) → Alice and Bob push the Tx to 2–3 nodes → nodes re-validate; tampered Tx rejected. - Pull / proof acquisition (
fabric.Fetch,fabric.TestFetchInclusion_SPVPull): Bob has a new TXID and asks a node for the Merkle path + the block header (ProofSource), assembles the inclusion proof, and verifies it against his most-work header chain. Unknown tx errors; tampered path or off-chain header is rejected. This is the classic "get the Merkle proof from a node" step.
Honest ceiling: a complete payment is path-verify plus one ECDSA signature verify. The signature — not hashing, not the path — is the real per-payment ceiling. The in-repo secp256k1 verifier is a correct reference (unoptimised); production uses the node's audited libsecp256k1 + batch verification + scale-out. Complete signed SPV is bounded by signature verification (a standard, scalable cost), with the MF-SPV inclusion path removed from the critical path.
Mutation testing (06 §4.4 acceptance gate). go run ./cmd/mutate is a
zero-dependency, offline gate that flips each security-critical comparison/boolean
(Fold, Verify*, VerifyAnchor, VerifyBlockInChain, low-S/onCurve, the RT-1
carrier gate, the alert evidence gate, Decide, value-conservation), runs the
suite, and asserts every mutant is killed — currently 17/17, score 1.000. The
external gremlins path (.gremlins.yaml, needs network to install) is also
provided.
The evaluation follows 06_EVALUATION_DESIGN.md (publication-grade, ACM-artifact
standard): known-answer tests against published vectors (double-SHA256, the secp256k1
2G constant, the RFC 6979 nonce), a differential Merkle oracle over odd cardinalities,
property tests at 10⁵ cases, Monte-Carlo inclusion-forgery (10⁶ trials, 0 accepted,
Clopper–Pearson p_upper ≤ 4.6×10⁻⁶), and a scaling-law regression that statistically
rejects linear-in-T (R²(log)=0.999 vs R²(T)=0.40). claims.csv maps every claim ID to
its falsifying test.
Demonstration output (abridged):
r T=r*600 depth proofB subtrees L1 L2 push pull
1e+06 600000000 30 960 573 20 10 0 960
1e+10 6000000000000 43 1376 5722046 20 23 0 1376
1e+11 60000000000000 46 1472 57220459 20 26 0 1472 <- 100 billion tx/s
Header dataset: 4204800 bytes/year (~4.2 MB/yr), constant in r.
== Running at 100 billion tx/s (1e11) — capacity ==
This machine: 3.46 M SHA-256d/s/core (software path).
r=1e+11 seal: 2.00e+11 hashes/s, ~57829 cores @sw / ~2000 cores @SHA-NI | edge proof: 1472 B
Edge @1e11 (depth 46, 1472 B): 81920 verifies/s/core, 450560 verifies/s on 8 cores (stateless, linear).
Bob decision: accepted=true ...
After double-spend: accepted=false reason="double-spend:utxo-spent" (inclusion still true)
At 10¹¹ tx/s the inclusion proof is depth 46 / 1472 bytes — just 3 hashes
(96 B) more than 10¹⁰, and 46 ≪ 255 (the one-byte depth ceiling). 10¹² is
headroom at depth 50 / 1600 B. The verifier's cost is logarithmic and decoupled
from throughput by construction, so there is no SPV-side ceiling.
"Running at that level" means two measurable things, both delivered:
- Edge (verification fabric): per-payment verification is ~13.7 µs (depth-46
fold) and stateless / embarrassingly parallel — aggregate capacity scales
linearly with cores (
bench.VerifyThroughput,ProfileEdge). 100 billion tx/s does not raise per-payment cost. - Sealing (Teranode's sharded job): building the Merkle forest for 10¹¹ tx/s
needs ~2×10¹¹ SHA-256d/s network-wide ≈ ~2,000 SHA-NI cores across
~95,367 subtrees/s — derived, not asserted, by
bench.PlanCapacityfrom a measured hash rate. This is exactly Teranode's horizontal-scale model;ris bounded by Teranode validation/propagation, not by SPV (Result 4.4).
- R1 (proof +416 B over 1e6→1e10):
bench.TestR1_ProofGrowth. - R2 (header dataset constant 4.2 MB/yr):
bench.TestT7_3_HeaderConstant/I-BE3. - R3 (verify ∝ depth, not T):
bench.TestT7_4_LogarithmicVerifymeasures real fold time. - R4 (push proof bytes = 0):
bench.TestT7_5_PushVsPull. - Depth law
depth == ceil(log2 T): integercommitment.CeilLog2, asserted inT1.3/T7. - Frozen core (anti-Utreexo):
bundle.TestT3_3_FrozenCoreasserts byte-equality acrossReanchor. - Inclusion ≠ double-spend:
bundle.TestT3_5andwalletbob.TestT4_4— a spent output's proof still verifies but is not accepted. - Absent-period honesty (I-A3):
accumulator.TestT2_3_GapHonesty— a height in a gap returnsinGap == true, never a false "in chain". - No keys at till (I-BB2):
walletbob.StorePrivateKeyalways errors;TestT4_3.
Inclusion soundness reduces to SHA-256d collision/preimage resistance: every
Verify recomputes the path bottom-up and compares to the header's committed root;
no input but the true sibling set folds to the root. Every adversarial test
(adversarial/) asserts a rejection — forged paths, off-chain headers,
evidence-free alert floods, and orphaned anchors are all refused. The L4 branch is
gated: VerifyBlockInChain is trusted only when VerifyAnchor confirms the
accumulator root is committed in a PoW-sealed generation transaction (I-A2).
Two honest implementation notes (stated, not hidden)
-
03_SCALING_MODEL.md/PAPER.mdsubtree count for 1e10 was off by one. The tables printed5,722,045(the floor); the derivedceil(6e12 / 2^20)is5,722,046(remainder 942,080 ≠ 0). Per the design's own rule ("every number is derived; deviations are bugs"), the code emits the correct value and the two docs were corrected to match. Every other table cell was already exact. -
The secp256k1 signer uses
math/bigand is therefore not constant-time. It is correct (deterministic RFC 6979 nonces, low-S, on-curve checks, round-trip tested) and dependency-free, but side-channel hardening is a production task. The MF-SPV inclusion-soundness claims rest on the Merkle layer, not on the signer; the signer provides the spend authorisation the push protocol needs. For deployment inside Teranode, swapcryptofor the node's audited secp256k1.
The teranode package ships an in-memory MockNode that builds real subtree and
block Merkle trees and a real header accumulator, so the bundle and wallet layers
are exercised against genuine cryptographic structures. Wiring ProofSource /
HeaderChain / UTXOClient to a pinned Teranode revision (01_ARCHITECTURE §7
dependency #2) is the remaining integration step; the interfaces are read-only by
construction (I-TA1).