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-05 22:46:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
commitceae3d076b5ad11ee84729ae43f14e9312896646 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /tests/scripts
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 '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())