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:
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:]))