Skip to content

Commit

Permalink
Merge branch 'main' into remove_p2sh_bitcoin_script
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Oct 13, 2023
2 parents c288524 + d827579 commit 1a2d0b1
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 53 deletions.
74 changes: 57 additions & 17 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
17 changes: 9 additions & 8 deletions cashu/mint/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions cashu/wallet/api/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pydantic import BaseModel

from ...core.base import Invoice
from ...core.base import Invoice, P2SHWitness


class PayResponse(BaseModel):
Expand Down Expand Up @@ -54,7 +54,7 @@ class LockResponse(BaseModel):


class LocksResponse(BaseModel):
locks: List[str]
locks: List[P2SHWitness]


class InvoicesResponse(BaseModel):
Expand Down
8 changes: 3 additions & 5 deletions cashu/wallet/htlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
11 changes: 7 additions & 4 deletions cashu/wallet/p2pk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ..core import bolt11 as bolt11
from ..core.base import (
BlindedMessage,
P2PKWitness,
Proof,
)
from ..core.crypto.secp import PrivateKey
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]:
Expand Down
6 changes: 2 additions & 4 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,7 @@ def _splitrequest_include_fields(proofs: List[Proof]):
"amount",
"secret",
"C",
"p2pksigs",
"htlcpreimage",
"htlcsignature",
"witness",
}
return {
"outputs": ...,
Expand Down Expand Up @@ -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": ...,
Expand Down
28 changes: 15 additions & 13 deletions tests/test_wallet_htlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand All @@ -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"
)
Expand All @@ -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.",
Expand All @@ -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),
Expand All @@ -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)

Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions tests/test_wallet_p2pk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 1a2d0b1

Please sign in to comment.