upleb.uk

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

summaryrefslogtreecommitdiff
path: root/scripts/prove-utxo.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/prove-utxo.py')
-rw-r--r--scripts/prove-utxo.py186
1 files changed, 186 insertions, 0 deletions
diff --git a/scripts/prove-utxo.py b/scripts/prove-utxo.py
new file mode 100644
index 0000000..105e677
--- /dev/null
+++ b/scripts/prove-utxo.py
@@ -0,0 +1,186 @@
1#!/usr/bin/env python3
2"""Create a Bitflash UTXO inclusion proof from local blk*.dat files.
3
4The proof is deliberately outside consensus. It proves that one txid:vout is
5included in the deterministic UTXO root produced by verify-utxo-set.py.
6"""
7
8import argparse
9import hashlib
10import json
11import sys
12from pathlib import Path
13
14import bitflash_chain as chain
15
16SCHEMA = "bitflash-utxo-inclusion-proof-1"
17
18
19def canonical_bytes(obj):
20 return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
21
22
23def split_outpoint(outpoint):
24 parts = outpoint.split(":")
25 if len(parts) != 2 or len(parts[0]) != 64:
26 raise SystemExit("outpoint must be txid:vout")
27 try:
28 vout = int(parts[1], 10)
29 except ValueError:
30 raise SystemExit("vout must be a decimal integer")
31 if vout < 0:
32 raise SystemExit("vout must not be negative")
33 return parts[0].lower(), vout
34
35
36def build_merkle_proof(keys, leaf_hashes, index):
37 current_index = index
38 layer = list(leaf_hashes)
39 proof = []
40 level = 0
41 while len(layer) > 1:
42 if current_index % 2 == 0:
43 sibling_index = current_index + 1
44 duplicated = sibling_index >= len(layer)
45 sibling = layer[current_index] if duplicated else layer[sibling_index]
46 side = "right"
47 else:
48 sibling_index = current_index - 1
49 duplicated = False
50 sibling = layer[sibling_index]
51 side = "left"
52 proof.append({
53 "level": level,
54 "side": side,
55 "hash": sibling.hex(),
56 "duplicated": duplicated,
57 })
58
59 next_layer = []
60 for i in range(0, len(layer), 2):
61 left = layer[i]
62 right = layer[i + 1] if i + 1 < len(layer) else left
63 next_layer.append(hashlib.sha256(chain.UTXO_NODE_DOMAIN + left + right).digest())
64 layer = next_layer
65 current_index //= 2
66 level += 1
67 return proof, layer[0].hex()
68
69
70def build_proof(args):
71 txid, vout = split_outpoint(args.outpoint)
72 outpoint = "%s:%d" % (txid, vout)
73
74 raw_blocks = chain.read_blocks(args.datadir, args.max_blocks)
75 if not raw_blocks:
76 raise SystemExit("no Bitflash blocks found in %s" % args.datadir)
77 try:
78 blocks = chain.select_main_chain(raw_blocks)
79 state = chain.apply_blocks(blocks, strict_duplicates=args.strict_duplicates)
80 except chain.ParseError as e:
81 raise SystemExit(str(e))
82 utxos = state["utxos"]
83 stats = state["stats"]
84 if outpoint not in utxos:
85 raise SystemExit("outpoint is not unspent in the scanned chain: %s" % outpoint)
86
87 keys = sorted(utxos)
88 leaf_hashes = [chain.utxo_leaf_hash(k, utxos[k]) for k in keys]
89 index = keys.index(outpoint)
90 proof_path, root = build_merkle_proof(keys, leaf_hashes, index)
91 direct_root = chain.utxo_root(utxos)
92 if root != direct_root:
93 raise SystemExit("internal proof root mismatch")
94
95 tip = blocks[-1]
96 utxo = utxos[outpoint]
97 report = {
98 "schema": SCHEMA,
99 "deterministic": True,
100 "chain": {
101 "name": "Bitflash",
102 "ticker": "BTF",
103 "message_start_hex": chain.MAGIC.hex(),
104 "coin": chain.COIN,
105 },
106 "commitment": {
107 "algorithm": "sorted-outpoint-merkle-sha256",
108 "leaf_domain": chain.UTXO_LEAF_DOMAIN,
109 "node_domain": chain.UTXO_NODE_DOMAIN_TAG,
110 "root": root,
111 "leaf_count": len(keys),
112 },
113 "target": {
114 "outpoint": outpoint,
115 "txid": txid,
116 "vout": vout,
117 "leaf_index": index,
118 "leaf_hash": leaf_hashes[index].hex(),
119 "utxo": utxo,
120 },
121 "proof": proof_path,
122 "scanned": {
123 "raw_block_count": len(raw_blocks),
124 "side_blocks_ignored": len(raw_blocks) - len(blocks),
125 "main_chain_block_count": len(blocks),
126 "first_height": 0,
127 "last_height": len(blocks) - 1,
128 "tip_hash": tip["hash"],
129 "tip_time": tip["time"],
130 "tip_time_utc": tip["time_utc"],
131 },
132 "warnings": {
133 "legacy_duplicate_output_overwrites": len(stats["duplicate_outputs"]),
134 },
135 "verdict": "UTXO_INCLUSION_PROOF_CREATED",
136 }
137 return report
138
139
140def print_text(proof):
141 data_hash = hashlib.sha256(canonical_bytes(proof)).hexdigest()
142 target = proof["target"]
143 utxo = target["utxo"]
144 print("Bitflash UTXO inclusion proof")
145 print("schema: %s" % proof["schema"])
146 print("canonical proof sha256: %s" % data_hash)
147 print()
148 print("Target")
149 print(" outpoint: %s" % target["outpoint"])
150 print(" value: %s BTF" % utxo["value_btf"])
151 print(" height: %d" % utxo["created_height"])
152 print(" index: %d of %d" % (target["leaf_index"], proof["commitment"]["leaf_count"]))
153 print()
154 print("Commitment")
155 print(" root: %s" % proof["commitment"]["root"])
156 print(" path: %d sibling hashes" % len(proof["proof"]))
157 print(" tip: %s" % proof["scanned"]["tip_hash"])
158 print()
159 print("Verdict: %s" % proof["verdict"])
160
161
162def main(argv):
163 ap = argparse.ArgumentParser(description=__doc__)
164 ap.add_argument("outpoint", help="unspent output to prove, as txid:vout")
165 ap.add_argument("--datadir", default=chain.default_datadir(), help="Bitflash data directory")
166 ap.add_argument("--max-blocks", type=int, default=0,
167 help="number of blocks to scan; 0 means all block files")
168 ap.add_argument("--strict-duplicates", action="store_true",
169 help="fail instead of using legacy last-write-wins duplicate output handling")
170 ap.add_argument("--json", action="store_true", help="print canonical JSON")
171 ap.add_argument("--out", help="write canonical JSON proof to this path")
172 args = ap.parse_args(argv)
173
174 proof = build_proof(args)
175 data = canonical_bytes(proof)
176 if args.out:
177 Path(args.out).write_bytes(data)
178 if args.json:
179 sys.stdout.buffer.write(data)
180 else:
181 print_text(proof)
182 return 0
183
184
185if __name__ == "__main__":
186 raise SystemExit(main(sys.argv[1:]))