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

Multinut LND #492

Merged
merged 21 commits into from
May 22, 2024
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
1 change: 1 addition & 0 deletions .github/workflows/regtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
MINT_LND_REST_ENDPOINT: https://localhost:8081/
MINT_LND_REST_CERT: ./regtest/data/lnd-3/tls.cert
MINT_LND_REST_MACAROON: ./regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
MINT_LND_ENABLE_MPP: true
# LND_GRPC_ENDPOINT: localhost
# LND_GRPC_PORT: 10009
# LND_GRPC_CERT: ./regtest/data/lnd-3/tls.cert
Expand Down
86 changes: 58 additions & 28 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,18 @@ def htlcpreimage(self) -> Union[str, None]:
return HTLCWitness.from_witness(self.witness).preimage


class Proofs(BaseModel):
# NOTE: not used in Pydantic validation
__root__: List[Proof]


class BlindedMessage(BaseModel):
"""
Blinded message or blinded secret or "output" which is to be signed by the mint
"""

amount: int
id: str
id: str # Keyset id
B_: str # Hex-encoded blinded message
witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL)

Expand All @@ -177,6 +182,28 @@ def p2pksigs(self) -> List[str]:
return P2PKWitness.from_witness(self.witness).signatures


class BlindedMessage_Deprecated(BaseModel):
"""
Deprecated: BlindedMessage for v0 protocol (deprecated api routes) have no id field.
Blinded message or blinded secret or "output" which is to be signed by the mint
"""

amount: int
B_: str # Hex-encoded blinded message
witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL)

@property
def p2pksigs(self) -> List[str]:
assert self.witness, "Witness missing in output"
return P2PKWitness.from_witness(self.witness).signatures


class BlindedMessages(BaseModel):
# NOTE: not used in Pydantic validation
__root__: List[BlindedMessage] = []


