# pyhdwallet v1.1.0 (hdwalletpy) A command-line tool for generating and recovering HD wallets (BIP39) with support for Ethereum, Solana, and Bitcoin. It is designed for offline operation, optional PGP encryption, and writing deterministic AES-encrypted ZIP "wallet artifacts" into a local `.wallet/` folder. Repository structure (current): - `src/pyhdwallet.py` — main CLI script - `tests/` — offline regression test suite - `test_vectors.py` — pytest suite for derivation integrity - `bootstrap_vectors.py` — one-time script to generate test vectors - `vectors.json` — frozen expected values (committed) - `data/recipient.asc` — test PGP key (committed) - `vendor/` — vendored Python wheels for offline installation - `macos-arm64/` — macOS Apple Silicon wheels (Python 3.12) - `linux-x86_64/` — Linux x86_64 wheels (Python 3.12) - `.wallet/` — generated wallet artifacts (gitignored) - `requirements.in`, `requirements.txt` - `install_offline.sh` — automated offline installer - `Dockerfile`, `Makefile` — Docker-based build tools - `README.md`, `playbook.md`, `LICENSE` **Note**: This tool requires a subcommand (e.g., `gen`, `recover`). Running without one displays help. Use ` -h` for detailed options. *** ## Features - **Offline-first**: `gen`, `recover`, and `test` block network access (best-effort in-process guard). - **Multi-chain support**: Derives addresses for Ethereum, Solana, and Bitcoin. - **PGP encryption**: Encrypts a JSON payload to a PGP public key and outputs an ASCII-armored PGP message (typically saved as `.asc`). - **AES-encrypted ZIP artifacts**: When `--file` is used, output is written as an AES-encrypted ZIP via `pyzipper`. - **TTY safety guard + --force**: If stdout is piped/redirected (non-TTY), the tool refuses to print sensitive data unless `--force` is explicitly set (to avoid accidental leaks into logs/files). `isatty()` is the classic way to detect whether stdout is connected to a terminal. - **Off-screen mode**: `--off-screen` suppresses printing sensitive data to stdout. - **Regression test suite**: Offline tests with frozen vectors ensure derivation logic, PGP fingerprinting, and seed generation remain stable across code changes. - **Vendored dependencies**: Pre-built wheels for macOS ARM64 and Linux x86_64 enable true air-gapped installation. *** ## Installation ### Prerequisites - Python 3.12.x (required for vendored wheels) - Git (for cloning repository) ### Quick Installation (macOS/Linux with Internet) ```bash # Clone repository git clone https://github.com//hdwalletpy.git cd hdwalletpy # Run automated installer ./install_offline.sh ``` The script automatically: - Detects your platform (macOS ARM64 or Linux x86_64) - Creates Python 3.12 virtual environment - Installs from vendored wheels (no PyPI access needed) - Verifies installation with test suite - Leaves you in activated venv ### Air-Gapped Installation (No Internet) For maximum security on an air-gapped machine (Tails OS, Ubuntu Live USB, etc.): **On online machine:** ```bash # Clone and verify git clone https://github.com//hdwalletpy.git cd hdwalletpy # Verify checksums cd vendor/linux-x86_64 # or macos-arm64 for Mac sha256sum -c SHA256SUMS # Linux shasum -a 256 -c SHA256SUMS # macOS # Transfer entire repo to USB ``` **On air-gapped machine:** ```bash # Ensure Python 3.12 is installed python3.12 --version # Navigate to repository cd hdwalletpy # Run installer (creates venv, installs offline) ./install_offline.sh # Generate wallet python src/pyhdwallet.py gen --off-screen --file ``` ### Manual Installation (Without Script) ```bash # Create venv python3.12 -m venv .venv source .venv/bin/activate # Install from vendored wheels # For macOS ARM64: pip install --no-index --find-links=vendor/macos-arm64 -r requirements.txt # For Linux x86_64: pip install --no-index --find-links=vendor/linux-x86_64 -r requirements.txt # Verify installation pytest -v tests/test_vectors.py python src/pyhdwallet.py test ``` ### Dependencies (Top-Level Intent) - `bip-utils` — BIP39 + BIP derivation logic - `PGPy` — encryption to OpenPGP public keys - `pynacl` + `base58` — Solana seed/key handling - `pyzipper` — AES-encrypted ZIP writing - `pytest` — test framework (development only) All dependencies are pre-downloaded in `vendor/` for offline use. *** ## Quick Start ### 1) Generate (debug/test; prints mnemonic) ```bash python ./src/pyhdwallet.py gen ``` ### 2) Generate and save AES ZIP artifact to `.wallet/` ```bash python ./src/pyhdwallet.py gen --file ``` ### 3) Generate with PGP encryption and ZIP (recommended for "at-rest" storage) ```bash python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/mykey.asc --file ``` ### 4) Recover (addresses) from mnemonic ```bash python ./src/pyhdwallet.py recover --mnemonic-stdin # (then paste mnemonic and press Ctrl-D) ``` ### 5) Recover with interactive input + ZIP artifact ```bash python ./src/pyhdwallet.py recover --interactive --file ``` ### 6) Run built-in smoke test ```bash python ./src/pyhdwallet.py test ``` ### 7) Run full regression test suite ```bash pytest -v tests/test_vectors.py ``` *** ## Testing ### Regression Test Suite The project includes a comprehensive offline test suite that validates critical functionality without requiring network access. All tests use committed test vectors to ensure deterministic, repeatable results. #### Test Coverage - **PGP fingerprint calculation**: Verifies that PGP key fingerprinting logic produces expected results and enforces fingerprint matching - **BIP39 seed derivation**: Ensures mnemonics (with and without passphrases) always produce the same seed hex - **Multi-chain address derivation**: Validates Ethereum, Bitcoin (3 address types), and Solana (3 profiles) derivation paths remain stable - **BIP85 child mnemonic derivation**: Verifies BIP85 entropy generation, path construction, and child mnemonic output against official test vectors - **CLI integration**: Smoke tests for `recover` and `gen-child` commands with correct and incorrect fingerprints #### Running Tests ```bash # Run all tests pytest -v tests/test_vectors.py # Run specific test pytest -v tests/test_vectors.py::test_address_derivation_integrity # Run with detailed output pytest -vv tests/test_vectors.py ``` #### Test Artifacts (Committed) - `tests/vectors.json` — Frozen expected values for all test cases - `tests/data/recipient.asc` — Test PGP public key (safe to commit; public key only) - `tests/bootstrap_vectors.py` — One-time script to regenerate vectors (run only when intentionally updating expected behavior) #### When to Regenerate Test Vectors Only regenerate `tests/vectors.json` when you **intentionally** change: - Derivation paths (e.g., switching from `m/44'/60'/0'/0/i` to a different path) - Solana profile logic - BIP39 seed generation To regenerate: ```bash rm tests/vectors.json python3 tests/bootstrap_vectors.py pytest -v tests/test_vectors.py # Verify all pass git add tests/vectors.json git commit -m "test: update vectors after intentional derivation change" ``` **Warning**: If tests fail after a code change and you didn't intend to change behavior, **do not regenerate vectors**. Fix the code regression instead. *** ## Vendored Dependencies ### What is Vendoring? This repository includes pre-built Python wheels in the `vendor/` directory, enabling installation without internet access or PyPI. This is critical for air-gapped security. **Supported platforms:** - macOS ARM64 (M1/M2/M3/M4) - Python 3.12 - Linux x86_64 (Ubuntu, Tails, Debian) - Python 3.12 ### Building Vendor Wheels (Maintainers) If you need to update dependencies or add new platforms: ```bash # Using Makefile (recommended) make vendor-all # Build for both macOS and Linux make vendor-macos # macOS ARM64 only make vendor-linux # Linux x86_64 only (requires Docker) make verify-vendor # Test offline installation # Commit updated wheels git add vendor/ git commit -m "vendor: update dependencies to latest versions" ``` **Manual build (macOS):** ```bash mkdir -p vendor/macos-arm64 source .venv312/bin/activate pip download --dest vendor/macos-arm64 -r requirements.txt cd vendor/macos-arm64 && shasum -a 256 *.whl > SHA256SUMS ``` **Manual build (Linux via Docker):** ```bash docker run --rm -v $(pwd):/work -w /work python:3.12-slim bash -c " apt-get update && apt-get install -y gcc g++ make libffi-dev pip install --upgrade pip mkdir -p vendor/linux-x86_64 pip download --dest vendor/linux-x86_64 -r requirements.txt cd vendor/linux-x86_64 && sha256sum *.whl > SHA256SUMS " ``` ### Verifying Vendor Integrity ```bash # Check checksums before using on air-gapped machine cd vendor/linux-x86_64 sha256sum -c SHA256SUMS # Linux shasum -a 256 -c SHA256SUMS # macOS ``` *** ## Outputs and File Structure ### Stdout Behavior - `gen` prints the mnemonic + derived addresses by default (intended for debug/test comparisons). - `--off-screen` suppresses printing sensitive data to stdout. ### `--file` Behavior (Deterministic, Secured Output) If `--file` is present, the tool writes **only** an AES-encrypted ZIP file (no raw `.json`/`.asc` file is left on disk). AES ZIP is implemented using `pyzipper`. Default output folder: - `./.wallet/` Override folder: - `--wallet-location /path/to/folder` Naming uses UTC timestamps (e.g. `20260108_011830Z`): - No PGP: zip contains `test_wallet_.json`, zip name `test_wallet_.zip` - With PGP: zip contains `encrypted_wallet_.asc`, zip name `encrypted_wallet_.zip` ### Password Handling for ZIP - Default: `--zip-password-mode prompt` prompts for the ZIP password (attempts hidden entry). - If hidden entry is not supported in the environment, it falls back to visible `input()` with loud warnings. - Optional: `--zip-password-mode auto` generates a Base58 password (length controlled by `--zip-password-len`). - If auto mode is used, password is shown **only if** `--show-generated-password` is set, and it prints to **stderr** (not stdout) to reduce accidental capture when stdout is redirected. `pyzipper` supports AES encryption via `AESZipFile` and password-setting APIs. *** ## Commands ### gen-child (offline) - **NEW in v1.1.0** Derive a child BIP39 mnemonic from a master mnemonic using BIP85 deterministic entropy. **What is BIP85?** BIP85 allows you to derive unlimited child mnemonics from a single master seed. Each child mnemonic is: - **Deterministic**: Same master + index always produces the same child - **Isolated**: Compromise of one child doesn't affect others or the master - **Interoperable**: Works with Coldcard, Ian Coleman tool, and other BIP85-compatible wallets - **Portable**: Transfer child mnemonics to hot wallets without exposing your master seed **Use cases:** - Generate separate wallets for different exchanges/services - Create disposable wallets for testing - Delegate wallets to family members or business partners - Maintain one master backup while having multiple operational wallets ```bash python ./src/pyhdwallet.py gen-child [options] ``` **Input options (choose one - required):** - `--interactive` — Word-by-word guided master mnemonic entry (English-only, per-word validation) - `--mnemonic-stdin` — Read master mnemonic from stdin (recommended; avoids shell history) **Child parameters:** - `--words {12,15,18,21,24}` — Child mnemonic word count (default: 12) - `--index N` — BIP85 derivation index (default: 0; use different indexes for different children) - `--passphrase` — Prompt for master BIP39 passphrase (optional but recommended if your master uses one) - `--passphrase-hint HINT` — Store hint only (requires `--passphrase`) **Output options:** - `--off-screen` — Suppress printing child mnemonic to stdout (prints metadata only) - `--force` — Allow printing to non-TTY stdout (dangerous; see security notes) **File output options:** - `--file` — Write AES-encrypted ZIP artifact to `.wallet/` (or `--wallet-location`) - `--pgp-pubkey-file FILE` — PGP-encrypt the inner JSON payload - `--expected-fingerprint FINGERPRINT` — Enforce PGP key pinning - `--wallet-location PATH` — Override default `.wallet/` directory - `--zip-password-mode {prompt,auto}` — ZIP password entry mode - `--zip-password-len N` — Auto-generated password length (default: 12) - `--show-generated-password` — Print auto-generated ZIP password to stderr **BIP85 Derivation Path:** The tool uses the standard BIP85 path for BIP39 application: ``` m/83696968'/39'/0'/{words}'/{index}' ``` - `83696968'` — BIP85 magic constant - `39'` — BIP39 application - `0'` — English language (only supported language in v1.1.0) - `{words}'` — Child mnemonic word count (12/15/18/21/24) - `{index}'` — Your chosen index **Examples:** ```bash # 1. Basic: Derive 12-word child at index 0 (interactive entry) python src/pyhdwallet.py gen-child --interactive --words 12 --index 0 # 2. Derive 24-word child with passphrase (from stdin) echo "your master mnemonic words here" | \ python src/pyhdwallet.py gen-child \ --mnemonic-stdin \ --passphrase \ --words 24 \ --index 0 # 3. Secure: Off-screen + PGP encryption + ZIP file python src/pyhdwallet.py gen-child \ --interactive \ --words 12 \ --index 1 \ --pgp-pubkey-file pubkeys/recipient.asc \ --expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \ --off-screen \ --file # 4. Multiple children: Derive index 0, 1, 2 for different purposes for i in 0 1 2; do echo "master mnemonic" | python src/pyhdwallet.py gen-child \ --mnemonic-stdin --words 12 --index $i --off-screen --file done ``` **Security Notes:** - **Master seed safety**: Never expose your master mnemonic to online systems. Use `gen-child` only on air-gapped machines. - **Index tracking**: Keep a record of which index corresponds to which purpose (e.g., index 0 = exchange A, index 1 = exchange B). - **Passphrase consistency**: If your master seed uses a BIP39 passphrase, you MUST provide the same passphrase when deriving children. - **Interoperability verification**: Always test child mnemonics in a separate wallet before sending funds to ensure compatibility. **Interoperability:** This implementation follows BIP85 specification and has been verified against official test vectors. Child mnemonics are compatible with: - Coldcard hardware wallet (BIP85 menu) - Ian Coleman BIP85 calculator ([https://iancoleman.io/bip39/](https://iancoleman.io/bip39/)) - Trezor (when BIP85 support is added) - Any other BIP85-compliant tool **Output Payload Structure:** When using `--file`, the encrypted ZIP contains a JSON payload with: ```json { "version": "pyhdwallet v1.1.0", "purpose": "BIP85 derived child mnemonic", "master_fingerprint": "ABCD1234", "master_word_count": 24, "passphrase_used": true, "passphrase_hint": "my hint", "bip85_metadata": { "path": "m/83696968'/39'/0'/12'/0'", "index": 0, "language": 0, "child_word_count": 12 }, "child_mnemonic": "word1 word2 ...", "interoperability_note": "..." } ``` ### fetchkey (online) Download and verify a PGP public key from a URL. ```bash python ./src/pyhdwallet.py fetchkey [--out FILE] [--timeout SECONDS] [--off-screen] [--expected-fingerprint FINGERPRINT] ``` Options: - `url`: URL to the ASCII-armored PGP public key - `--out FILE`: save key to file - `--timeout SECONDS`: request timeout (default 15) - `--off-screen`: reduced output / temp-file behavior - `--expected-fingerprint`: refuse if downloaded key fingerprint doesn't match (40 hex chars) *** ### gen (offline) Generate a BIP39 mnemonic and derive addresses. ```bash python ./src/pyhdwallet.py gen [options] ``` Core options: - `--words {12,15,18,21,24}` - `--dice-rolls "1 2 3 ..."` (space-separated; adds user entropy) - `--passphrase` (prompts for BIP39 passphrase) - `--passphrase-hint HINT` - `--chains ethereum solana bitcoin` - `--addresses N` - `--sol-profile {phantom_bip44change,phantom_bip44,phantom_deprecated,solana_bip39_first32}` PGP options: - `--pgp-pubkey-file FILE` (encrypt payload to pubkey; `.asc` content) - `--pgp-ignore-usage-flags` - `--expected-fingerprint FINGERPRINT` (refuse if PGP key fingerprint doesn't match) Artifact options: - `--file` (write AES ZIP only) - `--wallet-location PATH` (default: `./.wallet`) - `--zip-password-mode {prompt,auto}` - `--zip-password-len N` (default: 12) - `--show-generated-password` (auto mode only; prints password to stderr) Safety options: - `--off-screen` (don't print sensitive output) - `--force` (only for non-TTY stdout; see below) *** ### recover (offline) Recover addresses from mnemonic and optionally write a ZIP artifact. ```bash python ./src/pyhdwallet.py recover [options] ``` Input options (choose one): - `--mnemonic-stdin` (recommended; read from stdin to avoid shell history) - `--interactive` (word-by-word guided entry with validation) Additional options: - same chain/PGP/ZIP/off-screen/force options as `gen` - `--export-private` (requires `--pgp-pubkey-file`; includes private keys in encrypted payload) - `--include-source` (includes mnemonic in payload; requires `--pgp-pubkey-file`) Notes: - `--export-private` requires `--pgp-pubkey-file` (private keys only travel inside encrypted payload). - `--include-source` controls whether mnemonic is included inside the encrypted payload (only meaningful when PGP is used). *** ### test (offline) Run minimal self-tests. ```bash python ./src/pyhdwallet.py test ``` For comprehensive regression testing, use the pytest suite: ```bash pytest -v tests/test_vectors.py ``` *** ## When to Use `--force` Use `--force` only if you **intentionally** want to print sensitive output while stdout is being piped/redirected. Why it exists: - When you pipe/redirect output (e.g., `>` or `|`), stdout is typically **not a TTY**. - The program checks `sys.stdout.isatty()` and refuses to print secrets by default to avoid accidental leakage into files/logs. - `isatty()` is commonly used to detect terminal vs pipe/redirect. Examples: ```bash # This redirects stdout to a file (non-TTY). The tool will refuse unless forced. python ./src/pyhdwallet.py gen > out.txt # Explicitly override the safety guard (dangerous). python ./src/pyhdwallet.py gen --force > out.txt ``` If running normally in your interactive terminal, stdout is a TTY and `--force` does nothing (output looks the same). *** ## Security Notes (Practical) - `gen` printing the mnemonic is intentionally "debug/test" behavior. Assume stdout can be recorded (scrollback, logging, screen recording, CI logs). - Prefer `--off-screen` for reduced exposure. - Prefer `--file` so artifacts go into `.wallet/` and are AES-encrypted via `pyzipper`. - For stronger at-rest security: combine `--pgp-pubkey-file` + `--file` so the ZIP contains only an encrypted `.asc` payload. PGPy encryption style follows `PGPMessage.new(...)` then `pubkey.encrypt(...)`. - Use `--expected-fingerprint` to enforce PGP key pinning and prevent key substitution attacks. - Install from vendored wheels on air-gapped machines to avoid supply chain attacks. *** ## Offline Security Best Practices This tool is designed for offline use, but true security depends on the **environment** where you run it. Below are operational security recommendations for generating keys you can trust. ### Air-Gapped Setup (Highest Security) For maximum security when generating production wallets, use an **air-gapped computer**—a device that has never been and will never be connected to any network. **Recommended procedure:** 1. **Prepare a clean machine**: - Use a dedicated laptop or bootable USB with a fresh Linux installation (e.g., Tails, Ubuntu Live USB) - Never connect this device to WiFi, Ethernet, or Bluetooth - Physically disable network interfaces if possible (remove WiFi card, tape over Ethernet port) 2. **Transfer repository offline**: ```bash # On trusted online machine, clone and verify git clone https://github.com//hdwalletpy.git cd hdwalletpy # Verify checksums cd vendor/linux-x86_64 sha256sum -c SHA256SUMS # Transfer entire repo to USB ``` 3. **Verify code integrity on air-gapped machine**: ```bash # Check checksums again cd vendor/linux-x86_64 sha256sum -c SHA256SUMS # Run test suite to verify derivation logic ./install_offline.sh pytest -v tests/test_vectors.py ``` 4. **Generate wallet**: ```bash python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/recipient.asc --file --off-screen ``` 5. **Transfer output safely**: - Copy only the `.wallet/*.zip` file to USB (never copy `pyhdwallet.py` or Python environment back to online machine) - The ZIP is AES-encrypted; the inner `.asc` is PGP-encrypted 6. **Destroy or securely wipe** the USB after transfer if it contained unencrypted secrets ### Threats Air-Gapping Mitigates - **Remote attacks**: Malware cannot exfiltrate keys over the network - **Clipboard hijacking**: No clipboard manager or remote access tool can intercept data - **Browser/OS telemetry**: No accidental upload of terminal history or crash dumps ### Threats Air-Gapping Does NOT Fully Mitigate Research shows that sophisticated attackers with physical access can potentially exfiltrate data from air-gapped systems via covert channels (acoustic, electromagnetic, optical). However: - These attacks require physical proximity and pre-installed malware - For individual users (not nation-state targets), air-gapping remains highly effective - Countermeasures: Use the machine in a secure location, inspect for unfamiliar USB devices, verify software integrity before installation ### Physical Security **Mnemonic handling:** - Write the mnemonic on paper immediately; never store it digitally on the air-gapped machine - Use a metal backup (e.g., Cryptosteel) for fire/water resistance - Split storage across multiple secure locations if desired (Shamir's Secret Sharing for advanced users) **Device handling:** - After generating the wallet, optionally wipe the air-gapped device or destroy the bootable USB - If reusing the device, use secure-erase tools (not just `rm`) ### Verification Through Testing **Why the test suite matters for trust:** The committed test suite (`tests/test_vectors.py`) allows you to verify that derivation logic hasn't been tampered with before generating production keys: ```bash # On the air-gapped machine, before generating your wallet: pytest -v tests/test_vectors.py ``` If all tests pass, you have cryptographic proof that: - BIP39 seed derivation matches the well-known test vector ("abandon abandon..." → known seed) - Derivation paths produce addresses matching public BIP39/BIP44 test vectors - PGP fingerprinting logic works correctly If tests fail, **do not generate keys**—the code may be compromised or buggy. ### Entropy Sources **Built-in entropy (default):** - Python's `secrets.token_bytes()` uses OS-provided CSPRNG (`/dev/urandom` on Linux, `CryptGenRandom` on Windows) - This is cryptographically secure for typical use **Additional user entropy (optional):** ```bash python ./src/pyhdwallet.py gen --dice-rolls "4 2 6 1 3 5 ..." --file ``` - Roll a die 50+ times and input the results - Your dice rolls are mixed with OS entropy via SHA256 - Protects against potential CSPRNG backdoors (theoretical concern) ### PGP Key Fingerprint Pinning Always use `--expected-fingerprint` when encrypting to a PGP key to prevent key substitution attacks: ```bash # First, get and verify the fingerprint of your recipient key python ./src/pyhdwallet.py fetchkey https://example.com/mykey.asc --out mykey.asc # Manually verify fingerprint matches what you expect (check via another channel) # Then use it with pinning: python ./src/pyhdwallet.py gen \ --pgp-pubkey-file mykey.asc \ --expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \ --file ``` Without `--expected-fingerprint`, an attacker who controls the filesystem could swap `mykey.asc` with their own key. ### Operational Checklist Before generating production wallets: - [ ] Running on air-gapped machine or fresh Live USB - [ ] Network physically disabled (no WiFi/Ethernet/Bluetooth) - [ ] Code integrity verified (checksums + test suite passes) - [ ] Using `--off-screen` to minimize terminal scrollback exposure - [ ] Using `--file` to avoid leaving unencrypted files on disk - [ ] Using `--pgp-pubkey-file` with `--expected-fingerprint` for key pinning - [ ] Paper/metal backup prepared for mnemonic - [ ] Output ZIP password stored separately from ZIP file - [ ] Plan for USB secure wipe after transfer ### Lower-Risk Scenarios Air-gapping is overkill for: - **Learning/testing**: Use your regular laptop with `--off-screen` and `--file` - **Small amounts**: Generate on a clean, updated machine with minimal software - **Testnet wallets**: Standard laptop is fine Air-gapping is recommended for: - **Life savings or business funds** - **Long-term cold storage** (multi-year hold) - **Institutional custody scenarios** ### Trust But Verify The only way to fully trust this tool (or any wallet software) is to: 1. **Read the source code** (`src/pyhdwallet.py` is ~1400 lines, single file) 2. **Verify test vectors** match published BIP39 test data 3. **Run the test suite** on your air-gapped machine 4. **Generate a test wallet** and verify addresses on a block explorer using a separate tool Never trust wallet software blindly—especially for significant funds. *** ## Troubleshooting ### "Permission denied" running `./src/pyhdwallet.py` Run via python, or set executable bit: ```bash python ./src/pyhdwallet.py gen # or chmod +x ./src/pyhdwallet.py ./src/pyhdwallet.py gen ``` ### Python 3.12 not found Vendored wheels require Python 3.12: ```bash # macOS brew install python@3.12 # Ubuntu/Debian sudo apt-get install python3.12 python3.12-venv # Verify python3.12 --version ``` ### "Missing dependency" errors Make sure you're installing from the correct vendor directory: ```bash # Check your platform uname -sm # Install for macOS ARM64 pip install --no-index --find-links=vendor/macos-arm64 -r requirements.txt # Install for Linux x86_64 pip install --no-index --find-links=vendor/linux-x86_64 -r requirements.txt ``` ### Checksum verification failures If `SHA256SUMS` verification fails, the wheels may be corrupted or tampered with: ```bash cd vendor/linux-x86_64 sha256sum -c SHA256SUMS # If failures occur, re-download from trusted source # DO NOT use corrupted wheels for production wallets ``` ### Unzipping AES ZIP issues Some `unzip` tools may not support AES-encrypted ZIPs. Use Python + pyzipper to extract: ```bash python - <<'PY' import pyzipper zip_path = ".wallet/your_wallet.zip" out_dir = "unzip_out" pw = input("ZIP password: ").encode() with pyzipper.AESZipFile(zip_path) as zf: zf.pwd = pw zf.extractall(out_dir) print("Extracted to", out_dir) PY ``` ### Test failures after code changes If `pytest -v tests/test_vectors.py` fails after you modified code: 1. **Did you intentionally change derivation logic?** - Yes → Regenerate vectors: `rm tests/vectors.json && python3 tests/bootstrap_vectors.py` - No → You have a regression bug; revert your changes and fix the code 2. **Is only one test failing?** - Run with `-vv` for detailed diff: `pytest -vv tests/test_vectors.py::test_name` *** ## Development ### Building Vendor Wheels See [Vendored Dependencies](#vendored-dependencies) section above. ### Running Tests (Dev) ```bash # Quick test python src/pyhdwallet.py test # Full regression suite pytest -v tests/test_vectors.py # Specific test pytest -v tests/test_vectors.py::test_address_derivation_integrity # With coverage pytest --cov=src tests/ ``` ### Docker Development Environment ```bash # Build image make build-image # Start warm container make up make shell # Inside container python src/pyhdwallet.py test ``` *** ## Changelog - **v1.1.0** (2026-01-09) - **Added**: BIP85 child mnemonic derivation via new `gen-child` command - Supports deriving 12/15/18/21/24-word children from master seed - Optional master BIP39 passphrase support - Full interoperability with BIP85-compatible wallets (Coldcard, Ian Coleman tool) - Verified against official BIP85 test vectors - Added BIP85 regression tests to test suite - No new dependencies required (uses existing `bip-utils` + stdlib) - BIP85 path: `m/83696968'/39'/0'/{words}'/{index}'` - **v1.0.5** - `--unsafe-print` removed; `gen` prints mnemonic by default (debug/test behavior). - `--output` removed; file payload is always JSON (unencrypted) or `.asc` (PGP encrypted). - `--file` is now a boolean flag that writes **only** AES-encrypted ZIP artifacts (no raw output files). - Added: `--wallet-location`, `--zip-password-mode`, `--zip-password-len`, `--show-generated-password`. - Added: `--force` to override the non-TTY printing safety guard (use only when redirecting/piping). - Added: `--expected-fingerprint` for PGP key pinning. - Added: Offline regression test suite with frozen vectors (`tests/test_vectors.py`). - Added: Vendored dependencies for macOS ARM64 and Linux x86_64 (Python 3.12). - Added: `install_offline.sh` automated installer for air-gapped deployment. - Changed: `recover` now uses `--mnemonic-stdin` and `--interactive` instead of `--mnemonic` CLI arg. - Updated: Dockerfile and Makefile now support vendoring workflow. - **v1.0.3** Documentation and CLI behavior updates. - **v1.0.2** Added off-screen behaviors and security improvements. - **v1.0.1** Renamed to pyhdwallet and added version flag. - **v1.0.0** Initial release.