Skip to content

Commit b6a6de7

Browse files
claudeBenWestgate
authored andcommitted
api: Remove unused public entry points
core_descriptors(private=False) asked a named Bitcoin Core wallet for its HD key and built public descriptors from it, but only type-checked the seed it was given, so a wallet holding a different seed returned that wallet's descriptors. Nothing calls it: ms32 wallet hands Core the root xprv with addhdkey and Core builds the descriptors itself with createwalletdescriptor, and Core's listdescriptors and exportwatchonlywallet already provide public descriptors. We are pre-1.0, so delete public API that no command or tool calls instead of maintaining it: - core_descriptors, the WalletPublicDeriver protocol, the descriptor checksum (DESCSUM) and record helpers, and the Core adapter's public_descriptors, gethdkeys and derivehdkey calls; - generate_core_lightning_secret and CreationCeremony.core_lightning, which created fresh Core Lightning secrets that no command creates. Parsing, recovering, deriving and re-sharing an existing cl secret are unchanged. The real-Core fixture and installed-wheel smoke test now check xprv and tprv serialization through master_xprv. The package exports 22 names. The installed package drops from 5,092 to 4,868 logical lines. Fixes #128 Refs #63 Claude-Session: https://claude.ai/code/session_01T233rKgZqE5wzDm3EVTHL1
1 parent 1eed32c commit b6a6de7

12 files changed

Lines changed: 45 additions & 454 deletions

File tree

‎docs/developer/api.md‎

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ generic parse-length failure.
6767
`.text` attribute.
6868
- Sharing interpolates payload and checksum together, explicitly constructs the
6969
target header, and reparses the result.
70-
- `generation.py` is the only entropy owner and generates only `ms` and `cl`.
70+
- `generation.py` is the only entropy owner and generates only `ms`; it can
71+
also share an existing `cl` secret.
7172
- Correction never edits the HRP or separator and reparses every candidate.
7273
- `_bip32.py` stops at HMAC-SHA512 root derivation, scalar validity, and root
7374
xprv/tprv Base58Check serialization. It performs no child derivation or
74-
secp256k1 point arithmetic. `wallet.py` accepts only `MasterSeed`; EC-dependent
75-
public derivation is supplied through an explicit wallet integration and the
76-
CLI uses Bitcoin Core for that boundary.
75+
secp256k1 point arithmetic. `wallet.py` accepts only `MasterSeed`; Bitcoin
76+
Core performs all EC-dependent derivation.
7777
- `_cli_input.py` retains at most nine artifacts and delegates partial-set
7878
compatibility to `bip93.py`. Card confirmation clears the terminal and saved
7979
scrollback where supported, then displays only entered text after a mismatch.
@@ -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 22-name package `__all__`; direct use of private helpers is
104104
unsupported but remains in the review scope.
105105

106106
### Size budget
@@ -154,39 +154,37 @@ recovery, and share derivation. The `ms32` façade accepts only `ms` artifacts.
154154
## Secret generation
155155

156156
`generation.py` is the only module that draws entropy. It generates BIP93
157-
master seeds and Core Lightning HSM secrets, and splits either validated S type.
157+
master seeds and splits a validated master seed or Core Lightning HSM secret.
158158
Core Lightning now defaults to mnemonic recovery, but retains a codex32 HSM
159159
secret import path for recovery on an unused node.
160160

161161
Fresh unshared seeds default to 16 bytes and use the first 20 bits of their
162162
BIP32 fingerprint as public identifier metadata. Fresh shared sets use four
163-
independent random u5 identifier symbols. Raw bytes, re-shared secrets, and CL
164-
generation also use an independent random identifier unless one is supplied.
163+
independent random u5 identifier symbols. Raw bytes and re-shared secrets also
164+
use an independent random identifier unless one is supplied.
165165
Random re-sharing never repeats the source set header; an explicitly repeated
166166
source header is rejected.
167167

168168
The Python API and CLI accept the six PR #2258 `ms` sizes: 16, 20, 24, 28, 32,
169169
and 64 bytes. Other byte lengths are rejected at every public construction
170170
boundary; there is no legacy decoder.
171171

172-
One-shot functions create only unshared secrets:
172+
The one-shot function creates only unshared secrets:
173173

174174
```python
175175
generate_master_seed(seed_bytes=None, *, byte_length=None, identifier=None)
176-
generate_core_lightning_secret(secret_bytes=None, *, identifier=None)
177176
```
178177

