From a05b75402b84bc4772a5ad88d141c8b0fc10e4c7 Mon Sep 17 00:00:00 2001 From: elnosh Date: Wed, 5 Jun 2024 13:01:37 -0500 Subject: [PATCH] wallet - deterministic secret derivation --- .gitignore | 1 + cashu/nuts/nut13/nut13.go | 83 +++++++++++ cashu/nuts/nut13/nut13_test.go | 74 ++++++++++ cmd/nutw/nutw.go | 1 + crypto/keyset.go | 60 +++++++- go.mod | 1 + go.sum | 2 + wallet/storage/bolt.go | 128 +++++++++++++--- wallet/storage/storage.go | 8 +- wallet/wallet.go | 262 +++++++++++++++++++++++++-------- wallet/wallet_test.go | 43 ++++-- 11 files changed, 565 insertions(+), 98 deletions(-) create mode 100644 cashu/nuts/nut13/nut13.go create mode 100644 cashu/nuts/nut13/nut13_test.go diff --git a/.gitignore b/.gitignore index 1d9f1da..3f41fe4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ cmd/nutw/nutw **/.env *.txt +.vscode diff --git a/cashu/nuts/nut13/nut13.go b/cashu/nuts/nut13/nut13.go new file mode 100644 index 0000000..9bb83c6 --- /dev/null +++ b/cashu/nuts/nut13/nut13.go @@ -0,0 +1,83 @@ +package nut13 + +import ( + "encoding/binary" + "encoding/hex" + + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +func DeriveKeysetPath(master *hdkeychain.ExtendedKey, keysetId string) (*hdkeychain.ExtendedKey, error) { + keysetBytes, err := hex.DecodeString(keysetId) + if err != nil { + return nil, err + } + bigEndianBytes := binary.BigEndian.Uint64(keysetBytes) + keysetIdInt := bigEndianBytes % (1<<31 - 1) + + // m/129372 + purpose, err := master.Derive(hdkeychain.HardenedKeyStart + 129372) + if err != nil { + return nil, err + } + + // m/129372'/0' + coinType, err := purpose.Derive(hdkeychain.HardenedKeyStart + 0) + if err != nil { + return nil, err + } + + // m/129372'/0'/keyset_k_int' + keysetPath, err := coinType.Derive(hdkeychain.HardenedKeyStart + uint32(keysetIdInt)) + if err != nil { + return nil, err + } + + return keysetPath, nil +} + +func DeriveBlindingFactor(keysetPath *hdkeychain.ExtendedKey, counter uint32) (*secp256k1.PrivateKey, error) { + // m/129372'/0'/keyset_k_int'/counter' + counterPath, err := keysetPath.Derive(hdkeychain.HardenedKeyStart + counter) + if err != nil { + return nil, err + } + + // m/129372'/0'/keyset_k_int'/counter'/1 + rDerivationPath, err := counterPath.Derive(1) + if err != nil { + return nil, err + } + + rkey, err := rDerivationPath.ECPrivKey() + if err != nil { + return nil, err + } + + return rkey, nil +} + +func DeriveSecret(keysetPath *hdkeychain.ExtendedKey, counter uint32) (string, error) { + // m/129372'/0'/keyset_k_int'/counter' + counterPath, err := keysetPath.Derive(hdkeychain.HardenedKeyStart + counter) + if err != nil { + return "", err + } + + // m/129372'/0'/keyset_k_int'/counter'/0 + secretDerivationPath, err := counterPath.Derive(0) + if err != nil { + return "", err + } + + secretKey, err := secretDerivationPath.ECPrivKey() + if err != nil { + return "", err + } + + secretBytes := secretKey.Serialize() + secret := hex.EncodeToString(secretBytes) + + return secret, nil +} diff --git a/cashu/nuts/nut13/nut13_test.go b/cashu/nuts/nut13/nut13_test.go new file mode 100644 index 0000000..34c6a2f --- /dev/null +++ b/cashu/nuts/nut13/nut13_test.go @@ -0,0 +1,74 @@ +package nut13 + +import ( + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" + "github.com/tyler-smith/go-bip39" +) + +func TestSecretDerivation(t *testing.T) { + mnemonic := "half depart obvious quality work element tank gorilla view sugar picture humble" + keysetId := "009a1f293253e41e" + + seed := bip39.NewSeed(mnemonic, "") + master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + if err != nil { + t.Fatal(err) + } + + keysetPath, err := DeriveKeysetPath(master, keysetId) + if err != nil { + t.Fatalf("could not derive keyset path: %v", err) + } + + secrets := make([]string, 5) + rs := make([]string, 5) + + var i uint32 = 0 + for ; i < 5; i++ { + secret, err := DeriveSecret(keysetPath, i) + if err != nil { + t.Fatalf("error deriving secret: %v", err) + } + secrets[i] = secret + + rkey, err := DeriveBlindingFactor(keysetPath, i) + if err != nil { + t.Fatalf("error deriving r: %v", err) + } + + rbytes := rkey.Serialize() + r := hex.EncodeToString(rbytes) + rs[i] = r + } + + expectedSecrets := []string{ + "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae", + "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270", + "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8", + "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf", + "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0", + } + + expectedRs := []string{ + "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679", + "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248", + "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899", + "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29", + "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9", + } + + for i := 0; i < 5; i++ { + if expectedSecrets[i] != secrets[i] { + t.Fatalf("secret at index: %v does not match. Expected '%v' but got '%v'", i, expectedSecrets[i], secrets[i]) + } + + if expectedRs[i] != rs[i] { + t.Fatalf("r at index: %v does not match. Expected '%v' but got '%v'", i, expectedRs[i], rs[i]) + } + } + +} diff --git a/cmd/nutw/nutw.go b/cmd/nutw/nutw.go index eb014bd..d4f4e9f 100644 --- a/cmd/nutw/nutw.go +++ b/cmd/nutw/nutw.go @@ -178,6 +178,7 @@ func receive(ctx *cli.Context) error { } receivedAmount, err := nutw.Receive(*token, swap) + //receivedAmount, err := nutw.Receive(*token, false) if err != nil { printErr(err) } diff --git a/crypto/keyset.go b/crypto/keyset.go index e20d286..3b12d66 100644 --- a/crypto/keyset.go +++ b/crypto/keyset.go @@ -15,8 +15,7 @@ import ( const maxOrder = 64 type Keyset struct { - Id string - //MintURL string + Id string Unit string Active bool Keys map[uint64]KeyPair @@ -36,7 +35,7 @@ type WalletKeyset struct { Unit string Active bool PublicKeys map[uint64]*secp256k1.PublicKey - Counter uint64 + Counter uint32 } func GenerateKeyset(seed, derivationPath string) *Keyset { @@ -130,7 +129,6 @@ func (ks *Keyset) UnmarshalJSON(data []byte) error { } ks.Id = temp.Id - //ks.MintURL = temp.MintURL ks.Unit = temp.Unit ks.Active = temp.Active @@ -181,3 +179,57 @@ func (kp *KeyPair) UnmarshalJSON(data []byte) error { return nil } + +type WalletKeysetTemp struct { + Id string + MintURL string + Unit string + Active bool + PublicKeys map[uint64][]byte + Counter uint32 +} + +func (wk *WalletKeyset) MarshalJSON() ([]byte, error) { + temp := &WalletKeysetTemp{ + Id: wk.Id, + MintURL: wk.MintURL, + Unit: wk.Unit, + Active: wk.Active, + PublicKeys: func() map[uint64][]byte { + m := make(map[uint64][]byte) + for k, v := range wk.PublicKeys { + m[k] = v.SerializeCompressed() + } + return m + }(), + Counter: wk.Counter, + } + + return json.Marshal(temp) +} + +func (wk *WalletKeyset) UnmarshalJSON(data []byte) error { + temp := &WalletKeysetTemp{} + + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + wk.Id = temp.Id + wk.MintURL = temp.MintURL + wk.Unit = temp.Unit + wk.Active = temp.Active + wk.Counter = temp.Counter + + wk.PublicKeys = make(map[uint64]*secp256k1.PublicKey) + for k, v := range temp.PublicKeys { + kp, err := secp256k1.ParsePubKey(v) + if err != nil { + return err + } + + wk.PublicKeys[k] = kp + } + + return nil +} diff --git a/go.mod b/go.mod index c1b1303..ea4a55e 100644 --- a/go.mod +++ b/go.mod @@ -148,6 +148,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index 1249494..6e035de 100644 --- a/go.sum +++ b/go.sum @@ -630,6 +630,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/wallet/storage/bolt.go b/wallet/storage/bolt.go index f0eb7be..fabf8e3 100644 --- a/wallet/storage/bolt.go +++ b/wallet/storage/bolt.go @@ -15,6 +15,7 @@ const ( keysetsBucket = "keysets" proofsBucket = "proofs" invoicesBucket = "invoices" + seedBucket = "seed" ) type BoltDB struct { @@ -53,8 +54,31 @@ func (db *BoltDB) initWalletBuckets() error { return err } + _, err = tx.CreateBucketIfNotExists([]byte(seedBucket)) + if err != nil { + return err + } + + return nil + }) +} + +func (db *BoltDB) SaveSeed(seed []byte) { + db.bolt.Update(func(tx *bolt.Tx) error { + seedb := tx.Bucket([]byte(seedBucket)) + seedb.Put([]byte(seedBucket), seed) + return nil + }) +} + +func (db *BoltDB) GetSeed() []byte { + var seed []byte + db.bolt.View(func(tx *bolt.Tx) error { + seedb := tx.Bucket([]byte(seedBucket)) + seed = seedb.Get([]byte(seedBucket)) return nil }) + return seed } // return all proofs from db @@ -131,7 +155,7 @@ func (db *BoltDB) DeleteProof(secret string) error { }) } -func (db *BoltDB) SaveKeyset(keyset crypto.WalletKeyset) error { +func (db *BoltDB) SaveKeyset(keyset *crypto.WalletKeyset) error { jsonKeyset, err := json.Marshal(keyset) if err != nil { return fmt.Errorf("invalid keyset format: %v", err) @@ -158,7 +182,6 @@ func (db *BoltDB) GetKeysets() crypto.KeysetsMap { return keysetsb.ForEach(func(mintURL, v []byte) error { mintKeysets := make(map[string]crypto.WalletKeyset) - mintBucket := keysetsb.Bucket(mintURL) c := mintBucket.Cursor() @@ -167,7 +190,6 @@ func (db *BoltDB) GetKeysets() crypto.KeysetsMap { if err := json.Unmarshal(v, &keyset); err != nil { return err } - mintKeysets[string(k)] = keyset } @@ -179,29 +201,63 @@ func (db *BoltDB) GetKeysets() crypto.KeysetsMap { } return keysets +} +func (db *BoltDB) GetKeyset(keysetId string) *crypto.WalletKeyset { + var keyset *crypto.WalletKeyset + + db.bolt.View(func(tx *bolt.Tx) error { + keysetsb := tx.Bucket([]byte(keysetsBucket)) + + return keysetsb.ForEach(func(mintURL, v []byte) error { + mintBucket := keysetsb.Bucket(mintURL) + keysetBytes := mintBucket.Get([]byte(keysetId)) + if keysetBytes != nil { + err := json.Unmarshal(keysetBytes, &keyset) + if err != nil { + return err + } + } + return nil + }) + }) + + return keyset } -func (db *BoltDB) IncrementKeysetCounter(keysetId string) error { +func (db *BoltDB) IncrementKeysetCounter(keysetId string, num uint32) error { if err := db.bolt.Update(func(tx *bolt.Tx) error { keysetsb := tx.Bucket([]byte(keysetsBucket)) - keysetBytes := keysetsb.Get([]byte(keysetId)) - if keysetBytes == nil { - return errors.New("keyset does not exist") - } + var keyset *crypto.WalletKeyset + keysetFound := false - var keyset crypto.WalletKeyset - err := json.Unmarshal(keysetBytes, &keyset) - if err != nil { - return fmt.Errorf("error reading keyset from db: %v", err) - } - keyset.Counter += 1 + err := keysetsb.ForEach(func(mintURL, v []byte) error { + mintBucket := keysetsb.Bucket(mintURL) - jsonBytes, err := json.Marshal(keyset) - if err != nil { - return err + keysetBytes := mintBucket.Get([]byte(keysetId)) + if keysetBytes != nil { + err := json.Unmarshal(keysetBytes, &keyset) + if err != nil { + return fmt.Errorf("error reading keyset from db: %v", err) + } + keyset.Counter += num + + jsonBytes, err := json.Marshal(keyset) + if err != nil { + return err + } + keysetFound = true + return mintBucket.Put([]byte(keysetId), jsonBytes) + } + + return nil + }) + + if !keysetFound { + return errors.New("keyset does not exist") } - return keysetsb.Put([]byte(keysetId), jsonBytes) + + return err }); err != nil { return err } @@ -209,6 +265,42 @@ func (db *BoltDB) IncrementKeysetCounter(keysetId string) error { return nil } +func (db *BoltDB) GetKeysetCounter(keysetId string) uint32 { + var counter uint32 = 0 + + if err := db.bolt.Update(func(tx *bolt.Tx) error { + keysetsb := tx.Bucket([]byte(keysetsBucket)) + var keyset *crypto.WalletKeyset + keysetFound := false + + err := keysetsb.ForEach(func(mintURL, v []byte) error { + mintBucket := keysetsb.Bucket(mintURL) + + keysetBytes := mintBucket.Get([]byte(keysetId)) + if keysetBytes != nil { + err := json.Unmarshal(keysetBytes, &keyset) + if err != nil { + return err + } + counter = keyset.Counter + keysetFound = true + return nil + } + return nil + }) + + if !keysetFound { + return errors.New("keyset does not exist") + } + + return err + }); err != nil { + return 0 + } + + return counter +} + func (db *BoltDB) SaveInvoice(invoice Invoice) error { jsonbytes, err := json.Marshal(invoice) if err != nil { diff --git a/wallet/storage/storage.go b/wallet/storage/storage.go index e321aa1..9427966 100644 --- a/wallet/storage/storage.go +++ b/wallet/storage/storage.go @@ -24,13 +24,17 @@ func (quote QuoteType) String() string { } type DB interface { + SaveSeed([]byte) + GetSeed() []byte SaveProof(cashu.Proof) error GetProofsByKeysetId(string) cashu.Proofs GetProofs() cashu.Proofs DeleteProof(string) error - SaveKeyset(crypto.WalletKeyset) error + SaveKeyset(*crypto.WalletKeyset) error GetKeysets() crypto.KeysetsMap - IncrementKeysetCounter(string) error + GetKeyset(string) *crypto.WalletKeyset + IncrementKeysetCounter(string, uint32) error + GetKeysetCounter(string) uint32 SaveInvoice(Invoice) error GetInvoice(string) *Invoice GetInvoices() []Invoice diff --git a/wallet/wallet.go b/wallet/wallet.go index abfcb82..8bb8a3b 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1,7 +1,6 @@ package wallet import ( - "crypto/rand" "encoding/hex" "errors" "fmt" @@ -9,13 +8,18 @@ import ( "slices" "time" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/elnosh/gonuts/cashu" + "github.com/elnosh/gonuts/cashu/nuts/nut01" "github.com/elnosh/gonuts/cashu/nuts/nut03" "github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut05" + "github.com/elnosh/gonuts/cashu/nuts/nut13" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/wallet/storage" + "github.com/tyler-smith/go-bip39" decodepay "github.com/nbd-wtf/ln-decodepay" ) @@ -26,11 +30,12 @@ var ( ) type Wallet struct { - db storage.DB + db storage.DB + masterKey *hdkeychain.ExtendedKey // default mint currentMint *walletMint - // array of mints that have been trusted + // list of mints that have been trusted mints map[string]walletMint } @@ -53,8 +58,35 @@ func LoadWallet(config Config) (*Wallet, error) { return nil, fmt.Errorf("InitStorage: %v", err) } - wallet := &Wallet{db: db} - wallet.mints = wallet.getWalletMints() + // create new seed if none exists + seed := db.GetSeed() + if len(seed) == 0 { + // create and save new seed + entropy, err := bip39.NewEntropy(128) + if err != nil { + return nil, fmt.Errorf("error generating seed: %v", err) + } + + mnemonic, err := bip39.NewMnemonic(entropy) + if err != nil { + return nil, fmt.Errorf("error generating seed: %v", err) + } + + seed = bip39.NewSeed(mnemonic, "") + db.SaveSeed(seed) + } + + // TODO: what's the point of chain params here? + masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + + wallet := &Wallet{db: db, masterKey: masterKey} + wallet.mints, err = wallet.getWalletMints() + if err != nil { + return nil, err + } url, err := url.Parse(config.CurrentMintURL) if err != nil { return nil, fmt.Errorf("invalid mint url: %v", err) @@ -84,27 +116,37 @@ func LoadWallet(config Config) (*Wallet, error) { return nil, fmt.Errorf("error getting keysets from mint: %v", err) } - newKeyset := false + activeKeysetChanged := false for _, keyset := range keysetsResponse.Keysets { // check if last active recorded keyset is not active anymore if keyset.Id == lastActiveSatKeyset.Id && !keyset.Active { - newKeyset = true + activeKeysetChanged = true // there is new keyset, change last active to inactive lastActiveSatKeyset.Active = false - db.SaveKeyset(lastActiveSatKeyset) + wallet.db.SaveKeyset(&lastActiveSatKeyset) break } } - // if there is new active keyset, save it - if newKeyset { + // if active keyset changed, save accordingly + if activeKeysetChanged { activeKeysets, err := GetMintActiveKeysets(walletMint.mintURL) if err != nil { - return nil, fmt.Errorf("error getting keysets from mint: %v", err) + return nil, fmt.Errorf("error getting keyset from mint: %v", err) } - for _, keyset := range activeKeysets { - db.SaveKeyset(keyset) + + for i, keyset := range activeKeysets { + storedKeyset := db.GetKeyset(keyset.Id) + // save new keyset + if storedKeyset == nil { + db.SaveKeyset(&keyset) + } else { // if not new, change active to true + keyset.Active = true + keyset.Counter = storedKeyset.Counter + activeKeysets[i] = keyset + wallet.db.SaveKeyset(&keyset) + } } walletMint.activeKeysets = activeKeysets } @@ -143,10 +185,10 @@ func (w *Wallet) addMint(mint string) (*walletMint, error) { } for _, keyset := range mintInfo.activeKeysets { - w.db.SaveKeyset(keyset) + w.db.SaveKeyset(&keyset) } for _, keyset := range mintInfo.inactiveKeysets { - w.db.SaveKeyset(keyset) + w.db.SaveKeyset(&keyset) } w.mints[mintURL] = *mintInfo @@ -160,21 +202,14 @@ func GetMintActiveKeysets(mintURL string) (map[string]crypto.WalletKeyset, error } activeKeysets := make(map[string]crypto.WalletKeyset) - for i, keyset := range keysetsResponse.Keysets { + for _, keyset := range keysetsResponse.Keysets { if keyset.Unit == "sat" { activeKeyset := crypto.WalletKeyset{MintURL: mintURL, Unit: keyset.Unit, Active: true} - keys := make(map[uint64]*secp256k1.PublicKey) - for amount, key := range keysetsResponse.Keysets[i].Keys { - pkbytes, err := hex.DecodeString(key) - if err != nil { - return nil, err - } - pubkey, err := secp256k1.ParsePubKey(pkbytes) - if err != nil { - return nil, err - } - keys[amount] = pubkey + keys, err := mapPubKeys(keysetsResponse.Keysets[0].Keys) + if err != nil { + return nil, err } + activeKeyset.PublicKeys = keys id := crypto.DeriveKeysetId(activeKeyset.PublicKeys) activeKeyset.Id = id @@ -300,9 +335,11 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { return nil, errors.New("invoice not found") } - // create blinded messages activeKeyset := w.GetActiveSatKeyset() - blindedMessages, secrets, rs, err := createBlindedMessages(invoice.QuoteAmount, activeKeyset) + // get counter for keyset + counter := w.counterForKeyset(activeKeyset.Id) + + blindedMessages, secrets, rs, err := w.createBlindedMessages(invoice.QuoteAmount, activeKeyset.Id, counter) if err != nil { return nil, fmt.Errorf("error creating blinded messages: %v", err) } @@ -320,6 +357,18 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { return nil, fmt.Errorf("error constructing proofs: %v", err) } + // store proofs in db + err = w.saveProofs(proofs) + if err != nil { + return nil, fmt.Errorf("error storing proofs: %v", err) + } + + // only increase counter if mint was successful + err = w.incrementKeysetCounter(activeKeyset.Id, uint32(len(blindedMessages))) + if err != nil { + return nil, fmt.Errorf("error incrementing keyset counter: %v", err) + } + // mark invoice as redeemed invoice.Paid = true invoice.SettledAt = time.Now().Unix() @@ -328,12 +377,6 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { return nil, err } - // store proofs in db - err = w.saveProofs(proofs) - if err != nil { - return nil, fmt.Errorf("error storing proofs: %v", err) - } - return proofs, nil } @@ -380,10 +423,12 @@ func (w *Wallet) Receive(token cashu.Token, swap bool) (uint64, error) { break } + counter := w.counterForKeyset(activeSatKeyset.Id) + // create blinded messages - outputs, secrets, rs, err := createBlindedMessages(token.TotalAmount(), activeSatKeyset) + outputs, secrets, rs, err := w.createBlindedMessages(token.TotalAmount(), activeSatKeyset.Id, counter) if err != nil { - return 0, fmt.Errorf("CreateBlindedMessages: %v", err) + return 0, fmt.Errorf("createBlindedMessages: %v", err) } // make swap request to mint @@ -400,6 +445,12 @@ func (w *Wallet) Receive(token cashu.Token, swap bool) (uint64, error) { } w.saveProofs(proofs) + + err = w.incrementKeysetCounter(activeSatKeyset.Id, uint32(len(outputs))) + if err != nil { + return 0, fmt.Errorf("error incrementing keyset counter: %v", err) + } + return proofs.Amount(), nil } } @@ -587,20 +638,26 @@ func (w *Wallet) getProofsForAmount(amount uint64, mintURL string) (cashu.Proofs activeSatKeyset = k break } + + counter := w.counterForKeyset(activeSatKeyset.Id) + // blinded messages for send amount - send, secrets, rs, err := createBlindedMessages(amount, activeSatKeyset) + send, secrets, rs, err := w.createBlindedMessages(amount, activeSatKeyset.Id, counter) if err != nil { return nil, err } + counter += uint32(len(send)) + + blindedMessages := make(cashu.BlindedMessages, len(send)) + copy(blindedMessages, send) + // blinded messages for change amount - change, changeSecrets, changeRs, err := createBlindedMessages(currentProofsAmount-amount, activeSatKeyset) + change, changeSecrets, changeRs, err := w.createBlindedMessages(currentProofsAmount-amount, activeSatKeyset.Id, counter) if err != nil { return nil, err } - blindedMessages := make(cashu.BlindedMessages, len(send)) - copy(blindedMessages, send) blindedMessages = append(blindedMessages, change...) secrets = append(secrets, changeSecrets...) rs = append(rs, changeRs...) @@ -649,6 +706,12 @@ func (w *Wallet) getProofsForAmount(amount uint64, mintURL string) (cashu.Proofs // remaining proofs are change proofs to save to db w.saveProofs(proofs) + + err = w.incrementKeysetCounter(activeSatKeyset.Id, uint32(len(blindedMessages))) + if err != nil { + return nil, fmt.Errorf("error incrementing keyset counter: %v", err) + } + return proofsToSend, nil } @@ -658,7 +721,7 @@ func newBlindedMessage(id string, amount uint64, B_ *secp256k1.PublicKey) cashu. } // returns Blinded messages, secrets - [][]byte, and list of r -func createBlindedMessages(amount uint64, keyset crypto.WalletKeyset) (cashu.BlindedMessages, []string, []*secp256k1.PrivateKey, error) { +func (w *Wallet) createBlindedMessages(amount uint64, keysetId string, counter uint32) (cashu.BlindedMessages, []string, []*secp256k1.PrivateKey, error) { splitAmounts := cashu.AmountSplit(amount) splitLen := len(splitAmounts) @@ -666,30 +729,30 @@ func createBlindedMessages(amount uint64, keyset crypto.WalletKeyset) (cashu.Bli secrets := make([]string, splitLen) rs := make([]*secp256k1.PrivateKey, splitLen) + keysetDerivationPath, err := nut13.DeriveKeysetPath(w.masterKey, keysetId) + if err != nil { + return nil, nil, nil, err + } + for i, amt := range splitAmounts { - // generate new private key r - r, err := secp256k1.GeneratePrivateKey() + r, err := nut13.DeriveBlindingFactor(keysetDerivationPath, counter) if err != nil { return nil, nil, nil, err } var B_ *secp256k1.PublicKey - var secret string - // generate random secret until it finds valid point - for { - secretBytes := make([]byte, 32) - _, err = rand.Read(secretBytes) - if err != nil { - return nil, nil, nil, err - } - secret = hex.EncodeToString(secretBytes) - B_, r, err = crypto.BlindMessage(secret, r) - if err == nil { - break - } + secret, err := nut13.DeriveSecret(keysetDerivationPath, counter) + if err != nil { + return nil, nil, nil, err } - blindedMessage := newBlindedMessage(keyset.Id, amt, B_) + B_, r, err = crypto.BlindMessage(secret, r) + if err != nil { + return nil, nil, nil, err + } + counter += 1 + + blindedMessage := newBlindedMessage(keysetId, amt, B_) blindedMessages[i] = blindedMessage secrets[i] = secret rs[i] = r @@ -699,8 +762,12 @@ func createBlindedMessages(amount uint64, keyset crypto.WalletKeyset) (cashu.Bli } // constructProofs unblinds the blindedSignatures and returns the proofs -func constructProofs(blindedSignatures cashu.BlindedSignatures, - secrets []string, rs []*secp256k1.PrivateKey, keyset *crypto.WalletKeyset) (cashu.Proofs, error) { +func constructProofs( + blindedSignatures cashu.BlindedSignatures, + secrets []string, + rs []*secp256k1.PrivateKey, + keyset *crypto.WalletKeyset, +) (cashu.Proofs, error) { if len(blindedSignatures) != len(secrets) || len(blindedSignatures) != len(rs) { return nil, errors.New("lengths do not match") @@ -725,8 +792,12 @@ func constructProofs(blindedSignatures cashu.BlindedSignatures, C := crypto.UnblindSignature(C_, rs[i], pubkey) Cstr := hex.EncodeToString(C.SerializeCompressed()) - proof := cashu.Proof{Amount: blindedSignature.Amount, - Secret: secrets[i], C: Cstr, Id: blindedSignature.Id} + proof := cashu.Proof{ + Amount: blindedSignature.Amount, + Secret: secrets[i], + C: Cstr, + Id: blindedSignature.Id, + } proofs[i] = proof } @@ -734,6 +805,19 @@ func constructProofs(blindedSignatures cashu.BlindedSignatures, return proofs, nil } +func (w *Wallet) incrementKeysetCounter(keysetId string, num uint32) error { + err := w.db.IncrementKeysetCounter(keysetId, num) + if err != nil { + return err + } + return nil +} + +// keyset passed should exist in wallet +func (w *Wallet) counterForKeyset(keysetId string) uint32 { + return w.db.GetKeysetCounter(keysetId) +} + func (w *Wallet) GetActiveSatKeyset() crypto.WalletKeyset { var activeKeyset crypto.WalletKeyset for _, keyset := range w.currentMint.activeKeysets { @@ -745,7 +829,7 @@ func (w *Wallet) GetActiveSatKeyset() crypto.WalletKeyset { return activeKeyset } -func (w *Wallet) getWalletMints() map[string]walletMint { +func (w *Wallet) getWalletMints() (map[string]walletMint, error) { walletMints := make(map[string]walletMint) keysets := w.db.GetKeysets() @@ -753,16 +837,64 @@ func (w *Wallet) getWalletMints() map[string]walletMint { activeKeysets := make(map[string]crypto.WalletKeyset) inactiveKeysets := make(map[string]crypto.WalletKeyset) for _, keyset := range mintKeysets { + if len(keyset.PublicKeys) == 0 { + publicKeys, err := getKeysetKeys(keyset.MintURL, keyset.Id) + if err != nil { + return nil, err + } + keyset.PublicKeys = publicKeys + w.db.SaveKeyset(&keyset) + } + if keyset.Active { activeKeysets[keyset.Id] = keyset } else { inactiveKeysets[keyset.Id] = keyset } } - walletMints[k] = walletMint{mintURL: k, activeKeysets: activeKeysets, inactiveKeysets: inactiveKeysets} + + walletMints[k] = walletMint{ + mintURL: k, + activeKeysets: activeKeysets, + inactiveKeysets: inactiveKeysets, + } + } + + return walletMints, nil +} + +func getKeysetKeys(mintURL, id string) (map[uint64]*secp256k1.PublicKey, error) { + keysetsResponse, err := GetKeysetById(mintURL, id) + if err != nil { + return nil, fmt.Errorf("error getting keyset from mint: %v", err) + } + + var keys map[uint64]*secp256k1.PublicKey + if len(keysetsResponse.Keysets) > 0 && keysetsResponse.Keysets[0].Unit == "sat" { + var err error + keys, err = mapPubKeys(keysetsResponse.Keysets[0].Keys) + if err != nil { + return nil, err + } } - return walletMints + return keys, nil +} + +func mapPubKeys(keys nut01.KeysMap) (map[uint64]*secp256k1.PublicKey, error) { + publicKeys := make(map[uint64]*secp256k1.PublicKey) + for amount, key := range keys { + pkbytes, err := hex.DecodeString(key) + if err != nil { + return nil, err + } + pubkey, err := secp256k1.ParsePubKey(pkbytes) + if err != nil { + return nil, err + } + publicKeys[amount] = pubkey + } + return publicKeys, nil } // CurrentMint returns the current mint url diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 09aaf55..991b2ce 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -3,29 +3,41 @@ package wallet import ( + "crypto/sha256" "encoding/hex" + "math" "reflect" + "strconv" "testing" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/crypto" ) func TestCreateBlindedMessages(t *testing.T) { - keyset := crypto.Keyset{Id: "009a1f293253e41e"} + keyset := crypto.WalletKeyset{Id: "009a1f293253e41e"} + + seed, _ := hdkeychain.GenerateSeed(16) + master, _ := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + + testWallet := &Wallet{masterKey: master} tests := []struct { + wallet *Wallet amount uint64 - keyset crypto.Keyset + keyset crypto.WalletKeyset }{ - {420, keyset}, - {10000000, keyset}, - {2500, keyset}, + {testWallet, 420, keyset}, + {testWallet, 10000000, keyset}, + {testWallet, 2500, keyset}, } for _, test := range tests { - blindedMessages, _, _, _ := createBlindedMessages(test.amount, test.keyset) + blindedMessages, _, _, _ := test.wallet.createBlindedMessages(test.amount, test.keyset.Id, 0) amount := blindedMessages.Amount() if amount != test.amount { t.Errorf("expected '%v' but got '%v' instead", test.amount, amount) @@ -61,7 +73,7 @@ func TestConstructProofs(t *testing.T) { "6cc59e6effb48d89a56ff7052dc31ef09fc3a531ac1e2236da167fa4b9d008ab", "172233d8212522a84a1f6ff5472cabd949c2388f98420c222ef5e1229ac090bd", } - keyset := crypto.GenerateKeyset("mysecretkey", "0/0/0") + keyset := generateWalletKeyset("mysecretkey", "0/0/0") expected := cashu.Proofs{ { @@ -99,13 +111,13 @@ func TestConstructProofs(t *testing.T) { } func TestConstructProofsError(t *testing.T) { - keyset := crypto.GenerateKeyset("mysecretkey", "0/0/0") + keyset := generateWalletKeyset("mysecretkey", "0/0/0") tests := []struct { signatures cashu.BlindedSignatures secrets []string r_str []string - keyset *crypto.Keyset + keyset *crypto.WalletKeyset }{ { signatures: cashu.BlindedSignatures{ @@ -167,3 +179,16 @@ func TestConstructProofsError(t *testing.T) { } } } + +func generateWalletKeyset(seed, derivationPath string) *crypto.WalletKeyset { + keys := make(map[uint64]*secp256k1.PublicKey, 64) + + for i := 0; i < 64; i++ { + amount := uint64(math.Pow(2, float64(i))) + hash := sha256.Sum256([]byte(seed + derivationPath + strconv.FormatUint(amount, 10))) + _, pubKey := btcec.PrivKeyFromBytes(hash[:]) + keys[amount] = pubKey + } + keysetId := crypto.DeriveKeysetId(keys) + return &crypto.WalletKeyset{Id: keysetId, Unit: "sat", Active: true, PublicKeys: keys} +}