Skip to content

Commit

Permalink
Fix: ledger requires deprecated paid flags in db + wallet disable b…
Browse files Browse the repository at this point in the history
…ase64 keysets by default (#634)

* wallet: deprecate base64 keysets by default

* readd deprecated paid to melt quotes as well

* readd paid flag removed in #622 before
  • Loading branch information
callebtc authored Oct 4, 2024
1 parent 75ffa39 commit 7fdca3b
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 22 deletions.
13 changes: 9 additions & 4 deletions cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="")
Expand Down Expand Up @@ -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.",
)
Expand Down Expand Up @@ -232,10 +236,11 @@ class Settings(
FakeWalletSettings,
MintLimits,
MintBackends,
MintDeprecationFlags,
MintSettings,
MintInformation,
WalletSettings,
WalletFeatures,
WalletDeprecationFlags,
CashuSettings,
):
version: str = Field(default=VERSION)
Expand Down
61 changes: 54 additions & 7 deletions cashu/mint/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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 ""
),
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
*,
Expand Down
37 changes: 37 additions & 0 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import base64
import time
from typing import Dict, List, Mapping, Optional, Tuple

Expand All @@ -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,
)
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions cashu/mint/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -296,7 +297,6 @@ async def m011_add_quote_tables(db: Database):
);
"""
# NOTE: We remove the paid BOOL NOT NULL column
)

await conn.execute(
Expand All @@ -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},
Expand All @@ -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')} "
)

Expand Down
17 changes: 10 additions & 7 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 7fdca3b

Please sign in to comment.