Skip to content

Commit

Permalink
Coalesce all witness fields to Proof.witness (#342)
Browse files Browse the repository at this point in the history
* call proofs field witness

* test p2pk sig_all=True

* outputs also use witness field
  • Loading branch information
callebtc authored Oct 13, 2023
1 parent c3b3a45 commit d827579
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 75 deletions.
80 changes: 61 additions & 19 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
11 changes: 0 additions & 11 deletions cashu/core/p2pk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
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 @@ -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
Expand All @@ -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
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, P2SHScript
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[P2SHScript]
locks: List[P2SHWitness]


class InvoicesResponse(BaseModel):
Expand Down
10 changes: 5 additions & 5 deletions cashu/wallet/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -123,7 +123,7 @@ async def secret_used(


async def store_p2sh(
p2sh: P2SHScript,
p2sh: P2SHWitness,
db: Database,
conn: Optional[Connection] = None,
) -> None:
Expand All @@ -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] = []

Expand All @@ -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,
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
17 changes: 10 additions & 7 deletions cashu/wallet/p2pk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]:
Expand All @@ -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]:
Expand Down
7 changes: 2 additions & 5 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,10 +400,7 @@ def _splitrequest_include_fields(proofs: List[Proof]):
"amount",
"secret",
"C",
"p2shscript",
"p2pksigs",
"htlcpreimage",
"htlcsignature",
"witness",
}
return {
"outputs": ...,
Expand Down Expand Up @@ -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": ...,
Expand Down
Loading

0 comments on commit d827579

Please sign in to comment.