179-
Shared creation uses `CreationCeremony.master_seed(...)`,
180-
`CreationCeremony.core_lightning(...)`, or
178+
Shared creation uses `CreationCeremony.master_seed(...)` or
181179
`CreationCeremony.from_secret(...)`. Exactly one of `share_count` and `indices`
182180
is required. `next_share()` returns one pending share, `confirm(text)` must
183181
accept its independently re-entered string, and `finish()` returns the secret
184182
only after every requested share is confirmed. There is no public one-shot
185183
sharing or `split_secret` function. Fresh and existing Bitcoin CLI creation
186184
requires interactive input and output, preflights local Bitcoin Core before
187185
entropy or recovery input, and initializes a user-selected wallet after every
188-
share is confirmed. CLI creation does not accept Core Lightning profiles; CL
189-
generation and sharing remain API-only.
186+
share is confirmed. CLI creation does not accept Core Lightning profiles; sharing
187+
an existing CL secret remains API-only.
190188
Without `--existing`, omitting the Bitcoin header creates an unshared master
191189
seed. With `--existing` and no sharing threshold, a supplied codex32 secret is
192190
emitted and confirmed unchanged, and the original validated artifact initializes
@@ -580,27 +578,17 @@ Public wallet operations accept only a validated `MasterSeed`. `wallet.py` is
580578
stateless and never accepts shares, Core Lightning secrets, BIP39 migration
581579
artifacts, or raw bytes.
582580

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.
581+
The public adapter has one function: `master_xprv(secret, testnet=False)`
582+
returns the BIP32 root extended private key, using stdlib-only serialization.
583+
It grants authority over every key derived from the seed, and the CLI warns
584+
before printing it.
591585

592586
No installed Python dependency performs secp256k1 operations. The private
593587
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.
596-
597-
Public descriptors contain account xpubs. Private descriptors intentionally
598-
follow Bitcoin Core's root-key form: they contain the root xprv followed by the
599-
complete derivation path. They therefore grant authority over the entire root,
600-
not only the selected account. The CLI warns before printing them.
588+
create the four standard account-0 descriptor types. Core's own wallet
589+
commands provide public descriptors and watch-only exports.
601590

602-
Account, private/public mode, network serialization, and timestamp are explicit
603-
API inputs. The `ms32 wallet` CLI takes `--account 0` and `--timestamp`; the
591+
The `ms32 wallet` CLI takes `--account 0` and `--timestamp`; the
604592
selected Bitcoin Core chain is authoritative and there is no wallet
605593
`--testnet` flag. `ms32 xprv --testnet` remains explicit because it directly
606594
selects xprv versus tprv serialization. The timestamp defaults to `0` so
@@ -647,7 +635,7 @@ the BIP32 fingerprint.
647635

648636
The Core calls are fixed: `getnetworkinfo`, `getblockchaininfo`, `listwallets`,
649637
`getwalletinfo`, `listdescriptors`, `getdescriptorinfo`, `deriveaddresses`,
650-
`validateaddress`, `gethdkeys`, `derivehdkey`, `addhdkey`,
638+
`validateaddress`, `addhdkey`,
651639
`createwalletdescriptor`, `importdescriptors`, and `walletlock`.
652640
Bitcoin Core alone creates wallets, selects encryption, handles
653641
passphrases, stores keys, and provides normal wallet behavior.

‎src/codex32/__init__.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@
2020
from .generation import (
2121
ConfirmationResult,
2222
CreationCeremony,
23-
generate_core_lightning_secret,
2423
generate_master_seed,
2524
)
2625
from .profiles import Profile
2726
from .profiles.bip39 import Bip39Secret
2827
from .profiles.cl32 import CoreLightningSecret
2928
from .profiles.ms32 import MasterSeed
30-
from .wallet import core_descriptors, master_xprv
29+
from .wallet import master_xprv
3130

3231
__all__ = [
3332
"Bip39Secret",
@@ -45,11 +44,9 @@
4544
"Secret",
4645
"Share",
4746
"WorksheetCorrection",
48-
"core_descriptors",
4947
"correct",
5048
"correct_worksheet_residue",
5149
"derive_share",
52-
"generate_core_lightning_secret",
5350
"generate_master_seed",
5451
"master_xprv",
5552
"parse_codex32",

‎src/codex32/_bitcoin_core.py‎

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import hashlib
44
import json
5-
import re
65
import shutil
76
import string
87
import subprocess
@@ -15,7 +14,6 @@
1514
from codex32.bech32 import _u5_to_chars, convertbits
1615
from codex32.generation import _fingerprint_identifier
1716
from codex32.profiles.ms32 import MasterSeed
18-
from codex32.wallet import _descriptor_records
1917

2018

2119
class BitcoinCoreError(Exception):
@@ -34,9 +32,7 @@ class FingerprintMismatch(BitcoinCoreError):
3432
("regtest", "regtest"),
3533
)
3634

37-
_ORIGIN = re.compile(r"\[(?P<fingerprint>[0-9a-f]{8})(?P<path>(?:/[0-9]+[h']?)*)\]")
3835
_PRIVATE_MARKERS = ("xprv", "tprv")
39-
_PURPOSES = (44, 49, 84, 86)
4036
_OUTPUT_TYPES = ("legacy", "p2sh-segwit", "bech32", "bech32m")
4137

