diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
| commit | ceae3d076b5ad11ee84729ae43f14e9312896646 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /tests/scripts/test_bitflash_tools.py | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | 66e0b9158d050c46b1ff2509faeee3f4573cac1c (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/test_bitflash_tools.py')
| -rw-r--r-- | tests/scripts/test_bitflash_tools.py | 286 |
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 | |||
| 4 | import json | ||
| 5 | import os | ||
| 6 | import struct | ||
| 7 | import subprocess | ||
| 8 | import sys | ||
| 9 | import tempfile | ||
| 10 | from pathlib import Path | ||
| 11 | |||
| 12 | |||
| 13 | ROOT = Path(__file__).resolve().parents[2] | ||
| 14 | SCRIPTS = ROOT / "scripts" | ||
| 15 | FIXTURES = ROOT / "tests" / "fixtures" / "chain-tools" | ||
| 16 | sys.path.insert(0, str(SCRIPTS)) | ||
| 17 | |||
| 18 | import bitflash_chain as chain # noqa: E402 | ||
| 19 | |||
| 20 | |||
| 21 | def 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 | |||
| 36 | def 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 | |||
| 49 | def p2pkh_script(byte): | ||
| 50 | return "76a914%s88ac" % (bytes([byte]) * 20).hex() | ||
| 51 | |||
| 52 | |||
| 53 | def 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 | |||
| 64 | def 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 | |||
| 80 | def 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 | |||
| 96 | def 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 | |||
| 123 | def 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 | |||
| 156 | def 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 | |||
| 170 | def assert_true(cond, msg): | ||
| 171 | if not cond: | ||
| 172 | raise AssertionError(msg) | ||
| 173 | |||
| 174 | |||
| 175 | def 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 | |||
| 225 | def 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 | |||
| 277 | def 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 | |||
| 285 | if __name__ == "__main__": | ||
| 286 | raise SystemExit(main()) | ||