Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,12 @@ 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
- run: python -m mypy src/codex32
- run: python -m ruff check .
- run: python -m ruff format --check .
- run: python tools/differential_correction.py --verify

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore automated wallet derivation coverage

In the only push/PR workflow, removing the wallet differential leaves both replacement real-Core checks (bitcoin_core_regtest.py and bitcoin_core_main_smoke.py) uninvoked; a repo-wide search finds them referenced only by documentation. The remaining Core unit fake exercises public derivation only for mainnet account 0, whereas the removed differential covered arbitrary accounts and both network key versions, so regressions in those wallet paths can now merge without external verification; add a Core integration job or retain an offline independent check.

AGENTS.md reference: AGENTS.md:L68-L73

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping this PR focused on removing the Python wallet test dependencies. Restoring real-Bitcoin-Core CI would add a new integration job and runner dependency; that follow-up is tracked explicitly in #9. The offline unit coverage here remains seed-sensitive, and the frozen real-Core fixtures can be verified with tools/bitcoin_core_regtest.py.

- run: python tools/differential_wallet.py --verify
- run: python -m build --no-isolation
- run: python tools/verify_wheel_environment.py
2 changes: 1 addition & 1 deletion docs/security/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
30 changes: 0 additions & 30 deletions requirements/test-wallet-dependencies.txt

This file was deleted.

10 changes: 5 additions & 5 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +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_reference import fingerprint_seed
from tools._wallet_test_vectors import stub_fingerprint


@dataclass(frozen=True)
Expand Down Expand Up @@ -98,7 +98,7 @@ class _FakeBitcoinCore:
timestamp: int | str | None = None

def fingerprint_seed(self, seed: bytes) -> bytes:
return fingerprint_seed(seed)
return stub_fingerprint(seed)

def fingerprint(self, secret: MasterSeed) -> bytes:
return self.fingerprint_seed(secret.seed_bytes)
Expand Down Expand Up @@ -1224,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(fingerprint_seed(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:
Expand Down Expand Up @@ -1360,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(fingerprint_seed(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(
Expand Down Expand Up @@ -1549,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(
fingerprint_seed(fingerprinted_secret.seed_bytes)
stub_fingerprint(fingerprinted_secret.seed_bytes)
)
assert unshared_secret.header.identifier == "test"
assert len(automatic) == 3
Expand Down
8 changes: 4 additions & 4 deletions tests/test_correction_indel.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
_search_target,
)
from codex32.profiles.ms32 import TEXT_LENGTHS
from tools._wallet_reference import fingerprint_seed
from tools._wallet_test_vectors import FIXTURE_SEED, core_fingerprint
from tools.correction_capture import cross_length_classes

SOURCE = VECTOR_1["secret_s"]
Expand Down Expand Up @@ -410,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(fingerprint_seed(seed)))
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)
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: core_fingerprint(secret.seed_bytes))
assert _best((high_hamming, crc, fingerprint_match), fingerprint_match=matcher) == (fingerprint_match,)


Expand Down
25 changes: 13 additions & 12 deletions tests/test_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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_reference import fingerprint_seed
from tools._wallet_test_vectors import FIXTURE_SEED, core_fingerprint, stub_fingerprint


def _complete(ceremony: CreationCeremony) -> tuple[MasterSeed | CoreLightningSecret, tuple[Share, ...]]:
Expand All @@ -59,13 +59,22 @@ def _seed(byte_length: int) -> bytes:

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=stub_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(stub_fingerprint(secret.seed_bytes))
assert _has_generation_padding(secret)


@pytest.mark.parametrize(
("byte_length", "expected"),
((16, "3mga"), (20, "ex5l"), (24, "fhrx"), (28, "5hu7"), (32, "jef7"), (64, "48dx")),
)
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


def test_fresh_shared_ms_supports_every_bip93_size() -> None:
for byte_length in SEED_BYTE_LENGTHS:
secret, shares = _complete(
Expand Down Expand Up @@ -395,11 +404,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
8 changes: 6 additions & 2 deletions tests/test_generic_hrp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -167,9 +168,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:
Comment thread
BenWestgate marked this conversation as resolved.
@staticmethod
def fingerprint(_secret: object) -> bytes:
return STUB_FINGERPRINT

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"], "")
Expand Down
72 changes: 0 additions & 72 deletions tools/_wallet_reference.py

This file was deleted.

59 changes: 59 additions & 0 deletions tools/_wallet_test_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Bitcoin Core-derived public wallet fixtures used by tests and integration checks.

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 = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move frozen wallet vectors into tests/data

The newly added seed/fingerprint maps are frozen Bitcoin Core-derived test vectors, but they are stored in an executable helper under tools/ instead of the repository’s designated tests/data/ location. Keeping oracle values beside lookup and stub logic makes them easier to change together and leaves the central frozen-data inventory incomplete; move the values into tests/data/ and have both the tests and regtest utility consume them from there.

AGENTS.md reference: AGENTS.md:L23-L24

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, consider opening another PR to check all the tests for this sort of data storage issue.

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."""
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 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]
5 changes: 5 additions & 0 deletions tools/bitcoin_core_regtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading