Skip to content

Commit

Permalink
index on db and read spent proofs from db (#370)
Browse files Browse the repository at this point in the history
* index on db and read spent proofs from db

* add benchmark for testing

* remove benchmark

* add option to disable cached secrets

* disable python 3.9 tests
  • Loading branch information
callebtc authored Nov 26, 2023
1 parent bff30d4 commit fa5193c
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 165 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python-version: ["3.9", "3.10"]
python-version: ["3.10"]
poetry-version: ["1.5.1"]
mint-cache-secrets: ["true", "false"]
# db-url: ["", "postgres://cashu:cashu@localhost:5432/test"] # TODO: Postgres test not working
db-url: [""]
backend-wallet-class: ["FakeWallet"]
uses: ./.github/workflows/tests.yml
with:
python-version: ${{ matrix.python-version }}
poetry-version: ${{ matrix.poetry-version }}
mint-cache-secrets: ${{ matrix.mint-cache-secrets }}
regtest:
uses: ./.github/workflows/regtest.yml
strategy:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ on:
os:
default: "ubuntu-latest"
type: string
mint-cache-secrets:
default: "false"
type: string

jobs:
poetry:
Expand Down Expand Up @@ -47,6 +50,7 @@ jobs:
MINT_HOST: localhost
MINT_PORT: 3337
MINT_DATABASE: ${{ inputs.db-url }}
MINT_CACHE_SECRETS: ${{ inputs.mint-cache-secrets }}
TOR: false
run: |
make test
Expand Down
3 changes: 2 additions & 1 deletion cashu/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from contextlib import asynccontextmanager
from typing import Optional, Union

from loguru import logger
from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
Expand Down Expand Up @@ -130,7 +131,7 @@ def _parse_timestamp(value, _):
# )
else:
if not os.path.exists(self.db_location):
print(f"Creating database directory: {self.db_location}")
logger.info(f"Creating database directory: {self.db_location}")
os.makedirs(self.db_location)
self.path = os.path.join(self.db_location, f"{self.name}.sqlite3")
database_uri = f"sqlite:///{self.path}"
Expand Down
3 changes: 3 additions & 0 deletions cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class EnvSettings(CashuSettings):
debug: bool = Field(default=False)
log_level: str = Field(default="INFO")
cashu_dir: str = Field(default=os.path.join(str(Path.home()), ".cashu"))
debug_profiling: bool = Field(default=False)


class MintSettings(CashuSettings):
Expand All @@ -61,6 +62,8 @@ class MintSettings(CashuSettings):
mint_lnbits_endpoint: str = Field(default=None)
mint_lnbits_key: str = Field(default=None)

mint_cache_secrets: bool = Field(default=True)


class MintInformation(CashuSettings):
mint_info_name: str = Field(default="Cashu mint")
Expand Down
9 changes: 6 additions & 3 deletions cashu/mint/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
)
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

# from fastapi_profiler import PyInstrumentProfilerMiddleware
from loguru import logger
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
Expand All @@ -20,6 +18,9 @@
from .router import router
from .startup import start_mint_init

if settings.debug_profiling:
from fastapi_profiler import PyInstrumentProfilerMiddleware

# from starlette_context import context
# from starlette_context.middleware import RawContextMiddleware

Expand Down Expand Up @@ -108,7 +109,9 @@ def emit(self, record):
middleware=middleware,
)

# app.add_middleware(PyInstrumentProfilerMiddleware)
if settings.debug_profiling:
assert PyInstrumentProfilerMiddleware is not None
app.add_middleware(PyInstrumentProfilerMiddleware)

return app

Expand Down
38 changes: 34 additions & 4 deletions cashu/mint/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async def get_lightning_invoice(
db: Database,
id: str,
conn: Optional[Connection] = None,
):
) -> Optional[Invoice]:
return await get_lightning_invoice(
db=db,
id=id,
Expand All @@ -42,8 +42,23 @@ async def get_secrets_used(
self,
db: Database,
conn: Optional[Connection] = None,
):
return await get_secrets_used(db=db, conn=conn)
) -> List[str]:
return await get_secrets_used(
db=db,
conn=conn,
)

async def get_proof_used(
self,
db: Database,
proof: Proof,
conn: Optional[Connection] = None,
) -> Optional[Proof]:
return await get_proof_used(
db=db,
proof=proof,
conn=conn,
)

