1 // Package wallet is the swap wallet: one seed, one account per chain, P2WPKH addresses for
2 // deposits and change.
3 //
4 // It is deliberately small. The swap wallet holds the amount being swapped and nothing
5 // else, so it has no history, no labels, no multiple accounts and no coinjoin: those are
6 // features of a general wallet, and every one of them is a way to spend the wrong coin. What
7 // it does have is a derivation path it can write down, because an address that cannot be
8 // found again is a lost deposit.
9 package wallet
10 11 import (
12 "crypto/sha256"
13 "fmt"
14 "strings"
15 16 "git.smesh.lol/nostrino/pkg/chain"
17 "git.smesh.lol/tinywallet/pkg/bech32"
18 "git.smesh.lol/tinywallet/pkg/hd"
19 "golang.org/x/crypto/ripemd160"
20 )
21 22 // Profile is one chain's wallet facts: the prefix an address carries, and the account the
23 // keys come from.
24 //
25 // The address prefix and the coin type come from the nostrino chain profile, so the wallet
26 // and the filter client cannot disagree about which network they are on. The account is
27 // this wallet's own: the two chains share one seed, and a distinct account is what keeps
28 // them from deriving the same keys.
29 type Profile struct {
30 // Name is "btc" or "blake". The repository speaks "blake" for the forked chain;
31 // "xbt" is only ever a display name.
32 Name string
33 // HRP is the bech32 prefix from the network's chain profile.
34 HRP string
35 // CoinType is the BIP44/84 coin type from the network's chain profile.
36 CoinType uint32
37 // Account is the BIP32 account index for this wallet on this chain.
38 Account uint32
39 // Chain is the nostrino profile the wallet's filters come from. It is the network
40 // profile, not the chain name: the fork shares Bitcoin's genesis and network magic,
41 // so what the profile proves is which network a peer is on, and the account index is
42 // what keeps the two chains' keys apart.
43 Chain *chain.Profile
44 }
45 46 // The account indexes this wallet uses.
47 //
48 // BTC takes account 0, the path every other wallet uses. The forked chain has no registered
49 // BIP44 coin type, and inventing one would collide with somebody else's numbering, so it
50 // takes account 1 under coin type 0. That is a deliberate, recorded choice: one seed, two
51 // accounts, no two keys the same.
52 const (
53 AccountBTC uint32 = 0
54 AccountBlake uint32 = 1
55 )
56 57 // ProfileFor returns the wallet profile for one chain on one network.
58 //
59 // network is the nostrino chain profile name: "btc" for mainnet, "regtest", "testnet3" or
60 // "signet". chainName is the swap's own vocabulary: "btc" or "blake" (with "xbt" accepted as
61 // the display name of the forked chain).
62 //
63 // The two are separate because they answer different questions. The network profile is what
64 // the client verifies a peer against, and the forked chain runs the same network parameters
65 // as Bitcoin: the local two-chain rig demonstrates it, with both nodes reporting the same
66 // regtest genesis. What distinguishes the two chains is therefore the node they connect to
67 // and the account this wallet derives, not the genesis.
68 func ProfileFor(chainName, network string) (Profile, error) {
69 profile, err := chain.ByName(network)
70 71 if err != nil {
72 return Profile{}, err
73 }
74 75 switch strings.ToLower(strings.TrimSpace(chainName)) {
76 case "btc", "bitcoin":
77 return Profile{
78 Name: "btc", HRP: profile.Bech32HRP, CoinType: profile.CoinType,
79 Account: AccountBTC, Chain: profile,
80 }, nil
81 case "blake", "xbt":
82 return Profile{
83 Name: "blake", HRP: profile.Bech32HRP, CoinType: profile.CoinType,
84 Account: AccountBlake, Chain: profile,
85 }, nil
86 }
87 88 return Profile{}, fmt.Errorf("unknown chain %q: use btc or blake", chainName)
89 }
90 91 // Purpose is the BIP84 purpose, which is what makes an address a native segwit one.
92 const Purpose uint32 = 84
93 94 // AccountPath is the account's derivation path: m/84'/coin'/account'.
95 func (p Profile) AccountPath() hd.Path {
96 return hd.Path{
97 Purpose | hd.Hardened,
98 p.CoinType | hd.Hardened,
99 p.Account | hd.Hardened,
100 }
101 }
102 103 // AddressPath is one address's full path: m/84'/coin'/account'/chain/index.
104 //
105 // The chain level is 0 for a receive address and 1 for change, which is the BIP44
106 // convention: a change address a counterparty sees should not be an address a payer was
107 // told to use.
108 func (p Profile) AddressPath(change bool, index uint32) hd.Path {
109 path := append(hd.Path{}, p.AccountPath()...)
110 111 branch := uint32(0)
112 113 if change {
114 branch = 1
115 }
116 117 return append(path, branch, index)
118 }
119 120 // Wallet is one chain account derived from one seed.
121 type Wallet struct {
122 profile Profile
123 account *hd.Key
124 }
125 126 // New derives the account for one profile from a BIP39 seed.
127 func New(seed [64]byte, profile Profile) (*Wallet, error) {
128 if profile.HRP == "" {
129 return nil, fmt.Errorf("the %s profile has no address prefix", profile.Name)
130 }
131 132 if profile.Chain == nil {
133 return nil, fmt.Errorf("the %s profile has no chain profile", profile.Name)
134 }
135 136 master, err := hd.NewMaster(seed[:])
137 138 if err != nil {
139 return nil, err
140 }
141 142 account, err := master.Derive(profile.AccountPath())
143 144 if err != nil {
145 return nil, err
146 }
147 148 return &Wallet{profile: profile, account: account}, nil
149 }
150 151 // Profile is the wallet's chain profile.
152 func (w *Wallet) Profile() Profile { return w.profile }
153 154 // Account returns the account key, for a caller that needs to record a fingerprint.
155 func (w *Wallet) Account() *hd.Key { return w.account }
156 157 // Address is one receive or change address, with the key that spends it.
158 //
159 // The key comes back with the address because the wallet's next step is to watch that
160 // address in the filters and to sign for it: returning a string alone would make the caller
161 // re-derive it down a second path, which is how two code paths end up disagreeing.
162 func (w *Wallet) Address(change bool, index uint32) (Address, error) {
163 if index >= hd.Hardened {
164 return Address{}, fmt.Errorf("an address index is below 2^31, not %d", index)
165 }
166 167 path := w.profile.AddressPath(change, index)
168 169 key, err := w.account.Derive(hd.Path{path[len(path)-2], path[len(path)-1]})
170 171 if err != nil {
172 return Address{}, err
173 }
174 175 pubkey := key.Public()
176 177 program := hash160(pubkey)
178 179 encoded, err := bech32.EncodeSegwit(w.profile.HRP, 0, program[:])
180 181 if err != nil {
182 return Address{}, err
183 }
184 185 return Address{
186 Chain: w.profile.Name,
187 Text: encoded,
188 Index: index,
189 Change: change,
190 Path: path,
191 Pubkey: pubkey,
192 Program: program,
193 }, nil
194 }
195 196 // Address is one derived address and everything that identifies it.
197 type Address struct {
198 // Chain is the profile the address belongs to. An address is never valid on another
199 // chain, and carrying the name is what stops one being shown as the other.
200 Chain string
201 // Text is the bech32 address.
202 Text string
203 // Index is the address index at its level.
204 Index uint32
205 // Change marks the change branch.
206 Change bool
207 // Path is the derivation path, recorded so the address can be found again.
208 Path hd.Path
209 // Pubkey is the compressed public key.
210 Pubkey []byte
211 // Program is the 20-byte witness program.
212 Program [20]byte
213 }
214 215 // Script returns the output script the filters match: OP_0, a push of 20 bytes, the program.
216 func (a Address) Script() []byte {
217 out := make([]byte, 0, 22)
218 out = append(out, 0x00, 0x14)
219 out = append(out, a.Program[:]...)
220 221 return out
222 }
223 224 // Addresses returns a run of addresses on one branch.
225 func (w *Wallet) Addresses(change bool, from, count uint32) ([]Address, error) {
226 if count == 0 || count > 1000 {
227 return nil, fmt.Errorf("a run of addresses is 1 to 1000, not %d", count)
228 }
229 230 out := make([]Address, 0, count)
231 232 for offset := uint32(0); offset < count; offset++ {
233 index := from + offset
234 235 address, err := w.Address(change, index)
236 237 if err != nil {
238 return nil, err
239 }
240 241 out = append(out, address)
242 }
243 244 return out, nil
245 }
246 247 // hash160 is sha256 then ripemd160: what a P2WPKH address commits to.
248 func hash160(data []byte) [20]byte {
249 first := sha256.Sum256(data)
250 251 hasher := ripemd160.New()
252 hasher.Write(first[:])
253 254 var out [20]byte
255 256 copy(out[:], hasher.Sum(nil))
257 258 return out
259 }
260