Skip to content
Open
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
12 changes: 0 additions & 12 deletions cashu/core/htlc.py

This file was deleted.

115 changes: 115 additions & 0 deletions cashu/core/nuts/nut11.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import hashlib
from datetime import datetime, timedelta
from enum import Enum
from typing import List, Optional, Union

from loguru import logger

from ..base import BlindedMessage, Proof
from ..crypto.secp import PrivateKey, PublicKey
from ..secret import Secret, SecretKind, Tags


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 P2PKSecret(Secret):
@classmethod
def from_secret(cls, secret: Secret):
assert SecretKind(secret.kind) == SecretKind.P2PK, "Secret is not a P2PK secret"
# NOTE: exclude tags in .dict() because it doesn't deserialize it properly
# need to add it back in manually with tags=secret.tags
return cls(**secret.dict(exclude={"tags"}), tags=secret.tags)

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

@property
def sigflag(self) -> SigFlags:
sigflag = self.tags.get_tag("sigflag")
return SigFlags(sigflag) if sigflag else SigFlags.SIG_INPUTS

@property
def n_sigs(self) -> int:
n_sigs = self.tags.get_tag_int("n_sigs")
return int(n_sigs) if n_sigs else 1

@property
def n_sigs_refund(self) -> Union[None, int]:
n_sigs_refund = self.tags.get_tag_int("n_sigs_refund")
return n_sigs_refund


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_schnorr_signature(
message: bytes, pubkey: PublicKey, signature: bytes
) -> bool:
return pubkey.schnorr_verify(
hashlib.sha256(message).digest(), signature, None, raw=True
)


def create_p2pk_lock(
self,
data: str,
locktime_seconds: Optional[int] = None,
tags: Optional[Tags] = None,
sig_all: bool = False,
n_sigs: int = 1,
) -> P2PKSecret:
"""Generate a P2PK secret with the given pubkeys, locktime, tags, and signature flag.

Args:
data (str): Public key to lock to.
locktime_seconds (Optional[int], optional): Locktime in seconds. Defaults to None.
tags (Optional[Tags], optional): Tags to add to the secret. Defaults to None.
sig_all (bool, optional): Whether to use SIG_ALL spending condition. Defaults to False.
n_sigs (int, optional): Number of signatures required. Defaults to 1.

Returns:
P2PKSecret: P2PK secret with the given pubkeys, locktime, tags, and signature flag.
"""
logger.debug(f"Provided tags: {tags}")
if not tags:
tags = Tags()
logger.debug(f"Before tags: {tags}")
if locktime_seconds:
tags["locktime"] = str(
int((datetime.now() + timedelta(seconds=locktime_seconds)).timestamp())
)
tags["sigflag"] = SigFlags.SIG_ALL.value if sig_all else SigFlags.SIG_INPUTS.value
if n_sigs > 1:
tags["n_sigs"] = str(n_sigs)
logger.debug(f"After tags: {tags}")
return P2PKSecret(
kind=SecretKind.P2PK.value,
data=data,
tags=tags,
)


def sigall_message_to_sign(proofs: List[Proof], outputs: List[BlindedMessage]) -> str:
"""
Creates the message to sign for sigall spending conditions.
The message is a concatenation of all proof secrets and signatures + all output attributes (amount, id, B_).
"""

# Concatenate all proof secrets
message = "".join([p.secret + p.C for p in proofs])

# Concatenate all output attributes
message += "".join([str(o.amount) + o.id + o.B_ for o in outputs])

return message
18 changes: 13 additions & 5 deletions cashu/core/nuts/nut14.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@

from ..base import Proof
from ..errors import TransactionError
from ..htlc import HTLCSecret
from ..secret import Secret, SecretKind
from .nut11 import P2PKSecret


class HTLCSecret(P2PKSecret, Secret):
@classmethod
def from_secret(cls, secret: Secret):
assert SecretKind(secret.kind) == SecretKind.HTLC, "Secret is not a HTLC secret"
# NOTE: exclude tags in .dict() because it doesn't deserialize it properly
# need to add it back in manually with tags=secret.tags
return cls(**secret.dict(exclude={"tags"}), tags=secret.tags)


