Skip to content

Commit 43af20d

Browse files
committed
wallet: Privatize Core descriptor records
The package-level core_descriptors adapter exposes import-record construction that runtime callers no longer need. Bitcoin Core already owns public derivation, while private descriptor construction is only an implementation detail of the Core adapter. Remove the unused public-deriver protocol and branch, keep the record builder private, and make installed/public-API checks enforce that boundary. Internal tests and verification tools continue to exercise the same fixed descriptor templates and arbitrary account handling. Fixes #63
1 parent cebecfc commit 43af20d

7 files changed

Lines changed: 32 additions & 142 deletions

File tree

‎docs/developer/api.md‎

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ generic parse-length failure.
100100
hidden state.
101101

102102
Private Python names are convention rather than access control. The supported
103-
surface is the 25-name package `__all__`; direct use of private helpers is
103+
surface is the 23-name package `__all__`; direct use of private helpers is
104104
unsupported but remains in the review scope.
105105

106106
### Size budget
@@ -580,27 +580,24 @@ Public wallet operations accept only a validated `MasterSeed`. `wallet.py` is
580580
stateless and never accepts shares, Core Lightning secrets, BIP39 migration
581581
artifacts, or raw bytes.
582582

583-
The public adapter has two functions:
584-
585-
- `master_xprv(secret, testnet=False)` returns the BIP32 root extended private
586-
key.
587-
- `core_descriptors(...)` returns fixed BIP44, BIP49, BIP84, and BIP86 Bitcoin
588-
Core `importdescriptors` records. Private records use stdlib-only root xprv
589-
serialization; public records require an explicit wallet integration and the
590-
Core wallet whose imported root key will perform hardened derivation.
583+
The supported package surface exposes one wallet primitive:
584+
`master_xprv(secret, testnet=False)`, which returns the BIP32 root extended
585+
private key. Bitcoin Core descriptor-record construction is an internal
586+
test/reference detail rather than a supported package API.
591587

592588
No installed Python dependency performs secp256k1 operations. The private
593589
Bitcoin Core adapter gives Core the root xprv over stdin and asks Core to
594-
create the four standard account-0 descriptor types. Public descriptor
595-
derivation remains available through the explicit integration API.
590+
create the four standard account-0 descriptor types. Descriptor normalization
591+
and public derivation stay behind that private Core boundary.
596592

597593
Public descriptors contain account xpubs. Private descriptors intentionally
598594
follow Bitcoin Core's root-key form: they contain the root xprv followed by the
599595
complete derivation path. They therefore grant authority over the entire root,
600596
not only the selected account. The CLI warns before printing them.
601597

602-
Account, private/public mode, network serialization, and timestamp are explicit
603-
API inputs. The `ms32 wallet` CLI takes `--account 0` and `--timestamp`; the
598+
Account and timestamp remain explicit at the Core boundary, while network
599+
serialization is explicit for `master_xprv`. The `ms32 wallet` CLI takes
600+
`--account 0` and `--timestamp`; the
604601
selected Bitcoin Core chain is authoritative and there is no wallet
605602
`--testnet` flag. `ms32 xprv --testnet` remains explicit because it directly
606603
selects xprv versus tprv serialization. The timestamp defaults to `0` so

‎src/codex32/__init__.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from .profiles.bip39 import Bip39Secret
2828
from .profiles.cl32 import CoreLightningSecret
2929
from .profiles.ms32 import MasterSeed
30-
from .wallet import core_descriptors, master_xprv
30+
from .wallet import master_xprv
3131

