Skip to content
Merged
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
22 changes: 18 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,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
Expand All @@ -39,6 +36,23 @@ jobs:
- run: python -m ruff check .
- run: python -m ruff format --check .
- run: python tools/differential_correction.py --verify
Comment thread
BenWestgate marked this conversation as resolved.
- run: python tools/differential_wallet.py --verify
- run: python -m build --no-isolation
- run: python tools/verify_wheel_environment.py

compatibility:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.14", "3.15"]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: ${{ matrix.python-version == '3.15' }}
- 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 -e '.[dev]'
- run: python -m pip check
- run: python -m pytest -q
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ contains offline verification utilities.

## Development and verification

Use the existing virtual environment when available. Python 3.12 is the minimum;
CI also covers 3.13. Install development dependencies only when needed:
Use the existing virtual environment when available. Python 3.10 is the minimum;
the supported range is Python 3.10 through 3.15. Install development
dependencies only when needed:
`python -m pip install -e '.[dev]'`. Run the CLI with `codex32 --help`.

Choose checks according to the changed behavior:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ obtain an independent review before relying on it with funds. See

## Install

Python 3.12 or 3.13 is required. The installed package has no third-party
Python 3.10 through 3.15 is supported. The installed package has no third-party
runtime dependencies. To install it with the pinned build backend, run these
commands from the project folder:

Expand Down
63 changes: 31 additions & 32 deletions docs/developer/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,25 +588,24 @@ The public adapter has two functions:
Core wallet whose imported root key will perform hardened derivation.

No installed Python dependency performs secp256k1 operations. The private
Bitcoin Core adapter implements the wallet integration by sending root-xprv
descriptor material to `bitcoin-cli` over stdin and treating Core's returned
fingerprints, account xpubs, and normalized descriptors as untrusted external
data.
Bitcoin Core adapter gives Core the root xprv over stdin and asks Core to
create the four standard account-0 descriptor types. Public descriptor
derivation remains available through the explicit integration API.

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

Account, private/public mode, network serialization, and timestamp are explicit
API inputs. The `ms32 wallet` CLI takes only `--account` and `--timestamp`; the
API inputs. The `ms32 wallet` CLI takes `--account 0` and `--timestamp`; the
selected Bitcoin Core chain is authoritative and there is no wallet
`--testnet` flag. `ms32 xprv --testnet` remains explicit because it directly
selects xprv versus tprv serialization. The timestamp defaults to `0` so
recovery scans from genesis. A nonnegative Unix time or the literal `now` may
be supplied; `now` intentionally skips historical discovery. There is no
account database, descriptor parser, policy language, RPC library, or network
client.
recovery scans from genesis. A nonzero Unix time uses Core's timestamped
rescan with its two-hour safety window; `now` skips historical discovery.
Nonzero wallet accounts await upstream Core support. There is no account
database, descriptor parser, policy language, RPC library, or network client.

Bitcoin master-seed creation and restoration use a private CLI adapter. Before
entropy or recovery input it resolves `bitcoin-cli` from `PATH` and probes the
Expand All @@ -619,29 +618,29 @@ descriptors, transactions, keypool, or active scan. The operator selects by
number and confirms the escaped exact name; the adapter never infers a wallet
from list order or Bitcoin-Qt state.

Immediately before import, every target property is checked again. The original
`CreationCeremony.finish()` result or validated recovered master seed supplies
the four private multipath records. Confirmation text is never reparsed into
this source. Private descriptor material is sent only through
`bitcoin-cli -stdin`, raw Core errors are suppressed, and no passphrase
interface exists.

After all four private records import successfully, Core v32 exposes the one
wallet HD root with `gethdkeys`; `derivehdkey` performs the hardened
BIP44/49/84/86 account derivations. Python validates the returned origin paths,
fingerprint consistency, and network xpub/tpub versions, constructs only the
fixed descriptor templates, and asks `getdescriptorinfo` to validate and expand
their external/internal branches. The adapter then compares the exact eight
active public descriptors against `listdescriptors`. It relocks wallets Core
reports as encrypted. Master-fingerprint display is likewise delegated to Core:
Immediately before adding the key, every target property is checked again. The
original `CreationCeremony.finish()` result or validated recovered master seed
supplies the root xprv. Confirmation text is never reparsed into this source.
The key is sent only through `bitcoin-cli -stdin`; raw Core errors are suppressed,
and no passphrase interface exists.

