Skip to content

Commit

Permalink
Merge branch 'main' into sort-swap-inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Nov 5, 2024
2 parents 58da7a0 + 9cdfba5 commit c04b129
Show file tree
Hide file tree
Showing 57 changed files with 2,191 additions and 1,790 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ repos:
- id: mixed-line-ending
- id: check-case-conflict
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.1
rev: v0.7.1
hooks:
- id: ruff
args: [--fix]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ This command runs the mint on your local computer. Skip this step if you want to
## Docker

```
docker run -d -p 3338:3338 --name nutshell -e MINT_BACKEND_BOLT11_SAT=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.1 poetry run mint
docker run -d -p 3338:3338 --name nutshell -e MINT_BACKEND_BOLT11_SAT=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.2 poetry run mint
```

## From this repository
Expand Down
134 changes: 78 additions & 56 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import json
import math
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
Expand Down Expand Up @@ -95,21 +96,7 @@ def pending(self) -> bool:

class HTLCWitness(BaseModel):
preimage: Optional[str] = None
signature: Optional[str] = None

@classmethod
def from_witness(cls, witness: str):
return cls(**json.loads(witness))


class P2SHWitness(BaseModel):
"""
Unlocks P2SH spending condition of a Proof
"""

script: str
signature: str
address: Union[str, None] = None
signatures: Optional[List[str]] = None

@classmethod
def from_witness(cls, witness: str):
Expand Down Expand Up @@ -148,12 +135,12 @@ class Proof(BaseModel):
time_created: Union[None, str] = ""
time_reserved: Union[None, str] = ""
derivation_path: Union[None, str] = "" # derivation path of the proof
mint_id: Union[
None, str
] = None # holds the id of the mint operation that created this proof
melt_id: Union[
None, str
] = None # holds the id of the melt operation that destroyed this proof
mint_id: Union[None, str] = (
None # holds the id of the mint operation that created this proof
)
melt_id: Union[None, str] = (
None # holds the id of the melt operation that destroyed this proof
)

def __init__(self, **data):
super().__init__(**data)
Expand Down Expand Up @@ -206,10 +193,15 @@ def p2pksigs(self) -> List[str]:
return P2PKWitness.from_witness(self.witness).signatures

@property
def htlcpreimage(self) -> Union[str, None]:
def htlcpreimage(self) -> str | None:
assert self.witness, "Witness is missing for htlc preimage"
return HTLCWitness.from_witness(self.witness).preimage

@property
def htlcsigs(self) -> List[str] | None:
assert self.witness, "Witness is missing for htlc signatures"
return HTLCWitness.from_witness(self.witness).signatures


class Proofs(BaseModel):
# NOTE: not used in Pydantic validation
Expand Down Expand Up @@ -269,20 +261,7 @@ def from_row(cls, row: Row):
)


# ------- LIGHTNING INVOICE -------


class Invoice(BaseModel):
amount: int
bolt11: str
id: str
out: Union[None, bool] = None
payment_hash: Union[None, str] = None
preimage: Union[str, None] = None
issued: Union[None, bool] = False
paid: Union[None, bool] = False
time_created: Union[None, str, int, float] = ""
time_paid: Union[None, str, int, float] = ""
# ------- Quotes -------


class MeltQuoteState(Enum):
Expand All @@ -306,9 +285,10 @@ class MeltQuote(LedgerEvent):
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
fee_paid: int = 0
payment_preimage: str = ""
payment_preimage: Optional[str] = None
expiry: Optional[int] = None
change: Optional[List[BlindedSignature]] = None
mint: Optional[str] = None

@classmethod
def from_row(cls, row: Row):
Expand All @@ -323,6 +303,8 @@ def from_row(cls, row: Row):
paid_time = int(row["paid_time"].timestamp()) if row["paid_time"] else None
expiry = int(row["expiry"].timestamp()) if row["expiry"] else None

payment_preimage = row.get("payment_preimage") or row.get("proof") # type: ignore

# parse change from row as json
change = None
if row["change"]:
Expand All @@ -336,13 +318,37 @@ def from_row(cls, row: Row):
unit=row["unit"],
amount=row["amount"],
fee_reserve=row["fee_reserve"],
state=MeltQuoteState[row["state"]],
state=MeltQuoteState(row["state"]),
created_time=created_time,
paid_time=paid_time,
fee_paid=row["fee_paid"],
change=change,
expiry=expiry,
payment_preimage=row["proof"],
payment_preimage=payment_preimage,
)

