diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 981333cd..ab18910b 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -64,6 +64,10 @@ class MintSettings(CashuSettings): mint_input_fee_ppk: int = Field(default=0) +class MintDeprecationFlags(MintSettings): + mint_inactivate_base64_keysets: bool = Field(default=False) + + class MintBackends(MintSettings): mint_lightning_backend: str = Field(default="") # deprecated mint_backend_bolt11_sat: str = Field(default="") @@ -186,9 +190,9 @@ class WalletSettings(CashuSettings): wallet_target_amount_count: int = Field(default=3) -class WalletFeatures(CashuSettings): - wallet_inactivate_legacy_keysets: bool = Field( - default=False, +class WalletDeprecationFlags(CashuSettings): + wallet_inactivate_base64_keysets: bool = Field( + default=True, title="Inactivate legacy base64 keysets", description="If you turn on this flag, old bas64 keysets will be ignored and the wallet will ony use new keyset versions.", ) @@ -232,10 +236,11 @@ class Settings( FakeWalletSettings, MintLimits, MintBackends, + MintDeprecationFlags, MintSettings, MintInformation, WalletSettings, - WalletFeatures, + WalletDeprecationFlags, CashuSettings, ): version: str = Field(default=VERSION) diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index ff5457bb..b0587a9a 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -116,6 +116,16 @@ async def store_keyset( ) -> None: ... + @abstractmethod + async def update_keyset( + self, + *, + db: Database, + keyset: MintKeyset, + conn: Optional[Connection] = None, + ) -> None: + ... + @abstractmethod async def get_balance( self, @@ -433,8 +443,8 @@ async def store_mint_quote( await (conn or db).execute( f""" INSERT INTO {db.table_with_schema('mint_quotes')} - (quote, method, request, checking_id, unit, amount, issued, state, created_time, paid_time) - VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :issued, :state, :created_time, :paid_time) + (quote, method, request, checking_id, unit, amount, paid, issued, state, created_time, paid_time) + VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :paid, :issued, :state, :created_time, :paid_time) """, { "quote": quote.quote, @@ -443,6 +453,7 @@ async def store_mint_quote( "checking_id": quote.checking_id, "unit": quote.unit, "amount": quote.amount, + "paid": quote.paid, # this is deprecated! we need to store it because we have a NOT NULL constraint | we could also remove the column but sqlite doesn't support that (we would have to make a new table) "issued": quote.issued, # this is deprecated! we need to store it because we have a NOT NULL constraint | we could also remove the column but sqlite doesn't support that (we would have to make a new table) "state": quote.state.name, "created_time": db.to_timestamp( @@ -532,8 +543,8 @@ async def store_melt_quote( await (conn or db).execute( f""" INSERT INTO {db.table_with_schema('melt_quotes')} - (quote, method, request, checking_id, unit, amount, fee_reserve, state, created_time, paid_time, fee_paid, proof, change, expiry) - VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :created_time, :paid_time, :fee_paid, :proof, :change, :expiry) + (quote, method, request, checking_id, unit, amount, fee_reserve, state, paid, created_time, paid_time, fee_paid, proof, change, expiry) + VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :paid, :created_time, :paid_time, :fee_paid, :proof, :change, :expiry) """, { "quote": quote.quote, @@ -544,6 +555,7 @@ async def store_melt_quote( "amount": quote.amount, "fee_reserve": quote.fee_reserve or 0, "state": quote.state.name, + "paid": quote.paid, # this is deprecated! we need to store it because we have a NOT NULL constraint | we could also remove the column but sqlite doesn't support that (we would have to make a new table) "created_time": db.to_timestamp( db.timestamp_from_seconds(quote.created_time) or "" ), @@ -625,9 +637,11 @@ async def update_melt_quote( db.timestamp_from_seconds(quote.paid_time) or "" ), "proof": quote.payment_preimage, - "change": json.dumps([s.dict() for s in quote.change]) - if quote.change - else None, + "change": ( + json.dumps([s.dict() for s in quote.change]) + if quote.change + else None + ), "quote": quote.quote, "checking_id": quote.checking_id, }, @@ -720,6 +734,39 @@ async def get_keyset( ) return [MintKeyset(**row) for row in rows] + async def update_keyset( + self, + *, + db: Database, + keyset: MintKeyset, + conn: Optional[Connection] = None, + ) -> None: + await (conn or db).execute( + f""" + UPDATE {db.table_with_schema('keysets')} + SET seed = :seed, encrypted_seed = :encrypted_seed, seed_encryption_method = :seed_encryption_method, derivation_path = :derivation_path, valid_from = :valid_from, valid_to = :valid_to, first_seen = :first_seen, active = :active, version = :version, unit = :unit, input_fee_ppk = :input_fee_ppk + WHERE id = :id + """, + { + "id": keyset.id, + "seed": keyset.seed, + "encrypted_seed": keyset.encrypted_seed, + "seed_encryption_method": keyset.seed_encryption_method, + "derivation_path": keyset.derivation_path, + "valid_from": db.to_timestamp( + keyset.valid_from or db.timestamp_now_str() + ), + "valid_to": db.to_timestamp(keyset.valid_to or db.timestamp_now_str()), + "first_seen": db.to_timestamp( + keyset.first_seen or db.timestamp_now_str() + ), + "active": keyset.active, + "version": keyset.version, + "unit": keyset.unit.name, + "input_fee_ppk": keyset.input_fee_ppk, + }, + ) + async def get_proofs_used( self, *, diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 5441aa2e..f8b46ade 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -1,4 +1,5 @@ import asyncio +import base64 import time from typing import Dict, List, Mapping, Optional, Tuple @@ -24,6 +25,7 @@ from ..core.crypto import b_dhke from ..core.crypto.aes import AESCipher from ..core.crypto.keys import ( + derive_keyset_id, derive_pubkey, random_hash, ) @@ -248,6 +250,41 @@ async def init_keysets(self, autosave: bool = True) -> None: if not any([k.active for k in self.keysets.values()]): raise KeysetError("No active keyset found.") + # DEPRECATION 0.16.1 – disable base64 keysets if hex equivalent exists + if settings.mint_inactivate_base64_keysets: + await self.inactivate_base64_keysets() + + async def inactivate_base64_keysets(self) -> None: + """Inactivates all base64 keysets that have a hex equivalent.""" + for keyset in self.keysets.values(): + if not keyset.active or not keyset.public_keys: + continue + # test if the keyset id is a hex string, if not it's base64 + try: + int(keyset.id, 16) + except ValueError: + # verify that it's base64 + try: + _ = base64.b64decode(keyset.id) + except ValueError: + logger.error("Unexpected: keyset id is neither hex nor base64.") + continue + + # verify that we have a hex version of the same keyset by comparing public keys + hex_keyset_id = derive_keyset_id(keys=keyset.public_keys) + if hex_keyset_id not in [k.id for k in self.keysets.values()]: + logger.warning( + f"Keyset {keyset.id} is base64 but we don't have a hex version. Ignoring." + ) + continue + + logger.warning( + f"Keyset {keyset.id} is base64 and has a hex counterpart, setting inactive." + ) + keyset.active = False + self.keysets[keyset.id] = keyset + await self.crud.update_keyset(keyset=keyset, db=self.db) + def get_keyset(self, keyset_id: Optional[str] = None) -> Dict[int, str]: """Returns a dictionary of hex public keys of a specific keyset for each supported amount""" if keyset_id and keyset_id not in self.keysets: diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 58cc35b6..9186afa1 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -288,6 +288,7 @@ async def m011_add_quote_tables(db: Database): checking_id TEXT NOT NULL, unit TEXT NOT NULL, amount {db.big_int} NOT NULL, + paid BOOL NOT NULL, issued BOOL NOT NULL, created_time TIMESTAMP, paid_time TIMESTAMP, @@ -296,7 +297,6 @@ async def m011_add_quote_tables(db: Database): ); """ - # NOTE: We remove the paid BOOL NOT NULL column ) await conn.execute( @@ -309,6 +309,7 @@ async def m011_add_quote_tables(db: Database): unit TEXT NOT NULL, amount {db.big_int} NOT NULL, fee_reserve {db.big_int}, + paid BOOL NOT NULL, created_time TIMESTAMP, paid_time TIMESTAMP, fee_paid {db.big_int}, @@ -318,14 +319,13 @@ async def m011_add_quote_tables(db: Database): ); """ - # NOTE: We remove the paid BOOL NOT NULL column ) await conn.execute( f"INSERT INTO {db.table_with_schema('mint_quotes')} (quote, method," - " request, checking_id, unit, amount, issued, created_time," + " request, checking_id, unit, amount, paid, issued, created_time," " paid_time) SELECT id, 'bolt11', bolt11, COALESCE(payment_hash, 'None')," - f" 'sat', amount, issued, COALESCE(created, '{db.timestamp_now_str()}')," + f" 'sat', amount, False, issued, COALESCE(created, '{db.timestamp_now_str()}')," f" NULL FROM {db.table_with_schema('invoices')} " ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index ae0de013..f621b7fd 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -99,13 +99,13 @@ class Wallet( bip32: BIP32 # private_key: Optional[PrivateKey] = None - def __init__(self, url: str, db: str, name: str = "no_name", unit: str = "sat"): + def __init__(self, url: str, db: str, name: str = "wallet", unit: str = "sat"): """A Cashu wallet. Args: url (str): URL of the mint. db (str): Path to the database directory. - name (str, optional): Name of the wallet database file. Defaults to "no_name". + name (str, optional): Name of the wallet database file. Defaults to "wallet". """ self.db = Database("wallet", db) self.proofs: List[Proof] = [] @@ -124,7 +124,7 @@ async def with_db( cls, url: str, db: str, - name: str = "no_name", + name: str = "wallet", skip_db_read: bool = False, unit: str = "sat", load_all_keysets: bool = False, @@ -134,7 +134,7 @@ async def with_db( Args: url (str): URL of the mint. db (str): Path to the database. - name (str, optional): Name of the wallet. Defaults to "no_name". + name (str, optional): Name of the wallet. Defaults to "wallet". skip_db_read (bool, optional): If true, values from db like private key and keysets are not loaded. Useful for running only migrations and returning. Defaults to False. @@ -240,8 +240,13 @@ async def load_mint_keysets(self, force_old_keysets=False): keyset=keysets_in_db_dict[mint_keyset.id], db=self.db ) + await self.inactivate_base64_keysets(force_old_keysets) + + await self.load_keysets_from_db() + + async def inactivate_base64_keysets(self, force_old_keysets: bool) -> None: # BEGIN backwards compatibility: phase out keysets with base64 ID by treating them as inactive - if settings.wallet_inactivate_legacy_keysets and not force_old_keysets: + if settings.wallet_inactivate_base64_keysets and not force_old_keysets: keysets_in_db = await get_keysets(mint_url=self.url, db=self.db) for keyset in keysets_in_db: if not keyset.active: @@ -272,8 +277,6 @@ async def load_mint_keysets(self, force_old_keysets=False): await update_keyset(keyset=keyset, db=self.db) # END backwards compatibility - await self.load_keysets_from_db() - async def activate_keyset(self, keyset_id: Optional[str] = None) -> None: """Activates a keyset by setting self.keyset_id. Either activates a specific keyset of chooses one of the active keysets of the mint with the same unit as the wallet.