Core v32 accepts the key with `addhdkey` and creates external and internal
account-0 descriptors for BIP44/49/84/86 with `createwalletdescriptor`. Python
checks each call's result but trusts Core to derive and store the wallet policy.
Numeric recovery timestamps re-import one existing active private descriptor
through stdin with its range and next index preserved; Core then rescans the
whole wallet from the supplied time (or genesis for `0`). The adapter
relocks wallets Core reports as encrypted. Master-fingerprint display is
likewise delegated to Core:
a stateless root P2PKH descriptor is normalized, `deriveaddresses` derives its
address, and `validateaddress` returns the script hash whose first four bytes are
the BIP32 fingerprint.

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

The wallet CLI is one leaf command:
Expand All @@ -651,9 +650,9 @@ ms32 wallet --account 0 --timestamp 0
```

It preflights Core before recovery input, recovers one validated master seed,
selects and revalidates an empty private-key-enabled destination, imports
through `bitcoin-cli -stdin`, verifies the exact accepted public descriptor
set, and relocks an encrypted destination after success, failure, or
selects and revalidates an empty private-key-enabled destination, sends the root
xprv through `bitcoin-cli -stdin`, asks Core to create account-0 descriptors,
and relocks an encrypted destination after success, failure, or
interruption. It never handles a passphrase.

For offline signing/watch-only and multisig workflows, use Bitcoin Core v32's
Expand All @@ -664,11 +663,11 @@ The direct `ms32 xprv` primitive remains top-level and carries an explicit
secret-root warning.

`tools/bitcoin_core_regtest.py` is the repeatable integration check. It requires
Bitcoin Core 32 or newer and exercises direct wallet restoration, account and
timestamp handling, Core-normalized public descriptors, balance discovery,
Bitcoin Core 32 or newer and exercises direct wallet restoration, account-0
and timestamp handling, Core-created public descriptors, balance discovery,
sign/broadcast behavior on regtest, relocking, and mainnet/test-network root
serialization. `tools/bitcoin_core_main_smoke.py` repeats the descriptor,
account, timestamp, and relocking checks against an isolated main-chain Core
account-0, timestamp, and relocking checks against an isolated main-chain Core
instance without connecting to peers.

## Deliberate divergences and non-goals
Expand Down
6 changes: 3 additions & 3 deletions docs/security/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ and evidence.
admitted classes ranked equal to or better than the candidate, independently
of execution order.
6. Secrets stay out of arguments, logs, ordinary output, and public transfers.
Private descriptors exist only in Python memory and child stdin.
During wallet setup, codex32 transfers the master xprv only through child stdin.
7. Bitcoin Core chains are discovered before entropy or recovery input. The
operator confirms an eligible descriptor wallet by exact name.
8. Wallet state is revalidated before import. Every import must succeed and the
exact accepted public descriptor set must match.
8. Wallet state is revalidated before handing Core the master key. Core must
accept that key and create every requested account-0 wallet descriptor.
9. codex32 has no passphrase channel. An unlocked encrypted signer is relocked
and verified on every exit path.
10. External text, Core output, public wallet data, and PSBTs are untrusted.
Expand Down
12 changes: 6 additions & 6 deletions docs/security/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ The operator must:
- Creation feedback identifies correct groups but does not prove the recovery card was corrected.
- A fresh unshared master seed exposes a public 20-bit BIP32 fingerprint in its
default identifier; fingerprints are metadata, not secrets.
- Private Bitcoin Core descriptors contain the root xprv and temporarily exist
in Python objects, serialized JSON, and the child process's standard input.
- The master xprv temporarily exists in Python objects and the child process's
standard input during wallet initialization.
- Wallet encryption belongs to Bitcoin Core. codex32 accepts an eligible
unencrypted or unlocked encrypted wallet and never evaluates or handles a
passphrase.
Expand Down Expand Up @@ -230,9 +230,9 @@ signing setup belong to Bitcoin Core's maintained v32 workflow.
| Preflight | Before entropy or recovery input, explicit chain arguments probe the five standard local networks for Bitcoin Core 32 or newer. One response is selected automatically; multiple responses require operator selection. |
| Process boundary | codex32 invokes the reviewed `bitcoin-cli` from `PATH` as a child without a shell, direct RPC socket, wallet database, or wallet-creation operation. Every call uses loopback and the selected chain. |
| Destination | Only an empty descriptor wallet with private keys enabled, no external signer, transactions, descriptors, keypool entries, or active scan is eligible. One eligible wallet is offered directly; multiple wallets are selected by number. New wallets are detected by polling, and rejection returns to every eligible wallet. The escaped name is confirmed exactly. |
| Seed source | The original ceremony result or validated recovered master seed supplies root-xprv private descriptors for Core's reported chain. After import, Core v32's wallet HD-key RPCs derive the requested BIP44, BIP49, BIP84, and BIP86 account xpubs. |
| Secret channel | Private descriptor JSON is sent only through the child's standard input. It is absent from arguments, ordinary output, and diagnostics. codex32 has no passphrase channel and suppresses raw Core errors. |
| Revalidation | Every destination property is checked again immediately before import. Every private import must succeed before public verification begins. `gethdkeys` must expose one private wallet root; `derivehdkey` must return the requested hardened account paths with one consistent fingerprint and the correct network xpub/tpub version. `getdescriptorinfo` then validates and expands the fixed public templates, and the exact eight active descriptors must match Core's accepted set. |
| Seed source | The original ceremony result or validated recovered master seed supplies a root xprv for Core's reported chain. Core v32 creates BIP44, BIP49, BIP84, and BIP86 account-0 descriptors from that key. |
| Secret channel | The master xprv is sent only through the child's standard input. A timestamped rescan obtains one private descriptor from Core's captured stdout and returns it through stdin; neither value is printed or passed in arguments or diagnostics. codex32 has no passphrase channel and suppresses raw Core errors. |
| Revalidation | Every destination property is checked again immediately before adding the key. `addhdkey` must accept it and `createwalletdescriptor` must return two public descriptors for each requested address type. Numeric recovery timestamps trigger Core's time-based rescan (genesis for `0`), with no guessed block height. Core is trusted to derive and store the wallet policy. |
| Relocking | Once Core reports an encrypted private-key wallet unlocked, a `finally`-protected obligation requests `walletlock` and verifies the locked state after success, failure, state change, or interruption. |

The unlock command is entered in Bitcoin-Qt. Its
Expand All @@ -252,5 +252,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) |
7 changes: 5 additions & 2 deletions docs/user/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,13 @@ its public wallet data with the separate wallet record.
ms32 wallet --timestamp 0
```

