diff options
| author | bitflash-coin <bookofshamers@outlook.com> | 2026-08-04 06:34:32 -0700 |
|---|---|---|
| committer | bitflash-coin <bookofshamers@outlook.com> | 2026-08-04 06:34:32 -0700 |
| commit | 5ce650382352c1503f40abf08129ada69402af55 (patch) | |
| tree | 6a18198901f1e2c4933f1ac53c9365eec512bf63 /docs | |
| parent | f34113291da7e5e3dc7d2468d4f9c212c05c584b (diff) | |
public commit
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/derivation.md | 125 | ||||
| -rw-r--r-- | docs/discovery.png | bin | 0 -> 36535 bytes | |||
| -rw-r--r-- | docs/logo.png | bin | 0 -> 14622 bytes | |||
| -rw-r--r-- | docs/release-verification.md | 62 | ||||
| -rw-r--r-- | docs/social.png | bin | 0 -> 208957 bytes | |||
| -rw-r--r-- | docs/verify-derivation.py | 159 | ||||
| -rw-r--r-- | docs/wallet.png | bin | 0 -> 239723 bytes |
7 files changed, 346 insertions, 0 deletions
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 @@ | |||
| 1 | # Key derivation and address format | ||
| 2 | |||
| 3 | This document exists so that a Bitflash wallet can be rebuilt **without this | ||
| 4 | software**. If the project stops, the binaries are lost, or you simply do not | ||
| 5 | trust them, the twelve words plus what is written here are enough to recover | ||
| 6 | your private keys with ordinary cryptographic tools. | ||
| 7 | |||
| 8 | Everything below is verified by `docs/verify-derivation.py`, a self-contained | ||
| 9 | script that implements this document from scratch — no Bitflash code, no | ||
| 10 | libraries beyond Python's standard library — and checks that it reproduces the | ||
| 11 | addresses the wallet produces. If the two ever disagree, this document is wrong | ||
| 12 | and the script will say so. | ||
| 13 | |||
| 14 | ## Addresses | ||
| 15 | |||
| 16 | An address is the Base58Check encoding of a version byte followed by | ||
| 17 | `RIPEMD160(SHA256(public key))`. | ||
| 18 | |||
| 19 | | | | | ||
| 20 | |---|---| | ||
| 21 | | curve | secp256k1 | | ||
| 22 | | public key encoding | **uncompressed**, 65 bytes, `0x04 ‖ X ‖ Y` | | ||
| 23 | | address version byte | **25** (`0x19`), which is why addresses begin with `B` | | ||
| 24 | | checksum | first 4 bytes of `SHA256(SHA256(payload))`, appended before Base58 | | ||
| 25 | |||
| 26 | The public key is uncompressed. This matters more than anything else here: the | ||
| 27 | same private key hashed in compressed form gives a completely different, valid | ||
| 28 | looking address that holds nothing. Bitflash descends from Bitcoin 0.1.0, which | ||
| 29 | predates compressed keys. | ||
| 30 | |||
| 31 | Outputs paying an address use the usual script: | ||
| 32 | |||
| 33 | ``` | ||
| 34 | OP_DUP OP_HASH160 <hash160> OP_EQUALVERIFY OP_CHECKSIG | ||
| 35 | ``` | ||
| 36 | |||
| 37 | Mined coins and payments made directly to a public key use the older form | ||
| 38 | `<pubkey> OP_CHECKSIG`. A wallet scanning the chain must recognise both. | ||
| 39 | |||
| 40 | ## From twelve words to a master key | ||
| 41 | |||
| 42 | Standard BIP-39 and BIP-32, with no modifications: | ||
| 43 | |||
| 44 | 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`. | ||
| 45 | 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]`. | ||
| 46 | |||
| 47 | Child derivation is BIP-32 `CKDpriv`, hardened and non-hardened as usual. | ||
| 48 | |||
| 49 | ## Two layouts | ||
| 50 | |||
| 51 | A wallet records which layout it uses in `wallet.dat` under the key `hdschema`. | ||
| 52 | Wallets created before that field existed are treated as legacy. | ||
| 53 | |||
| 54 | ### Legacy (`hdschema` absent or 1) | ||
| 55 | |||
| 56 | ``` | ||
| 57 | m/i' | ||
| 58 | ``` | ||
| 59 | |||
| 60 | Hardened children of the master key, one flat sequence. Index 0 is the wallet's | ||
| 61 | default receiving address; the key pool and change take the following indices. | ||
| 62 | |||
| 63 | Used by every wallet created before BIP-44 support existed, and kept forever: | ||
| 64 | those phrases must keep working. | ||
| 65 | |||
| 66 | ### BIP-44 (`hdschema` = 2) | ||
| 67 | |||
| 68 | ``` | ||
| 69 | m/44'/4346950'/0'/0/i external chain (receiving) | ||
| 70 | m/44'/4346950'/0'/1/i internal chain (change) | ||
| 71 | ``` | ||
| 72 | |||
| 73 | Coin type **4346950** is `0x425446`, the ASCII bytes `BTF`. It is | ||
| 74 | [registered for Bitflash under the symbol BITFLASH](https://github.com/satoshilabs/slips/blob/master/slip-0044.md). | ||
| 75 | The account level is fixed at `0'`; Bitflash does not expose multiple accounts. | ||
| 76 | |||
| 77 | A wallet restored from a phrase scans **both** layouts, because the words alone | ||
| 78 | do not say which one produced them. | ||
| 79 | |||
| 80 | ## Test vectors | ||
| 81 | |||
| 82 | Using the public BIP-39 test mnemonic: | ||
| 83 | |||
| 84 | ``` | ||
| 85 | abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about | ||
| 86 | ``` | ||
| 87 | |||
| 88 | ### Legacy | ||
| 89 | |||
| 90 | | path | address | | ||
| 91 | |---|---| | ||
| 92 | | `m/0'` | `BHJNmVvkVv59fgn7gmgP78BtciQi2HY8Hy` | | ||
| 93 | | `m/1'` | `B5qh9opG8sH8Mv9k2xzHoTeSUd9BkT6XEZ` | | ||
| 94 | | `m/2'` | `BCNH8uGyvkiz3uXjhA9iiF27PWAaTStNin` | | ||
| 95 | | `m/3'` | `BC1QNaXuTPGDgLZy53PghZMHEmZcp5GLgD` | | ||
| 96 | |||
| 97 | ### BIP-44 external chain | ||
| 98 | |||
| 99 | | path | address | | ||
| 100 | |---|---| | ||
| 101 | | `m/44'/4346950'/0'/0/0` | `BF1ukigB1afmqxH64gJymPwUqFBrRqWbrb` | | ||
| 102 | | `m/44'/4346950'/0'/0/1` | `BACY8pcuCapNQr897eRHroNrgN38GSsEzV` | | ||
| 103 | | `m/44'/4346950'/0'/0/2` | `BMzkMN2k1phNBqy8RPb4Pv1i2QpwsWkKPa` | | ||
| 104 | | `m/44'/4346950'/0'/0/3` | `BNPZ8U6FHXr9Ererk8psyM2RqEVtF6sfBQ` | | ||
| 105 | |||
| 106 | Check them yourself: | ||
| 107 | |||
| 108 | ``` | ||
| 109 | python docs/verify-derivation.py | ||
| 110 | ``` | ||
| 111 | |||
| 112 | Both tables were produced by the wallet — the legacy one by the v1.2.15 binary, | ||
| 113 | which only knows that layout — and then reproduced independently by the script. | ||
| 114 | |||
| 115 | ## What this document does not give you | ||
| 116 | |||
| 117 | Deriving the keys is not the same as spending. To move recovered coins you still | ||
| 118 | need a node that knows the network: the chain, the peer protocol, and the | ||
| 119 | transaction format. This document is about **not losing the keys**, which is the | ||
| 120 | part that cannot be reconstructed later by reading source code that no longer | ||
| 121 | exists anywhere. | ||
| 122 | |||
| 123 | Note also that `wallet.dat` stores private keys unencrypted. A recovery phrase | ||
| 124 | protects you against losing the file; it does not protect the file. Anyone who | ||
| 125 | can read it can spend the coins. | ||
diff --git a/docs/discovery.png b/docs/discovery.png new file mode 100644 index 0000000..8dda52c --- /dev/null +++ b/docs/discovery.png | |||
| Binary files differ | |||
diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..0f5d0f5 --- /dev/null +++ b/docs/logo.png | |||
| Binary files differ | |||
diff --git a/docs/release-verification.md b/docs/release-verification.md new file mode 100644 index 0000000..ecacd5f --- /dev/null +++ b/docs/release-verification.md | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | # Release Verification | ||
| 2 | |||
| 3 | Bitflash releases publish binaries and a `SHA256SUMS` file. The checksum file | ||
| 4 | proves that the file you downloaded matches the file the release page names. | ||
| 5 | A detached signature, `SHA256SUMS.asc`, proves that the checksum file itself was | ||
| 6 | signed by a trusted release key. | ||
| 7 | |||
| 8 | That gives users two separate checks: | ||
| 9 | |||
| 10 | 1. `gpg --verify SHA256SUMS.asc SHA256SUMS` checks who signed the checksums. | ||
| 11 | 2. `sha256sum -c SHA256SUMS` checks the binaries against those checksums. | ||
| 12 | |||
| 13 | ## User Check | ||
| 14 | |||
| 15 | From a shell with `curl` or `wget` and `sha256sum`: | ||
| 16 | |||
| 17 | ```bash | ||
| 18 | scripts/verify-release.sh latest | ||
| 19 | ``` | ||
| 20 | |||
| 21 | For a specific release: | ||
| 22 | |||
| 23 | ```bash | ||
| 24 | scripts/verify-release.sh v1.2.13 | ||
| 25 | ``` | ||
| 26 | |||
| 27 | For release audits, require the signature: | ||
| 28 | |||
| 29 | ```bash | ||
| 30 | scripts/verify-release.sh v1.2.13 --require-signature | ||
| 31 | ``` | ||
| 32 | |||
| 33 | Older releases may not have `SHA256SUMS.asc`. In that case the script warns and | ||
| 34 | still checks file integrity. New release audits should use `--require-signature`. | ||
| 35 | |||
| 36 | ## Maintainer Flow | ||
| 37 | |||
| 38 | After building release assets in the repository root: | ||
| 39 | |||
| 40 | ```bash | ||
| 41 | scripts/make-release-checksums.sh --sign --local-user RELEASE_KEY_ID | ||
| 42 | ``` | ||
| 43 | |||
| 44 | Upload all built assets plus: | ||
| 45 | |||
| 46 | ```text | ||
| 47 | SHA256SUMS | ||
| 48 | SHA256SUMS.asc | ||
| 49 | ``` | ||
| 50 | |||
| 51 | Keep the private signing key offline or on a dedicated release machine. Publish | ||
| 52 | the public key fingerprint in the release notes and keep using the same key for | ||
| 53 | future releases unless there is a clearly announced rotation. | ||
| 54 | |||
| 55 | ## Why This Matters | ||
| 56 | |||
| 57 | `SHA256SUMS` alone protects against a broken download, but not against someone | ||
| 58 | replacing both a binary and the checksum file. Signing `SHA256SUMS` means an | ||
| 59 | attacker must also have the release signing key to make the replacement verify. | ||
| 60 | |||
| 61 | This is not reproducible builds yet. It is the smaller, immediate step that | ||
| 62 | makes every release asset auditable by users before they run it. | ||
diff --git a/docs/social.png b/docs/social.png new file mode 100644 index 0000000..19e2a3b --- /dev/null +++ b/docs/social.png | |||
| Binary files differ | |||
diff --git a/docs/verify-derivation.py b/docs/verify-derivation.py new file mode 100644 index 0000000..ba8e883 --- /dev/null +++ b/docs/verify-derivation.py | |||
| @@ -0,0 +1,159 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Check doc/derivation.md against the addresses the wallet really produces. | ||
| 3 | |||
| 4 | This implements the document and nothing else: BIP-39 to seed, BIP-32 to child | ||
| 5 | keys, uncompressed secp256k1 public keys, and Base58Check with version byte 25. | ||
| 6 | It shares no code with Bitflash and needs nothing outside the standard library, | ||
| 7 | so agreement between the two is evidence that the document is enough to recover | ||
| 8 | a wallet without this project's software. | ||
| 9 | |||
| 10 | python doc/verify-derivation.py | ||
| 11 | |||
| 12 | Exits non-zero if any address disagrees. | ||
| 13 | """ | ||
| 14 | |||
| 15 | import hashlib | ||
| 16 | import hmac | ||
| 17 | import sys | ||
| 18 | |||
| 19 | MNEMONIC = ("abandon abandon abandon abandon abandon abandon " | ||
| 20 | "abandon abandon abandon abandon abandon about") | ||
| 21 | COIN_TYPE = 4346950 # 0x425446, the ASCII bytes "BTF" | ||
| 22 | ADDRESS_VERSION = 25 # 0x19, so addresses start with "B" | ||
| 23 | HARDENED = 0x80000000 | ||
| 24 | |||
| 25 | LEGACY = [ | ||
| 26 | "BHJNmVvkVv59fgn7gmgP78BtciQi2HY8Hy", | ||
| 27 | "B5qh9opG8sH8Mv9k2xzHoTeSUd9BkT6XEZ", | ||
| 28 | "BCNH8uGyvkiz3uXjhA9iiF27PWAaTStNin", | ||
| 29 | "BC1QNaXuTPGDgLZy53PghZMHEmZcp5GLgD", | ||
| 30 | ] | ||
| 31 | |||
| 32 | BIP44_EXTERNAL = [ | ||
| 33 | "BF1ukigB1afmqxH64gJymPwUqFBrRqWbrb", | ||
| 34 | "BACY8pcuCapNQr897eRHroNrgN38GSsEzV", | ||
| 35 | "BMzkMN2k1phNBqy8RPb4Pv1i2QpwsWkKPa", | ||
| 36 | "BNPZ8U6FHXr9Ererk8psyM2RqEVtF6sfBQ", | ||
| 37 | ] | ||
| 38 | |||
| 39 | # ---- secp256k1, plain and slow on purpose: it should be readable ------------ | ||
| 40 | |||
| 41 | P = 2**256 - 2**32 - 977 | ||
| 42 | N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 | ||
| 43 | G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, | ||
| 44 | 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) | ||
| 45 | |||
| 46 | |||
| 47 | def point_add(p, q): | ||
| 48 | if p is None: | ||
| 49 | return q | ||
| 50 | if q is None: | ||
| 51 | return p | ||
| 52 | if p[0] == q[0] and (p[1] + q[1]) % P == 0: | ||
| 53 | return None | ||
| 54 | if p == q: | ||
| 55 | lam = 3 * p[0] * p[0] * pow(2 * p[1], P - 2, P) % P | ||
| 56 | else: | ||
| 57 | lam = (q[1] - p[1]) * pow(q[0] - p[0], P - 2, P) % P | ||
| 58 | x = (lam * lam - p[0] - q[0]) % P | ||
| 59 | return (x, (lam * (p[0] - x) - p[1]) % P) | ||
| 60 | |||
| 61 | |||
| 62 | def point_mul(k, p=G): | ||
| 63 | result = None | ||
| 64 | while k: | ||
| 65 | if k & 1: | ||
| 66 | result = point_add(result, p) | ||
| 67 | p = point_add(p, p) | ||
| 68 | k >>= 1 | ||
| 69 | return result | ||
| 70 | |||
| 71 | |||
| 72 | def pub_uncompressed(point): | ||
| 73 | return b"\x04" + point[0].to_bytes(32, "big") + point[1].to_bytes(32, "big") | ||
| 74 | |||
| 75 | |||
| 76 | def pub_compressed(point): | ||
| 77 | return bytes([2 + (point[1] & 1)]) + point[0].to_bytes(32, "big") | ||
| 78 | |||
| 79 | |||
| 80 | # ---- encoding -------------------------------------------------------------- | ||
| 81 | |||
| 82 | def base58check(payload): | ||
| 83 | alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" | ||
| 84 | checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4] | ||
| 85 | data = payload + checksum | ||
| 86 | number = int.from_bytes(data, "big") | ||
| 87 | out = "" | ||
| 88 | while number: | ||
| 89 | number, rem = divmod(number, 58) | ||
| 90 | out = alphabet[rem] + out | ||
| 91 | leading_zeros = len(data) - len(data.lstrip(b"\x00")) | ||
| 92 | return "1" * leading_zeros + out | ||
| 93 | |||
| 94 | |||
| 95 | def address(privkey): | ||
| 96 | pub = pub_uncompressed(point_mul(privkey)) | ||
| 97 | h160 = hashlib.new("ripemd160", hashlib.sha256(pub).digest()).digest() | ||
| 98 | return base58check(bytes([ADDRESS_VERSION]) + h160) | ||
| 99 | |||
| 100 | |||
| 101 | # ---- BIP-39 and BIP-32 ----------------------------------------------------- | ||
| 102 | |||
| 103 | def master_from_mnemonic(mnemonic, passphrase=""): | ||
| 104 | seed = hashlib.pbkdf2_hmac("sha512", mnemonic.encode("utf-8"), | ||
| 105 | ("mnemonic" + passphrase).encode("utf-8"), | ||
| 106 | 2048, 64) | ||
| 107 | i = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest() | ||
| 108 | return int.from_bytes(i[:32], "big"), i[32:] | ||
| 109 | |||
| 110 | |||
| 111 | def ckd_priv(key, chain, index): | ||
| 112 | if index >= HARDENED: | ||
| 113 | data = b"\x00" + key.to_bytes(32, "big") | ||
| 114 | else: | ||
| 115 | data = pub_compressed(point_mul(key)) | ||
| 116 | i = hmac.new(chain, data + index.to_bytes(4, "big"), hashlib.sha512).digest() | ||
| 117 | return (int.from_bytes(i[:32], "big") + key) % N, i[32:] | ||
| 118 | |||
| 119 | |||
| 120 | def derive(path): | ||
| 121 | key, chain = master_from_mnemonic(MNEMONIC) | ||
| 122 | for index in path: | ||
| 123 | key, chain = ckd_priv(key, chain, index) | ||
| 124 | return key | ||
| 125 | |||
| 126 | |||
| 127 | # ---- the check ------------------------------------------------------------- | ||
| 128 | |||
| 129 | def main(): | ||
| 130 | failures = 0 | ||
| 131 | |||
| 132 | print("legacy m/i'") | ||
| 133 | for i, expected in enumerate(LEGACY): | ||
| 134 | got = address(derive([i + HARDENED])) | ||
| 135 | ok = got == expected | ||
| 136 | failures += 0 if ok else 1 | ||
| 137 | print(" m/%d' %s %s" % (i, got, "ok" if ok else | ||
| 138 | "MISMATCH, wallet says " + expected)) | ||
| 139 | |||
| 140 | print() | ||
| 141 | print("bip-44 m/44'/%d'/0'/0/i" % COIN_TYPE) | ||
| 142 | for i, expected in enumerate(BIP44_EXTERNAL): | ||
| 143 | got = address(derive([44 + HARDENED, COIN_TYPE + HARDENED, | ||
| 144 | 0 + HARDENED, 0, i])) | ||
| 145 | ok = got == expected | ||
| 146 | failures += 0 if ok else 1 | ||
| 147 | print(" .../%d %s %s" % (i, got, "ok" if ok else | ||
| 148 | "MISMATCH, wallet says " + expected)) | ||
| 149 | |||
| 150 | print() | ||
| 151 | if failures: | ||
| 152 | print("%d address(es) disagree -- doc/derivation.md is wrong" % failures) | ||
| 153 | return 1 | ||
| 154 | print("all addresses match: the document reproduces the wallet") | ||
| 155 | return 0 | ||
| 156 | |||
| 157 | |||
| 158 | if __name__ == "__main__": | ||
| 159 | sys.exit(main()) | ||
diff --git a/docs/wallet.png b/docs/wallet.png new file mode 100644 index 0000000..addf21e --- /dev/null +++ b/docs/wallet.png | |||
| Binary files differ | |||