// Package wallet is the swap wallet: one seed, one account per chain, P2WPKH addresses for // deposits and change. // // It is deliberately small. The swap wallet holds the amount being swapped and nothing // else, so it has no history, no labels, no multiple accounts and no coinjoin: those are // features of a general wallet, and every one of them is a way to spend the wrong coin. What // it does have is a derivation path it can write down, because an address that cannot be // found again is a lost deposit. package wallet import ( "crypto/sha256" "fmt" "strings" "git.smesh.lol/nostrino/pkg/chain" "git.smesh.lol/tinywallet/pkg/bech32" "git.smesh.lol/tinywallet/pkg/hd" "golang.org/x/crypto/ripemd160" ) // Profile is one chain's wallet facts: the prefix an address carries, and the account the // keys come from. // // The address prefix and the coin type come from the nostrino chain profile, so the wallet // and the filter client cannot disagree about which network they are on. The account is // this wallet's own: the two chains share one seed, and a distinct account is what keeps // them from deriving the same keys. type Profile struct { // Name is "btc" or "blake". The repository speaks "blake" for the forked chain; // "xbt" is only ever a display name. Name string // HRP is the bech32 prefix from the network's chain profile. HRP string // CoinType is the BIP44/84 coin type from the network's chain profile. CoinType uint32 // Account is the BIP32 account index for this wallet on this chain. Account uint32 // Chain is the nostrino profile the wallet's filters come from. It is the network // profile, not the chain name: the fork shares Bitcoin's genesis and network magic, // so what the profile proves is which network a peer is on, and the account index is // what keeps the two chains' keys apart. Chain *chain.Profile } // The account indexes this wallet uses. // // BTC takes account 0, the path every other wallet uses. The forked chain has no registered // BIP44 coin type, and inventing one would collide with somebody else's numbering, so it // takes account 1 under coin type 0. That is a deliberate, recorded choice: one seed, two // accounts, no two keys the same. const ( AccountBTC uint32 = 0 AccountBlake uint32 = 1 ) // ProfileFor returns the wallet profile for one chain on one network. // // network is the nostrino chain profile name: "btc" for mainnet, "regtest", "testnet3" or // "signet". chainName is the swap's own vocabulary: "btc" or "blake" (with "xbt" accepted as // the display name of the forked chain). // // The two are separate because they answer different questions. The network profile is what // the client verifies a peer against, and the forked chain runs the same network parameters // as Bitcoin: the local two-chain rig demonstrates it, with both nodes reporting the same // regtest genesis. What distinguishes the two chains is therefore the node they connect to // and the account this wallet derives, not the genesis. func ProfileFor(chainName, network string) (Profile, error) { profile, err := chain.ByName(network) if err != nil { return Profile{}, err } switch strings.ToLower(strings.TrimSpace(chainName)) { case "btc", "bitcoin": return Profile{ Name: "btc", HRP: profile.Bech32HRP, CoinType: profile.CoinType, Account: AccountBTC, Chain: profile, }, nil case "blake", "xbt": return Profile{ Name: "blake", HRP: profile.Bech32HRP, CoinType: profile.CoinType, Account: AccountBlake, Chain: profile, }, nil } return Profile{}, fmt.Errorf("unknown chain %q: use btc or blake", chainName) } // Purpose is the BIP84 purpose, which is what makes an address a native segwit one. const Purpose uint32 = 84 // AccountPath is the account's derivation path: m/84'/coin'/account'. func (p Profile) AccountPath() hd.Path { return hd.Path{ Purpose | hd.Hardened, p.CoinType | hd.Hardened, p.Account | hd.Hardened, } } // AddressPath is one address's full path: m/84'/coin'/account'/chain/index. // // The chain level is 0 for a receive address and 1 for change, which is the BIP44 // convention: a change address a counterparty sees should not be an address a payer was // told to use. func (p Profile) AddressPath(change bool, index uint32) hd.Path { path := append(hd.Path{}, p.AccountPath()...) branch := uint32(0) if change { branch = 1 } return append(path, branch, index) } // Wallet is one chain account derived from one seed. type Wallet struct { profile Profile account *hd.Key } // New derives the account for one profile from a BIP39 seed. func New(seed [64]byte, profile Profile) (*Wallet, error) { if profile.HRP == "" { return nil, fmt.Errorf("the %s profile has no address prefix", profile.Name) } if profile.Chain == nil { return nil, fmt.Errorf("the %s profile has no chain profile", profile.Name) } master, err := hd.NewMaster(seed[:]) if err != nil { return nil, err } account, err := master.Derive(profile.AccountPath()) if err != nil { return nil, err } return &Wallet{profile: profile, account: account}, nil } // Profile is the wallet's chain profile. func (w *Wallet) Profile() Profile { return w.profile } // Account returns the account key, for a caller that needs to record a fingerprint. func (w *Wallet) Account() *hd.Key { return w.account } // Address is one receive or change address, with the key that spends it. // // The key comes back with the address because the wallet's next step is to watch that // address in the filters and to sign for it: returning a string alone would make the caller // re-derive it down a second path, which is how two code paths end up disagreeing. func (w *Wallet) Address(change bool, index uint32) (Address, error) { if index >= hd.Hardened { return Address{}, fmt.Errorf("an address index is below 2^31, not %d", index) } path := w.profile.AddressPath(change, index) key, err := w.account.Derive(hd.Path{path[len(path)-2], path[len(path)-1]}) if err != nil { return Address{}, err } pubkey := key.Public() program := hash160(pubkey) encoded, err := bech32.EncodeSegwit(w.profile.HRP, 0, program[:]) if err != nil { return Address{}, err } return Address{ Chain: w.profile.Name, Text: encoded, Index: index, Change: change, Path: path, Pubkey: pubkey, Program: program, }, nil } // Address is one derived address and everything that identifies it. type Address struct { // Chain is the profile the address belongs to. An address is never valid on another // chain, and carrying the name is what stops one being shown as the other. Chain string // Text is the bech32 address. Text string // Index is the address index at its level. Index uint32 // Change marks the change branch. Change bool // Path is the derivation path, recorded so the address can be found again. Path hd.Path // Pubkey is the compressed public key. Pubkey []byte // Program is the 20-byte witness program. Program [20]byte } // Script returns the output script the filters match: OP_0, a push of 20 bytes, the program. func (a Address) Script() []byte { out := make([]byte, 0, 22) out = append(out, 0x00, 0x14) out = append(out, a.Program[:]...) return out } // Addresses returns a run of addresses on one branch. func (w *Wallet) Addresses(change bool, from, count uint32) ([]Address, error) { if count == 0 || count > 1000 { return nil, fmt.Errorf("a run of addresses is 1 to 1000, not %d", count) } out := make([]Address, 0, count) for offset := uint32(0); offset < count; offset++ { index := from + offset address, err := w.Address(change, index) if err != nil { return nil, err } out = append(out, address) } return out, nil } // hash160 is sha256 then ripemd160: what a P2WPKH address commits to. func hash160(data []byte) [20]byte { first := sha256.Sum256(data) hasher := ripemd160.New() hasher.Write(first[:]) var out [20]byte copy(out[:], hasher.Sum(nil)) return out }