From 00e7aaa4e21a45fdddf16ed69401888fb10f8ae2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:34:58 +0200 Subject: [PATCH] refactor conditions and fix htlc multisig --- cashu/core/base.py | 24 ++--- cashu/core/htlc.py | 18 ++++ cashu/core/p2pk.py | 10 +- cashu/mint/conditions.py | 158 ++++++++++++++-------------- cashu/mint/db/write.py | 2 +- cashu/wallet/htlc.py | 24 +++-- cashu/wallet/p2pk.py | 61 +++++------ cashu/wallet/wallet.py | 4 +- tests/test_wallet_htlc.py | 210 ++++++++++++++++++++++++++++++++++---- tests/test_wallet_p2pk.py | 10 +- 10 files changed, 349 insertions(+), 172 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 89306724..66b4bce8 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -95,21 +95,7 @@ def pending(self) -> bool: class HTLCWitness(BaseModel): preimage: Optional[str] = None - signature: Optional[str] = None - - @classmethod - def from_witness(cls, witness: str): - return cls(**json.loads(witness)) - - -class P2SHWitness(BaseModel): - """ - Unlocks P2SH spending condition of a Proof - """ - - script: str - signature: str - address: Union[str, None] = None + signatures: Optional[List[str]] = None @classmethod def from_witness(cls, witness: str): @@ -206,10 +192,15 @@ def p2pksigs(self) -> List[str]: return P2PKWitness.from_witness(self.witness).signatures @property - def htlcpreimage(self) -> Union[str, None]: + def htlcpreimage(self) -> str | None: assert self.witness, "Witness is missing for htlc preimage" return HTLCWitness.from_witness(self.witness).preimage + @property + def htlcsigs(self) -> List[str] | None: + assert self.witness, "Witness is missing for htlc signatures" + return HTLCWitness.from_witness(self.witness).signatures + class Proofs(BaseModel): # NOTE: not used in Pydantic validation @@ -647,6 +638,7 @@ def deserialize(serialized: str) -> Dict[int, PublicKey]: int(amount): PublicKey(bytes.fromhex(hex_key), raw=True) for amount, hex_key in dict(json.loads(serialized)).items() } + return cls( id=row["id"], unit=row["unit"], diff --git a/cashu/core/htlc.py b/cashu/core/htlc.py index d75f7c2e..9cc8fb5f 100644 --- a/cashu/core/htlc.py +++ b/cashu/core/htlc.py @@ -1,8 +1,16 @@ +from enum import Enum from typing import Union 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): @classmethod def from_secret(cls, secret: Secret): @@ -15,3 +23,13 @@ def from_secret(cls, secret: Secret): def locktime(self) -> Union[None, int]: locktime = self.tags.get_tag("locktime") return int(locktime) if locktime else None + + @property + def sigflag(self) -> Union[None, SigFlags]: + sigflag = self.tags.get_tag("sigflag") + return SigFlags(sigflag) if sigflag else None + + @property + def n_sigs(self) -> Union[None, int]: + n_sigs = self.tags.get_tag("n_sigs") + return int(n_sigs) if n_sigs else None diff --git a/cashu/core/p2pk.py b/cashu/core/p2pk.py index f42a3a95..0da0ae23 100644 --- a/cashu/core/p2pk.py +++ b/cashu/core/p2pk.py @@ -68,18 +68,16 @@ def n_sigs(self) -> Union[None, int]: return int(n_sigs) if n_sigs else None -def sign_p2pk_sign(message: bytes, private_key: PrivateKey) -> bytes: - # ecdsa version - # signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message)) +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_p2pk_signature(message: bytes, pubkey: PublicKey, signature: bytes) -> bool: - # ecdsa version - # return pubkey.ecdsa_verify(message, pubkey.ecdsa_deserialize(signature)) +def verify_schnorr_signature( + message: bytes, pubkey: PublicKey, signature: bytes +) -> bool: return pubkey.schnorr_verify( hashlib.sha256(message).digest(), signature, None, raw=True ) diff --git a/cashu/mint/conditions.py b/cashu/mint/conditions.py index 2f1f0cb7..c05ef5ca 100644 --- a/cashu/mint/conditions.py +++ b/cashu/mint/conditions.py @@ -4,7 +4,7 @@ from loguru import logger -from ..core.base import BlindedMessage, HTLCWitness, Proof +from ..core.base import BlindedMessage, Proof from ..core.crypto.secp import PublicKey from ..core.errors import ( TransactionError, @@ -13,7 +13,7 @@ from ..core.p2pk import ( P2PKSecret, SigFlags, - verify_p2pk_signature, + verify_schnorr_signature, ) from ..core.secret import Secret, SecretKind @@ -50,62 +50,9 @@ def _verify_p2pk_spending_conditions(self, proof: Proof, secret: Secret) -> bool if not pubkeys: return True - assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique." - logger.trace(f"pubkeys: {pubkeys}") - - # verify that signatures are present - if not proof.p2pksigs: - # no signature present although secret indicates one - logger.error(f"no p2pk signatures in proof: {proof.p2pksigs}") - raise TransactionError("no p2pk signatures in proof.") - - # we make sure that there are no duplicate signatures - if len(set(proof.p2pksigs)) != len(proof.p2pksigs): - raise TransactionError("p2pk signatures must be unique.") - - # we parse the secret as a P2PK commitment - # assert len(proof.secret.split(":")) == 5, "p2pk secret format invalid." - - # INPUTS: check signatures proof.p2pksigs against pubkey - # we expect the signature to be on the pubkey (=message) itself - n_sigs_required = p2pk_secret.n_sigs or 1 - assert n_sigs_required > 0, "n_sigs must be positive." - - # check if enough signatures are present - assert ( - len(proof.p2pksigs) >= n_sigs_required - ), f"not enough signatures provided: {len(proof.p2pksigs)} < {n_sigs_required}." - - n_valid_sigs_per_output = 0 - # loop over all signatures in output - for input_sig in proof.p2pksigs: - for pubkey in pubkeys: - logger.trace(f"verifying signature {input_sig} by pubkey {pubkey}.") - logger.trace(f"Message: {p2pk_secret.serialize().encode('utf-8')}") - if verify_p2pk_signature( - message=proof.secret.encode("utf-8"), - pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), - signature=bytes.fromhex(input_sig), - ): - n_valid_sigs_per_output += 1 - logger.trace( - f"p2pk signature on input is valid: {input_sig} on {pubkey}." - ) - - # check if we have enough valid signatures - assert n_valid_sigs_per_output, "no valid signature provided for input." - assert n_valid_sigs_per_output >= n_sigs_required, ( - f"signature threshold not met. {n_valid_sigs_per_output} <" - f" {n_sigs_required}." - ) - - logger.trace( - f"{n_valid_sigs_per_output} of {n_sigs_required} valid signatures found." + return self._verify_secret_signatures( + proof, pubkeys, proof.p2pksigs, p2pk_secret.n_sigs ) - logger.trace(proof.p2pksigs) - logger.trace("p2pk signature on inputs is valid.") - - return True def _verify_htlc_spending_conditions(self, proof: Proof, secret: Secret) -> bool: """ @@ -149,18 +96,9 @@ def _verify_htlc_spending_conditions(self, proof: Proof, secret: Secret) -> bool if htlc_secret.locktime and htlc_secret.locktime < time.time(): refund_pubkeys = htlc_secret.tags.get_tag_all("refund") if refund_pubkeys: - assert proof.witness, TransactionError("no HTLC refund signature.") - signature = HTLCWitness.from_witness(proof.witness).signature - assert signature, TransactionError("no HTLC refund signature provided") - for pubkey in refund_pubkeys: - if verify_p2pk_signature( - message=proof.secret.encode("utf-8"), - pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), - signature=bytes.fromhex(signature), - ): - # a signature matches - return True - raise TransactionError("HTLC refund signatures did not match.") + return self._verify_secret_signatures( + proof, refund_pubkeys, proof.p2pksigs, htlc_secret.n_sigs + ) # no pubkeys given in secret, anyone can spend return True @@ -173,23 +111,77 @@ def _verify_htlc_spending_conditions(self, proof: Proof, secret: Secret) -> bool ).digest() == bytes.fromhex(htlc_secret.data): raise TransactionError("HTLC preimage does not match.") - # then we check whether a signature is required + # then we check whether signatures are required hashlock_pubkeys = htlc_secret.tags.get_tag_all("pubkeys") - if hashlock_pubkeys: - assert proof.witness, TransactionError("no HTLC hash lock signature.") - signature = HTLCWitness.from_witness(proof.witness).signature - assert signature, TransactionError("HTLC no hash lock signatures provided.") - for pubkey in hashlock_pubkeys: - if verify_p2pk_signature( + if not hashlock_pubkeys: + # no pubkeys given in secret, anyone can spend + return True + + return self._verify_secret_signatures( + proof, hashlock_pubkeys, proof.htlcsigs or [], htlc_secret.n_sigs + ) + + def _verify_secret_signatures( + self, + proof: Proof, + pubkeys: List[str], + signatures: List[str], + n_sigs_required: int | None = 1, + ) -> bool: + assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique." + logger.trace(f"pubkeys: {pubkeys}") + + # verify that signatures are present + if not signatures: + # no signature present although secret indicates one + logger.error(f"no signatures in proof: {proof}") + raise TransactionError("no signatures in proof.") + + # we make sure that there are no duplicate signatures + if len(set(signatures)) != len(signatures): + raise TransactionError("signatures must be unique.") + + # we parse the secret as a P2PK commitment + # assert len(proof.secret.split(":")) == 5, "p2pk secret format invalid." + + # INPUTS: check signatures against pubkey + # we expect the signature to be on the pubkey (=message) itself + n_sigs_required = n_sigs_required or 1 + assert n_sigs_required > 0, "n_sigs must be positive." + + # check if enough signatures are present + assert ( + len(signatures) >= n_sigs_required + ), f"not enough signatures provided: {len(signatures)} < {n_sigs_required}." + + n_valid_sigs_per_output = 0 + # loop over all signatures in input + for input_sig in signatures: + for pubkey in pubkeys: + logger.trace(f"verifying signature {input_sig} by pubkey {pubkey}.") + logger.trace(f"Message: {proof.secret}") + if verify_schnorr_signature( message=proof.secret.encode("utf-8"), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), - signature=bytes.fromhex(signature), + signature=bytes.fromhex(input_sig), ): - # a signature matches - return True - # none of the pubkeys had a match - raise TransactionError("HTLC hash lock signatures did not match.") - # no pubkeys were included, anyone can spend + n_valid_sigs_per_output += 1 + logger.trace( + f"p2pk signature on input is valid: {input_sig} on {pubkey}." + ) + + # check if we have enough valid signatures + assert n_valid_sigs_per_output, "no valid signature provided for input." + assert n_valid_sigs_per_output >= n_sigs_required, ( + f"signature threshold not met. {n_valid_sigs_per_output} <" + f" {n_sigs_required}." + ) + + logger.trace( + f"{n_valid_sigs_per_output} of {n_sigs_required} valid signatures found." + ) + logger.trace("p2pk signature on inputs is valid.") + return True def _verify_input_spending_conditions(self, proof: Proof) -> bool: @@ -304,7 +296,7 @@ def _verify_output_p2pk_spending_conditions( # loop over all signatures in output for sig in p2pksigs: for pubkey in pubkeys: - if verify_p2pk_signature( + if verify_schnorr_signature( message=bytes.fromhex(output.B_), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), signature=bytes.fromhex(sig), diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index 239b529a..f7a1e5f5 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -55,7 +55,7 @@ async def _verify_spent_proofs_and_set_pending( logger.trace("_verify_spent_proofs_and_set_pending acquiring lock") async with self.db.get_connection( lock_table="proofs_pending", - lock_timeout=1, + lock_timeout=10, ) as conn: logger.trace("checking whether proofs are already spent") await self.db_read._verify_proofs_spendable(proofs, conn) diff --git a/cashu/wallet/htlc.py b/cashu/wallet/htlc.py index 58e32df1..12b10000 100644 --- a/cashu/wallet/htlc.py +++ b/cashu/wallet/htlc.py @@ -1,6 +1,6 @@ import hashlib from datetime import datetime, timedelta -from typing import List, Optional +from typing import List from ..core.base import HTLCWitness, Proof from ..core.db import Database @@ -17,27 +17,31 @@ class WalletHTLC(SupportsDb): async def create_htlc_lock( self, *, - preimage: Optional[str] = None, - preimage_hash: Optional[str] = None, - hashlock_pubkey: Optional[str] = None, - locktime_seconds: Optional[int] = None, - locktime_pubkey: Optional[str] = None, + preimage: str | None = None, + preimage_hash: str | None = None, + hashlock_pubkeys: List[str] | None = None, + hashlock_n_sigs: int | None = None, + locktime_seconds: int | None = None, + locktime_pubkeys: List[str] | None = None, ) -> HTLCSecret: tags = Tags() if locktime_seconds: tags["locktime"] = str( int((datetime.now() + timedelta(seconds=locktime_seconds)).timestamp()) ) - if locktime_pubkey: - tags["refund"] = locktime_pubkey + if locktime_pubkeys: + tags["refund"] = locktime_pubkeys if not preimage_hash and preimage: preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() assert preimage_hash, "preimage_hash or preimage must be provided" - if hashlock_pubkey: - tags["pubkeys"] = hashlock_pubkey + if hashlock_pubkeys: + tags["pubkeys"] = hashlock_pubkeys + + if hashlock_n_sigs: + tags["n_sigs"] = str(hashlock_n_sigs) return HTLCSecret( kind=SecretKind.HTLC.value, diff --git a/cashu/wallet/p2pk.py b/cashu/wallet/p2pk.py index e5d5a7fe..4f1543ad 100644 --- a/cashu/wallet/p2pk.py +++ b/cashu/wallet/p2pk.py @@ -13,7 +13,7 @@ from ..core.p2pk import ( P2PKSecret, SigFlags, - sign_p2pk_sign, + schnorr_sign, ) from ..core.secret import Secret, SecretKind, Tags from .protocols import SupportsDb, SupportsPrivateKey @@ -21,7 +21,7 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb): db: Database - private_key: Optional[PrivateKey] = None + private_key: PrivateKey # ---------- P2PK ---------- async def create_p2pk_pubkey(self): @@ -61,10 +61,15 @@ async def create_p2pk_lock( tags=tags, ) - async def sign_p2pk_proofs(self, proofs: List[Proof]) -> List[str]: - assert ( - self.private_key - ), "No private key set in settings. Set NOSTR_PRIVATE_KEY in .env" + def sign_proofs(self, proofs: List[Proof]) -> List[str]: + """Signs proof secrets with the private key of the wallet. + + Args: + proofs (List[Proof]): Proofs to sign + + Returns: + List[str]: List of signatures for each proof + """ private_key = self.private_key assert private_key.pubkey logger.trace( @@ -76,7 +81,7 @@ async def sign_p2pk_proofs(self, proofs: List[Proof]) -> List[str]: logger.trace(f"Signing message: {proof.secret}") signatures = [ - sign_p2pk_sign( + schnorr_sign( message=proof.secret.encode("utf-8"), private_key=private_key, ).hex() @@ -85,21 +90,21 @@ async def sign_p2pk_proofs(self, proofs: List[Proof]) -> List[str]: logger.debug(f"Signatures: {signatures}") return signatures - async def sign_p2pk_outputs(self, outputs: List[BlindedMessage]) -> List[str]: + def sign_outputs(self, outputs: List[BlindedMessage]) -> List[str]: assert ( self.private_key ), "No private key set in settings. Set NOSTR_PRIVATE_KEY in .env" private_key = self.private_key assert private_key.pubkey return [ - sign_p2pk_sign( + schnorr_sign( message=bytes.fromhex(output.B_), private_key=private_key, ).hex() for output in outputs ] - async def add_p2pk_witnesses_to_outputs( + def add_signature_witnesses_to_outputs( self, outputs: List[BlindedMessage] ) -> List[BlindedMessage]: """Takes a list of outputs and adds a P2PK signatures to each. @@ -108,12 +113,12 @@ async def add_p2pk_witnesses_to_outputs( Returns: List[BlindedMessage]: Outputs with P2PK signatures added """ - p2pk_signatures = await self.sign_p2pk_outputs(outputs) + p2pk_signatures = self.sign_outputs(outputs) for o, s in zip(outputs, p2pk_signatures): o.witness = P2PKWitness(signatures=[s]).json() return outputs - async def add_witnesses_to_outputs( + def add_witnesses_to_outputs( self, proofs: List[Proof], outputs: List[BlindedMessage] ) -> List[BlindedMessage]: """Adds witnesses to outputs if the inputs (proofs) indicate an appropriate signature flag @@ -139,13 +144,11 @@ async def add_witnesses_to_outputs( for p in proofs ] ): - outputs = await self.add_p2pk_witnesses_to_outputs(outputs) + outputs = self.add_signature_witnesses_to_outputs(outputs) return outputs - async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: - p2pk_signatures = await self.sign_p2pk_proofs(proofs) - logger.debug(f"Unlock signatures for {len(proofs)} proofs: {p2pk_signatures}") - logger.debug(f"Proofs: {proofs}") + def add_signature_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: + p2pk_signatures = self.sign_proofs(proofs) # attach unlock signatures to proofs assert len(proofs) == len(p2pk_signatures), "wrong number of signatures" for p, s in zip(proofs, p2pk_signatures): @@ -157,14 +160,14 @@ async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof] p.witness = P2PKWitness(signatures=[s]).json() return proofs - async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: + def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: """Adds witnesses to proofs for P2PK redemption. This method parses the secret of each proof and determines the correct witness type and adds it to the proof if we have it available. Note: In order for this method to work, all proofs must have the same secret type. - For P2PK, we use an individual signature for each token in proofs. + For P2PK and HTLC, we use an individual signature for each token in proofs. Args: proofs (List[Proof]): List of proofs to add witnesses to @@ -172,22 +175,22 @@ async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: Returns: List[Proof]: List of proofs with witnesses added """ - - # iterate through proofs and produce witnesses for each - # first we check whether all tokens have serialized secrets as their secret try: for p in proofs: - Secret.deserialize(p.secret) + secret = Secret.deserialize(p.secret) except Exception: # if not, we do not add witnesses (treat as regular token secret) return proofs logger.debug("Spending conditions detected.") - # P2PK signatures - if all( - [Secret.deserialize(p.secret).kind == SecretKind.P2PK.value for p in proofs] - ): - logger.debug("P2PK redemption detected.") - proofs = await self.add_p2pk_witnesses_to_proofs(proofs) + # check if all secrets are either P2PK or HTLC + if all([secret.kind == SecretKind.P2PK.value for p in proofs]): + proofs = self.add_signature_witnesses_to_proofs(proofs) + + # if all([secret.kind == SecretKind.HTLC.value for p in proofs]): + # for p in proofs: + # htlc_secret = HTLCSecret.deserialize(p.secret) + # if htlc_secret.tags.get_tag("pubkeys"): + # p = self.add_signature_witnesses_to_proofs([p])[0] return proofs diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index d8915ed5..6533aac2 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -668,7 +668,7 @@ async def split( proofs = copy.copy(proofs) # potentially add witnesses to unlock provided proofs (if they indicate one) - proofs = await self.add_witnesses_to_proofs(proofs) + proofs = self.add_witnesses_to_proofs(proofs) input_fees = self.get_fees_for_proofs(proofs) logger.debug(f"Input fees: {input_fees}") @@ -700,7 +700,7 @@ async def split( outputs, rs = self._construct_outputs(amounts, secrets, rs, self.keyset_id) # potentially add witnesses to outputs based on what requirement the proofs indicate - outputs = await self.add_witnesses_to_outputs(proofs, outputs) + outputs = self.add_witnesses_to_outputs(proofs, outputs) # Call swap API promises = await super().split(proofs, outputs) diff --git a/tests/test_wallet_htlc.py b/tests/test_wallet_htlc.py index c1f75aaa..ce9b94b2 100644 --- a/tests/test_wallet_htlc.py +++ b/tests/test_wallet_htlc.py @@ -120,14 +120,14 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock( - preimage=preimage, hashlock_pubkey=pubkey_wallet1 + preimage=preimage, hashlock_pubkeys=[pubkey_wallet1] ) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) for p in send_proofs: p.witness = HTLCWitness(preimage=preimage).json() await assert_err( wallet2.redeem(send_proofs), - "Mint Error: HTLC no hash lock signatures provided.", + "Mint Error: no signatures in proof.", ) @@ -140,18 +140,18 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock( - preimage=preimage, hashlock_pubkey=pubkey_wallet1 + preimage=preimage, hashlock_pubkeys=[pubkey_wallet1] ) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) - signatures = await wallet1.sign_p2pk_proofs(send_proofs) + signatures = wallet1.sign_proofs(send_proofs) for p, s in zip(send_proofs, signatures): p.witness = HTLCWitness( - preimage=preimage, signature=f"{s[:-5]}11111" + preimage=preimage, signatures=[f"{s[:-5]}11111"] ).json() # wrong signature await assert_err( wallet2.redeem(send_proofs), - "Mint Error: HTLC hash lock signatures did not match.", + "Mint Error: no valid signature provided for input.", ) @@ -164,17 +164,187 @@ async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wall pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock( - preimage=preimage, hashlock_pubkey=pubkey_wallet1 + preimage=preimage, hashlock_pubkeys=[pubkey_wallet1] ) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) - signatures = await wallet1.sign_p2pk_proofs(send_proofs) + signatures = wallet1.sign_proofs(send_proofs) for p, s in zip(send_proofs, signatures): - p.witness = HTLCWitness(preimage=preimage, signature=s).json() + p.witness = HTLCWitness(preimage=preimage, signatures=[s]).json() await wallet2.redeem(send_proofs) +@pytest.mark.asyncio +async def test_htlc_redeem_with_2_of_1_signatures(wallet1: Wallet, wallet2: Wallet): + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id) + preimage = "00000000000000000000000000000000" + pubkey_wallet1 = await wallet1.create_p2pk_pubkey() + pubkey_wallet2 = await wallet2.create_p2pk_pubkey() + # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + secret = await wallet1.create_htlc_lock( + preimage=preimage, + hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2], + hashlock_n_sigs=1, + ) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) + + signatures1 = wallet1.sign_proofs(send_proofs) + signatures2 = wallet2.sign_proofs(send_proofs) + for p, s1, s2 in zip(send_proofs, signatures1, signatures2): + p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json() + + await wallet2.redeem(send_proofs) + + +@pytest.mark.asyncio +async def test_htlc_redeem_with_2_of_2_signatures(wallet1: Wallet, wallet2: Wallet): + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id) + preimage = "00000000000000000000000000000000" + pubkey_wallet1 = await wallet1.create_p2pk_pubkey() + pubkey_wallet2 = await wallet2.create_p2pk_pubkey() + # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + secret = await wallet1.create_htlc_lock( + preimage=preimage, + hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2], + hashlock_n_sigs=2, + ) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) + + signatures1 = wallet1.sign_proofs(send_proofs) + signatures2 = wallet2.sign_proofs(send_proofs) + for p, s1, s2 in zip(send_proofs, signatures1, signatures2): + p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json() + + await wallet2.redeem(send_proofs) + + +@pytest.mark.asyncio +async def test_htlc_redeem_with_2_of_2_signatures_with_duplicate_pubkeys( + wallet1: Wallet, wallet2: Wallet +): + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id) + preimage = "00000000000000000000000000000000" + pubkey_wallet1 = await wallet1.create_p2pk_pubkey() + pubkey_wallet2 = pubkey_wallet1 + # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + secret = await wallet1.create_htlc_lock( + preimage=preimage, + hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2], + hashlock_n_sigs=2, + ) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) + + signatures1 = wallet1.sign_proofs(send_proofs) + signatures2 = wallet2.sign_proofs(send_proofs) + for p, s1, s2 in zip(send_proofs, signatures1, signatures2): + p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json() + + await assert_err( + wallet2.redeem(send_proofs), + "Mint Error: pubkeys must be unique.", + ) + + +@pytest.mark.asyncio +async def test_htlc_redeem_with_3_of_3_signatures_but_only_2_provided( + wallet1: Wallet, wallet2: Wallet +): + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id) + preimage = "00000000000000000000000000000000" + pubkey_wallet1 = await wallet1.create_p2pk_pubkey() + pubkey_wallet2 = await wallet2.create_p2pk_pubkey() + # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + secret = await wallet1.create_htlc_lock( + preimage=preimage, + hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2], + hashlock_n_sigs=3, + ) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) + + signatures1 = wallet1.sign_proofs(send_proofs) + signatures2 = wallet2.sign_proofs(send_proofs) + for p, s1, s2 in zip(send_proofs, signatures1, signatures2): + p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json() + + await assert_err( + wallet2.redeem(send_proofs), + "Mint Error: not enough signatures provided: 2 < 3.", + ) + + +@pytest.mark.asyncio +async def test_htlc_redeem_with_2_of_3_signatures_with_2_valid_and_1_invalid_provided( + wallet1: Wallet, wallet2: Wallet +): + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id) + preimage = "00000000000000000000000000000000" + pubkey_wallet1 = await wallet1.create_p2pk_pubkey() + pubkey_wallet2 = await wallet2.create_p2pk_pubkey() + privatekey_wallet3 = PrivateKey(secrets.token_bytes(32), raw=True) + assert privatekey_wallet3.pubkey + pubkey_wallet3 = privatekey_wallet3.pubkey.serialize().hex() + + # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + secret = await wallet1.create_htlc_lock( + preimage=preimage, + hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3], + hashlock_n_sigs=2, + ) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) + + signatures1 = wallet1.sign_proofs(send_proofs) + signatures2 = wallet2.sign_proofs(send_proofs) + signatures3 = [f"{s[:-5]}11111" for s in signatures1] # wrong signature + for p, s1, s2, s3 in zip(send_proofs, signatures1, signatures2, signatures3): + p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2, s3]).json() + + await wallet2.redeem(send_proofs) + + +@pytest.mark.asyncio +async def test_htlc_redeem_with_3_of_3_signatures_with_2_valid_and_1_invalid_provided( + wallet1: Wallet, wallet2: Wallet +): + invoice = await wallet1.request_mint(64) + await pay_if_regtest(invoice.bolt11) + await wallet1.mint(64, id=invoice.id) + preimage = "00000000000000000000000000000000" + pubkey_wallet1 = await wallet1.create_p2pk_pubkey() + pubkey_wallet2 = await wallet2.create_p2pk_pubkey() + privatekey_wallet3 = PrivateKey(secrets.token_bytes(32), raw=True) + assert privatekey_wallet3.pubkey + pubkey_wallet3 = privatekey_wallet3.pubkey.serialize().hex() + + # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + secret = await wallet1.create_htlc_lock( + preimage=preimage, + hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3], + hashlock_n_sigs=3, + ) + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) + + signatures1 = wallet1.sign_proofs(send_proofs) + signatures2 = wallet2.sign_proofs(send_proofs) + signatures3 = [f"{s[:-5]}11111" for s in signatures1] # wrong signature + for p, s1, s2, s3 in zip(send_proofs, signatures1, signatures2, signatures3): + p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2, s3]).json() + + await assert_err( + wallet2.redeem(send_proofs), "Mint Error: signature threshold not met. 2 < 3." + ) + + @pytest.mark.asyncio async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( wallet1: Wallet, wallet2: Wallet @@ -188,20 +358,20 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock( preimage=preimage, - hashlock_pubkey=pubkey_wallet2, + hashlock_pubkeys=[pubkey_wallet2], locktime_seconds=2, - locktime_pubkey=pubkey_wallet1, + locktime_pubkeys=[pubkey_wallet1], ) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) - signatures = await wallet1.sign_p2pk_proofs(send_proofs) + signatures = wallet1.sign_proofs(send_proofs) for p, s in zip(send_proofs, signatures): - p.witness = HTLCWitness(preimage=preimage, signature=s).json() + p.witness = HTLCWitness(preimage=preimage, signatures=[s]).json() # should error because we used wallet2 signatures for the hash lock await assert_err( wallet1.redeem(send_proofs), - "Mint Error: HTLC hash lock signatures did not match.", + "Mint Error: no valid signature provided for input.", ) await asyncio.sleep(2) @@ -222,27 +392,27 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature( # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() secret = await wallet1.create_htlc_lock( preimage=preimage, - hashlock_pubkey=pubkey_wallet2, + hashlock_pubkeys=[pubkey_wallet2], locktime_seconds=2, - locktime_pubkey=pubkey_wallet1, + locktime_pubkeys=[pubkey_wallet1], ) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) - signatures = await wallet1.sign_p2pk_proofs(send_proofs) + signatures = wallet1.sign_proofs(send_proofs) for p, s in zip(send_proofs, signatures): p.witness = HTLCWitness( - preimage=preimage, signature=f"{s[:-5]}11111" + preimage=preimage, signatures=[f"{s[:-5]}11111"] ).json() # wrong signature # should error because we used wallet2 signatures for the hash lock await assert_err( wallet1.redeem(send_proofs), - "Mint Error: HTLC hash lock signatures did not match.", + "Mint Error: no valid signature provided for input.", ) await asyncio.sleep(2) # should fail since lock time has passed and we provided a wrong signature for timelock await assert_err( wallet1.redeem(send_proofs), - "Mint Error: HTLC refund signatures did not match.", + "Mint Error: no valid signature provided for input.", ) diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index d86e036f..3414f2a9 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -267,7 +267,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet): wallet1.proofs, 8, secret_lock=secret_lock ) # add signatures of wallet1 - send_proofs = await wallet1.add_p2pk_witnesses_to_proofs(send_proofs) + send_proofs = wallet1.add_signature_witnesses_to_proofs(send_proofs) # here we add the signatures of wallet2 await wallet2.redeem(send_proofs) @@ -289,10 +289,10 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle wallet1.proofs, 8, secret_lock=secret_lock ) # add signatures of wallet2 – this is a duplicate signature - send_proofs = await wallet2.add_p2pk_witnesses_to_proofs(send_proofs) + send_proofs = wallet2.add_signature_witnesses_to_proofs(send_proofs) # here we add the signatures of wallet2 await assert_err( - wallet2.redeem(send_proofs), "Mint Error: p2pk signatures must be unique." + wallet2.redeem(send_proofs), "Mint Error: signatures must be unique." ) @@ -334,7 +334,7 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal wallet1.proofs, 8, secret_lock=secret_lock ) # add signatures of wallet1 - send_proofs = await wallet1.add_p2pk_witnesses_to_proofs(send_proofs) + send_proofs = wallet1.add_signature_witnesses_to_proofs(send_proofs) # here we add the signatures of wallet2 await assert_err( wallet2.redeem(send_proofs), @@ -381,7 +381,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key( wallet1.proofs, 8, secret_lock=secret_lock ) # add signatures of wallet1 - send_proofs = await wallet1.add_p2pk_witnesses_to_proofs(send_proofs) + send_proofs = wallet1.add_signature_witnesses_to_proofs(send_proofs) await assert_err( wallet2.redeem(send_proofs), "Mint Error: signature threshold not met. 1 < 2." )