From 3d2acdadb36642be589cecc9ade38ed9a0cb5ec7 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Thu, 17 Sep 2026 06:33:37 -0500 Subject: [PATCH 1/3] Remove Python wallet test dependencies Drop the bip32/Coincurve test-only oracle and its CI install path now that wallet interoperability is verified with frozen fixtures and real Bitcoin Core v32 integration. Keep fingerprint callback tests deterministic without adding another EC implementation.\n\nSecurity: no Python secp256k1 dependency remains in runtime, CI, or test tooling.\n\nValidation: 857 pytest tests and 857 optimized tests pass; mypy, Ruff, formatting, diff check, 57-case correction differential, build/Twine, clean no-deps wheel smoke, Core v32 regtest, and isolated Core v32 main-chain account-7 smoke all pass.\n\nFixes #3. Fixes #6. --- .github/workflows/python-package.yml | 4 -- docs/security/model.md | 2 +- requirements/test-wallet-dependencies.txt | 30 -------- tests/test_cli.py | 17 +++-- tests/test_correction_indel.py | 9 ++- tests/test_generation.py | 19 ++--- tests/test_generic_hrp.py | 7 +- tools/_wallet_reference.py | 72 ------------------- tools/differential_wallet.py | 85 ----------------------- 9 files changed, 31 insertions(+), 214 deletions(-) delete mode 100644 requirements/test-wallet-dependencies.txt delete mode 100644 tools/_wallet_reference.py delete mode 100644 tools/differential_wallet.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9f99e62..165dd45 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -22,9 +22,6 @@ jobs: python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip - run: python -m pip install --require-hashes -r requirements/cli-build-dependencies.txt - - run: >- - python -m pip install --no-build-isolation --require-hashes - -r requirements/test-wallet-dependencies.txt - run: python -m pip install --no-build-isolation -e '.[dev]' - run: python -m pip check - run: python -m pytest -q @@ -32,6 +29,5 @@ jobs: - run: python -m ruff check . - run: python -m ruff format --check . - run: python tools/differential_correction.py --verify - - run: python tools/differential_wallet.py --verify - run: python -m build --no-isolation - run: python tools/verify_wheel_environment.py diff --git a/docs/security/model.md b/docs/security/model.md index 0ce515c..6f96d80 100644 --- a/docs/security/model.md +++ b/docs/security/model.md @@ -248,5 +248,5 @@ initialization. | Parsing and profiles | [`test_bech32.py`](../../tests/test_bech32.py), [`test_bip93.py`](../../tests/test_bip93.py), and [`test_profiles.py`](../../tests/test_profiles.py) | | Creation, sharing, and recovery | [`test_generation.py`](../../tests/test_generation.py), [`test_sharing.py`](../../tests/test_sharing.py), and the BIP93 vectors under `tests/data/` | | Correction | [`test_correction_bch.py`](../../tests/test_correction_bch.py), [`test_correction_indel.py`](../../tests/test_correction_indel.py), [`correction_capture.py`](../../tools/correction_capture.py), and [`differential_correction.py --verify`](../../tools/differential_correction.py) | -| Bitcoin Core and wallets | [`test_bitcoin_core.py`](../../tests/test_bitcoin_core.py), [`test_wallet.py`](../../tests/test_wallet.py), [`bitcoin_core_regtest.py`](../../tools/bitcoin_core_regtest.py), and [`differential_wallet.py`](../../tools/differential_wallet.py) | +| Bitcoin Core and wallets | [`test_bitcoin_core.py`](../../tests/test_bitcoin_core.py), [`test_wallet.py`](../../tests/test_wallet.py), [`bitcoin_core_regtest.py`](../../tools/bitcoin_core_regtest.py), and [`bitcoin_core_main_smoke.py`](../../tools/bitcoin_core_main_smoke.py) | | CLI channels and input | [`test_cli.py`](../../tests/test_cli.py) | diff --git a/requirements/test-wallet-dependencies.txt b/requirements/test-wallet-dependencies.txt deleted file mode 100644 index cff377f..0000000 --- a/requirements/test-wallet-dependencies.txt +++ /dev/null @@ -1,30 +0,0 @@ ---require-hashes ---only-binary=coincurve - -# Test-only independent wallet oracle; never installed by the documented CLI path. -# Owner-authored upstream PR #53 changes only coincurve's upper bound and its CI matrix. -bip32 @ https://github.com/darosior/python-bip32/archive/45db547bdf5a5bc19a8c55ef447dbf9169928792.tar.gz \ - --hash=sha256:a25be30641b381eed9a5249bbc4a73124905223c9c50f93140f519bcf259e415 - -# Every published CPython 3.12 and 3.13 wheel for the supported OS families. -coincurve==21.0.0 \ - --hash=sha256:f60ad56113f08e8c540bb89f4f35f44d434311433195ffff22893ccfa335070c \ - --hash=sha256:1cb1cd19fb0be22e68ecb60ad950b41f18b9b02eebeffaac9391dc31f74f08f2 \ - --hash=sha256:05d7e255a697b3475d7ae7640d3bdef3d5bc98ce9ce08dd387f780696606c33b \ - --hash=sha256:5a366c314df7217e3357bb8c7d2cda540b0bce180705f7a0ce2d1d9e28f62ad4 \ - --hash=sha256:1b04778b75339c6e46deb9ae3bcfc2250fbe48d1324153e4310fc4996e135715 \ - --hash=sha256:8efcbdcd50cc219989a2662e6c6552f455efc000a15dd6ab3ebf4f9b187f41a3 \ - --hash=sha256:6df44b4e3b7acdc1453ade52a52e3f8a5b53ecdd5a06bd200f1ec4b4e250f7d9 \ - --hash=sha256:bcc0831f07cb75b91c35c13b1362e7b9dc76c376b27d01ff577bec52005e22a8 \ - --hash=sha256:5dd7b66b83b143f3ad3861a68fc0279167a0bae44fe3931547400b7a200e90b1 \ - --hash=sha256:78dbe439e8cb22389956a4f2f2312813b4bd0531a0b691d4f8e868c7b366555d \ - --hash=sha256:9df5ceb5de603b9caf270629996710cf5ed1d43346887bc3895a11258644b65b \ - --hash=sha256:154467858d23c48f9e5ab380433bc2625027b50617400e2984cc16f5799ab601 \ - --hash=sha256:f57f07c44d14d939bed289cdeaba4acb986bba9f729a796b6a341eab1661eedc \ - --hash=sha256:3fb03e3a388a93d31ed56a442bdec7983ea404490e21e12af76fb1dbf097082a \ - --hash=sha256:d09ba4fd9d26b00b06645fcd768c5ad44832a1fa847ebe8fb44970d3204c3cb7 \ - --hash=sha256:1a1e7ee73bc1b3bcf14c7b0d1f44e6485785d3b53ef7b16173c36d3cefa57f93 \ - --hash=sha256:ad05952b6edc593a874df61f1bc79db99d716ec48ba4302d699e14a419fe6f51 \ - --hash=sha256:4d2bf350ced38b73db9efa1ff8fd16a67a1cb35abb2dda50d89661b531f03fd3 \ - --hash=sha256:54d9500c56d5499375e579c3917472ffcf804c3584dd79052a79974280985c74 \ - --hash=sha256:773917f075ec4b94a7a742637d303a3a082616a115c36568eb6c873a8d950d18 diff --git a/tests/test_cli.py b/tests/test_cli.py index 5a2157e..6f76998 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -37,7 +37,6 @@ from codex32.cli import main, ms_main from codex32.generation import _fingerprint_identifier from codex32.profiles.ms32 import SEED_BYTE_LENGTHS -from tools._wallet_reference import fingerprint_seed @dataclass(frozen=True) @@ -88,6 +87,14 @@ def isatty(self) -> bool: return self.pretty or self.checks == 1 +def _fake_fingerprint(seed: bytes) -> bytes: + frozen = { + bytes.fromhex(VECTOR_1["secret_hex"]): bytes.fromhex("3f3521a6"), + bytes.fromhex(VECTOR_2["secret_hex"]): bytes.fromhex("fab6868a"), + } + return frozen.get(seed, seed[:4]) + + @dataclass class _FakeBitcoinCore: chain: str = "main" @@ -98,7 +105,7 @@ class _FakeBitcoinCore: timestamp: int | str | None = None def fingerprint_seed(self, seed: bytes) -> bytes: - return fingerprint_seed(seed) + return _fake_fingerprint(seed) def fingerprint(self, secret: MasterSeed) -> bytes: return self.fingerprint_seed(secret.seed_bytes) @@ -1224,7 +1231,7 @@ def test_create_defaults_to_an_unshared_128_bit_master_seed() -> None: secret = artifacts[0] assert isinstance(secret, MasterSeed) and len(secret.seed_bytes) == 16 assert secret.header.threshold == 0 - assert secret.header.identifier == _fingerprint_identifier(fingerprint_seed(secret.seed_bytes)) + assert secret.header.identifier == _fingerprint_identifier(_fake_fingerprint(secret.seed_bytes)) def test_fresh_bitcoin_terminal_and_core_preflight_precede_entropy() -> None: @@ -1360,7 +1367,7 @@ def answer(prompt: str) -> str: assert ms_main(["create"]) == 0 artifact = parse_codex32(emitted[0]) assert isinstance(artifact, MasterSeed) - assert artifact.header.identifier == _fingerprint_identifier(fingerprint_seed(artifact.seed_bytes)) + assert artifact.header.identifier == _fingerprint_identifier(_fake_fingerprint(artifact.seed_bytes)) def test_fresh_shared_create_confirms_each_card_on_a_terminal( @@ -1549,7 +1556,7 @@ def test_create_accepts_positional_headers_and_preserves_index_order() -> None: shares = _output_artifacts(shared) assert isinstance(fingerprinted_secret, MasterSeed) assert fingerprinted_secret.header.identifier == _fingerprint_identifier( - fingerprint_seed(fingerprinted_secret.seed_bytes) + _fake_fingerprint(fingerprinted_secret.seed_bytes) ) assert unshared_secret.header.identifier == "test" assert len(automatic) == 3 diff --git a/tests/test_correction_indel.py b/tests/test_correction_indel.py index 9dcb99a..3cd0310 100644 --- a/tests/test_correction_indel.py +++ b/tests/test_correction_indel.py @@ -32,7 +32,6 @@ _search_target, ) from codex32.profiles.ms32 import TEXT_LENGTHS -from tools._wallet_reference import fingerprint_seed from tools.correction_capture import cross_length_classes SOURCE = VECTOR_1["secret_s"] @@ -43,6 +42,10 @@ def _groups(text: str) -> list[str]: return [text[start : start + 4] for start in range(0, len(text), 4)] +def _fake_fingerprint(seed: bytes) -> bytes: + return seed[:4] + + def _character_damage(source: str, inserted: int, omitted: int) -> str: characters = list(source) for position in reversed((14, 22, 30, 38)[:omitted]): @@ -411,14 +414,14 @@ def test_duplicate_reconstruction_keeps_lower_hamming_path() -> None: def test_cli_tie_breaks_follow_hamming_crc_then_fingerprint() -> None: seed = bytes(range(16)) - fingerprint = MasterSeed.from_seed(seed, identifier=_fingerprint_identifier(fingerprint_seed(seed))) + fingerprint = MasterSeed.from_seed(seed, identifier=_fingerprint_identifier(_fake_fingerprint(seed))) mismatch = MasterSeed.from_seed(seed, identifier="test") high_hamming = CorrectionCandidate(mismatch, (), 10, 0, 3, True) crc = CorrectionCandidate(mismatch, (), 10, 0, 2, True) fingerprint_match = CorrectionCandidate(fingerprint, (), 10, 0, 2, True) assert len(_primary((high_hamming, crc, fingerprint_match))) == 3 - matcher = _fingerprint_matcher(lambda secret: fingerprint_seed(secret.seed_bytes)) + matcher = _fingerprint_matcher(lambda secret: _fake_fingerprint(secret.seed_bytes)) assert _best((high_hamming, crc, fingerprint_match), fingerprint_match=matcher) == (fingerprint_match,) diff --git a/tests/test_generation.py b/tests/test_generation.py index 44cd688..c99a716 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -6,7 +6,7 @@ import pytest from _codex32_oracle import oracle_encode -from data.bip93_vectors import VECTOR_1, VECTOR_2, VECTOR_3, VECTOR_4, VECTOR_6 +from data.bip93_vectors import VECTOR_2, VECTOR_4, VECTOR_6 from data.sharing_vectors import SHARING_VECTORS from hypothesis import given, settings from hypothesis import strategies as st @@ -33,7 +33,6 @@ ) from codex32.generation import ORDINARY_INDICES, _fingerprint_identifier from codex32.profiles.ms32 import SEED_BYTE_LENGTHS, _has_generation_padding -from tools._wallet_reference import fingerprint_seed def _complete(ceremony: CreationCeremony) -> tuple[MasterSeed | CoreLightningSecret, tuple[Share, ...]]: @@ -57,12 +56,16 @@ def _seed(byte_length: int) -> bytes: return bytes((position * 109 + byte_length) % 256 for position in range(byte_length)) +def _fake_fingerprint(seed: bytes) -> bytes: + return seed[:4] + + def test_fresh_unshared_ms_supports_every_bip93_size() -> None: for byte_length in SEED_BYTE_LENGTHS: - secret = generate_master_seed(byte_length=byte_length, fingerprint=fingerprint_seed) + secret = generate_master_seed(byte_length=byte_length, fingerprint=_fake_fingerprint) assert len(secret.seed_bytes) == byte_length assert secret.header.threshold == 0 - assert secret.header.identifier == _fingerprint_identifier(fingerprint_seed(secret.seed_bytes)) + assert secret.header.identifier == _fingerprint_identifier(_fake_fingerprint(secret.seed_bytes)) assert _has_generation_padding(secret) @@ -395,11 +398,3 @@ def test_from_secret_rejects_non_secret_artifacts() -> None: indices="ac", identifier="test", # type: ignore[arg-type] ) - - -@pytest.mark.parametrize( - ("vector", "expected"), - ((VECTOR_1, "8u6j"), (VECTOR_2, "l2mg"), (VECTOR_3, "regv")), -) -def test_unshared_fingerprint_identifier_vectors(vector: dict[str, str], expected: str) -> None: - assert _fingerprint_identifier(fingerprint_seed(bytes.fromhex(vector["secret_hex"]))) == expected diff --git a/tests/test_generic_hrp.py b/tests/test_generic_hrp.py index 341e504..032819b 100644 --- a/tests/test_generic_hrp.py +++ b/tests/test_generic_hrp.py @@ -167,9 +167,12 @@ def test_cli_split_and_unknown_neutral_summary() -> None: def test_cli_share_is_generic_and_ms32_share_is_scoped(monkeypatch) -> None: - from tools._wallet_reference import ReferenceCore + class _FakeCore: + @staticmethod + def fingerprint(_secret: object) -> bytes: + return b"\0\0\0\0" - monkeypatch.setattr("codex32.cli.BitcoinCore.connect", lambda *args, **kwargs: ReferenceCore()) + monkeypatch.setattr("codex32.cli.BitcoinCore.connect", lambda *args, **kwargs: _FakeCore()) basis = UNKNOWN["short"]["S"] + "\n" + UNKNOWN["short"]["A"] + "\n" status, output, error = _invoke(main, ["share", "d", "--plain"], basis) assert (status, output.strip(), error) == (0, UNKNOWN["short"]["D"], "") diff --git a/tools/_wallet_reference.py b/tools/_wallet_reference.py deleted file mode 100644 index 796efa8..0000000 --- a/tools/_wallet_reference.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Test-only Core descriptor oracle backed by the independent bip32 package. - -Never use this adapter with real wallet material. Runtime code delegates public -derivation to Bitcoin Core; this oracle keeps offline vectors reproducible. -""" - -import re -from typing import Literal - -from bip32 import BIP32 - -from codex32._bitcoin_core import BitcoinCore -from codex32.profiles.ms32 import MasterSeed -from codex32.wallet import _with_checksum, core_descriptors - - -def fingerprint_seed(seed: bytes) -> bytes: - return BIP32.from_seed(seed).get_fingerprint() - - -def descriptor_info(descriptor: str) -> dict[str, object]: - descriptor = descriptor.strip().split("#", 1)[0] - - def public(match: re.Match[str]) -> str: - root = BIP32.from_xpriv(match.group(1)) - path = match.group(2) - return f"[{root.get_fingerprint().hex()}{path}]{root.get_xpub_from_path('m' + path)}" - - normalized = re.sub(r"([xt]prv[1-9A-HJ-NP-Za-km-z]+)((?:/\d+h)*)", public, descriptor) - return { - "descriptor": _with_checksum(normalized), - "multipath_expansion": [ - _with_checksum(normalized.replace("<0;1>", str(branch))) for branch in (0, 1) - ], - } - - -class ReferenceCore(BitcoinCore): - def __init__(self, *, testnet: bool = False) -> None: - super().__init__("test-only", "test" if testnet else "main", 320000) - - def fingerprint(self, secret: MasterSeed) -> bytes: - return fingerprint_seed(secret.seed_bytes) - - def public_descriptors( - self, - secret: MasterSeed, - *, - wallet: str, - account: int = 0, - timestamp: int | Literal["now"] = 0, - ) -> tuple[dict[str, object], ...]: - del wallet - private = core_descriptors( - secret, - account=account, - testnet=self.chain != "main", - private=True, - timestamp=timestamp, - ) - return tuple( - { - "desc": descriptor_info(str(record["desc"]))["descriptor"], - "active": True, - "timestamp": timestamp, - } - for record in private - ) - - def _rpc(self, *arguments: str, wallet: str | None = None, stdin: str | None = None) -> object: - assert arguments == ("getdescriptorinfo",) and wallet is None and stdin is not None - return descriptor_info(stdin) diff --git a/tools/differential_wallet.py b/tools/differential_wallet.py deleted file mode 100644 index f8f8f23..0000000 --- a/tools/differential_wallet.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Emit a deterministic digest of the wallet interoperability boundary.""" - -from __future__ import annotations - -import argparse -import hashlib -import json - -from _wallet_reference import ReferenceCore - -from codex32 import MasterSeed, core_descriptors, master_xprv - -_DOMAIN = b"python-codex32 differential wallet corpus v2" -_SEED_LENGTHS = (16, 20, 24, 28, 32, 64) -_EXPECTED_CASES = 64 -_EXPECTED_DIGEST = "9b3342af401765e4ec73acb3d17142060c1844fc70fda4d87e77f1191f6aec40" - - -def _seed(case: int, length: int) -> bytes: - material = _DOMAIN + case.to_bytes(4, "big") + bytes([length]) - return hashlib.sha512(material).digest()[:length] - - -def _record(case: int, length: int, testnet: bool) -> dict[str, object]: - seed = _seed(case, length) - account = int.from_bytes(hashlib.sha256(seed).digest()[:4], "big") % 2**31 - secret = MasterSeed.from_seed(seed, identifier="test") - core = ReferenceCore(testnet=testnet) - return { - "case": case, - "length": length, - "testnet": testnet, - "account": account, - "xprv": master_xprv(secret, testnet=testnet), - "public": core_descriptors( - secret, - integration=core, - wallet="test-only", - account=account, - testnet=testnet, - timestamp=case, - ), - "private": core_descriptors( - secret, - account=account, - testnet=testnet, - private=True, - timestamp=case, - ), - } - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--cases", type=int, default=_EXPECTED_CASES) - parser.add_argument("--verify", action="store_true") - arguments = parser.parse_args() - if arguments.cases < 1: - raise SystemExit("--cases must be positive") - - digest = hashlib.sha256() - records = 0 - for case in range(arguments.cases): - for length in _SEED_LENGTHS: - for testnet in (False, True): - encoded = json.dumps( - _record(case, length, testnet), - sort_keys=True, - separators=(",", ":"), - ).encode() - digest.update(len(encoded).to_bytes(4, "big")) - digest.update(encoded) - records += 1 - - actual = digest.hexdigest() - print(json.dumps({"records": records, "sha256": actual})) - if arguments.verify: - if arguments.cases != _EXPECTED_CASES: - raise SystemExit(f"--verify requires --cases {_EXPECTED_CASES}") - if not _EXPECTED_DIGEST or actual != _EXPECTED_DIGEST: - raise SystemExit("wallet differential digest mismatch") - - -if __name__ == "__main__": - main() From 8280e21dfbf2107ba8f748406c900999a81b82f9 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Thu, 17 Sep 2026 08:12:37 -0500 Subject: [PATCH 2/3] Tests: verify Core fingerprints centrally Consolidate wallet fingerprint fixtures so tests that assert fingerprint semantics use values independently verified by Bitcoin Core. Keep arbitrary-seed CLI orchestration on an explicit stable stub while preserving real fixture values for known public seeds. Restore the unshared identifier expectations in generation coverage without treating them as BIP93 fingerprint vectors, and make the real Core v32 regtest verify every frozen fingerprint.\n\nrefs #3\n\nValidation: pytest -q (860 passed); python -O -m pytest -q (860 passed); mypy src/codex32; ruff check .; ruff format --check .; Bitcoin Core v32.0rc1 regtest. --- tests/test_cli.py | 17 +++++------------ tests/test_correction_indel.py | 11 ++++------- tests/test_generation.py | 20 +++++++++++++------- tools/_wallet_test_vectors.py | 20 ++++++++++++++++++++ tools/bitcoin_core_regtest.py | 5 +++++ 5 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 tools/_wallet_test_vectors.py diff --git a/tests/test_cli.py b/tests/test_cli.py index 6f76998..42304ae 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -37,6 +37,7 @@ from codex32.cli import main, ms_main from codex32.generation import _fingerprint_identifier from codex32.profiles.ms32 import SEED_BYTE_LENGTHS +from tools._wallet_test_vectors import stub_fingerprint @dataclass(frozen=True) @@ -87,14 +88,6 @@ def isatty(self) -> bool: return self.pretty or self.checks == 1 -def _fake_fingerprint(seed: bytes) -> bytes: - frozen = { - bytes.fromhex(VECTOR_1["secret_hex"]): bytes.fromhex("3f3521a6"), - bytes.fromhex(VECTOR_2["secret_hex"]): bytes.fromhex("fab6868a"), - } - return frozen.get(seed, seed[:4]) - - @dataclass class _FakeBitcoinCore: chain: str = "main" @@ -105,7 +98,7 @@ class _FakeBitcoinCore: timestamp: int | str | None = None def fingerprint_seed(self, seed: bytes) -> bytes: - return _fake_fingerprint(seed) + return stub_fingerprint(seed) def fingerprint(self, secret: MasterSeed) -> bytes: return self.fingerprint_seed(secret.seed_bytes) @@ -1231,7 +1224,7 @@ def test_create_defaults_to_an_unshared_128_bit_master_seed() -> None: secret = artifacts[0] assert isinstance(secret, MasterSeed) and len(secret.seed_bytes) == 16 assert secret.header.threshold == 0 - assert secret.header.identifier == _fingerprint_identifier(_fake_fingerprint(secret.seed_bytes)) + assert secret.header.identifier == _fingerprint_identifier(stub_fingerprint(secret.seed_bytes)) def test_fresh_bitcoin_terminal_and_core_preflight_precede_entropy() -> None: @@ -1367,7 +1360,7 @@ def answer(prompt: str) -> str: assert ms_main(["create"]) == 0 artifact = parse_codex32(emitted[0]) assert isinstance(artifact, MasterSeed) - assert artifact.header.identifier == _fingerprint_identifier(_fake_fingerprint(artifact.seed_bytes)) + assert artifact.header.identifier == _fingerprint_identifier(stub_fingerprint(artifact.seed_bytes)) def test_fresh_shared_create_confirms_each_card_on_a_terminal( @@ -1556,7 +1549,7 @@ def test_create_accepts_positional_headers_and_preserves_index_order() -> None: shares = _output_artifacts(shared) assert isinstance(fingerprinted_secret, MasterSeed) assert fingerprinted_secret.header.identifier == _fingerprint_identifier( - _fake_fingerprint(fingerprinted_secret.seed_bytes) + stub_fingerprint(fingerprinted_secret.seed_bytes) ) assert unshared_secret.header.identifier == "test" assert len(automatic) == 3 diff --git a/tests/test_correction_indel.py b/tests/test_correction_indel.py index 3cd0310..eb64dda 100644 --- a/tests/test_correction_indel.py +++ b/tests/test_correction_indel.py @@ -32,6 +32,7 @@ _search_target, ) from codex32.profiles.ms32 import TEXT_LENGTHS +from tools._wallet_test_vectors import core_fingerprint from tools.correction_capture import cross_length_classes SOURCE = VECTOR_1["secret_s"] @@ -42,10 +43,6 @@ def _groups(text: str) -> list[str]: return [text[start : start + 4] for start in range(0, len(text), 4)] -def _fake_fingerprint(seed: bytes) -> bytes: - return seed[:4] - - def _character_damage(source: str, inserted: int, omitted: int) -> str: characters = list(source) for position in reversed((14, 22, 30, 38)[:omitted]): @@ -413,15 +410,15 @@ def test_duplicate_reconstruction_keeps_lower_hamming_path() -> None: def test_cli_tie_breaks_follow_hamming_crc_then_fingerprint() -> None: - seed = bytes(range(16)) - fingerprint = MasterSeed.from_seed(seed, identifier=_fingerprint_identifier(_fake_fingerprint(seed))) + seed = bytes.fromhex(VECTOR_1["secret_hex"]) + fingerprint = MasterSeed.from_seed(seed, identifier=_fingerprint_identifier(core_fingerprint(seed))) mismatch = MasterSeed.from_seed(seed, identifier="test") high_hamming = CorrectionCandidate(mismatch, (), 10, 0, 3, True) crc = CorrectionCandidate(mismatch, (), 10, 0, 2, True) fingerprint_match = CorrectionCandidate(fingerprint, (), 10, 0, 2, True) assert len(_primary((high_hamming, crc, fingerprint_match))) == 3 - matcher = _fingerprint_matcher(lambda secret: _fake_fingerprint(secret.seed_bytes)) + matcher = _fingerprint_matcher(lambda secret: core_fingerprint(secret.seed_bytes)) assert _best((high_hamming, crc, fingerprint_match), fingerprint_match=matcher) == (fingerprint_match,) diff --git a/tests/test_generation.py b/tests/test_generation.py index c99a716..703de6f 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -6,7 +6,7 @@ import pytest from _codex32_oracle import oracle_encode -from data.bip93_vectors import VECTOR_2, VECTOR_4, VECTOR_6 +from data.bip93_vectors import VECTOR_1, VECTOR_2, VECTOR_3, VECTOR_4, VECTOR_6 from data.sharing_vectors import SHARING_VECTORS from hypothesis import given, settings from hypothesis import strategies as st @@ -33,6 +33,7 @@ ) from codex32.generation import ORDINARY_INDICES, _fingerprint_identifier from codex32.profiles.ms32 import SEED_BYTE_LENGTHS, _has_generation_padding +from tools._wallet_test_vectors import core_fingerprint, stub_fingerprint def _complete(ceremony: CreationCeremony) -> tuple[MasterSeed | CoreLightningSecret, tuple[Share, ...]]: @@ -56,19 +57,24 @@ def _seed(byte_length: int) -> bytes: return bytes((position * 109 + byte_length) % 256 for position in range(byte_length)) -def _fake_fingerprint(seed: bytes) -> bytes: - return seed[:4] - - def test_fresh_unshared_ms_supports_every_bip93_size() -> None: for byte_length in SEED_BYTE_LENGTHS: - secret = generate_master_seed(byte_length=byte_length, fingerprint=_fake_fingerprint) + secret = generate_master_seed(byte_length=byte_length, fingerprint=stub_fingerprint) assert len(secret.seed_bytes) == byte_length assert secret.header.threshold == 0 - assert secret.header.identifier == _fingerprint_identifier(_fake_fingerprint(secret.seed_bytes)) + assert secret.header.identifier == _fingerprint_identifier(stub_fingerprint(secret.seed_bytes)) assert _has_generation_padding(secret) +@pytest.mark.parametrize( + ("vector", "expected"), + ((VECTOR_1, "8u6j"), (VECTOR_2, "l2mg"), (VECTOR_3, "regv")), +) +def test_unshared_identifier_from_core_fingerprint(vector: dict[str, str], expected: str) -> None: + seed = bytes.fromhex(vector["secret_hex"]) + assert _fingerprint_identifier(core_fingerprint(seed)) == expected + + def test_fresh_shared_ms_supports_every_bip93_size() -> None: for byte_length in SEED_BYTE_LENGTHS: secret, shares = _complete( diff --git a/tools/_wallet_test_vectors.py b/tools/_wallet_test_vectors.py new file mode 100644 index 0000000..fb2c2bd --- /dev/null +++ b/tools/_wallet_test_vectors.py @@ -0,0 +1,20 @@ +"""Bitcoin Core-derived public wallet fixtures used by tests and integration checks.""" + +CORE_FINGERPRINTS = { + bytes.fromhex("318c6318c6318c6318c6318c6318c631"): bytes.fromhex("3f3521a6"), + bytes.fromhex("d1808e096b35b209ca12132b264662a5"): bytes.fromhex("fab6868a"), + bytes.fromhex("ffeeddccbbaa99887766554433221100"): bytes.fromhex("1e50c111"), +} + + +def core_fingerprint(seed: bytes) -> bytes: + """Return a frozen fingerprint that the real-Core regtest verifies.""" + try: + return CORE_FINGERPRINTS[seed] + except KeyError as error: + raise AssertionError("missing Bitcoin Core fingerprint fixture") from error + + +def stub_fingerprint(seed: bytes) -> bytes: + """Return real fixture data when known, otherwise a stable test-double value.""" + return CORE_FINGERPRINTS.get(seed, b"\x00\x00\x00\x01") diff --git a/tools/bitcoin_core_regtest.py b/tools/bitcoin_core_regtest.py index 4057e0e..d9a70d2 100644 --- a/tools/bitcoin_core_regtest.py +++ b/tools/bitcoin_core_regtest.py @@ -12,6 +12,8 @@ from pathlib import Path from typing import Any +from _wallet_test_vectors import CORE_FINGERPRINTS + from codex32._bitcoin_core import BitcoinCore from codex32.bip93 import parse_codex32 from codex32.profiles.ms32 import MasterSeed @@ -123,6 +125,9 @@ def rpc(*rpc_arguments: str, wallet: str | None = None, stdin: str | None = None if not isinstance(secret, MasterSeed): raise TypeError("synthetic fixture was not a master seed") client = BitcoinCore.connect() + for seed, expected_fingerprint in CORE_FINGERPRINTS.items(): + if client.fingerprint_seed(seed) != expected_fingerprint: + raise RuntimeError("Bitcoin Core fingerprint fixture mismatch") answers = iter(("yes",)) if ( client.initialize( From 341ccc73db2bedd960f51d3173652486fbe176b9 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Fri, 18 Sep 2026 15:43:49 -0500 Subject: [PATCH 3/3] Tests: keep wallet fingerprints seed-sensitive Separate wallet-derived fingerprints from BIP93 vector semantics while retaining one central fixture table. Unknown seeds now use a deterministic seed-sensitive test double, so generation tests still catch passing the wrong seed to the fingerprint callback without restoring the removed Python wallet dependency. Real Core-derived fixtures remain the oracle for known seeds; tools/bitcoin_core_regtest.py verifies those values. CI integration of that verifier remains tracked separately in #9. refs #3 Validation: 336 focused tests passed; ruff check passed; git diff --check passed. --- tests/test_correction_indel.py | 4 +-- tests/test_generation.py | 12 ++++----- tests/test_generic_hrp.py | 3 ++- tools/_wallet_test_vectors.py | 47 +++++++++++++++++++++++++++++++--- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/tests/test_correction_indel.py b/tests/test_correction_indel.py index eb64dda..05d6d97 100644 --- a/tests/test_correction_indel.py +++ b/tests/test_correction_indel.py @@ -32,7 +32,7 @@ _search_target, ) from codex32.profiles.ms32 import TEXT_LENGTHS -from tools._wallet_test_vectors import core_fingerprint +from tools._wallet_test_vectors import FIXTURE_SEED, core_fingerprint from tools.correction_capture import cross_length_classes SOURCE = VECTOR_1["secret_s"] @@ -410,7 +410,7 @@ def test_duplicate_reconstruction_keeps_lower_hamming_path() -> None: def test_cli_tie_breaks_follow_hamming_crc_then_fingerprint() -> None: - seed = bytes.fromhex(VECTOR_1["secret_hex"]) + seed = FIXTURE_SEED[16] fingerprint = MasterSeed.from_seed(seed, identifier=_fingerprint_identifier(core_fingerprint(seed))) mismatch = MasterSeed.from_seed(seed, identifier="test") high_hamming = CorrectionCandidate(mismatch, (), 10, 0, 3, True) diff --git a/tests/test_generation.py b/tests/test_generation.py index 703de6f..18ff9a2 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -6,7 +6,7 @@ import pytest from _codex32_oracle import oracle_encode -from data.bip93_vectors import VECTOR_1, VECTOR_2, VECTOR_3, VECTOR_4, VECTOR_6 +from data.bip93_vectors import VECTOR_2, VECTOR_4, VECTOR_6 from data.sharing_vectors import SHARING_VECTORS from hypothesis import given, settings from hypothesis import strategies as st @@ -33,7 +33,7 @@ ) from codex32.generation import ORDINARY_INDICES, _fingerprint_identifier from codex32.profiles.ms32 import SEED_BYTE_LENGTHS, _has_generation_padding -from tools._wallet_test_vectors import core_fingerprint, stub_fingerprint +from tools._wallet_test_vectors import FIXTURE_SEED, core_fingerprint, stub_fingerprint def _complete(ceremony: CreationCeremony) -> tuple[MasterSeed | CoreLightningSecret, tuple[Share, ...]]: @@ -67,11 +67,11 @@ def test_fresh_unshared_ms_supports_every_bip93_size() -> None: @pytest.mark.parametrize( - ("vector", "expected"), - ((VECTOR_1, "8u6j"), (VECTOR_2, "l2mg"), (VECTOR_3, "regv")), + ("byte_length", "expected"), + ((16, "3mga"), (20, "ex5l"), (24, "fhrx"), (28, "5hu7"), (32, "jef7"), (64, "48dx")), ) -def test_unshared_identifier_from_core_fingerprint(vector: dict[str, str], expected: str) -> None: - seed = bytes.fromhex(vector["secret_hex"]) +def test_unshared_identifier_from_core_fingerprint(byte_length: int, expected: str) -> None: + seed = FIXTURE_SEED[byte_length] assert _fingerprint_identifier(core_fingerprint(seed)) == expected diff --git a/tests/test_generic_hrp.py b/tests/test_generic_hrp.py index 032819b..c4dc3c4 100644 --- a/tests/test_generic_hrp.py +++ b/tests/test_generic_hrp.py @@ -19,6 +19,7 @@ ) from codex32.cli import main, ms_main from codex32.errors import MismatchedHrp, MismatchedProfile +from tools._wallet_test_vectors import STUB_FINGERPRINT UNKNOWN = { "short": { @@ -170,7 +171,7 @@ def test_cli_share_is_generic_and_ms32_share_is_scoped(monkeypatch) -> None: class _FakeCore: @staticmethod def fingerprint(_secret: object) -> bytes: - return b"\0\0\0\0" + return STUB_FINGERPRINT monkeypatch.setattr("codex32.cli.BitcoinCore.connect", lambda *args, **kwargs: _FakeCore()) basis = UNKNOWN["short"]["S"] + "\n" + UNKNOWN["short"]["A"] + "\n" diff --git a/tools/_wallet_test_vectors.py b/tools/_wallet_test_vectors.py index fb2c2bd..aa54914 100644 --- a/tools/_wallet_test_vectors.py +++ b/tools/_wallet_test_vectors.py @@ -1,11 +1,47 @@ -"""Bitcoin Core-derived public wallet fixtures used by tests and integration checks.""" +"""Bitcoin Core-derived public wallet fixtures used by tests and integration checks. -CORE_FINGERPRINTS = { +Everything here is a BIP32 master fingerprint, which is a wallet property and not a +codex32 one. Some seeds below also appear as BIP93 test vectors, but only as convenient +public seed material: BIP93 assigns those vectors identifiers of its own (``test``, +``name``, ``cash``) that have nothing to do with these fingerprints. An identifier +derived from a fingerprint is therefore never a BIP93-specified value, so assert such +identifiers against ``FIXTURE_SEED`` rather than against vector material. + +``tools/bitcoin_core_regtest.py`` verifies every fingerprint below against real Core. +""" + +import hashlib + +# BIP93 vector seeds, reused here only as wallet fixtures. +_VECTOR_FINGERPRINTS = { bytes.fromhex("318c6318c6318c6318c6318c6318c631"): bytes.fromhex("3f3521a6"), bytes.fromhex("d1808e096b35b209ca12132b264662a5"): bytes.fromhex("fab6868a"), bytes.fromhex("ffeeddccbbaa99887766554433221100"): bytes.fromhex("1e50c111"), } +# Arbitrary fixtures, one per BIP93 seed length, carrying no test-vector meaning. +_FIXTURE_FINGERPRINTS = { + bytes.fromhex("107dea57c4319e0b78e552bf2c990673"): bytes.fromhex("8ed1dab8"), + bytes.fromhex("1481ee5bc835a20f7ce956c3309d0a77e451be2b"): bytes.fromhex("c9a9f9c8"), + bytes.fromhex("1885f25fcc39a61380ed5ac734a10e7be855c22f9c0976e3"): bytes.fromhex("4dc66f0e"), + bytes.fromhex("1c89f663d03daa1784f15ecb38a5127fec59c633a00d7ae754c12e9b"): bytes.fromhex("a5f9e972"), + bytes.fromhex("208dfa67d441ae1b88f562cf3ca91683f05dca37a4117eeb58c5329f0c79e653"): bytes.fromhex( + "9653e4ec" + ), + bytes.fromhex( + "40ad1a87f461ce3ba81582ef5cc936a3107dea57c4319e0b78e552bf2c990673" + "e04dba2794016edb48b5228ffc69d643b01d8af764d13eab1885f25fcc39a613" + ): bytes.fromhex("a9da6294"), +} + +CORE_FINGERPRINTS = _VECTOR_FINGERPRINTS | _FIXTURE_FINGERPRINTS + +# Arbitrary fixture seed by byte length; excludes vector material by construction. +FIXTURE_SEED = {len(seed): seed for seed in _FIXTURE_FINGERPRINTS} + +# Stands in where the fingerprint value itself is irrelevant to the test. +STUB_FINGERPRINT = b"\x00\x00\x00\x01" + def core_fingerprint(seed: bytes) -> bytes: """Return a frozen fingerprint that the real-Core regtest verifies.""" @@ -16,5 +52,8 @@ def core_fingerprint(seed: bytes) -> bytes: def stub_fingerprint(seed: bytes) -> bytes: - """Return real fixture data when known, otherwise a stable test-double value.""" - return CORE_FINGERPRINTS.get(seed, b"\x00\x00\x00\x01") + """Return real fixture data when known, otherwise a seed-sensitive test double.""" + fixture = CORE_FINGERPRINTS.get(seed) + if fixture is not None: + return fixture + return hashlib.sha256(b"codex32 test fingerprint\0" + seed).digest()[:4]