upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/scripts
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-06 01:53:03 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-06 01:53:03 +0000
commitef9f06d43fc6577c15df2256abbb25b20d4227b8 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /tests/scripts
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parentceae3d076b5ad11ee84729ae43f14e9312896646 (diff)
Merge branch 'chore/main-catchup-2026-08-26' into 'main'HEADmain
Bring main onto the line this project actually ships (incl. the 1.2.23 compactblock port) Closes #6, #7, #8, #11, #35, #34, #29, #28, #27, and #16 See merge request !16
Diffstat (limited to 'tests/scripts')
-rw-r--r--tests/scripts/test_bitflash_tools.py286
1 files changed, 286 insertions, 0 deletions
diff --git a/tests/scripts/test_bitflash_tools.py b/tests/scripts/test_bitflash_tools.py
new file mode 100644
index 0000000..3b4c846
--- /dev/null
+++ b/tests/scripts/test_bitflash_tools.py
@@ -0,0 +1,286 @@
1#!/usr/bin/env python3
2"""Regression tests for deterministic Bitflash Python tooling."""
3
4import json
5import os
6import struct
7import subprocess
8import sys
9import tempfile
10from pathlib import Path
11
12
13ROOT = Path(__file__).resolve().parents[2]
14SCRIPTS = ROOT / "scripts"
15FIXTURES = ROOT / "tests" / "fixtures" / "chain-tools"
16sys.path.insert(0, str(SCRIPTS))
17
18import bitflash_chain as chain # noqa: E402
19
20
21def run(args, **kwargs):
22 env = dict(os.environ)
23 env["PYTHONIOENCODING"] = "utf-8"
24 return subprocess.run(
25 [sys.executable] + [str(a) for a in args],
26 cwd=str(ROOT),
27 env=env,
28 check=True,
29 text=True,
30 stdout=subprocess.PIPE,
31 stderr=subprocess.PIPE,
32 **kwargs
33 )
34
35
36def write_block_file(path, blocks):
37 with Path(path).open("wb") as f:
38 f.write(b"noise")
39 f.write(chain.MAGIC)
40 f.write(struct.pack("<I", chain.MAX_BLOCK_PAYLOAD + 1))
41 f.write(b"still-noise")
42 for block in blocks:
43 payload = chain.ser_block(block)
44 f.write(chain.MAGIC)
45 f.write(struct.pack("<I", len(payload)))
46 f.write(payload)
47
48
49def p2pkh_script(byte):
50 return "76a914%s88ac" % (bytes([byte]) * 20).hex()
51
52
53def make_tx(vin, vout):
54 tx = {
55 "version": 1,
56 "vin": vin,
57 "vout": vout,
58 "lock_time": 0,
59 }
60 tx["txid"] = chain.hash_hex(chain.ser_tx(tx))
61 return tx
62
63
64def make_coinbase(label, script_byte, value=50 * chain.COIN):
65 return make_tx(
66 [{
67 "prevout_hash": "0" * 64,
68 "prevout_n": 0xFFFFFFFF,
69 "script_sig_hex": label.encode("ascii").hex(),
70 "sequence": 0xFFFFFFFF,
71 }],
72 [{
73 "value_satoshis": value,
74 "value_btf": chain.format_money(value),
75 "script_pub_key_hex": p2pkh_script(script_byte),
76 }],
77 )
78
79
80def make_spend(prev_txid, prev_n, outputs, label):
81 return make_tx(
82 [{
83 "prevout_hash": prev_txid,
84 "prevout_n": prev_n,
85 "script_sig_hex": label.encode("ascii").hex(),
86 "sequence": 0xFFFFFFFF,
87 }],
88 [{
89 "value_satoshis": value,
90 "value_btf": chain.format_money(value),
91 "script_pub_key_hex": p2pkh_script(script_byte),
92 } for value, script_byte in outputs],
93 )
94
95
96def make_block(prev_hash, txs, ntime, nonce):
97 merkle = chain.merkle_root([tx["txid"] for tx in txs])
98 header = (
99 struct.pack("<i", 1)
100 + bytes.fromhex(prev_hash)[::-1]
101 + bytes.fromhex(merkle)[::-1]
102 + struct.pack("<III", ntime, chain.GENESIS_BITS, nonce)
103 )
104 block = {
105 "hash": chain.hash_hex(header),
106 "version": 1,
107 "previous_hash": prev_hash,
108 "merkle_root": merkle,
109 "computed_merkle_root": merkle,
110 "time": ntime,
111 "time_utc": chain.utc(ntime),
112 "bits": "0x%08x" % chain.GENESIS_BITS,
113 "nonce": nonce,
114 "tx_count": len(txs),
115 "transactions": txs,
116 "source_file": "synthetic",
117 "source_offset": 0,
118 "size": 80 + len(chain.ser_compact_size(len(txs))) + sum(len(chain.ser_tx(tx)) for tx in txs),
119 }
120 return block
121
122
123def fixture_chain():
124 genesis = chain.reconstructed_genesis()
125 spend1 = make_spend(
126 chain.GENESIS_MERKLE,
127 0,
128 [(30 * chain.COIN, 0x22), (19 * chain.COIN, 0x33)],
129 "spend genesis with 1 BTF fee",
130 )
131 block1 = make_block(
132 genesis["hash"],
133 [make_coinbase("fixture coinbase 1", 0x11), spend1],
134 chain.GENESIS_TIME + 120,
135 4001,
136 )
137 spend2 = make_spend(
138 spend1["txid"],
139 0,
140 [(12 * chain.COIN, 0x44), (17 * chain.COIN + 50_000_000, 0x55)],
141 "split spend with 0.5 BTF fee",
142 )
143 block2 = make_block(
144 block1["hash"],
145 [make_coinbase("fixture coinbase 2", 0x66), spend2],
146 chain.GENESIS_TIME + 240,
147 4002,
148 )
149 return [genesis, block1, block2], {
150 "genesis_outpoint": "%s:0" % chain.GENESIS_MERKLE,
151 "first_spend_txid": spend1["txid"],
152 "proof_outpoint": "%s:1" % spend2["txid"],
153 }
154
155
156def compare_golden(name, actual, update):
157 path = FIXTURES / name
158 if update:
159 path.parent.mkdir(parents=True, exist_ok=True)
160 path.write_bytes(actual)
161 return
162 # The tools promise canonical LF-terminated JSON. On Windows checkouts a
163 # fixture may still appear as CRLF in the working tree; normalize only line
164 # endings so content, field order and compact serialization still drift-test.
165 expected = path.read_bytes().replace(b"\r\n", b"\n")
166 if actual.replace(b"\r\n", b"\n") != expected:
167 raise AssertionError("golden mismatch: %s" % name)
168
169
170def assert_true(cond, msg):
171 if not cond:
172 raise AssertionError(msg)
173
174
175def run_genesis_smoke():
176 genesis = chain.reconstructed_genesis()
177 outpoint = "%s:0" % chain.GENESIS_MERKLE
178
179 with tempfile.TemporaryDirectory(prefix="bitflash-tool-tests-") as tmp:
180 tmp = Path(tmp)
181 datadir = tmp / "datadir"
182 datadir.mkdir()
183 blk1 = datadir / "blk0001.dat"
184 blk2 = datadir / "blk0002.dat"
185 write_block_file(blk1, [genesis])
186 blk2.write_bytes(b"")
187
188 fair = run([SCRIPTS / "verify-fair-launch.py", "--datadir", datadir,
189 "--max-blocks", "1", "--json"]).stdout
190 fair_report = json.loads(fair)
191 assert_true(fair_report["verdict"] == "FAIR_LAUNCH_BASELINE_VERIFIED",
192 "fair launch verifier failed synthetic genesis")
193
194 utxo = run([SCRIPTS / "verify-utxo-set.py", "--datadir", datadir,
195 "--json"]).stdout
196 utxo_report = json.loads(utxo)
197 assert_true(utxo_report["verdict"] == "UTXO_COMMITMENT_VERIFIED",
198 "UTXO verifier failed synthetic genesis")
199
200 proof_path = tmp / "proof.json"
201 run([SCRIPTS / "prove-utxo.py", outpoint, "--datadir", datadir,
202 "--out", proof_path, "--json"])
203 proof_check = run([SCRIPTS / "verify-utxo-proof.py", proof_path,
204 "--root", utxo_report["commitment"]["root"], "--json"]).stdout
205 proof_report = json.loads(proof_check)
206 assert_true(proof_report["verdict"] == "UTXO_PROOF_VERIFIED",
207 "UTXO proof verifier rejected synthetic proof")
208
209 explorer_dir = tmp / "explorer"
210 run([SCRIPTS / "build-explorer.py", blk1, blk2, explorer_dir])
211 blocks = json.loads((explorer_dir / "blocks.json").read_text(encoding="ascii"))
212 assert_true(blocks["tipHeight"] == 0 and blocks["count"] == 1,
213 "explorer did not build the synthetic chain")
214 index_html = (explorer_dir / "index.html").read_text(encoding="utf-8")
215 assert_true("innerHTML" not in index_html, "explorer viewer still uses innerHTML")
216
217 try:
218 chain.apply_blocks([genesis, genesis], strict_duplicates=True)
219 except chain.ParseError:
220 pass
221 else:
222 raise AssertionError("strict duplicate output mode did not fail")
223
224
225def run_golden_fixture(update):
226 blocks, meta = fixture_chain()
227
228 with tempfile.TemporaryDirectory(prefix="bitflash-tool-golden-") as tmp:
229 tmp = Path(tmp)
230 datadir = tmp / "datadir"
231 datadir.mkdir()
232 blk1 = datadir / "blk0001.dat"
233 blk2 = datadir / "blk0002.dat"
234 write_block_file(blk1, blocks[:2])
235 write_block_file(blk2, blocks[2:])
236
237 fair = run([SCRIPTS / "verify-fair-launch.py", "--datadir", datadir,
238 "--max-blocks", "3", "--json"]).stdout.encode("ascii")
239 compare_golden("fair-launch-report.json", fair, update)
240 fair_report = json.loads(fair)
241 assert_true(fair_report["scanned"]["non_coinbase_transactions"] == 2,
242 "fair-launch fixture did not count normal transactions")
243
244 utxo = run([SCRIPTS / "verify-utxo-set.py", "--datadir", datadir,
245 "--include-utxos", "--strict-duplicates", "--json"]).stdout.encode("ascii")
246 compare_golden("utxo-report.json", utxo, update)
247 utxo_report = json.loads(utxo)
248 assert_true(utxo_report["stats"]["normal_transactions"] == 2,
249 "UTXO fixture did not apply normal transactions")
250 assert_true(utxo_report["stats"]["total_fees_btf"] == "1.50000000",
251 "UTXO fixture fee total changed")
252 assert_true(meta["genesis_outpoint"] not in utxo_report["utxos"],
253 "spent genesis output still appears in the UTXO map")
254
255 proof_path = tmp / "fixture-proof.json"
256 proof = run([SCRIPTS / "prove-utxo.py", meta["proof_outpoint"],
257 "--datadir", datadir, "--strict-duplicates",
258 "--out", proof_path, "--json"]).stdout.encode("ascii")
259 compare_golden("utxo-proof.json", proof, update)
260
261 proof_check = run([SCRIPTS / "verify-utxo-proof.py", proof_path,
262 "--root", utxo_report["commitment"]["root"], "--json"]).stdout.encode("ascii")
263 compare_golden("utxo-proof-verification.json", proof_check, update)
264 proof_report = json.loads(proof_check)
265 assert_true(proof_report["verdict"] == "UTXO_PROOF_VERIFIED",
266 "fixture UTXO proof did not verify")
267
268 explorer_dir = tmp / "explorer"
269 run([SCRIPTS / "build-explorer.py", blk1, blk2, explorer_dir])
270 compare_golden("explorer-blocks.json",
271 (explorer_dir / "blocks.json").read_bytes(), update)
272 detail = json.loads((explorer_dir / "block" / "1.json").read_text(encoding="ascii"))
273 assert_true(len(detail["txs"]) == 2,
274 "explorer fixture block did not include the normal transaction")
275
276
277def main():
278 update = "--update-goldens" in sys.argv[1:]
279 run_genesis_smoke()
280 run_golden_fixture(update)
281 print("Bitflash script tool regressions passed")
282 return 0
283
284
285if __name__ == "__main__":
286 raise SystemExit(main())