def verify_htlc_spending_conditions(
Expand All @@ -27,11 +36,10 @@ def verify_htlc_spending_conditions(
try:
if len(proof.htlcpreimage) != 64:
raise TransactionError("HTLC preimage must be 64 characters hex.")
if not sha256(
bytes.fromhex(proof.htlcpreimage)
).digest() == bytes.fromhex(htlc_secret.data):
if not sha256(bytes.fromhex(proof.htlcpreimage)).digest() == bytes.fromhex(
htlc_secret.data
):
raise TransactionError("HTLC preimage does not match.")
except ValueError:
raise TransactionError("invalid preimage for HTLC: not a hex string.")
return True

57 changes: 0 additions & 57 deletions cashu/core/p2pk.py

This file was deleted.

12 changes: 6 additions & 6 deletions cashu/mint/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
from ..core.errors import (
TransactionError,
)
from ..core.htlc import HTLCSecret
from ..core.nuts.nut14 import verify_htlc_spending_conditions
from ..core.p2pk import (
from ..core.nuts import nut11, nut14
from ..core.nuts.nut11 import (
P2PKSecret,
SigFlags,
verify_schnorr_signature,
)
from ..core.nuts.nut14 import HTLCSecret
from ..core.secret import Secret, SecretKind


Expand Down Expand Up @@ -163,7 +163,7 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool:
# HTLC
if SecretKind(secret.kind) == SecretKind.HTLC:
htlc_secret = HTLCSecret.from_secret(secret)
verify_htlc_spending_conditions(proof)
nut14.verify_htlc_spending_conditions(proof)
return self._verify_p2pk_sig_inputs(proof, htlc_secret)

# no spending condition present
Expand Down Expand Up @@ -285,8 +285,8 @@ def _verify_sigall_spending_conditions(
if not pubkeys:
return True

message_to_sign = message_to_sign or "".join(
[p.secret for p in proofs] + [o.B_ for o in outputs]
message_to_sign = message_to_sign or nut11.sigall_message_to_sign(
proofs, outputs
)

# validation
Expand Down
5 changes: 2 additions & 3 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
PostMeltQuoteResponse,
PostMintQuoteRequest,
)
from ..core.nuts import nut11
from ..core.settings import settings
from ..core.split import amount_split
from ..lightning.base import (
Expand Down Expand Up @@ -886,9 +887,7 @@ async def melt(
)

# verify SIG_ALL signatures
message_to_sign = (
"".join([p.secret for p in proofs] + [o.B_ for o in outputs or []]) + quote
)
message_to_sign = nut11.sigall_message_to_sign(proofs, outputs or []) + quote
self._verify_sigall_spending_conditions(proofs, outputs or [], message_to_sign)

# verify that the amount of the input proofs is equal to the amount of the quote
Expand Down
2 changes: 1 addition & 1 deletion cashu/wallet/htlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from ..core.base import HTLCWitness, Proof
from ..core.db import Database
from ..core.htlc import (
from ..core.nuts.nut14 import (
HTLCSecret,
)
from ..core.secret import SecretKind, Tags
Expand Down
14 changes: 6 additions & 8 deletions cashu/wallet/p2pk.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

from loguru import logger

from cashu.core.htlc import HTLCSecret

from ..core.base import (
BlindedMessage,
HTLCWitness,
Expand All @@ -13,11 +11,13 @@
)
from ..core.crypto.secp import PrivateKey
from ..core.db import Database
from ..core.p2pk import (
from ..core.nuts import nut11
from ..core.nuts.nut11 import (
P2PKSecret,
SigFlags,
schnorr_sign,
)
from ..core.nuts.nut14 import HTLCSecret
from ..core.secret import Secret, SecretKind, Tags
from .protocols import SupportsDb, SupportsPrivateKey

Expand Down Expand Up @@ -157,8 +157,8 @@ def add_witness_swap_sig_all(
secrets = set([Secret.deserialize(p.secret) for p in proofs])
if not len(secrets) == 1:
raise Exception("Secrets not identical")
message_to_sign = message_to_sign or "".join(
[p.secret for p in proofs] + [o.B_ for o in outputs]
message_to_sign = message_to_sign or nut11.sigall_message_to_sign(
proofs, outputs
)
signature = self.schnorr_sign_message(message_to_sign)
# add witness to only the first proof
Expand Down Expand Up @@ -195,9 +195,7 @@ def sign_proofs_inplace_melt(
) -> List[Proof]:
# sign proofs if they are P2PK SIG_INPUTS
proofs = self.add_witnesses_sig_inputs(proofs)
message_to_sign = (
"".join([p.secret for p in proofs] + [o.B_ for o in outputs]) + quote_id
)
message_to_sign = nut11.sigall_message_to_sign(proofs, outputs) + quote_id
# sign first proof if swap is SIG_ALL
return self.add_witness_swap_sig_all(proofs, outputs, message_to_sign)

Expand Down
2 changes: 1 addition & 1 deletion cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
PostMeltQuoteResponse,
)
from ..core.nuts import nut20
from ..core.p2pk import Secret
from ..core.secret import Secret
from ..core.settings import settings
from . import migrations
from .compat import WalletCompat
Expand Down
3 changes: 2 additions & 1 deletion tests/mint/test_mint_p2pk.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest_asyncio

from cashu.core.base import P2PKWitness
from cashu.core.nuts import nut11
from cashu.mint.ledger import Ledger
from cashu.wallet.wallet import Wallet as Wallet1
from tests.conftest import SERVER_ENDPOINT
Expand Down Expand Up @@ -192,7 +193,7 @@ async def test_ledger_verify_sigall_validation(wallet1: Wallet1, ledger: Ledger)
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)

# Create the message to sign (all inputs + all outputs)
message_to_sign = "".join([p.secret for p in send_proofs] + [o.B_ for o in outputs])
message_to_sign = nut11.sigall_message_to_sign(send_proofs, outputs)

# Sign the message with the wallet's private key
signature = wallet1.schnorr_sign_message(message_to_sign)
Expand Down
Loading
Loading