upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/scripts/verify-utxo-proof.py
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
commitceae3d076b5ad11ee84729ae43f14e9312896646 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /scripts/verify-utxo-proof.py
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parent66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff)
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git merge-base returns no result at all -- so anyone cloning this repository landed on a default branch that was neither current nor related to the running node. The 28 commits on main were audited before this merge: every one is already present in the tree by content, including the two that are hardest to see because they were rewritten afterwards -- the payout reorg-safety work (PayoutBlockStillOnChain, since evolved to re-check on every retry rather than once at maturity) and the nostr relay certificate verification (SSL_set1_host, now with a Windows root-store path). Nothing on main is lost by this merge. Recorded as a merge rather than a force-push: main only ever moves forward, its old history stays reachable as the first parent, and this is revertible. The tree is taken wholesale from the merged branch, so main's content is now exactly what was built and tested. What this brings onto main: - compactblock: BIP152-style compact block relay ported from upstream 1.2.23, primitives and unit test only, with the RAND_bytes failure path closed and the reconstruction allocation bounded. - upstream/main 17741d9: isolated testnet parameters and genesis, optional 2 MB large pages for RandomX. Mainnet genesis verified byte-identical through upstream's BuildGenesisBlock() refactor. - deploy: every image built on bookworm so binaries run on Debian 12, and the container build now honours the RandomX and libsecp256k1 pins it had been silently ignoring since before 1.2.15. Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus constants unchanged against e4f898c apart from the pre-existing MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'scripts/verify-utxo-proof.py')
-rw-r--r--scripts/verify-utxo-proof.py146
1 files changed, 146 insertions, 0 deletions
diff --git a/scripts/verify-utxo-proof.py b/scripts/verify-utxo-proof.py
new file mode 100644
index 0000000..50d9356
--- /dev/null
+++ b/scripts/verify-utxo-proof.py
@@ -0,0 +1,146 @@
1#!/usr/bin/env python3
2"""Verify a Bitflash UTXO inclusion proof against a UTXO root."""
3
4import argparse
5import hashlib
6import json
7import sys
8from pathlib import Path
9
10import bitflash_chain as chain
11
12SCHEMA = "bitflash-utxo-inclusion-proof-1"
13
14
15def canonical_bytes(obj):
16 return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
17
18
19def load_proof(path):
20 try:
21 return json.loads(Path(path).read_text(encoding="ascii"))
22 except Exception as e:
23 raise SystemExit("could not read proof: %s" % e)
24
25
26def verify(proof, expected_root=None):
27 failures = []
28 if proof.get("schema") != SCHEMA:
29 failures.append("unexpected schema")
30 commitment = proof.get("commitment", {})
31 if commitment.get("algorithm") != "sorted-outpoint-merkle-sha256":
32 failures.append("unexpected commitment algorithm")
33 if commitment.get("leaf_domain") != chain.UTXO_LEAF_DOMAIN:
34 failures.append("unexpected leaf domain")
35 if commitment.get("node_domain") != chain.UTXO_NODE_DOMAIN_TAG:
36 failures.append("unexpected node domain")
37
38 target = proof.get("target", {})
39 outpoint = target.get("outpoint", "")
40 utxo = target.get("utxo", {})
41 if outpoint != "%s:%s" % (target.get("txid"), target.get("vout")):
42 failures.append("target outpoint does not match txid:vout")
43
44 try:
45 current = chain.utxo_leaf_hash(outpoint, utxo)
46 leaf_ok = target.get("leaf_hash") == current.hex()
47 except Exception as e:
48 failures.append("could not hash target leaf: %s" % e)
49 current = b"\x00" * 32
50 leaf_ok = False
51
52 if not leaf_ok:
53 failures.append("target leaf hash mismatch")
54
55 for i, step in enumerate(proof.get("proof", [])):
56 try:
57 sibling = bytes.fromhex(step["hash"])
58 except Exception:
59 failures.append("bad sibling hash at proof level %d" % i)
60 sibling = b"\x00" * 32
61 if len(sibling) != 32:
62 failures.append("bad sibling hash length at proof level %d" % i)
63 side = step.get("side")
64 if side == "right":
65 current = chain.sha256(chain.UTXO_NODE_DOMAIN + current + sibling)
66 elif side == "left":
67 current = chain.sha256(chain.UTXO_NODE_DOMAIN + sibling + current)
68 else:
69 failures.append("bad sibling side at proof level %d" % i)
70
71 computed_root = current.hex()
72 root = expected_root or commitment.get("root")
73 if computed_root != root:
74 failures.append("computed root does not match expected root")
75
76 return {
77 "schema": "bitflash-utxo-proof-verification-1",
78 "deterministic": True,
79 "target": {
80 "outpoint": outpoint,
81 "value_satoshis": utxo.get("value_satoshis"),
82 "value_btf": utxo.get("value_btf"),
83 "created_height": utxo.get("created_height"),
84 },
85 "commitment": {
86 "expected_root": root,
87 "computed_root": computed_root,
88 "leaf_hash": target.get("leaf_hash"),
89 "path_length": len(proof.get("proof", [])),
90 },
91 "checks": {
92 "schema_is_supported": proof.get("schema") == SCHEMA,
93 "leaf_hash_matches_target": leaf_ok,
94 "root_matches": computed_root == root,
95 },
96 "failures": failures,
97 "verdict": "UTXO_PROOF_VERIFIED" if not failures else "UTXO_PROOF_FAILED",
98 }
99
100
101def print_text(result):
102 data_hash = hashlib.sha256(canonical_bytes(result)).hexdigest()
103 print("Bitflash UTXO proof verifier")
104 print("schema: %s" % result["schema"])
105 print("canonical verification sha256: %s" % data_hash)
106 print()
107 print("Target")
108 print(" outpoint: %s" % result["target"]["outpoint"])
109 print(" value: %s BTF" % result["target"]["value_btf"])
110 print(" height: %s" % result["target"]["created_height"])
111 print()
112 print("Commitment")
113 print(" expected: %s" % result["commitment"]["expected_root"])
114 print(" computed: %s" % result["commitment"]["computed_root"])
115 print(" path: %d sibling hashes" % result["commitment"]["path_length"])
116 print()
117 print("Checks")
118 for key in sorted(result["checks"]):
119 print(" %s %s" % ("OK " if result["checks"][key] else "FAIL", key))
120 if result["failures"]:
121 print()
122 print("Failures")
123 for failure in result["failures"]:
124 print(" - %s" % failure)
125 print()
126 print("Verdict: %s" % result["verdict"])
127
128
129def main(argv):
130 ap = argparse.ArgumentParser(description=__doc__)
131 ap.add_argument("proof", help="canonical JSON proof from prove-utxo.py")
132 ap.add_argument("--root", help="expected UTXO root; defaults to root embedded in proof")
133 ap.add_argument("--json", action="store_true", help="print canonical JSON")
134 args = ap.parse_args(argv)
135
136 result = verify(load_proof(args.proof), args.root)
137 data = canonical_bytes(result)
138 if args.json:
139 sys.stdout.buffer.write(data)
140 else:
141 print_text(result)
142 return 0 if result["verdict"] == "UTXO_PROOF_VERIFIED" else 1
143
144
145if __name__ == "__main__":
146 raise SystemExit(main(sys.argv[1:]))