upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
committerbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
commit5ce650382352c1503f40abf08129ada69402af55 (patch)
tree6a18198901f1e2c4933f1ac53c9365eec512bf63 /docs
parentf34113291da7e5e3dc7d2468d4f9c212c05c584b (diff)
public commit
Diffstat (limited to 'docs')
-rw-r--r--docs/derivation.md125
-rw-r--r--docs/discovery.pngbin0 -> 36535 bytes
-rw-r--r--docs/logo.pngbin0 -> 14622 bytes
-rw-r--r--docs/release-verification.md62
-rw-r--r--docs/social.pngbin0 -> 208957 bytes
-rw-r--r--docs/verify-derivation.py159
-rw-r--r--docs/wallet.pngbin0 -> 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
3This document exists so that a Bitflash wallet can be rebuilt **without this
4software**. If the project stops, the binaries are lost, or you simply do not
5trust them, the twelve words plus what is written here are enough to recover
6your private keys with ordinary cryptographic tools.
7
8Everything below is verified by `docs/verify-derivation.py`, a self-contained
9script that implements this document from scratch — no Bitflash code, no
10libraries beyond Python's standard library — and checks that it reproduces the
11addresses the wallet produces. If the two ever disagree, this document is wrong
12and the script will say so.
13
14## Addresses
15
16An 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
26The public key is uncompressed. This matters more than anything else here: the
27same private key hashed in compressed form gives a completely different, valid
28looking address that holds nothing. Bitflash descends from Bitcoin 0.1.0, which
29predates compressed keys.
30
31Outputs paying an address use the usual script:
32
33```
34OP_DUP OP_HASH160 <hash160> OP_EQUALVERIFY OP_CHECKSIG
35```
36
37Mined 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
42Standard BIP-39 and BIP-32, with no modifications:
43
441. **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`.
452. **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
47Child derivation is BIP-32 `CKDpriv`, hardened and non-hardened as usual.
48
49## Two layouts
50
51A wallet records which layout it uses in `wallet.dat` under the key `hdschema`.
52Wallets created before that field existed are treated as legacy.
53
54### Legacy (`hdschema` absent or 1)
55
56```
57m/i'
58```
59
60Hardened children of the master key, one flat sequence. Index 0 is the wallet's
61default receiving address; the key pool and change take the following indices.
62
63Used by every wallet created before BIP-44 support existed, and kept forever:
64those phrases must keep working.
65
66### BIP-44 (`hdschema` = 2)
67
68```
69m/44'/4346950'/0'/0/i external chain (receiving)
70m/44'/4346950'/0'/1/i internal chain (change)
71```
72
73Coin 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).
75The account level is fixed at `0'`; Bitflash does not expose multiple accounts.
76
77A wallet restored from a phrase scans **both** layouts, because the words alone
78do not say which one produced them.
79
80## Test vectors
81
82Using the public BIP-39 test mnemonic:
83
84```
85abandon 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
106Check them yourself:
107
108```
109python docs/verify-derivation.py
110```
111
112Both tables were produced by the wallet — the legacy one by the v1.2.15 binary,
113which only knows that layout — and then reproduced independently by the script.
114
115## What this document does not give you
116
117Deriving the keys is not the same as spending. To move recovered coins you still
118need a node that knows the network: the chain, the peer protocol, and the
119transaction format. This document is about **not losing the keys**, which is the
120part that cannot be reconstructed later by reading source code that no longer
121exists anywhere.
122
123Note also that `wallet.dat` stores private keys unencrypted. A recovery phrase
124protects you against losing the file; it does not protect the file. Anyone who
125can 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
3Bitflash releases publish binaries and a `SHA256SUMS` file. The checksum file
4proves that the file you downloaded matches the file the release page names.
5A detached signature, `SHA256SUMS.asc`, proves that the checksum file itself was
6signed by a trusted release key.
7
8That gives users two separate checks:
9
101. `gpg --verify SHA256SUMS.asc SHA256SUMS` checks who signed the checksums.
112. `sha256sum -c SHA256SUMS` checks the binaries against those checksums.
12
13## User Check
14
15From a shell with `curl` or `wget` and `sha256sum`:
16
17```bash
18scripts/verify-release.sh latest
19```
20
21For a specific release:
22
23```bash
24scripts/verify-release.sh v1.2.13
25```
26
27For release audits, require the signature:
28
29```bash
30scripts/verify-release.sh v1.2.13 --require-signature
31```
32
33Older releases may not have `SHA256SUMS.asc`. In that case the script warns and
34still checks file integrity. New release audits should use `--require-signature`.
35
36## Maintainer Flow
37
38After building release assets in the repository root:
39
40```bash
41scripts/make-release-checksums.sh --sign --local-user RELEASE_KEY_ID
42```
43
44Upload all built assets plus:
45
46```text
47SHA256SUMS
48SHA256SUMS.asc
49```
50
51Keep the private signing key offline or on a dedicated release machine. Publish
52the public key fingerprint in the release notes and keep using the same key for
53future 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
58replacing both a binary and the checksum file. Signing `SHA256SUMS` means an
59attacker must also have the release signing key to make the replacement verify.
60
61This is not reproducible builds yet. It is the smaller, immediate step that
62makes 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
4This implements the document and nothing else: BIP-39 to seed, BIP-32 to child
5keys, uncompressed secp256k1 public keys, and Base58Check with version byte 25.
6It shares no code with Bitflash and needs nothing outside the standard library,
7so agreement between the two is evidence that the document is enough to recover
8a wallet without this project's software.
9
10 python doc/verify-derivation.py
11
12Exits non-zero if any address disagrees.
13"""
14
15import hashlib
16import hmac
17import sys
18
19MNEMONIC = ("abandon abandon abandon abandon abandon abandon "
20 "abandon abandon abandon abandon abandon about")
21COIN_TYPE = 4346950 # 0x425446, the ASCII bytes "BTF"
22ADDRESS_VERSION = 25 # 0x19, so addresses start with "B"
23HARDENED = 0x80000000
24
25LEGACY = [
26 "BHJNmVvkVv59fgn7gmgP78BtciQi2HY8Hy",
27 "B5qh9opG8sH8Mv9k2xzHoTeSUd9BkT6XEZ",
28 "BCNH8uGyvkiz3uXjhA9iiF27PWAaTStNin",
29 "BC1QNaXuTPGDgLZy53PghZMHEmZcp5GLgD",
30]
31
32BIP44_EXTERNAL = [
33 "BF1ukigB1afmqxH64gJymPwUqFBrRqWbrb",
34 "BACY8pcuCapNQr897eRHroNrgN38GSsEzV",
35 "BMzkMN2k1phNBqy8RPb4Pv1i2QpwsWkKPa",
36 "BNPZ8U6FHXr9Ererk8psyM2RqEVtF6sfBQ",
37]
38
39# ---- secp256k1, plain and slow on purpose: it should be readable ------------
40
41P = 2**256 - 2**32 - 977
42N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
43G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
44 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
45
46
47def 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
62def 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
72def pub_uncompressed(point):
73 return b"\x04" + point[0].to_bytes(32, "big") + point[1].to_bytes(32, "big")
74
75
76def pub_compressed(point):
77 return bytes([2 + (point[1] & 1)]) + point[0].to_bytes(32, "big")
78
79
80# ---- encoding --------------------------------------------------------------
81
82def 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
95def 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
103def 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
111def 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
120def 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
129def 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
158if __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