3232
__all__ = [
3333
"Bip39Secret",
@@ -45,7 +45,6 @@
4545
"Secret",
4646
"Share",
4747
"WorksheetCorrection",
48-
"core_descriptors",
4948
"correct",
5049
"correct_worksheet_residue",
5150
"derive_share",

‎src/codex32/wallet.py‎

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Bitcoin wallet interoperability for validated master seeds."""
22

3-
from typing import Literal, Protocol
3+
from typing import Literal
44

55
from codex32._bip32 import _master_xprv_from_seed
66
from codex32.bech32 import _u5_to_chars
@@ -18,21 +18,6 @@
1818
)
1919

2020

21-
class WalletPublicDeriver(Protocol):
22-
"""Out-of-process provider for EC-dependent BIP32 public derivation."""
23-
24-
def fingerprint(self, secret: MasterSeed) -> bytes: ...
25-
26-
def public_descriptors(
27-
self,
28-
secret: MasterSeed,
29-
*,
30-
wallet: str,
31-
account: int = 0,
32-
timestamp: int | Literal["now"] = 0,
33-
) -> tuple[dict[str, object], ...]: ...
34-
35-
3621
def _master(secret: MasterSeed) -> MasterSeed:
3722
if not isinstance(secret, MasterSeed):
3823
raise TypeError("wallet operations accept only MasterSeed")
@@ -86,42 +71,22 @@ def master_xprv(secret: MasterSeed, *, testnet: bool = False) -> str:
8671
return _master_xprv_from_seed(_master(secret).seed_bytes, testnet=testnet)
8772

8873

89-
def core_descriptors(
74+
def _core_descriptors(
9075
secret: MasterSeed,
9176
*,
92-
integration: WalletPublicDeriver | None = None,
93-
wallet: str | None = None,
9477
account: int = 0,
9578
testnet: bool = False,
96-
private: bool = False,
9779
timestamp: int | Literal["now"] = 0,
9880
) -> tuple[dict[str, object], ...]:
99-
"""Return fixed Bitcoin Core records.
100-
101-
Private records are constructed with stdlib-only root xprv serialization.
102-
Public records require an explicit out-of-process integration provider.
103-
"""
81+
"""Return fixed private descriptor records for verification tooling."""
10482
_master(secret)
10583
account = _account(account)
10684
if not isinstance(testnet, bool):
10785
raise TypeError("testnet must be bool")
108-
if not isinstance(private, bool):
109-
raise TypeError("private must be bool")
11086
if timestamp != "now" and (
11187
isinstance(timestamp, bool) or not isinstance(timestamp, int) or timestamp < 0
11288
):
11389
raise ValueError("timestamp must be a nonnegative integer or 'now'")
114-
if not private:
115-
if integration is None:
116-
raise TypeError("public descriptors require a wallet integration")
117-
if wallet is None:
118-
raise TypeError("public descriptors require a Bitcoin Core wallet name")
119-
return integration.public_descriptors(
120-
secret,
121-
wallet=wallet,
122-
account=account,
123-
timestamp=timestamp,
124-
)
12590
coin_type = int(testnet)
12691
xprv = master_xprv(secret, testnet=testnet)
12792
keys = []

‎tests/test_public_api.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ def test_checksum_completion_is_not_public_api() -> None:
5959
assert not hasattr(codex32, "complete_checksum")
6060

6161

62+
def test_bitcoin_core_descriptor_records_are_not_public_api() -> None:
63+
assert "core_descriptors" not in codex32.__all__
64+
assert not hasattr(codex32, "core_descriptors")
65+
66+
6267
def test_share_has_symbols_but_no_byte_or_padding_api() -> None:
6368
share = parse_codex32(VECTOR_2["share_A"])
6469
assert isinstance(share, Share)

‎tests/test_wallet.py‎

Lines changed: 7 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,57 +4,8 @@
44
from data.bip93_vectors import VECTOR_1, VECTOR_2, VECTOR_3, VECTOR_4, VECTOR_5
55
from data.sharing_vectors import SHARING_VECTORS
66

7-
from codex32 import (
8-
MasterSeed,
9-
core_descriptors,
10-
master_xprv,
11-
parse_codex32,
12-
)
13-
from codex32.wallet import _with_checksum
14-
15-
_MAIN_DESCRIPTORS = (
16-
(
17-
"pkh([3f3521a6/44h/0h/0h]"
18-
"xpub6CeZ5XxHp6rXSwi2GCi7UT25rswWQtoPvj36MbzRBr3QEoEmBFNGgnMy329ZMk"
19-
"fjRKBZHtKKpYfpkrPWohTjHZZn7y1NR9EHnojaGLKdMAR/<0;1>/*)#smv8ra2a"
20-
),
21-
(
22-
"sh(wpkh([3f3521a6/49h/0h/0h]"
23-
"xpub6D9YUddFXuNKQvNrT9RQh8ueiTvHwF3RzdgU6uTEri73WTnBpKaDCGhTUiPBTy"
24-
"VJxtR5u2atDmCHE7tw369ahXddCNqJBxFpseud3j7pjX8/<0;1>/*))#gylcnnd3"
25-
),
26-
(
27-
"wpkh([3f3521a6/84h/0h/0h]"
28-
"xpub6CNhWVRpA49Bz3LSaBibGqfBV4qa5NH1CStbQfsxWKScwrws5jioMunWKj2uM2"
29-
"rrfdJSroNuJBNDUmmdYXQw5LwVro39pH5nqEgAqrzTPyc/<0;1>/*)#zy06y40v"
30-
),
31-
(
32-
"tr([3f3521a6/86h/0h/0h]"
33-
"xpub6C5pT77VWNhWvrB3TqSEbpm7NCpMYEzbJreYbB68RCUoAMkT7rdhafinmdKL4M5"
34-
"275TyDqNAWCnssYnDNaPoXMiAg3sWvCgAiYqY8dHk1k4/<0;1>/*)#r8r04qrm"
35-
),
36-
)
37-
38-
39-
class _FakePublicDeriver:
40-
def fingerprint(self, secret: MasterSeed) -> bytes:
41-
del secret
42-
return bytes.fromhex("3f3521a6")
43-
44-
def public_descriptors(
45-
self,
46-
secret: MasterSeed,
47-
*,
48-
wallet: str,
49-
account: int = 0,
50-
timestamp: int | str = 0,
51-
) -> tuple[dict[str, object], ...]:
52-
del secret
53-
if wallet != "signer":
54-
raise AssertionError("unexpected wallet")
55-
if account != 0:
56-
raise AssertionError("unexpected frozen descriptor request")
57-
return tuple({"desc": desc, "active": True, "timestamp": timestamp} for desc in _MAIN_DESCRIPTORS)
7+
from codex32 import MasterSeed, master_xprv, parse_codex32
8+
from codex32.wallet import _core_descriptors, _with_checksum
589

5910

6011
def _master() -> MasterSeed:
@@ -71,29 +22,8 @@ def test_master_xprv_matches_bip93_vectors(vector: dict[str, str]) -> None:
7122
assert master_xprv(secret) == vector["xprv"]
7223

7324

74-
def test_public_core_descriptors_are_fixed_and_private_free() -> None:
75-
records = core_descriptors(_master(), integration=_FakePublicDeriver(), wallet="signer")
76-
77-
assert len(records) == 4
78-
assert [record["desc"].split("(", 1)[0] for record in records] == [
79-
"pkh",
80-
"sh",
81-
"wpkh",
82-
"tr",
83-
]
84-
assert all(record["active"] is True for record in records)
85-
assert all(record["timestamp"] == 0 for record in records)
86-
assert all("xpub" in str(record["desc"]) for record in records)
87-
assert all("xprv" not in str(record["desc"]) for record in records)
88-
assert records[0]["desc"] == (
89-
"pkh([3f3521a6/44h/0h/0h]"
90-
"xpub6CeZ5XxHp6rXSwi2GCi7UT25rswWQtoPvj36MbzRBr3QEoEmBFNGgnMy329ZMk"
91-
"fjRKBZHtKKpYfpkrPWohTjHZZn7y1NR9EHnojaGLKdMAR/<0;1>/*)#smv8ra2a"
92-
)
93-
94-
9525
def test_private_core_descriptors_use_root_xprv_and_explicit_inputs() -> None:
96-
records = core_descriptors(_master(), account=3, testnet=True, private=True, timestamp=123)
26+
records = _core_descriptors(_master(), account=3, testnet=True, timestamp=123)
9727

9828
assert all(record["timestamp"] == 123 for record in records)
9929
for purpose, record in zip((44, 49, 84, 86), records, strict=True):
@@ -103,12 +33,7 @@ def test_private_core_descriptors_use_root_xprv_and_explicit_inputs() -> None:
10333

10434

10535
def test_core_descriptors_accept_bitcoin_core_now_timestamp() -> None:
106-
assert all(
107-
record["timestamp"] == "now"
108-
for record in core_descriptors(
109-
_master(), timestamp="now", integration=_FakePublicDeriver(), wallet="signer"
110-
)
111-
)
36+
assert all(record["timestamp"] == "now" for record in _core_descriptors(_master(), timestamp="now"))
11237

11338

11439
def test_descriptor_checksum_matches_published_example() -> None:
@@ -125,18 +50,18 @@ def test_descriptor_checksum_matches_published_example() -> None:
12550
),
12651
)
12752
def test_wallet_boundary_rejects_every_non_master_seed(invalid: object) -> None:
128-
for function in (master_xprv, core_descriptors):
53+
for function in (master_xprv, _core_descriptors):
12954
with pytest.raises(TypeError, match="only MasterSeed"):
13055
function(invalid) # type: ignore[arg-type]
13156

13257

13358
@pytest.mark.parametrize("account", (-1, 2**31, True, "0"))
13459
def test_account_is_explicitly_bounded(account: object) -> None:
13560
with pytest.raises((TypeError, ValueError)):
136-
core_descriptors(_master(), account=account, integration=_FakePublicDeriver(), wallet="signer") # type: ignore[arg-type]
61+
_core_descriptors(_master(), account=account) # type: ignore[arg-type]
13762

13863

13964
@pytest.mark.parametrize("timestamp", (-1, True, "yesterday"))
14065
def test_timestamp_is_a_supported_core_value(timestamp: object) -> None:
14166
with pytest.raises((TypeError, ValueError)):
142-
core_descriptors(_master(), timestamp=timestamp) # type: ignore[arg-type]
67+
_core_descriptors(_master(), timestamp=timestamp) # type: ignore[arg-type]

‎tools/bitcoin_core_regtest.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from codex32._bitcoin_core import BitcoinCore
1919
from codex32.bip93 import parse_codex32
2020
from codex32.profiles.ms32 import MasterSeed
21-
from codex32.wallet import core_descriptors
21+
from codex32.wallet import _core_descriptors
2222

2323
# Frozen public BIP93 vector material; it has never controlled a funded wallet.
2424
_SEED = "ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"
@@ -225,8 +225,8 @@ def rpc(*rpc_arguments: str, wallet: str | None = None, stdin: str | None = None
225225
if rpc("gettransaction", spend, wallet="restore")["confirmations"] < 1:
226226
raise RuntimeError("recovered wallet did not sign and broadcast")
227227

228-
main_private = core_descriptors(secret, private=True, timestamp=0)
229-
test_private = core_descriptors(secret, testnet=True, private=True, timestamp=0)
228+
main_private = _core_descriptors(secret, timestamp=0)
229+
test_private = _core_descriptors(secret, testnet=True, timestamp=0)
230230
if "xprv" not in json.dumps(main_private) or "tprv" not in json.dumps(test_private):
231231
raise RuntimeError("mainnet/test-network root serialization was not separated")
232232

‎tools/verify_installed_wheel.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
import importlib.util
66
import sys
77

8+
import codex32
89
from codex32 import (
910
CorrectionContext,
1011
CreationCeremony,
1112
MasterSeed,
1213
Profile,
13-
core_descriptors,
1414
correct,
1515
derive_share,
1616
master_xprv,
@@ -45,9 +45,8 @@ def main() -> None:
4545
secret = parse_codex32(_SECRET)
4646
assert isinstance(secret, MasterSeed)
4747
assert master_xprv(secret) == _XPRV
48-
private = core_descriptors(secret, private=True)
49-
assert len(private) == 4
50-
assert all("xprv" in record["desc"] for record in private)
48+
assert "core_descriptors" not in codex32.__all__
49+
assert not hasattr(codex32, "core_descriptors")
5150
assert "bip32" not in sys.modules
5251
assert "coincurve" not in sys.modules
5352

0 commit comments

Comments
 (0)