wallet.go raw

   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