class BlindedSignature(BaseModel):
"""
Blinded signature or "promise" which is the signature on a `BlindedMessage`
Expand Down Expand Up @@ -314,7 +341,13 @@ class GetInfoResponse(BaseModel):
description_long: Optional[str] = None
contact: Optional[List[List[str]]] = None
motd: Optional[str] = None
nuts: Optional[Dict[int, Dict[str, Any]]] = None
nuts: Optional[Dict[int, Any]] = None


class Nut15MppSupport(BaseModel):
method: str
unit: str
mpp: bool


class GetInfoResponse_deprecated(BaseModel):
Expand All @@ -329,19 +362,6 @@ class GetInfoResponse_deprecated(BaseModel):
parameter: Optional[dict] = None


class BlindedMessage_Deprecated(BaseModel):
# Same as BlindedMessage, but without the id field
amount: int
B_: str # Hex-encoded blinded message
id: Optional[str] = None
witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL)

@property
def p2pksigs(self) -> List[str]:
assert self.witness, "Witness missing in output"
return P2PKWitness.from_witness(self.witness).signatures


# ------- API: KEYS -------


Expand Down Expand Up @@ -425,6 +445,7 @@ class PostMeltQuoteRequest(BaseModel):
request: str = Field(
..., max_length=settings.mint_max_request_length
) # output payment request
amount: Optional[int] = Field(default=None, gt=0) # input amount


class PostMeltQuoteResponse(BaseModel):
Expand Down Expand Up @@ -551,6 +572,12 @@ class PostRestoreRequest(BaseModel):
)


class PostRestoreRequest_Deprecated(BaseModel):
outputs: List[BlindedMessage_Deprecated] = Field(
..., max_items=settings.mint_max_request_length
)


class PostRestoreResponse(BaseModel):
outputs: List[BlindedMessage] = []
signatures: List[BlindedSignature] = []
Expand Down Expand Up @@ -656,6 +683,7 @@ def __init__(
valid_to=None,
first_seen=None,
active=True,
use_deprecated_id=False, # BACKWARDS COMPATIBILITY < 0.15.0
):
self.valid_from = valid_from
self.valid_to = valid_to
Expand All @@ -670,10 +698,19 @@ def __init__(
else:
self.id = id

# BEGIN BACKWARDS COMPATIBILITY < 0.15.0
if use_deprecated_id:
logger.warning(
"Using deprecated keyset id derivation for backwards compatibility <"
" 0.15.0"
)
self.id = derive_keyset_id_deprecated(self.public_keys)
# END BACKWARDS COMPATIBILITY < 0.15.0

self.unit = Unit[unit]

logger.trace(f"Derived keyset id {self.id} from public keys.")
if id and id != self.id:
if id and id != self.id and use_deprecated_id:
logger.warning(
f"WARNING: Keyset id {self.id} does not match the given id {id}."
" Overwriting."
Expand Down Expand Up @@ -728,6 +765,8 @@ class MintKeyset:
first_seen: Optional[str] = None
version: Optional[str] = None

duplicate_keyset_id: Optional[str] = None # BACKWARDS COMPATIBILITY < 0.15.0

def __init__(
self,
*,
Expand Down Expand Up @@ -808,12 +847,6 @@ def generate_keys(self):
assert self.seed, "seed not set"
assert self.derivation_path, "derivation path not set"

# we compute the keyset id from the public keys only if it is not
# loaded from the database. This is to allow for backwards compatibility
# with old keysets with new id's and vice versa. This code can be removed
# if there are only new keysets in the mint (> 0.15.0)
id_in_db = self.id

if self.version_tuple < (0, 12):
# WARNING: Broken key derivation for backwards compatibility with < 0.12
self.private_keys = derive_keys_backwards_compatible_insecure_pre_0_12(
Expand All @@ -824,22 +857,19 @@ def generate_keys(self):
f"WARNING: Using weak key derivation for keyset {self.id} (backwards"
" compatibility < 0.12)"
)
# load from db or derive
self.id = id_in_db or derive_keyset_id_deprecated(self.public_keys) # type: ignore
self.id = derive_keyset_id_deprecated(self.public_keys) # type: ignore
elif self.version_tuple < (0, 15):
self.private_keys = derive_keys_sha256(self.seed, self.derivation_path)
logger.trace(
f"WARNING: Using non-bip32 derivation for keyset {self.id} (backwards"
" compatibility < 0.15)"
)
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
# load from db or derive
self.id = id_in_db or derive_keyset_id_deprecated(self.public_keys) # type: ignore
self.id = derive_keyset_id_deprecated(self.public_keys) # type: ignore
else:
self.private_keys = derive_keys(self.seed, self.derivation_path)
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
# load from db or derive
self.id = id_in_db or derive_keyset_id(self.public_keys) # type: ignore
self.id = derive_keyset_id(self.public_keys) # type: ignore


# ------- TOKEN -------
Expand Down
9 changes: 8 additions & 1 deletion cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class MintLimits(MintSettings):
)
mint_max_request_length: int = Field(
default=1000,
gt=0,
title="Maximum request length",
description="Maximum length of REST API request arrays.",
)
Expand All @@ -100,16 +101,21 @@ class MintLimits(MintSettings):
)
mint_max_peg_in: int = Field(
default=None,
gt=0,
title="Maximum peg-in",
description="Maximum amount for a mint operation.",
)
mint_max_peg_out: int = Field(
default=None,
gt=0,
title="Maximum peg-out",
description="Maximum amount for a melt operation.",
)
mint_max_balance: int = Field(
default=None, title="Maximum mint balance", description="Maximum mint balance."
default=None,
gt=0,
title="Maximum mint balance",
description="Maximum mint balance.",
)


Expand Down Expand Up @@ -171,6 +177,7 @@ class LndRestFundingSource(MintSettings):
mint_lnd_rest_macaroon: Optional[str] = Field(default=None)
mint_lnd_rest_admin_macaroon: Optional[str] = Field(default=None)
mint_lnd_rest_invoice_macaroon: Optional[str] = Field(default=None)
mint_lnd_enable_mpp: bool = Field(default=False)


class CoreLightningRestFundingSource(MintSettings):
Expand Down
10 changes: 8 additions & 2 deletions cashu/lightning/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@

from pydantic import BaseModel

from ..core.base import Amount, MeltQuote, Unit
from ..core.base import (
Amount,
MeltQuote,
PostMeltQuoteRequest,
Unit,
)


class StatusResponse(BaseModel):
Expand Down Expand Up @@ -62,6 +67,7 @@ def __str__(self) -> str:


class LightningBackend(ABC):
supports_mpp: bool = False
supported_units: set[Unit]
unit: Unit

Expand Down Expand Up @@ -107,7 +113,7 @@ def get_payment_status(
@abstractmethod
async def get_payment_quote(
self,
bolt11: str,
melt_quote: PostMeltQuoteRequest,
) -> PaymentQuoteResponse:
pass

Expand Down
7 changes: 5 additions & 2 deletions cashu/lightning/blink.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
)
from loguru import logger

from ..core.base import Amount, MeltQuote, Unit
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
from ..core.settings import settings
from .base import (
InvoiceResponse,
Expand Down Expand Up @@ -375,7 +375,10 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus:
preimage=preimage,
)

async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
async def get_payment_quote(
self, melt_quote: PostMeltQuoteRequest
) -> PaymentQuoteResponse:
bolt11 = melt_quote.request
variables = {
"input": {
"paymentRequest": bolt11,
Expand Down
8 changes: 5 additions & 3 deletions cashu/lightning/corelightningrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)
from loguru import logger

from ..core.base import Amount, MeltQuote, Unit
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
from ..core.helpers import fee_reserve
from ..core.settings import settings
from .base import (
Expand Down Expand Up @@ -316,8 +316,10 @@ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
)
await asyncio.sleep(0.02)

async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
invoice_obj = decode(bolt11)
async def get_payment_quote(
self, melt_quote: PostMeltQuoteRequest
) -> PaymentQuoteResponse:
invoice_obj = decode(melt_quote.request)
assert invoice_obj.amount_msat, "invoice has no amount."
amount_msat = int(invoice_obj.amount_msat)
fees_msat = fee_reserve(amount_msat)
Expand Down
8 changes: 5 additions & 3 deletions cashu/lightning/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
encode,
)

from ..core.base import Amount, MeltQuote, Unit
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
from ..core.helpers import fee_reserve
from ..core.settings import settings
from .base import (
Expand Down Expand Up @@ -152,8 +152,10 @@ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
# amount = invoice_obj.amount_msat
# return InvoiceQuoteResponse(checking_id="", amount=amount)

async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
invoice_obj = decode(bolt11)
async def get_payment_quote(
self, melt_quote: PostMeltQuoteRequest
) -> PaymentQuoteResponse:
invoice_obj = decode(melt_quote.request)
assert invoice_obj.amount_msat, "invoice has no amount."

if self.unit == Unit.sat:
Expand Down
8 changes: 5 additions & 3 deletions cashu/lightning/lnbits.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
decode,
)

from ..core.base import Amount, MeltQuote, Unit
from ..core.base import Amount, MeltQuote, PostMeltQuoteRequest, Unit
from ..core.helpers import fee_reserve
from ..core.settings import settings
from .base import (
Expand Down Expand Up @@ -167,8 +167,10 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus:
preimage=data["preimage"],
)

async def get_payment_quote(self, bolt11: str) -> PaymentQuoteResponse:
invoice_obj = decode(bolt11)
async def get_payment_quote(
self, melt_quote: PostMeltQuoteRequest
) -> PaymentQuoteResponse:
invoice_obj = decode(melt_quote.request)
assert invoice_obj.amount_msat, "invoice has no amount."
amount_msat = int(invoice_obj.amount_msat)
fees_msat = fee_reserve(amount_msat)
Expand Down
Loading
Loading