1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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 <hash160> OP_EQUALVERIFY OP_CHECKSIG
```
Mined coins and payments made directly to a public key use the older form
`<pubkey> 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.
|