diff --git a/cashu/core/base.py b/cashu/core/base.py index 426a0080..5305ae7e 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -6,7 +6,12 @@ from loguru import logger from pydantic import BaseModel -from .crypto.keys import derive_keys, derive_keyset_id, derive_pubkeys +from .crypto.keys import ( + derive_keys, + derive_keyset_id, + derive_keyset_id_deprecated, + derive_pubkeys, +) from .crypto.secp import PrivateKey, PublicKey from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12 from .p2pk import P2SHScript @@ -320,6 +325,7 @@ def __init__( self.public_keys = public_keys # overwrite id by deriving it from the public keys self.id = derive_keyset_id(self.public_keys) + self.id_deprecated = derive_keyset_id_deprecated(self.public_keys) def serialize(self): return json.dumps( @@ -400,27 +406,37 @@ def public_keys_hex(self) -> Dict[int, str]: def generate_keys(self, seed): """Generates keys of a keyset from a seed.""" - backwards_compatibility_pre_0_12 = False if ( self.version and len(self.version.split(".")) > 1 and int(self.version.split(".")[0]) == 0 and int(self.version.split(".")[1]) <= 11 ): - backwards_compatibility_pre_0_12 = True # WARNING: Broken key derivation for backwards compatibility with < 0.12 self.private_keys = derive_keys_backwards_compatible_insecure_pre_0_12( seed, self.derivation_path ) + logger.warning( + f"WARNING: Using weak key derivation for keyset {self.id} (backwards" + " compatibility < 0.12)" + ) else: self.private_keys = derive_keys(seed, self.derivation_path) self.public_keys = derive_pubkeys(self.private_keys) # type: ignore - self.id = derive_keyset_id(self.public_keys) # type: ignore - if backwards_compatibility_pre_0_12: + + if ( + self.version + and len(self.version.split(".")) > 1 + and int(self.version.split(".")[0]) == 0 + and int(self.version.split(".")[1]) <= 13 + ): + self.id = derive_keyset_id_deprecated(self.public_keys) # type: ignore logger.warning( - f"WARNING: Using weak key derivation for keyset {self.id} (backwards" - " compatibility < 0.12)" + "WARNING: Using deriving keyset id with old base64 format" + f" {self.id} (backwards compatibility < 0.14)" ) + else: + self.id = derive_keyset_id(self.public_keys) # type: ignore class MintKeysets: diff --git a/cashu/core/crypto/keys.py b/cashu/core/crypto/keys.py index fcf8c9f0..599a3306 100644 --- a/cashu/core/crypto/keys.py +++ b/cashu/core/crypto/keys.py @@ -50,6 +50,16 @@ def derive_keyset_id(keys: Dict[int, PublicKey]): # sort public keys by amount sorted_keys = dict(sorted(keys.items())) pubkeys_concat = "".join([p.serialize().hex() for _, p in sorted_keys.items()]) + return hashlib.sha256(pubkeys_concat.encode("utf-8")).hexdigest()[:16] + + +def derive_keyset_id_deprecated(keys: Dict[int, PublicKey]): + """DEPRECATED: Deterministic derivation keyset_id from set of public keys. + DEPRECATION: This method produces base64 keyset ids. Use `derive_keyset_id` instead. + """ + # sort public keys by amount + sorted_keys = dict(sorted(keys.items())) + pubkeys_concat = "".join([p.serialize().hex() for _, p in sorted_keys.items()]) return base64.b64encode( hashlib.sha256((pubkeys_concat).encode("utf-8")).digest() ).decode()[:12] diff --git a/cashu/wallet/secrets.py b/cashu/wallet/secrets.py index 45601758..57bf9349 100644 --- a/cashu/wallet/secrets.py +++ b/cashu/wallet/secrets.py @@ -117,9 +117,17 @@ async def generate_determinstic_secret( """ assert self.bip32, "BIP32 not initialized yet." # integer keyset id modulo max number of bip32 child keys - keyest_id = int.from_bytes(base64.b64decode(self.keyset_id), "big") % ( - 2**31 - 1 - ) + try: + keyest_id = int.from_bytes(bytes.fromhex(self.keyset_id), "big") % ( + 2**31 - 1 + ) + except ValueError: + # BEGIN: backwards compatibility keyset id is not hex + keyest_id = int.from_bytes(base64.b64decode(self.keyset_id), "big") % ( + 2**31 - 1 + ) + # END: backwards compatibility keyset id is not hex + logger.trace(f"keyset id: {self.keyset_id} becomes {keyest_id}") token_derivation_path = f"m/129372'/0'/{keyest_id}'/{counter}'" # for secret diff --git a/tests/test_mint.py b/tests/test_mint.py index 07908e86..fb9a0fbd 100644 --- a/tests/test_mint.py +++ b/tests/test_mint.py @@ -54,7 +54,7 @@ async def test_privatekeys(ledger: Ledger): async def test_keysets(ledger: Ledger): assert len(ledger.keysets.keysets) assert len(ledger.keysets.get_ids()) - assert ledger.keyset.id == "1cCNIAZ2X/w1" + assert ledger.keyset.id == "d5c08d2006765ffc" @pytest.mark.asyncio diff --git a/tests/test_mint_api.py b/tests/test_mint_api.py index 163c61f8..a4b17bf0 100644 --- a/tests/test_mint_api.py +++ b/tests/test_mint_api.py @@ -31,9 +31,7 @@ async def test_api_keysets(ledger): @pytest.mark.asyncio async def test_api_keyset_keys(ledger): - response = requests.get( - f"{BASE_URL}/keys/{'1cCNIAZ2X/w1'.replace('/', '_').replace('+', '-')}" - ) + response = requests.get(f"{BASE_URL}/keys/d5c08d2006765ffc") assert response.status_code == 200, f"{response.url} {response.status_code}" assert response.json()["keysets"][0]["keys"] == { str(k): v.serialize().hex() for k, v in ledger.keyset.public_keys.items() diff --git a/tests/test_wallet.py b/tests/test_wallet.py index b8519332..27c33e63 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -93,7 +93,8 @@ async def test_get_keys(wallet1: Wallet): assert len(wallet1.keys.public_keys) == settings.max_order keyset = await wallet1._get_keys(wallet1.url) assert keyset.id is not None - assert keyset.id == "1cCNIAZ2X/w1" + assert keyset.id_deprecated == "1cCNIAZ2X/w1" + assert keyset.id == "d5c08d2006765ffc" assert isinstance(keyset.id, str) assert len(keyset.id) > 0 diff --git a/tests/test_wallet_restore.py b/tests/test_wallet_restore.py index 7a52d84a..79d20a8d 100644 --- a/tests/test_wallet_restore.py +++ b/tests/test_wallet_restore.py @@ -94,23 +94,23 @@ async def test_bump_secret_derivation(wallet3: Wallet): ) secrets1, rs1, derivation_paths1 = await wallet3.generate_n_secrets(5) secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(0, 4) - assert wallet3.keyset_id == "1cCNIAZ2X/w1" + assert wallet3.keyset_id == "d5c08d2006765ffc" assert secrets1 == secrets2 assert [r.private_key for r in rs1] == [r.private_key for r in rs2] assert derivation_paths1 == derivation_paths2 assert secrets1 == [ - "9d32fc57e6fa2942d05ee475d28ba6a56839b8cb8a3f174b05ed0ed9d3a420f6", - "1c0f2c32e7438e7cc992612049e9dfcdbffd454ea460901f24cc429921437802", - "327c606b761af03cbe26fa13c4b34a6183b868c52cda059fe57fdddcb4e1e1e7", - "53476919560398b56c0fdc5dd92cf8628b1e06de6f2652b0f7d6e8ac319de3b7", - "b2f5d632229378a716be6752fc79ac8c2b43323b820859a7956f2dfe5432b7b4", + "064d8f585385fdc01c5e1363cf4d80cae8af6aa36263459cd902667041afacae", + "e273fe79f152071a9d430d0de56248f1e8f88b06c6e231d48b8d7f073d5dc852", + "7f59ebf0c1d841819b9e2d0c221a5309022aadd34f65426158bf3906faca31ec", + "9b0d6cc849823923bbcfa2101874755c108b4cf7a3b721f851d0ee7662581f2d", + "1f23400a377aa089d04fa17163a93aab77e397f22a3381fc5ee0ab3328594f5c", ] assert derivation_paths1 == [ - "m/129372'/0'/2004500376'/0'", - "m/129372'/0'/2004500376'/1'", - "m/129372'/0'/2004500376'/2'", - "m/129372'/0'/2004500376'/3'", - "m/129372'/0'/2004500376'/4'", + "m/129372'/0'/838302271'/0'", + "m/129372'/0'/838302271'/1'", + "m/129372'/0'/838302271'/2'", + "m/129372'/0'/838302271'/3'", + "m/129372'/0'/838302271'/4'", ]