Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

index on db and read spent proofs from db #370

Merged
merged 5 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -60,6 +61,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 @@ -205,7 +220,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 @@ -243,6 +258,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 @@ -158,9 +157,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 @@ -530,70 +530,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 @@ -620,13 +609,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 @@ -638,24 +626,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
Loading