async def invalidate_proof(
self,
Expand Down Expand Up @@ -215,7 +230,7 @@ async def get_promise(
async def get_secrets_used(
db: Database,
conn: Optional[Connection] = None,
):
) -> List[str]:
rows = await (conn or db).fetchall(f"""
SELECT secret from {table_with_schema(db, 'proofs_used')}
""")
Expand Down Expand Up @@ -253,6 +268,21 @@ async def get_proofs_pending(
return [Proof(**r) for r in rows]


async def get_proof_used(
db: Database,
proof: Proof,
conn: Optional[Connection] = None,
) -> Optional[Proof]:
row = await (conn or db).fetchone(
f"""
SELECT 1 from {table_with_schema(db, 'proofs_used')}
WHERE secret = ?
""",
(str(proof.secret),),
)
return Proof(**row) if row else None


async def set_proof_pending(
db: Database,
proof: Proof,
Expand Down
130 changes: 60 additions & 70 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
import math
from typing import Dict, List, Optional, Set, Tuple
from typing import Dict, List, Optional, Tuple

import bolt11
from loguru import logger
Expand Down Expand Up @@ -49,7 +49,6 @@ def __init__(
crud: LedgerCrud,
derivation_path="",
):
self.secrets_used: Set[str] = set()
self.master_key = seed
self.derivation_path = derivation_path

Expand Down Expand Up @@ -162,9 +161,10 @@ async def _invalidate_proofs(self, proofs: List[Proof]) -> None:
# Mark proofs as used and prepare new promises
secrets = set([p.secret for p in proofs])
self.secrets_used |= secrets
# store in db
for p in proofs:
await self.crud.invalidate_proof(proof=p, db=self.db)
async with self.db.connect() as conn:
# store in db
for p in proofs:
await self.crud.invalidate_proof(proof=p, db=self.db, conn=conn)

async def _generate_change_promises(
self,
Expand Down Expand Up @@ -538,70 +538,59 @@ async def restore(
async def _generate_promises(
self, B_s: List[BlindedMessage], keyset: Optional[MintKeyset] = None
) -> list[BlindedSignature]:
"""Generates promises that sum to the given amount.
Args:
B_s (List[BlindedMessage]): _description_
keyset (Optional[MintKeyset], optional): _description_. Defaults to None.
Returns:
list[BlindedSignature]: _description_
"""
return [
await self._generate_promise(
b.amount, PublicKey(bytes.fromhex(b.B_), raw=True), keyset
)
for b in B_s
]

async def _generate_promise(
self, amount: int, B_: PublicKey, keyset: Optional[MintKeyset] = None
) -> BlindedSignature:
"""Generates a promise (Blind signature) for given amount and returns a pair (amount, C').
"""Generates a promises (Blind signatures) for given amount and returns a pair (amount, C').
Args:
amount (int): Amount of the promise.
B_ (PublicKey): Blinded secret (point on curve)
B_s (List[BlindedMessage]): Blinded secret (point on curve)
keyset (Optional[MintKeyset], optional): Which keyset to use. Private keys will be taken from this keyset. Defaults to None.
Returns:
BlindedSignature: Generated promise.
list[BlindedSignature]: Generated BlindedSignatures.
"""
keyset = keyset if keyset else self.keyset
logger.trace(f"Generating promise with keyset {keyset.id}.")
private_key_amount = keyset.private_keys[amount]
C_, e, s = b_dhke.step2_bob(B_, private_key_amount)
logger.trace(f"crud: _generate_promise storing promise for {amount}")
await self.crud.store_promise(
amount=amount,
B_=B_.serialize().hex(),
C_=C_.serialize().hex(),
e=e.serialize(),
s=s.serialize(),
db=self.db,
id=keyset.id,
)
logger.trace(f"crud: _generate_promise stored promise for {amount}")
return BlindedSignature(
id=keyset.id,
amount=amount,
C_=C_.serialize().hex(),
dleq=DLEQ(e=e.serialize(), s=s.serialize()),
)
promises = []
for b in B_s:
amount = b.amount
B_ = PublicKey(bytes.fromhex(b.B_), raw=True)
logger.trace(f"Generating promise with keyset {keyset.id}.")
private_key_amount = keyset.private_keys[amount]
C_, e, s = b_dhke.step2_bob(B_, private_key_amount)
promises.append((B_, amount, C_, e, s))

signatures = []
async with self.db.connect() as conn:
for promise in promises:
B_, amount, C_, e, s = promise
logger.trace(f"crud: _generate_promise storing promise for {amount}")
await self.crud.store_promise(
amount=amount,
id=keyset.id,
B_=B_.serialize().hex(),
C_=C_.serialize().hex(),
e=e.serialize(),
s=s.serialize(),
db=self.db,
conn=conn,
)
logger.trace(f"crud: _generate_promise stored promise for {amount}")
signature = BlindedSignature(
id=keyset.id,
amount=amount,
C_=C_.serialize().hex(),
dleq=DLEQ(e=e.serialize(), s=s.serialize()),
)
signatures.append(signature)
return signatures

# ------- PROOFS -------

async def load_used_proofs(self) -> None:
"""Load all used proofs from database."""
logger.trace("crud: loading used proofs")
logger.debug("Loading used proofs into memory")
secrets_used = await self.crud.get_secrets_used(db=self.db)
logger.trace(f"crud: loaded {len(secrets_used)} used proofs")
logger.debug(f"Loaded {len(secrets_used)} used proofs")
self.secrets_used = set(secrets_used)

def _check_spendable(self, proof: Proof) -> bool:
"""Checks whether the proof was already spent."""
return proof.secret not in self.secrets_used

async def _check_pending(self, proofs: List[Proof]) -> List[bool]:
"""Checks whether the proof is still pending."""
proofs_pending = await self.crud.get_proofs_pending(db=self.db)
Expand All @@ -628,13 +617,12 @@ async def check_proof_state(
List[bool]: List of which proof is still spendable (True if still spendable, else False)
List[bool]: List of which proof are pending (True if pending, else False)
"""
spendable = [self._check_spendable(p) for p in proofs]

spendable = await self._check_proofs_spendable(proofs)
pending = await self._check_pending(proofs)
return spendable, pending

async def _set_proofs_pending(
self, proofs: List[Proof], conn: Optional[Connection] = None
) -> None:
async def _set_proofs_pending(self, proofs: List[Proof]) -> None:
"""If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
the list of pending proofs or removes them. Used as a mutex for proofs.
Expand All @@ -646,24 +634,26 @@ async def _set_proofs_pending(
"""
# first we check whether these proofs are pending already
async with self.proofs_pending_lock:
await self._validate_proofs_pending(proofs, conn)
for p in proofs:
try:
await self.crud.set_proof_pending(proof=p, db=self.db, conn=conn)
except Exception:
raise TransactionError("proofs already pending.")

async def _unset_proofs_pending(
self, proofs: List[Proof], conn: Optional[Connection] = None
) -> None:
async with self.db.connect() as conn:
await self._validate_proofs_pending(proofs, conn)
for p in proofs:
try:
await self.crud.set_proof_pending(
proof=p, db=self.db, conn=conn
)
except Exception:
raise TransactionError("proofs already pending.")

async def _unset_proofs_pending(self, proofs: List[Proof]) -> None:
"""Deletes proofs from pending table.
Args:
proofs (List[Proof]): Proofs to delete.
"""
async with self.proofs_pending_lock:
for p in proofs:
await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn)
async with self.db.connect() as conn:
for p in proofs:
await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn)

async def _validate_proofs_pending(
self, proofs: List[Proof], conn: Optional[Connection] = None
Expand Down
10 changes: 10 additions & 0 deletions cashu/mint/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,13 @@ async def m009_add_out_to_invoices(db: Database):
await conn.execute(
f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN out BOOL"
)


async def m010_add_index_to_proofs_used(db: Database):
# create index on proofs_used table for secret
async with db.connect() as conn:
await conn.execute(
"CREATE INDEX IF NOT EXISTS"
f" {table_with_schema(db, 'proofs_used')}_secret_idx ON"
f" {table_with_schema(db, 'proofs_used')} (secret)"
)
3 changes: 2 additions & 1 deletion cashu/mint/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ async def rotate_keys(n_seconds=10):

async def start_mint_init():
await migrate_databases(ledger.db, migrations)
await ledger.load_used_proofs()
if settings.mint_cache_secrets:
await ledger.load_used_proofs()
await ledger.init_keysets()

if settings.lightning:
Expand Down
Loading

0 comments on commit fa5193c

Please sign in to comment.