@classmethod
def from_resp_wallet(
cls, melt_quote_resp, mint: str, amount: int, unit: str, request: str
):
# BEGIN: BACKWARDS COMPATIBILITY < 0.16.0: "paid" field to "state"
if melt_quote_resp.state is None:
if melt_quote_resp.paid is True:
melt_quote_resp.state = MeltQuoteState.paid
elif melt_quote_resp.paid is False:
melt_quote_resp.state = MeltQuoteState.unpaid
# END: BACKWARDS COMPATIBILITY < 0.16.0
return cls(
quote=melt_quote_resp.quote,
method="bolt11",
request=request,
checking_id="",
unit=unit,
amount=amount,
fee_reserve=melt_quote_resp.fee_reserve,
state=MeltQuoteState(melt_quote_resp.state),
mint=mint,
change=melt_quote_resp.change,
)

@property
Expand Down Expand Up @@ -406,6 +412,7 @@ class MintQuote(LedgerEvent):
created_time: Union[int, None] = None
paid_time: Union[int, None] = None
expiry: Optional[int] = None
mint: Optional[str] = None

@classmethod
def from_row(cls, row: Row):
Expand All @@ -426,11 +433,33 @@ def from_row(cls, row: Row):
checking_id=row["checking_id"],
unit=row["unit"],
amount=row["amount"],
state=MintQuoteState[row["state"]],
state=MintQuoteState(row["state"]),
created_time=created_time,
paid_time=paid_time,
)

@classmethod
def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str):
# BEGIN: BACKWARDS COMPATIBILITY < 0.16.0: "paid" field to "state"
if mint_quote_resp.state is None:
if mint_quote_resp.paid is True:
mint_quote_resp.state = MintQuoteState.paid
elif mint_quote_resp.paid is False:
mint_quote_resp.state = MintQuoteState.unpaid
# END: BACKWARDS COMPATIBILITY < 0.16.0
return cls(
quote=mint_quote_resp.quote,
method="bolt11",
request=mint_quote_resp.request,
checking_id="",
unit=unit,
amount=amount,
state=MintQuoteState(mint_quote_resp.state),
mint=mint,
expiry=mint_quote_resp.expiry,
created_time=int(time.time()),
)

@property
def identifier(self) -> str:
"""Implementation of the abstract method from LedgerEventManager"""
Expand Down Expand Up @@ -647,6 +676,7 @@ def deserialize(serialized: str) -> Dict[int, PublicKey]:
int(amount): PublicKey(bytes.fromhex(hex_key), raw=True)
for amount, hex_key in dict(json.loads(serialized)).items()
}

return cls(
id=row["id"],
unit=row["unit"],
Expand Down Expand Up @@ -809,43 +839,35 @@ def generate_keys(self):
class Token(ABC):
@property
@abstractmethod
def proofs(self) -> List[Proof]:
...
def proofs(self) -> List[Proof]: ...

@property
@abstractmethod
def amount(self) -> int:
...
def amount(self) -> int: ...

@property
@abstractmethod
def mint(self) -> str:
...
def mint(self) -> str: ...

@property
@abstractmethod
def keysets(self) -> List[str]:
...
def keysets(self) -> List[str]: ...

@property
@abstractmethod
def memo(self) -> Optional[str]:
...
def memo(self) -> Optional[str]: ...

@memo.setter
@abstractmethod
def memo(self, memo: Optional[str]):
...
def memo(self, memo: Optional[str]): ...

@property
@abstractmethod
def unit(self) -> str:
...
def unit(self) -> str: ...

@unit.setter
@abstractmethod
def unit(self, unit: str):
...
def unit(self, unit: str): ...


class TokenV3Token(BaseModel):
Expand Down
20 changes: 10 additions & 10 deletions cashu/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def big_int(self) -> str:
def table_with_schema(self, table: str):
return f"{self.references_schema if self.schema else ''}{table}"


# https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.CursorResult
class Connection(Compat):
def __init__(self, conn: AsyncSession, txn, typ, name, schema):
Expand All @@ -82,7 +83,9 @@ def rewrite_query(self, query) -> TextClause:

async def fetchall(self, query: str, values: dict = {}):
result = await self.conn.execute(self.rewrite_query(query), values)
return [r._mapping for r in result.all()] # will return [] if result list is empty
return [
r._mapping for r in result.all()
] # will return [] if result list is empty

async def fetchone(self, query: str, values: dict = {}):
result = await self.conn.execute(self.rewrite_query(query), values)
Expand Down Expand Up @@ -134,13 +137,13 @@ def __init__(self, db_name: str, db_location: str):
if not settings.db_connection_pool:
kwargs["poolclass"] = NullPool
elif self.type == POSTGRES:
kwargs["poolclass"] = AsyncAdaptedQueuePool # type: ignore[assignment]
kwargs["pool_size"] = 50 # type: ignore[assignment]
kwargs["max_overflow"] = 100 # type: ignore[assignment]
kwargs["poolclass"] = AsyncAdaptedQueuePool # type: ignore[assignment]
kwargs["pool_size"] = 50 # type: ignore[assignment]
kwargs["max_overflow"] = 100 # type: ignore[assignment]

self.engine = create_async_engine(database_uri, **kwargs)
self.async_session = sessionmaker(
self.engine,
self.engine, # type: ignore
expire_on_commit=False,
class_=AsyncSession, # type: ignore
)
Expand Down Expand Up @@ -225,14 +228,11 @@ def _is_lock_exception(e):
)
else:
logger.error(f"Error in session trial: {trial} ({random_int}): {e}")
raise e
raise
finally:
logger.trace(f"Closing session trial: {trial} ({random_int})")
await session.close()
# if not inherited:
# logger.trace("Closing session")
# await session.close()
# self._connection = None

