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.py202
1 files changed, 202 insertions, 0 deletions
diff --git a/scripts/prove-utxo.py b/scripts/prove-utxo.py
new file mode 100644
index 0000000..7a10d2a
--- /dev/null
+++ b/scripts/prove-utxo.py
@@ -0,0 +1,202 @@
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 importlib.util
11import json
12import os
13import sys
14from pathlib import Path
15
16
17SCHEMA = "bitflash-utxo-inclusion-proof-1"
18NODE_DOMAIN = b"BTFNODE1|"
19
20
21def load_utxo_tool():
22 path = Path(__file__).with_name("verify-utxo-set.py")
23 spec = importlib.util.spec_from_file_location("bitflash_verify_utxo_set", path)
24 if spec is None or spec.loader is None:
25 raise SystemExit("could not load %s" % path)
26 mod = importlib.util.module_from_spec(spec)
27 spec.loader.exec_module(mod)
28 return mod
29
30
31def canonical_bytes(obj):
32 return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
33
34
35def split_outpoint(outpoint):
36 parts = outpoint.split(":")
37 if len(parts) != 2 or len(parts[0]) != 64:
38 raise SystemExit("outpoint must be txid:vout")
39 try:
40 vout = int(parts[1], 10)
41 except ValueError:
42 raise SystemExit("vout must be a decimal integer")
43 if vout < 0:
44 raise SystemExit("vout must not be negative")
45 return parts[0].lower(), vout
46
47
48def build_merkle_proof(keys, leaf_hashes, index):
49 current_index = index
50 layer = list(leaf_hashes)
51 proof = []
52 level = 0
53 while len(layer) > 1:
54 if current_index % 2 == 0:
55 sibling_index = current_index + 1
56 duplicated = sibling_index >= len(layer)
57 sibling = layer[current_index] if duplicated else layer[sibling_index]
58 side = "right"
59 else:
60 sibling_index = current_index - 1
61 duplicated = False
62 sibling = layer[sibling_index]
63 side = "left"
64 proof.append({
65 "level": level,
66 "side": side,
67 "hash": sibling.hex(),
68 "duplicated": duplicated,
69 })
70
71 next_layer = []
72 for i in range(0, len(layer), 2):
73 left = layer[i]
74 right = layer[i + 1] if i + 1 < len(layer) else left
75 next_layer.append(hashlib.sha256(NODE_DOMAIN + left + right).digest())
76 layer = next_layer
77 current_index //= 2
78 level += 1
79 return proof, layer[0].hex()
80
81
82def build_proof(args):
83 tool = load_utxo_tool()
84 txid, vout = split_outpoint(args.outpoint)
85 outpoint = "%s:%d" % (txid, vout)
86
87 raw_blocks = tool.read_blocks(args.datadir, args.max_blocks)
88 if not raw_blocks:
89 raise SystemExit("no Bitflash blocks found in %s" % args.datadir)
90 blocks = tool.select_main_chain(raw_blocks)
91 state = tool.apply_blocks(blocks)
92 utxos = state["utxos"]
93 stats = state["stats"]
94 if outpoint not in utxos:
95 raise SystemExit("outpoint is not unspent in the scanned chain: %s" % outpoint)
96
97 keys = sorted(utxos)
98 leaf_hashes = [tool.utxo_leaf_hash(k, utxos[k]) for k in keys]
99 index = keys.index(outpoint)
100 proof_path, root = build_merkle_proof(keys, leaf_hashes, index)
101 direct_root = tool.utxo_root(utxos)
102 if root != direct_root:
103 raise SystemExit("internal proof root mismatch")
104
105 tip = blocks[-1]
106 utxo = utxos[outpoint]
107 report = {
108 "schema": SCHEMA,
109 "deterministic": True,
110 "chain": {
111 "name": "Bitflash",
112 "ticker": "BTF",
113 "message_start_hex": tool.MAGIC.hex(),
114 "coin": tool.COIN,
115 },
116 "commitment": {
117 "algorithm": "sorted-outpoint-merkle-sha256",
118 "leaf_domain": "BTFUTXO1",
119 "node_domain": "BTFNODE1",
120 "root": root,
121 "leaf_count": len(keys),
122 },
123 "target": {
124 "outpoint": outpoint,
125 "txid": txid,
126 "vout": vout,
127 "leaf_index": index,
128 "leaf_hash": leaf_hashes[index].hex(),
129 "utxo": utxo,
130 },
131 "proof": proof_path,
132 "scanned": {
133 "raw_block_count": len(raw_blocks),
134 "side_blocks_ignored": len(raw_blocks) - len(blocks),
135 "main_chain_block_count": len(blocks),
136 "first_height": 0,
137 "last_height": len(blocks) - 1,
138 "tip_hash": tip["hash"],
139 "tip_time": tip["time"],
140 "tip_time_utc": tip["time_utc"],
141 },
142 "warnings": {
143 "legacy_duplicate_output_overwrites": len(stats["duplicate_outputs"]),
144 },
145 "verdict": "UTXO_INCLUSION_PROOF_CREATED",
146 }
147 return report
148
149
150def print_text(proof):
151 data_hash = hashlib.sha256(canonical_bytes(proof)).hexdigest()
152 target = proof["target"]
153 utxo = target["utxo"]
154 print("Bitflash UTXO inclusion proof")
155 print("schema: %s" % proof["schema"])
156 print("canonical proof sha256: %s" % data_hash)
157 print()
158 print("Target")
159 print(" outpoint: %s" % target["outpoint"])
160 print(" value: %s BTF" % utxo["value_btf"])
161 print(" height: %d" % utxo["created_height"])
162 print(" index: %d of %d" % (target["leaf_index"], proof["commitment"]["leaf_count"]))
163 print()
164 print("Commitment")
165 print(" root: %s" % proof["commitment"]["root"])
166 print(" path: %d sibling hashes" % len(proof["proof"]))
167 print(" tip: %s" % proof["scanned"]["tip_hash"])
168 print()
169 print("Verdict: %s" % proof["verdict"])
170
171
172def default_datadir():
173 if os.name == "nt" and os.environ.get("APPDATA"):
174 return os.path.join(os.environ["APPDATA"], "Bitflash")
175 if os.environ.get("HOME"):
176 return os.path.join(os.environ["HOME"], ".bitflash")
177 return "."
178
179
180def main(argv):
181 ap = argparse.ArgumentParser(description=__doc__)
182 ap.add_argument("outpoint", help="unspent output to prove, as txid:vout")
183 ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory")
184 ap.add_argument("--max-blocks", type=int, default=0,
185 help="number of blocks to scan; 0 means all block files")
186 ap.add_argument("--json", action="store_true", help="print canonical JSON")
187 ap.add_argument("--out", help="write canonical JSON proof to this path")
188 args = ap.parse_args(argv)
189
190 proof = build_proof(args)
191 data = canonical_bytes(proof)
192 if args.out:
193 Path(args.out).write_bytes(data)
194 if args.json:
195 sys.stdout.buffer.write(data)
196 else:
197 print_text(proof)
198 return 0
199
200
201if __name__ == "__main__":
202 raise SystemExit(main(sys.argv[1:]))