From a2592156207ed04671d6532cec4997fb1dab1595 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Tue, 4 Aug 2026 10:19:38 -0300 Subject: docs: one documentation directory, not two (#137) docs/ has held the README's images since the fair-launch release. I added doc/ yesterday for the derivation spec without looking, and #108 then correctly used docs/ -- leaving the repository with both, one letter apart, which is the kind of thing that quietly costs someone ten minutes a year forever. The mistake was mine, so the move is mine: derivation.md and verify-derivation.py go to docs/, and the two README links follow. No content changes. The verifier still reproduces the wallet from its new path, which is the only thing here that can actually break. --- docs/derivation.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs/derivation.md (limited to 'docs/derivation.md') diff --git a/docs/derivation.md b/docs/derivation.md new file mode 100644 index 0000000..c5ee3ad --- /dev/null +++ b/docs/derivation.md @@ -0,0 +1,125 @@ +# Key derivation and address format + +This document exists so that a Bitflash wallet can be rebuilt **without this +software**. If the project stops, the binaries are lost, or you simply do not +trust them, the twelve words plus what is written here are enough to recover +your private keys with ordinary cryptographic tools. + +Everything below is verified by `docs/verify-derivation.py`, a self-contained +script that implements this document from scratch — no Bitflash code, no +libraries beyond Python's standard library — and checks that it reproduces the +addresses the wallet produces. If the two ever disagree, this document is wrong +and the script will say so. + +## Addresses + +An address is the Base58Check encoding of a version byte followed by +`RIPEMD160(SHA256(public key))`. + +| | | +|---|---| +| curve | secp256k1 | +| public key encoding | **uncompressed**, 65 bytes, `0x04 ‖ X ‖ Y` | +| address version byte | **25** (`0x19`), which is why addresses begin with `B` | +| checksum | first 4 bytes of `SHA256(SHA256(payload))`, appended before Base58 | + +The public key is uncompressed. This matters more than anything else here: the +same private key hashed in compressed form gives a completely different, valid +looking address that holds nothing. Bitflash descends from Bitcoin 0.1.0, which +predates compressed keys. + +Outputs paying an address use the usual script: + +``` +OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG +``` + +Mined coins and payments made directly to a public key use the older form +` OP_CHECKSIG`. A wallet scanning the chain must recognise both. + +## From twelve words to a master key + +Standard BIP-39 and BIP-32, with no modifications: + +1. **Seed.** `PBKDF2-HMAC-SHA512(password = mnemonic, salt = "mnemonic" + passphrase, iterations = 2048, length = 64 bytes)`. Bitflash always uses an empty passphrase, so the salt is the literal string `mnemonic`. +2. **Master key.** `I = HMAC-SHA512(key = "Bitcoin seed", data = seed)`. The master private key is `I[0..32]`, the master chain code is `I[32..64]`. + +Child derivation is BIP-32 `CKDpriv`, hardened and non-hardened as usual. + +## Two layouts + +A wallet records which layout it uses in `wallet.dat` under the key `hdschema`. +Wallets created before that field existed are treated as legacy. + +### Legacy (`hdschema` absent or 1) + +``` +m/i' +``` + +Hardened children of the master key, one flat sequence. Index 0 is the wallet's +default receiving address; the key pool and change take the following indices. + +Used by every wallet created before BIP-44 support existed, and kept forever: +those phrases must keep working. + +### BIP-44 (`hdschema` = 2) + +``` +m/44'/4346950'/0'/0/i external chain (receiving) +m/44'/4346950'/0'/1/i internal chain (change) +``` + +Coin type **4346950** is `0x425446`, the ASCII bytes `BTF`. It is +[registered for Bitflash under the symbol BITFLASH](https://github.com/satoshilabs/slips/blob/master/slip-0044.md). +The account level is fixed at `0'`; Bitflash does not expose multiple accounts. + +A wallet restored from a phrase scans **both** layouts, because the words alone +do not say which one produced them. + +## Test vectors + +Using the public BIP-39 test mnemonic: + +``` +abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about +``` + +### Legacy + +| path | address | +|---|---| +| `m/0'` | `BHJNmVvkVv59fgn7gmgP78BtciQi2HY8Hy` | +| `m/1'` | `B5qh9opG8sH8Mv9k2xzHoTeSUd9BkT6XEZ` | +| `m/2'` | `BCNH8uGyvkiz3uXjhA9iiF27PWAaTStNin` | +| `m/3'` | `BC1QNaXuTPGDgLZy53PghZMHEmZcp5GLgD` | + +### BIP-44 external chain + +| path | address | +|---|---| +| `m/44'/4346950'/0'/0/0` | `BF1ukigB1afmqxH64gJymPwUqFBrRqWbrb` | +| `m/44'/4346950'/0'/0/1` | `BACY8pcuCapNQr897eRHroNrgN38GSsEzV` | +| `m/44'/4346950'/0'/0/2` | `BMzkMN2k1phNBqy8RPb4Pv1i2QpwsWkKPa` | +| `m/44'/4346950'/0'/0/3` | `BNPZ8U6FHXr9Ererk8psyM2RqEVtF6sfBQ` | + +Check them yourself: + +``` +python docs/verify-derivation.py +``` + +Both tables were produced by the wallet — the legacy one by the v1.2.15 binary, +which only knows that layout — and then reproduced independently by the script. + +## What this document does not give you + +Deriving the keys is not the same as spending. To move recovered coins you still +need a node that knows the network: the chain, the peer protocol, and the +transaction format. This document is about **not losing the keys**, which is the +part that cannot be reconstructed later by reading source code that no longer +exists anywhere. + +Note also that `wallet.dat` stores private keys unencrypted. A recovery phrase +protects you against losing the file; it does not protect the file. Anyone who +can read it can spend the coins. -- cgit v1.2.3