raise Exception(
f"failed to acquire database lock on {lock_table} after {timeout}s and {trial} trials ({random_int})"
)
Expand Down
18 changes: 18 additions & 0 deletions cashu/core/htlc.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from enum import Enum
from typing import Union

from .secret import Secret, SecretKind


class SigFlags(Enum):
# require signatures only on the inputs (default signature flag)
SIG_INPUTS = "SIG_INPUTS"
# require signatures on inputs and outputs
SIG_ALL = "SIG_ALL"


class HTLCSecret(Secret):
@classmethod
def from_secret(cls, secret: Secret):
Expand All @@ -15,3 +23,13 @@ def from_secret(cls, secret: Secret):
def locktime(self) -> Union[None, int]:
locktime = self.tags.get_tag("locktime")
return int(locktime) if locktime else None

@property
def sigflag(self) -> Union[None, SigFlags]:
sigflag = self.tags.get_tag("sigflag")
return SigFlags(sigflag) if sigflag else None

@property
def n_sigs(self) -> Union[None, int]:
n_sigs = self.tags.get_tag("n_sigs")
return int(n_sigs) if n_sigs else None
6 changes: 3 additions & 3 deletions cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ class PostMeltQuoteResponse(BaseModel):
quote: str # quote id
amount: int # input amount
fee_reserve: int # input fee reserve
paid: Optional[
bool
] = None # whether the request has been paid # DEPRECATED as per NUT PR #136
paid: Optional[bool] = (
None # whether the request has been paid # DEPRECATED as per NUT PR #136
)
state: Optional[str] # state of the quote
expiry: Optional[int] # expiry of the quote
payment_preimage: Optional[str] = None # payment preimage
Expand Down
1 change: 1 addition & 0 deletions cashu/core/nuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
P2PK_NUT = 11
DLEQ_NUT = 12
DETERMINSTIC_SECRETS_NUT = 13
HTLC_NUT = 14
MPP_NUT = 15
WEBSOCKETS_NUT = 17
10 changes: 4 additions & 6 deletions cashu/core/p2pk.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,16 @@ def n_sigs(self) -> Union[None, int]:
return int(n_sigs) if n_sigs else None


def sign_p2pk_sign(message: bytes, private_key: PrivateKey) -> bytes:
# ecdsa version
# signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message))
def schnorr_sign(message: bytes, private_key: PrivateKey) -> bytes:
signature = private_key.schnorr_sign(
hashlib.sha256(message).digest(), None, raw=True
)
return signature


def verify_p2pk_signature(message: bytes, pubkey: PublicKey, signature: bytes) -> bool:
# ecdsa version
# return pubkey.ecdsa_verify(message, pubkey.ecdsa_deserialize(signature))
def verify_schnorr_signature(
message: bytes, pubkey: PublicKey, signature: bytes
) -> bool:
return pubkey.schnorr_verify(
hashlib.sha256(message).digest(), signature, None, raw=True
)
Expand Down
Loading

0 comments on commit c04b129

Please sign in to comment.