4238

@@ -218,79 +214,6 @@ def verify_identity(self, secret: MasterSeed, expected_fingerprint: bytes | None
218214
"Bitcoin Core was not changed."
219215
)
220216

221-
def _root_xpub(self, wallet: str) -> str:
222-
result = self._rpc("gethdkeys", wallet=wallet)
223-
if not isinstance(result, list) or len(result) != 1 or not isinstance(result[0], dict):
224-
raise BitcoinCoreError("Bitcoin Core did not return the expected wallet HD key.")
225-
xpub = result[0].get("xpub")
226-
prefix = "xpub" if self.chain == "main" else "tpub"
227-
if (
228-
result[0].get("has_private") is not True
229-
or not isinstance(xpub, str)
230-
or not xpub.startswith(prefix)
231-
):
232-
raise BitcoinCoreError("Bitcoin Core did not return the expected private wallet HD key.")
233-
return xpub
234-
235-
def _derived_key(self, wallet: str, root_xpub: str, path: str) -> tuple[bytes, str]:
236-
result = self._rpc(
237-
"-named",
238-
"derivehdkey",
239-
f"path=m{path}",
240-
f"hdkey={root_xpub}",
241-
wallet=wallet,
242-
)
243-
origin = result.get("origin") if isinstance(result, dict) else None
244-
xpub = result.get("xpub") if isinstance(result, dict) else None
245-
match = _ORIGIN.fullmatch(origin) if isinstance(origin, str) else None
246-
prefix = "xpub" if self.chain == "main" else "tpub"
247-
if match is None or not isinstance(xpub, str) or not xpub.startswith(prefix):
248-
raise BitcoinCoreError("Bitcoin Core did not return the expected derived HD key.")
249-
normalized_path = match.group("path").replace("'", "h")
250-
if normalized_path != path:
251-
raise BitcoinCoreError("Bitcoin Core returned an unexpected derivation path.")
252-
return bytes.fromhex(match.group("fingerprint")), f"{origin}{xpub}/<0;1>/*"
253-
254-
def public_descriptors(
255-
self,
256-
secret: MasterSeed,
257-
*,
258-
wallet: str,
259-
account: int = 0,
260-
timestamp: int | Literal["now"] = 0,
261-
) -> tuple[dict[str, object], ...]:
262-
"""Ask Core to derive account xpubs, then normalize their public descriptors."""
263-
if not isinstance(secret, MasterSeed):
264-
raise TypeError("wallet operations accept only MasterSeed")
265-
root_xpub = self._root_xpub(wallet)
266-
keys: list[str] = []
267-
expected_fingerprint: bytes | None = None
268-
for purpose in _PURPOSES:
269-
path = f"/{purpose}h/{int(self.chain != 'main')}h/{account}h"
270-
fingerprint, key = self._derived_key(wallet, root_xpub, path)
271-
if expected_fingerprint is None:
272-
expected_fingerprint = fingerprint
273-
elif fingerprint != expected_fingerprint:
274-
raise BitcoinCoreError("Bitcoin Core returned inconsistent master fingerprints.")
275-
keys.append(key)
276-
records = _descriptor_records(tuple(keys), timestamp) # type: ignore[arg-type]
277-
for record in records:
278-
detail = self._rpc("getdescriptorinfo", stdin=str(record["desc"]) + "\n")
279-
expansion = detail.get("multipath_expansion") if isinstance(detail, dict) else None
280-
if (
281-
not isinstance(detail, dict)
282-
or detail.get("hasprivatekeys") is not False
283-
or not isinstance(expansion, list)
284-
or len(expansion) != 2
285-
or not all(
286-
isinstance(descriptor, str)
287-
and not any(marker in descriptor for marker in _PRIVATE_MARKERS)
288-
for descriptor in expansion
289-
)
290-
):
291-
raise BitcoinCoreError("Bitcoin Core did not validate the expected public descriptor.")
292-
return records
293-
294217
def _target(self, name: str) -> tuple[bool, bool] | None:
295218
listing, info = self._rpc("listdescriptors", wallet=name), self._rpc("getwalletinfo", wallet=name)
296219
if not isinstance(info, dict) or not isinstance(listing, dict):

