diff --git a/cashu/core/base.py b/cashu/core/base.py index 5cef1499..ad944130 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -9,7 +9,6 @@ from .crypto.keys import derive_keys, derive_keyset_id, derive_pubkeys from .crypto.secp import PrivateKey, PublicKey from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12 -from .p2pk import P2SHScript class DLEQ(BaseModel): @@ -34,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 @@ -46,10 +80,11 @@ class Proof(BaseModel): C: str = "" # signature on secret, unblinded by wallet dleq: Union[DLEQWallet, None] = None # DLEQ proof - p2pksigs: Union[List[str], None] = [] # P2PK signature - p2shscript: Union[P2SHScript, None] = None # P2SH spending condition - htlcpreimage: Union[str, None] = None # HTLC unlocking preimage - htlcsignature: Union[str, None] = None # HTLC unlocking signature + witness: Union[None, str] = "" # witness for spending condition + # p2pksigs: Union[List[str], None] = [] # P2PK signature + # p2shscript: Union[P2SHWitness, None] = None # P2SH spending condition + # 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 @@ -93,6 +128,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 @@ -106,7 +156,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): @@ -206,19 +261,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/core/p2pk.py b/cashu/core/p2pk.py index 1364649b..181c784e 100644 --- a/cashu/core/p2pk.py +++ b/cashu/core/p2pk.py @@ -3,7 +3,6 @@ from typing import List, Union from loguru import logger -from pydantic import BaseModel from .crypto.secp import PrivateKey, PublicKey from .secret import Secret, SecretKind @@ -64,16 +63,6 @@ def n_sigs(self) -> Union[None, int]: return int(n_sigs) if n_sigs else None -class P2SHScript(BaseModel): - """ - Unlocks P2SH spending condition of a Proof - """ - - script: str - signature: str - address: Union[str, None] = None - - def sign_p2pk_sign(message: bytes, private_key: PrivateKey): # ecdsa version # signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message)) diff --git a/cashu/mint/conditions.py b/cashu/mint/conditions.py index 98843950..aaf5dd7c 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, @@ -149,14 +146,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 @@ -176,14 +175,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 625a78af..9b2312fc 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, P2SHScript +from ...core.base import Invoice, P2SHWitness class PayResponse(BaseModel): @@ -54,7 +54,7 @@ class LockResponse(BaseModel): class LocksResponse(BaseModel): - locks: List[P2SHScript] + locks: List[P2SHWitness] class InvoicesResponse(BaseModel): diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index fcdfab1a..375e62a3 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -2,7 +2,7 @@ import time from typing import Any, List, Optional, Tuple -from ..core.base import Invoice, P2SHScript, Proof, WalletKeyset +from ..core.base import Invoice, P2SHWitness, Proof, WalletKeyset from ..core.db import Connection, Database @@ -123,7 +123,7 @@ async def secret_used( async def store_p2sh( - p2sh: P2SHScript, + p2sh: P2SHWitness, db: Database, conn: Optional[Connection] = None, ) -> None: @@ -146,7 +146,7 @@ async def get_unused_locks( address: str = "", db: Optional[Database] = None, conn: Optional[Connection] = None, -) -> List[P2SHScript]: +) -> List[P2SHWitness]: clause: List[str] = [] args: List[str] = [] @@ -167,11 +167,11 @@ async def get_unused_locks( """, tuple(args), ) - return [P2SHScript(**r) for r in rows] + return [P2SHWitness(**r) for r in rows] async def update_p2sh_used( - p2sh: P2SHScript, + p2sh: P2SHWitness, used: bool, db: Optional[Database] = None, conn: Optional[Connection] = None, 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 79d929a6..21d7e593 100644 --- a/cashu/wallet/p2pk.py +++ b/cashu/wallet/p2pk.py @@ -7,13 +7,14 @@ from ..core import bolt11 as bolt11 from ..core.base import ( BlindedMessage, + P2PKWitness, + P2SHWitness, Proof, ) from ..core.crypto.secp import PrivateKey from ..core.db import Database from ..core.p2pk import ( P2PKSecret, - P2SHScript, SigFlags, sign_p2pk_sign, ) @@ -44,7 +45,7 @@ async def create_p2sh_address_and_store(self) -> str: txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() txin_signature_b64 = base64.urlsafe_b64encode(txin_signature).decode() - p2shScript = P2SHScript( + p2shScript = P2SHWitness( script=txin_redeemScript_b64, signature=txin_signature_b64, address=str(txin_p2sh_address), @@ -154,7 +155,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( @@ -201,7 +202,7 @@ async def add_p2sh_witnesses_to_proofs( # attach unlock scripts to proofs for p in proofs: - p.p2shscript = P2SHScript(script=p2sh_script, signature=p2sh_signature) + p.witness = P2SHWitness(script=p2sh_script, signature=p2sh_signature).json() return proofs async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: @@ -211,10 +212,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 c761e507..61966eb5 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -400,10 +400,7 @@ def _splitrequest_include_fields(proofs: List[Proof]): "amount", "secret", "C", - "p2shscript", - "p2pksigs", - "htlcpreimage", - "htlcsignature", + "witness", } return { "outputs": ..., @@ -472,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)