Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into lnmarkets
Browse files Browse the repository at this point in the history
  • Loading branch information
lollerfirst committed Nov 14, 2024
2 parents c510540 + 6a4f1bd commit 2269f9c
Show file tree
Hide file tree
Showing 21 changed files with 271 additions and 153 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ MINT_INFO_DESCRIPTION="The short mint description"
MINT_INFO_DESCRIPTION_LONG="A long mint description that can be a long piece of text."
MINT_INFO_CONTACT=[["email","[email protected]"], ["twitter","@me"], ["nostr", "npub..."]]
MINT_INFO_MOTD="Message to users"
MINT_INFO_ICON_URL="https://mint.host/icon.jpg"
MINT_INFO_URLS=["https://mint.host", "http://mint8gv0sq5ul602uxt2fe0t80e3c2bi9fy0cxedp69v1vat6ruj81wv.onion"]

MINT_PRIVATE_KEY=supersecretprivatekey

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.2 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.3 poetry run mint
```

## From this repository
Expand Down
7 changes: 2 additions & 5 deletions cashu/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,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
2 changes: 0 additions & 2 deletions cashu/core/htlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
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):
Expand Down
1 change: 1 addition & 0 deletions cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GetInfoResponse(BaseModel):
contact: Optional[List[MintInfoContact]] = None
motd: Optional[str] = None
icon_url: Optional[str] = None
urls: Optional[List[str]] = None
time: Optional[int] = None
nuts: Optional[Dict[int, Any]] = None

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
33 changes: 1 addition & 32 deletions cashu/core/p2pk.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import hashlib
import time
from enum import Enum
from typing import List, Union

from loguru import logger
from typing import Union

from .crypto.secp import PrivateKey, PublicKey
from .secret import Secret, SecretKind
Expand All @@ -24,34 +21,6 @@ def from_secret(cls, secret: Secret):
# need to add it back in manually with tags=secret.tags
return cls(**secret.dict(exclude={"tags"}), tags=secret.tags)

def get_p2pk_pubkey_from_secret(self) -> List[str]:
"""Gets the P2PK pubkey from a Secret depending on the locktime.
If locktime is passed, only the refund pubkeys are returned.
Else, the pubkeys in the data field and in the 'pubkeys' tag are returned.
Args:
secret (Secret): P2PK Secret in ecash token
Returns:
str: pubkey to use for P2PK, empty string if anyone can spend (locktime passed)
"""
# the pubkey in the data field is the pubkey to use for P2PK
pubkeys: List[str] = [self.data]

# get all additional pubkeys from tags for multisig
pubkeys += self.tags.get_tag_all("pubkeys")

# check if locktime is passed and if so, only return refund pubkeys
now = time.time()
if self.locktime and self.locktime < now:
logger.trace(f"p2pk locktime ran out ({self.locktime}<{now}).")
# check tags if a refund pubkey is present.
# If yes, we demand the signature to be from the refund pubkey
return self.tags.get_tag_all("refund")

return pubkeys

@property
def locktime(self) -> Union[None, int]:
locktime = self.tags.get_tag("locktime")
Expand Down
7 changes: 4 additions & 3 deletions cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

env = Env()

VERSION = "0.16.2"
VERSION = "0.16.3"


def find_env_file():
Expand Down Expand Up @@ -153,6 +153,7 @@ class MintInformation(CashuSettings):
mint_info_contact: List[List[str]] = Field(default=[])
mint_info_motd: str = Field(default=None)
mint_info_icon_url: str = Field(default=None)
mint_info_urls: List[str] = Field(default=None)


class WalletSettings(CashuSettings):
Expand Down Expand Up @@ -208,7 +209,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)
mint_lnd_enable_mpp: bool = Field(default=True)


class LndRPCFundingSource(MintSettings):
Expand All @@ -221,7 +222,7 @@ class CLNRestFundingSource(MintSettings):
mint_clnrest_url: Optional[str] = Field(default=None)
mint_clnrest_cert: Optional[str] = Field(default=None)
mint_clnrest_rune: Optional[str] = Field(default=None)
mint_clnrest_enable_mpp: bool = Field(default=False)
mint_clnrest_enable_mpp: bool = Field(default=True)


class CoreLightningRestFundingSource(MintSettings):
Expand Down
9 changes: 9 additions & 0 deletions cashu/lightning/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

from bolt11 import (
Bolt11,
Feature,
Features,
FeatureState,
MilliSatoshi,
TagChar,
Tags,
Expand Down Expand Up @@ -90,6 +93,12 @@ async def create_invoice(
) -> InvoiceResponse:
self.assert_unit_supported(amount.unit)
tags = Tags()
tags.add(
TagChar.features,
Features.from_feature_list(
{Feature.payment_secret: FeatureState.supported}
),
)

if description_hash:
tags.add(TagChar.description_hash, description_hash.hex())
Expand Down
52 changes: 42 additions & 10 deletions cashu/mint/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,28 @@ def _verify_p2pk_spending_conditions(self, proof: Proof, secret: Secret) -> bool

# extract pubkeys that we require signatures from depending on whether the
# locktime has passed (refund) or not (pubkeys in secret.data and in tags)
# This is implemented in get_p2pk_pubkey_from_secret()
pubkeys = p2pk_secret.get_p2pk_pubkey_from_secret()
# we will get an empty list if the locktime has passed and no refund pubkey is present
if not pubkeys:
return True

# the pubkey in the data field is the pubkey to use for P2PK
pubkeys: List[str] = [p2pk_secret.data]

# get all additional pubkeys from tags for multisig
pubkeys += p2pk_secret.tags.get_tag_all("pubkeys")

# check if locktime is passed and if so, only consider refund pubkeys
now = time.time()
if p2pk_secret.locktime and p2pk_secret.locktime < now:
logger.trace(f"p2pk locktime ran out ({p2pk_secret.locktime}<{now}).")
# If a refund pubkey is present, we demand the signature to be from it
refund_pubkeys = p2pk_secret.tags.get_tag_all("refund")
if not refund_pubkeys:
# no refund pubkey is present, anyone can spend
return True
return self._verify_secret_signatures(
proof,
refund_pubkeys,
proof.p2pksigs,
1, # only 1 sig required for refund
)

return self._verify_secret_signatures(
proof, pubkeys, proof.p2pksigs, p2pk_secret.n_sigs
Expand Down Expand Up @@ -97,7 +114,10 @@ def _verify_htlc_spending_conditions(self, proof: Proof, secret: Secret) -> bool
refund_pubkeys = htlc_secret.tags.get_tag_all("refund")
if refund_pubkeys:
return self._verify_secret_signatures(
proof, refund_pubkeys, proof.p2pksigs, htlc_secret.n_sigs
proof,
refund_pubkeys,
proof.p2pksigs,
1, # only one refund signature required
)
# no pubkeys given in secret, anyone can spend
return True
Expand Down Expand Up @@ -257,18 +277,30 @@ def _verify_output_p2pk_spending_conditions(

# extract all pubkeys and n_sigs from secrets
pubkeys_per_proof = [
secret.get_p2pk_pubkey_from_secret() for secret in p2pk_secrets
[p2pk_secret.data] + p2pk_secret.tags.get_tag_all("pubkeys")
for p2pk_secret in p2pk_secrets
]
n_sigs_per_proof = [secret.n_sigs for secret in p2pk_secrets]
n_sigs_per_proof = [p2pk_secret.n_sigs for p2pk_secret in p2pk_secrets]

# if locktime passed, we only require the refund pubkeys and 1 signature
for p2pk_secret in p2pk_secrets:
now = time.time()
if p2pk_secret.locktime and p2pk_secret.locktime < now:
refund_pubkeys = p2pk_secret.tags.get_tag_all("refund")
if refund_pubkeys:
pubkeys_per_proof.append(refund_pubkeys)
n_sigs_per_proof.append(1) # only 1 sig required for refund

# if no pubkeys are present, anyone can spend
if not pubkeys_per_proof:
return True

# all pubkeys and n_sigs must be the same
assert (
len({tuple(pubs_output) for pubs_output in pubkeys_per_proof}) == 1
), "pubkeys in all proofs must match."
assert len(set(n_sigs_per_proof)) == 1, "n_sigs in all proofs must match."

# TODO: add limit for maximum number of pubkeys

# validation successful

pubkeys: List[str] = pubkeys_per_proof[0]
Expand Down
2 changes: 2 additions & 0 deletions cashu/mint/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ..core.nuts import (
DLEQ_NUT,
FEE_RETURN_NUT,
HTLC_NUT,
MELT_NUT,
MINT_NUT,
MPP_NUT,
Expand Down Expand Up @@ -58,6 +59,7 @@ def mint_features(self) -> Dict[int, Union[List[Any], Dict[str, Any]]]:
SCRIPT_NUT: supported_dict,
P2PK_NUT: supported_dict,
DLEQ_NUT: supported_dict,
HTLC_NUT: supported_dict,
}

# signal which method-unit pairs support MPP
Expand Down
1 change: 1 addition & 0 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ async def info() -> GetInfoResponse:
contact=contact_info,
nuts=mint_features,
icon_url=settings.mint_info_icon_url,
urls=settings.mint_info_urls,
motd=settings.mint_info_motd,
time=int(time.time()),
)
Expand Down
7 changes: 6 additions & 1 deletion cashu/wallet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from os.path import isdir, join
from typing import Optional, Union

import bolt11
import click
from click import Context
from loguru import logger
Expand Down Expand Up @@ -49,6 +50,7 @@
verify_mint,
)
from ..helpers import (
check_payment_preimage,
deserialize_token_from_string,
init_wallet,
list_mints,
Expand Down Expand Up @@ -209,6 +211,7 @@ async def pay(
wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint()
await print_balance(ctx)
payment_hash = bolt11.decode(invoice).payment_hash
quote = await wallet.melt_quote(invoice, amount)
logger.debug(f"Quote: {quote}")
total_amount = quote.amount + quote.fee_reserve
Expand Down Expand Up @@ -252,9 +255,11 @@ async def pay(
melt_response.payment_preimage
and melt_response.payment_preimage != "0" * 64
):
if not check_payment_preimage(payment_hash, melt_response.payment_preimage):
print(" Error: Invalid preimage!", end="", flush=True)
print(f" (Preimage: {melt_response.payment_preimage}).")
else:
print(".")
print(" Mint did not provide a preimage.")
elif MintQuoteState(melt_response.state) == MintQuoteState.pending:
print(" Invoice pending.")
elif MintQuoteState(melt_response.state) == MintQuoteState.unpaid:
Expand Down
9 changes: 9 additions & 0 deletions cashu/wallet/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hashlib
import os
from typing import Optional

Expand Down Expand Up @@ -169,3 +170,11 @@ async def send(

await wallet.set_reserved(send_proofs, reserved=True)
return wallet.available_balance, token

def check_payment_preimage(
payment_hash: str,
preimage: str,
) -> bool:
return bytes.fromhex(payment_hash) == hashlib.sha256(
bytes.fromhex(preimage)
).digest()
22 changes: 18 additions & 4 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ def split_wallet_state(self, amount: int) -> List[int]:
# sort by increasing amount
amounts_we_want.sort()

logger.debug(
logger.trace(
f"Amounts we have: {[(a, amounts_we_have.count(a)) for a in set(amounts_we_have)]}"
)
amounts: list[int] = []
Expand All @@ -470,7 +470,7 @@ def split_wallet_state(self, amount: int) -> List[int]:
amounts += amount_split(remaining_amount)
amounts.sort()

logger.debug(f"Amounts we want: {amounts}")
logger.trace(f"Amounts we want: {amounts}")
if sum(amounts) != amount:
raise Exception(f"Amounts do not sum to {amount}.")

Expand Down Expand Up @@ -643,7 +643,7 @@ async def split(
proofs = self.add_witnesses_to_proofs(proofs)

input_fees = self.get_fees_for_proofs(proofs)
logger.debug(f"Input fees: {input_fees}")
logger.trace(f"Input fees: {input_fees}")
# create a suitable amounts to keep and send.
keep_outputs, send_outputs = self.determine_output_amounts(
proofs,
Expand Down Expand Up @@ -674,8 +674,22 @@ async def split(
# potentially add witnesses to outputs based on what requirement the proofs indicate
outputs = self.add_witnesses_to_outputs(proofs, outputs)

# sort outputs by amount, remember original order
sorted_outputs_with_indices = sorted(
enumerate(outputs), key=lambda p: p[1].amount
)
original_indices, sorted_outputs = zip(*sorted_outputs_with_indices)

# Call swap API
promises = await super().split(proofs, outputs)
sorted_promises = await super().split(proofs, sorted_outputs)

# sort promises back to original order
promises = [
promise
for _, promise in sorted(
zip(original_indices, sorted_promises), key=lambda x: x[0]
)
]

# Construct proofs from returned promises (i.e., unblind the signatures)
new_proofs = await self._construct_proofs(
Expand Down
Loading

0 comments on commit 2269f9c

Please sign in to comment.