diff --git a/cashu/nuts/nut13/nut13.go b/cashu/nuts/nut13/nut13.go index 9bb83c6..b42e7ad 100644 --- a/cashu/nuts/nut13/nut13.go +++ b/cashu/nuts/nut13/nut13.go @@ -1,20 +1,50 @@ package nut13 import ( - "encoding/binary" "encoding/hex" + "errors" + "fmt" + "math/big" + + "encoding/base64" + "regexp" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/decred/dcrd/dcrec/secp256k1/v4" ) +var ( + ErrCollidingKeysetId = errors.New("error: colliding keyset detected") +) + +func keysetIdToBigInt(id string) (*big.Int, error) { + hexPattern := regexp.MustCompile("^[0-9a-fA-F]+$") + + var result *big.Int + modulus := big.NewInt(2147483647) // 2^31 - 1 + + if hexPattern.MatchString(id) { + result = new(big.Int) + result.SetString(id, 16) + } else { + decoded, err := base64.StdEncoding.DecodeString(id) + if err != nil { + return nil, err + } + + hexStr := hex.EncodeToString(decoded) + result = new(big.Int) + result.SetString(hexStr, 16) + } + + return result.Mod(result, modulus), nil +} + func DeriveKeysetPath(master *hdkeychain.ExtendedKey, keysetId string) (*hdkeychain.ExtendedKey, error) { - keysetBytes, err := hex.DecodeString(keysetId) + keysetIdInt, err := keysetIdToBigInt(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) @@ -29,7 +59,7 @@ func DeriveKeysetPath(master *hdkeychain.ExtendedKey, keysetId string) (*hdkeych } // m/129372'/0'/keyset_k_int' - keysetPath, err := coinType.Derive(hdkeychain.HardenedKeyStart + uint32(keysetIdInt)) + keysetPath, err := coinType.Derive(hdkeychain.HardenedKeyStart + uint32(keysetIdInt.Uint64())) if err != nil { return nil, err } @@ -81,3 +111,30 @@ func DeriveSecret(keysetPath *hdkeychain.ExtendedKey, counter uint32) (string, e return secret, nil } + +func CheckCollidingKeysets(currentKeysetIds []string, newMintKeysetIds []string) error { + + for i := range currentKeysetIds { + keysetIdInt, err := keysetIdToBigInt(currentKeysetIds[i]) + if err != nil { + return err + } + + for j := range newMintKeysetIds { + if currentKeysetIds[i] == newMintKeysetIds[j] { + return fmt.Errorf("%w. KeysetId: %+v", ErrCollidingKeysetId, currentKeysetIds[i]) + } + + keysetIdIntToCompare, err := keysetIdToBigInt(newMintKeysetIds[j]) + if err != nil { + return err + } + + if keysetIdInt == keysetIdIntToCompare { + return fmt.Errorf("%w. KeysetId: %+v", ErrCollidingKeysetId, currentKeysetIds[i]) + } + } + } + + return nil +} diff --git a/cashu/nuts/nut13/nut13_test.go b/cashu/nuts/nut13/nut13_test.go index 34c6a2f..df25416 100644 --- a/cashu/nuts/nut13/nut13_test.go +++ b/cashu/nuts/nut13/nut13_test.go @@ -2,6 +2,7 @@ package nut13 import ( "encoding/hex" + "errors" "testing" "github.com/btcsuite/btcd/btcutil/hdkeychain" @@ -72,3 +73,26 @@ func TestSecretDerivation(t *testing.T) { } } + +func TestCollisionOfIdNoCollision(t *testing.T) { + keysetId := []string{"009a1f293253e41e"} + + keysets := []string{"009a1f293253d41e", "009a1f283253e41e"} + + err := CheckCollidingKeysets(keysetId, keysets) + + if err != nil { + t.Errorf("There should not have been any keyset collision") + } +} +func TestCollisionOfIdWithCollision(t *testing.T) { + keysetId := []string{"009a1f293253e41e", "009a1f293253e41d"} + + oldKeysets := []string{"009b1f293253e41d", "009a1f293253e41e"} + + err := CheckCollidingKeysets(keysetId, oldKeysets) + + if !errors.Is(err, ErrCollidingKeysetId) { + t.Errorf("there should have been a keyset collition error") + } +} diff --git a/crypto/keyset.go b/crypto/keyset.go index 23bdb2b..713c6b5 100644 --- a/crypto/keyset.go +++ b/crypto/keyset.go @@ -286,6 +286,17 @@ func (kp *KeyPair) UnmarshalJSON(data []byte) error { // KeysetsMap maps a mint url to map of string keyset id to keyset type KeysetsMap map[string][]WalletKeyset +func (k KeysetsMap) GetAllKeysetIds() []string { + keysetList := []string{} + for _, mint := range k { + for _, walletKeyset := range mint { + keysetList = append(keysetList, walletKeyset.Id) + } + } + + return keysetList +} + type WalletKeyset struct { Id string MintURL string diff --git a/testutils/utils.go b/testutils/utils.go index f5687fb..7b82423 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -1138,7 +1138,7 @@ func CreateNutshellMintContainer(ctx context.Context, inputFeePpk int, lnd *lnd. req := testcontainers.ContainerRequest{ //Image: "cashubtc/nutshell:0.16.5", - Image: "cashubtc/nutshell:latest", + Image: "cashubtc/nutshell:0.16.5", ExposedPorts: []string{"3338"}, Cmd: []string{ "poetry", diff --git a/wallet/keyset.go b/wallet/keyset.go index 3a772f6..fb55953 100644 --- a/wallet/keyset.go +++ b/wallet/keyset.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/elnosh/gonuts/cashu" + "github.com/elnosh/gonuts/cashu/nuts/nut13" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/wallet/client" ) @@ -77,6 +78,17 @@ func GetKeysetKeys(mintURL, id string) (crypto.PublicKeys, error) { return keysetsResponse.Keysets[0].Keys, nil } +func getListOfIdsFromMap(walletMap map[string]crypto.WalletKeyset) []string { + + keysetIdList := []string{} + for _, keyset := range walletMap { + + keysetIdList = append(keysetIdList, keyset.Id) + + } + return keysetIdList +} + // getActiveKeyset returns the active keyset for the mint passed. // if mint passed is known and the latest active keyset has changed, // it will inactivate the previous active and save new active to db @@ -124,6 +136,15 @@ func (w *Wallet) getActiveKeyset(mintURL string) (*crypto.WalletKeyset, error) { if storedKeyset != nil { storedKeyset.Active = true storedKeyset.InputFeePpk = keyset.InputFeePpk + + // Verify before collision + keysetsMap := w.db.GetKeysets() + listOfCurrentKeysets := keysetsMap.GetAllKeysetIds() + err = nut13.CheckCollidingKeysets(listOfCurrentKeysets, []string{activeKeyset.Id}) + if err != nil { + return nil, err + } + if err := w.db.SaveKeyset(storedKeyset); err != nil { return nil, err } diff --git a/wallet/wallet.go b/wallet/wallet.go index 7fec14c..5753b4b 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -175,6 +175,17 @@ func (w *Wallet) AddMint(mint string) (*walletMint, error) { return nil, err } + newMintKeysetIdList := []string{activeKeyset.Id} + newMintKeysetIdList = append(newMintKeysetIdList, getListOfIdsFromMap(inactiveKeysets)...) + + keysetsMap := w.db.GetKeysets() + listOfCurrentKeysets := keysetsMap.GetAllKeysetIds() + + err = nut13.CheckCollidingKeysets(listOfCurrentKeysets, newMintKeysetIdList) + if err != nil { + return nil, err + } + if err := w.db.SaveKeyset(activeKeyset); err != nil { return nil, err }