Coalesce all witness fields to Proof.witness (#342)

* call proofs field witness

* test p2pk sig_all=True

* outputs also use witness field
This commit is contained in:
callebtc
2023-10-13 21:33:21 +02:00
committed by GitHub
parent c3b3a45436
commit d827579e65
10 changed files with 122 additions and 75 deletions

View File

@@ -9,7 +9,6 @@ from pydantic import BaseModel
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 @@ class Proof(BaseModel):
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):

View File

@@ -3,7 +3,6 @@ import time
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 @@ class P2PKSecret(Secret):
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))

View File

@@ -4,10 +4,7 @@ from typing import List
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 @@ class LedgerSpendingConditions:
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 @@ class LedgerSpendingConditions:
# 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

View File

@@ -2,7 +2,7 @@ from typing import Dict, List, Optional
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):

View File

@@ -2,7 +2,7 @@ import json
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,

View File

@@ -3,9 +3,7 @@ from datetime import datetime, timedelta
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 @@ class WalletHTLC(SupportsDb):
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

View File

@@ -7,13 +7,14 @@ from loguru import logger
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 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
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 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
"""
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 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
# 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 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
# 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]:

View File

@@ -400,10 +400,7 @@ class LedgerAPI(object):
"amount",
"secret",
"C",
"p2shscript",
"p2pksigs",
"htlcpreimage",
"htlcsignature",
"witness",
}
return {
"outputs": ...,
@@ -472,7 +469,7 @@ class LedgerAPI(object):
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": ...,

View File

@@ -6,7 +6,7 @@ from typing import List
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(

View File

@@ -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)