Skip to content

Commit

Permalink
Multinut LND (cashubtc#492)
Browse files Browse the repository at this point in the history
* amount in melt request

* apply fee limit

* more error handling

* wip: signal flag in /info

* clean up multinut

* decode mypy error lndrest

* fix test

* fix tests

* signal feature and blindmessages_deprecated

* setting

* fix blindedsignature method

* fix tests

* mint info file

* test mpp with lnd regtest

* nuts optionsl mint
 info

* try to enable mpp with lnd

* test mpp with third payment
  • Loading branch information
callebtc committed May 22, 2024
1 parent 71b4051 commit 61cf7de
Show file tree
Hide file tree
Showing 27 changed files with 502 additions and 110 deletions.
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

0 comments on commit 61cf7de

Please sign in to comment.