If you know when the wallet was first used, an earlier Unix timestamp can
shorten the rescan; `0` remains the safest choice when unsure.

5. Select and confirm that wallet. If it is locked, follow the displayed
Bitcoin-Qt Console instructions; codex32 waits and continues automatically.
It imports the private descriptors, verifies the public set, and relocks an
encrypted wallet.
It gives Core the master private key, asks Core to create the standard
account-0 descriptors, scans history, and relocks an encrypted wallet.
6. If you need an online watch-only counterpart, keep the restored signer
offline and follow Bitcoin Core v32's
[offline-signing tutorial](https://github.com/bitcoin/bitcoin/blob/v32.0rc1/doc/offline-signing-tutorial.md)
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ authors = [
]
description = "Python reference implementation for codex32 (BIP93) and codex32-encoded master seeds."
readme = "README.md"
requires-python = ">=3.12,<3.14"
requires-python = ">=3.10,<3.16"
license = "MIT AND BSD-3-Clause"
license-files = ["LICENSE*", "LICENSES/*"]
maintainers = [
Expand All @@ -22,8 +22,12 @@ classifiers = [
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Operating System :: OS Independent",
"Topic :: Security :: Cryptography",
"Topic :: Software Development :: Libraries",
Expand Down Expand Up @@ -52,7 +56,7 @@ where = ["src"]
testpaths = ["tests"]

[tool.mypy]
python_version = "3.12"
python_version = "3.10"
strict = true

[tool.ruff]
Expand Down
30 changes: 0 additions & 30 deletions requirements/test-wallet-dependencies.txt

This file was deleted.

Loading
Loading