From 602687b21505dea6e84aff0a91a36708f8b03d05 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:02:54 +0100 Subject: [PATCH] refactor pubkey extraction and set n_sigs=1 for refund spend path (#644) --- cashu/core/htlc.py | 2 -- cashu/core/p2pk.py | 33 +------------------------ cashu/mint/conditions.py | 52 ++++++++++++++++++++++++++++++++-------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/cashu/core/htlc.py b/cashu/core/htlc.py index 9cc8fb5..e2bcea2 100644 --- a/cashu/core/htlc.py +++ b/cashu/core/htlc.py @@ -7,8 +7,6 @@ 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): diff --git a/cashu/core/p2pk.py b/cashu/core/p2pk.py index 0da0ae2..23f379d 100644 --- a/cashu/core/p2pk.py +++ b/cashu/core/p2pk.py @@ -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 @@ -24,34 +21,6 @@ class P2PKSecret(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") diff --git a/cashu/mint/conditions.py b/cashu/mint/conditions.py index 1b9b61f..ea7c53a 100644 --- a/cashu/mint/conditions.py +++ b/cashu/mint/conditions.py @@ -44,11 +44,28 @@ class LedgerSpendingConditions: # 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 @@ -97,7 +114,10 @@ class LedgerSpendingConditions: 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 @@ -257,9 +277,23 @@ class LedgerSpendingConditions: # 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 ( @@ -267,8 +301,6 @@ class LedgerSpendingConditions: ), "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]