diff --git a/cashu/core/base.py b/cashu/core/base.py index b23fad64..f2cbeb7a 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -33,6 +33,41 @@ class DLEQWallet(BaseModel): # ------- PROOFS ------- +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 + + @classmethod + def from_witness(cls, witness: str): + return cls(**json.loads(witness)) + + +class P2PKWitness(BaseModel): + """ + Unlocks P2PK spending condition of a Proof + """ + + signatures: List[str] + + @classmethod + def from_witness(cls, witness: str): + return cls(**json.loads(witness)) + + class Proof(BaseModel): """ Value token @@ -44,10 +79,8 @@ class Proof(BaseModel): secret: str = "" # secret or message to be blinded and signed C: str = "" # signature on secret, unblinded by wallet dleq: Union[DLEQWallet, None] = None # DLEQ proof + witness: Union[None, str] = "" # witness for spending condition - p2pksigs: Union[List[str], None] = [] # P2PK signature - htlcpreimage: Union[str, None] = None # HTLC unlocking preimage - htlcsignature: Union[str, None] = None # HTLC unlocking signature # whether this proof is reserved for sending, used for coin management in the wallet reserved: Union[None, bool] = False # unique ID of send attempt, used for grouping pending tokens in the wallet @@ -91,6 +124,21 @@ def __getitem__(self, key): def __setitem__(self, key, val): self.__setattr__(key, val) + @property + def p2pksigs(self) -> List[str]: + assert self.witness, "Witness is missing" + return P2PKWitness.from_witness(self.witness).signatures + + @property + def p2shscript(self) -> P2SHWitness: + assert self.witness, "Witness is missing" + return P2SHWitness.from_witness(self.witness) + + @property + def htlcpreimage(self) -> Union[str, None]: + assert self.witness, "Witness is missing" + return HTLCWitness.from_witness(self.witness).preimage + class Proofs(BaseModel): # NOTE: not used in Pydantic validation @@ -104,7 +152,12 @@ class BlindedMessage(BaseModel): amount: int B_: str # Hex-encoded blinded message - p2pksigs: Union[List[str], None] = None # signature for p2pk with SIG_ALL + witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL) + + @property + def p2pksigs(self) -> List[str]: + assert self.witness, "Witness is missing" + return P2PKWitness.from_witness(self.witness).signatures class BlindedSignature(BaseModel): @@ -204,19 +257,6 @@ class PostSplitRequest(BaseModel): proofs: List[Proof] amount: Optional[int] = None # deprecated since 0.13.0 outputs: List[BlindedMessage] - # signature: Optional[str] = None - - # def sign(self, private_key: PrivateKey): - # """ - # Create a signed split request. The signature is over the `proofs` and `outputs` fields. - # """ - # # message = json.dumps(self.proofs).encode("utf-8") + json.dumps( - # # self.outputs - # # ).encode("utf-8") - # message = json.dumps(self.dict(include={"proofs": ..., "outputs": ...})).encode( - # "utf-8" - # ) - # self.signature = sign_p2pk_sign(message, private_key) class PostSplitResponse(BaseModel): diff --git a/cashu/mint/conditions.py b/cashu/mint/conditions.py index f8a13856..312b1e1f 100644 --- a/cashu/mint/conditions.py +++ b/cashu/mint/conditions.py @@ -4,10 +4,7 @@ from loguru import logger -from ..core.base import ( - BlindedMessage, - Proof, -) +from ..core.base import BlindedMessage, HTLCWitness, Proof from ..core.crypto.secp import PublicKey from ..core.errors import ( TransactionError, @@ -118,14 +115,16 @@ def _verify_input_spending_conditions(self, proof: Proof) -> 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.htlcsignature, TransactionError( + 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=htlc_secret.serialize().encode("utf-8"), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), - signature=bytes.fromhex(proof.htlcsignature), + signature=bytes.fromhex(signature), ): # a signature matches return True @@ -145,14 +144,16 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: # then we check whether a signature is required hashlock_pubkeys = htlc_secret.tags.get_tag_all("pubkeys") if hashlock_pubkeys: - assert proof.htlcsignature, TransactionError( + 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( message=htlc_secret.serialize().encode("utf-8"), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), - signature=bytes.fromhex(proof.htlcsignature), + signature=bytes.fromhex(signature), ): # a signature matches return True diff --git a/cashu/wallet/api/responses.py b/cashu/wallet/api/responses.py index b77d198d..cbdf7679 100644 --- a/cashu/wallet/api/responses.py +++ b/cashu/wallet/api/responses.py @@ -2,7 +2,7 @@ from pydantic import BaseModel -from ...core.base import Invoice +from ...core.base import Invoice, P2SHWitness class PayResponse(BaseModel): @@ -54,7 +54,7 @@ class LockResponse(BaseModel): class LocksResponse(BaseModel): - locks: List[str] + locks: List[P2SHWitness] class InvoicesResponse(BaseModel): diff --git a/cashu/wallet/htlc.py b/cashu/wallet/htlc.py index dae24571..a9e6e87c 100644 --- a/cashu/wallet/htlc.py +++ b/cashu/wallet/htlc.py @@ -3,9 +3,7 @@ from typing import List, Optional from ..core import bolt11 as bolt11 -from ..core.base import ( - Proof, -) +from ..core.base import HTLCWitness, Proof from ..core.db import Database from ..core.htlc import ( HTLCSecret, @@ -51,6 +49,6 @@ async def create_htlc_lock( async def add_htlc_preimage_to_proofs( self, proofs: List[Proof], preimage: str ) -> List[Proof]: - for p, s in zip(proofs, preimage): - p.htlcpreimage = s + for p in proofs: + p.witness = HTLCWitness(preimage=preimage).json() return proofs diff --git a/cashu/wallet/p2pk.py b/cashu/wallet/p2pk.py index 7d4a77a8..2844bc89 100644 --- a/cashu/wallet/p2pk.py +++ b/cashu/wallet/p2pk.py @@ -6,6 +6,7 @@ from ..core import bolt11 as bolt11 from ..core.base import ( BlindedMessage, + P2PKWitness, Proof, ) from ..core.crypto.secp import PrivateKey @@ -108,7 +109,7 @@ async def add_p2pk_witnesses_to_outputs( """ p2pk_signatures = await self.sign_p2pk_outputs(outputs) for o, s in zip(outputs, p2pk_signatures): - o.p2pksigs = [s] + o.witness = P2PKWitness(signatures=[s]).json() return outputs async def add_witnesses_to_outputs( @@ -147,10 +148,12 @@ async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof] # attach unlock signatures to proofs assert len(proofs) == len(p2pk_signatures), "wrong number of signatures" for p, s in zip(proofs, p2pk_signatures): - if p.p2pksigs: - p.p2pksigs.append(s) + # if there are already signatures, append + if p.witness and P2PKWitness.from_witness(p.witness).signatures: + signatures = P2PKWitness.from_witness(p.witness).signatures + p.witness = P2PKWitness(signatures=signatures + [s]).json() else: - p.p2pksigs = [s] + p.witness = P2PKWitness(signatures=[s]).json() return proofs async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index e6538400..d75fa8dd 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -400,9 +400,7 @@ def _splitrequest_include_fields(proofs: List[Proof]): "amount", "secret", "C", - "p2pksigs", - "htlcpreimage", - "htlcsignature", + "witness", } return { "outputs": ..., @@ -471,7 +469,7 @@ async def pay_lightning( def _meltrequest_include_fields(proofs: List[Proof]): """strips away fields from the model that aren't necessary for the /melt""" - proofs_include = {"id", "amount", "secret", "C", "script"} + proofs_include = {"id", "amount", "secret", "C", "witness"} return { "proofs": {i: proofs_include for i in range(len(proofs))}, "pr": ..., diff --git a/tests/test_wallet_htlc.py b/tests/test_wallet_htlc.py index 6171aa56..f518b235 100644 --- a/tests/test_wallet_htlc.py +++ b/tests/test_wallet_htlc.py @@ -6,7 +6,7 @@ import pytest import pytest_asyncio -from cashu.core.base import Proof +from cashu.core.base import HTLCWitness, Proof from cashu.core.crypto.secp import PrivateKey from cashu.core.htlc import HTLCSecret from cashu.core.migrations import migrate_databases @@ -89,7 +89,7 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet): # p2pk test _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) for p in send_proofs: - p.htlcpreimage = preimage + p.witness = HTLCWitness(preimage=preimage).json() await wallet2.redeem(send_proofs) @@ -99,11 +99,13 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet) await wallet1.mint(64, hash=invoice.hash) preimage = "00000000000000000000000000000000" # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() - secret = await wallet1.create_htlc_lock(preimage=preimage[:-5] + "11111") + secret = await wallet1.create_htlc_lock( + preimage=preimage[:-5] + "11111" + ) # wrong preimage # p2pk test _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) for p in send_proofs: - p.htlcpreimage = preimage + p.witness = HTLCWitness(preimage=preimage).json() await assert_err( wallet2.redeem(send_proofs), "Mint Error: HTLC preimage does not match" ) @@ -122,7 +124,7 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet): # p2pk test _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) for p in send_proofs: - p.htlcpreimage = preimage + p.witness = HTLCWitness(preimage=preimage).json() await assert_err( wallet2.redeem(send_proofs), "Mint Error: HTLC no hash lock signatures provided.", @@ -144,8 +146,9 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) signatures = await wallet1.sign_p2pk_proofs(send_proofs) for p, s in zip(send_proofs, signatures): - p.htlcpreimage = preimage - p.htlcsignature = s[:-5] + "11111" # wrong signature + p.witness = HTLCWitness( + preimage=preimage, signature=s[:-5] + "11111" + ).json() # wrong signature await assert_err( wallet2.redeem(send_proofs), @@ -168,8 +171,7 @@ async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wall signatures = await wallet1.sign_p2pk_proofs(send_proofs) for p, s in zip(send_proofs, signatures): - p.htlcpreimage = preimage - p.htlcsignature = s + p.witness = HTLCWitness(preimage=preimage, signature=s).json() await wallet2.redeem(send_proofs) @@ -195,8 +197,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( signatures = await wallet1.sign_p2pk_proofs(send_proofs) for p, s in zip(send_proofs, signatures): - p.htlcpreimage = preimage - p.htlcsignature = s + p.witness = HTLCWitness(preimage=preimage, signature=s).json() # should error because we used wallet2 signatures for the hash lock await assert_err( @@ -230,8 +231,9 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature( signatures = await wallet1.sign_p2pk_proofs(send_proofs) for p, s in zip(send_proofs, signatures): - p.htlcpreimage = preimage - p.htlcsignature = s[:-5] + "11111" # wrong signature + p.witness = HTLCWitness( + preimage=preimage, signature=s[:-5] + "11111" + ).json() # wrong signature # should error because we used wallet2 signatures for the hash lock await assert_err( diff --git a/tests/test_wallet_p2pk.py b/tests/test_wallet_p2pk.py index 8985f0ad..10dd7750 100644 --- a/tests/test_wallet_p2pk.py +++ b/tests/test_wallet_p2pk.py @@ -78,6 +78,21 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet): await wallet2.redeem(send_proofs) +@pytest.mark.asyncio +async def test_p2pk_sig_all(wallet1: Wallet, wallet2: Wallet): + invoice = await wallet1.request_mint(64) + await wallet1.mint(64, hash=invoice.hash) + pubkey_wallet2 = await wallet2.create_p2pk_pubkey() + # p2pk test + secret_lock = await wallet1.create_p2pk_lock( + pubkey_wallet2, sig_all=True + ) # sender side + _, send_proofs = await wallet1.split_to_send( + wallet1.proofs, 8, secret_lock=secret_lock + ) + await wallet2.redeem(send_proofs) + + @pytest.mark.asyncio async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet): invoice = await wallet1.request_mint(64)