‎src/codex32/checksums.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Immutable checksum specifications used by codex32 and descriptors."""
1+
"""Immutable checksum specifications used by codex32."""
22

33
from dataclasses import dataclass
44

@@ -16,7 +16,6 @@
1616
0x0C577EAECCF1990D13C,
1717
0x1887F74F8DC71B10651,
1818
)
19-
_DESCSUM_GEN = (0xF5DEE51989, 0xA9FDCA3312, 0x1BAB10E32D, 0x3706B1677A, 0x644D626FFD)
2019

2120

2221
@dataclass(frozen=True, slots=True)
@@ -52,9 +51,6 @@ def create(self, values: list[int] | tuple[int, ...]) -> list[int]:
5251
_CODEX32 = _Checksum("codex32", _CODEX32_GEN, 13, 0x10CE0795C2FD1E62A, 93)
5352
_CODEX32_LONG = _Checksum("Long codex32", _CODEX32_LONG_GEN, 15, 0x43381E570BF4798AB26, 1023)
5453

55-
# Descriptor checksum remains an independently specified, non-codex32 helper.
56-
DESCSUM = _Checksum("Descriptor", _DESCSUM_GEN, 8, 1)
57-
5854
_CRC = (
5955
None,
6056
# ``_Checksum`` consumes input bits most-significant bit first. With its

‎src/codex32/generation.py‎

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,7 @@
2929
InvalidThreshold,
3030
)
3131
from codex32.profiles import Profile
32-
from codex32.profiles.cl32 import PAYLOAD_LENGTH as CL_PAYLOAD_LENGTH
33-
from codex32.profiles.cl32 import (
34-
CoreLightningSecret,
35-
_secret_from_bytes,
36-
)
32+
from codex32.profiles.cl32 import CoreLightningSecret
3733
from codex32.profiles.cl32 import (
3834
_has_generation_padding as _cl_padding,
3935
)
@@ -175,14 +171,6 @@ def generate_master_seed(
175171
return MasterSeed.from_seed(fresh, identifier=_fingerprint_identifier(fingerprint(fresh)))
176172

177173

178-
def generate_core_lightning_secret(
179-
secret_bytes: bytes | None = None, *, identifier: str | None = None
180-
) -> CoreLightningSecret:
181-
"""Generate or encode one unshared Core Lightning HSM secret."""
182-
identifier = _random_identifier() if identifier is None else _identifier(identifier)
183-
return _secret_from_bytes(secrets.token_bytes(32) if secret_bytes is None else secret_bytes, identifier)
184-
185-
186174
class CreationCeremony:
187175
"""Generate and confirm one shared-backup card at a time."""
188176

@@ -256,28 +244,6 @@ def master_seed(
256244
None,
257245
)
258246

259-
@classmethod
260-
def core_lightning(
261-
cls,
262-
*,
263-
threshold: int,
264-
share_count: int | None = None,
265-
indices: Sequence[str] | str | None = None,
266-
identifier: str | None = None,
267-
) -> CreationCeremony:
268-
"""Start a ceremony for a fresh shared Core Lightning secret."""
269-
threshold = _threshold(threshold, allow_zero=False)
270-
identifier = _random_identifier() if identifier is None else _identifier(identifier)
271-
return cls._start(
272-
Profile.CL,
273-
CL_PAYLOAD_LENGTH,
274-
threshold,
275-
share_count,
276-
indices,
277-
identifier,
278-
None,
279-
)
280-
281247
@classmethod
282248
def from_secret(
283249
cls,

‎src/codex32/profiles/cl32.py‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from __future__ import annotations
44

55
from codex32.bech32 import convertbits
6-
from codex32.bip93 import Header, Secret, _from_parts
6+
from codex32.bip93 import Secret
77
from codex32.errors import InvalidLength
88
from codex32.profiles import Profile
99

10-
SECRET_BYTES, PAYLOAD_LENGTH, TEXT_LENGTH = 32, 52, 74
10+
PAYLOAD_LENGTH, TEXT_LENGTH = 52, 74
1111

1212

1313
def _has_generation_padding(secret: CoreLightningSecret) -> bool:
@@ -25,21 +25,6 @@ def secret_bytes(self) -> bytes:
2525
return bytes(convertbits(self.payload_symbols, 5, 8, pad=False, accept_any_padding=True))
2626

2727

28-
def _secret_from_bytes(
29-
secret_bytes: bytes,
30-
identifier: str,
31-
threshold: int = 0,
32-
) -> CoreLightningSecret:
33-
if not isinstance(secret_bytes, bytes):
34-
raise TypeError("secret_bytes must be bytes")
35-
if len(secret_bytes) != SECRET_BYTES:
36-
raise InvalidLength("Core Lightning secrets must contain exactly 32 bytes")
37-
payload = tuple(convertbits(secret_bytes, 8, 5, pad=True, pad_value=0))
38-
artifact = _from_parts(Profile.CL, Header(threshold, identifier, "s"), payload)
39-
assert isinstance(artifact, CoreLightningSecret)
40-
return artifact
41-
42-
4328
class _Cl32Rules:
4429
profile, label = Profile.CL, "Core Lightning HSM secret"
4530
secret_type = CoreLightningSecret

0 commit comments

Comments
 (0)