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.keys import derive_keys, derive_keyset_id, derive_pubkeys
from .crypto.secp import PrivateKey, PublicKey from .crypto.secp import PrivateKey, PublicKey
from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12 from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12
from .p2pk import P2SHScript
class DLEQ(BaseModel): class DLEQ(BaseModel):
@@ -34,6 +33,41 @@ class DLEQWallet(BaseModel):
# ------- PROOFS ------- # ------- 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): class Proof(BaseModel):
""" """
Value token Value token
@@ -46,10 +80,11 @@ class Proof(BaseModel):
C: str = "" # signature on secret, unblinded by wallet C: str = "" # signature on secret, unblinded by wallet
dleq: Union[DLEQWallet, None] = None # DLEQ proof dleq: Union[DLEQWallet, None] = None # DLEQ proof
p2pksigs: Union[List[str], None] = [] # P2PK signature witness: Union[None, str] = "" # witness for spending condition
p2shscript: Union[P2SHScript, None] = None # P2SH spending condition # p2pksigs: Union[List[str], None] = [] # P2PK signature
htlcpreimage: Union[str, None] = None # HTLC unlocking preimage # p2shscript: Union[P2SHWitness, None] = None # P2SH spending condition
htlcsignature: Union[str, None] = None # HTLC unlocking 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 # whether this proof is reserved for sending, used for coin management in the wallet
reserved: Union[None, bool] = False reserved: Union[None, bool] = False
# unique ID of send attempt, used for grouping pending tokens in the wallet # 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): def __setitem__(self, key, val):
self.__setattr__(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): class Proofs(BaseModel):
# NOTE: not used in Pydantic validation # NOTE: not used in Pydantic validation
@@ -106,7 +156,12 @@ class BlindedMessage(BaseModel):
amount: int amount: int
B_: str # Hex-encoded blinded message 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): class BlindedSignature(BaseModel):
@@ -206,19 +261,6 @@ class PostSplitRequest(BaseModel):
proofs: List[Proof] proofs: List[Proof]
amount: Optional[int] = None # deprecated since 0.13.0 amount: Optional[int] = None # deprecated since 0.13.0
outputs: List[BlindedMessage] 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): class PostSplitResponse(BaseModel):

View File

@@ -3,7 +3,6 @@ import time
from typing import List, Union from typing import List, Union
from loguru import logger from loguru import logger
from pydantic import BaseModel
from .crypto.secp import PrivateKey, PublicKey from .crypto.secp import PrivateKey, PublicKey
from .secret import Secret, SecretKind from .secret import Secret, SecretKind
@@ -64,16 +63,6 @@ class P2PKSecret(Secret):
return int(n_sigs) if n_sigs else None 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): def sign_p2pk_sign(message: bytes, private_key: PrivateKey):
# ecdsa version # ecdsa version
# signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message)) # 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 loguru import logger
from ..core.base import ( from ..core.base import BlindedMessage, HTLCWitness, Proof
BlindedMessage,
Proof,
)
from ..core.crypto.secp import PublicKey from ..core.crypto.secp import PublicKey
from ..core.errors import ( from ..core.errors import (
TransactionError, TransactionError,
@@ -149,14 +146,16 @@ class LedgerSpendingConditions:
if htlc_secret.locktime and htlc_secret.locktime < time.time(): if htlc_secret.locktime and htlc_secret.locktime < time.time():
refund_pubkeys = htlc_secret.tags.get_tag_all("refund") refund_pubkeys = htlc_secret.tags.get_tag_all("refund")
if refund_pubkeys: 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" "no HTLC refund signature provided"
) )
for pubkey in refund_pubkeys: for pubkey in refund_pubkeys:
if verify_p2pk_signature( if verify_p2pk_signature(
message=htlc_secret.serialize().encode("utf-8"), message=htlc_secret.serialize().encode("utf-8"),
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
signature=bytes.fromhex(proof.htlcsignature), signature=bytes.fromhex(signature),
): ):
# a signature matches # a signature matches
return True return True
@@ -176,14 +175,16 @@ class LedgerSpendingConditions:
# then we check whether a signature is required # then we check whether a signature is required
hashlock_pubkeys = htlc_secret.tags.get_tag_all("pubkeys") hashlock_pubkeys = htlc_secret.tags.get_tag_all("pubkeys")
if hashlock_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." "HTLC no hash lock signatures provided."
) )
for pubkey in hashlock_pubkeys: for pubkey in hashlock_pubkeys:
if verify_p2pk_signature( if verify_p2pk_signature(
message=htlc_secret.serialize().encode("utf-8"), message=htlc_secret.serialize().encode("utf-8"),
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
signature=bytes.fromhex(proof.htlcsignature), signature=bytes.fromhex(signature),
): ):
# a signature matches # a signature matches
return True return True

View File

@@ -2,7 +2,7 @@ from typing import Dict, List, Optional
from pydantic import BaseModel from pydantic import BaseModel
from ...core.base import Invoice, P2SHScript from ...core.base import Invoice, P2SHWitness
class PayResponse(BaseModel): class PayResponse(BaseModel):
@@ -54,7 +54,7 @@ class LockResponse(BaseModel):
class LocksResponse(BaseModel): class LocksResponse(BaseModel):
locks: List[P2SHScript] locks: List[P2SHWitness]
class InvoicesResponse(BaseModel): class InvoicesResponse(BaseModel):

View File

@@ -2,7 +2,7 @@ import json
import time import time
from typing import Any, List, Optional, Tuple 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 from ..core.db import Connection, Database
@@ -123,7 +123,7 @@ async def secret_used(
async def store_p2sh( async def store_p2sh(
p2sh: P2SHScript, p2sh: P2SHWitness,
db: Database, db: Database,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
@@ -146,7 +146,7 @@ async def get_unused_locks(
address: str = "", address: str = "",
db: Optional[Database] = None, db: Optional[Database] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[P2SHScript]: ) -> List[P2SHWitness]:
clause: List[str] = [] clause: List[str] = []
args: List[str] = [] args: List[str] = []
@@ -167,11 +167,11 @@ async def get_unused_locks(
""", """,
tuple(args), tuple(args),
) )
return [P2SHScript(**r) for r in rows] return [P2SHWitness(**r) for r in rows]
async def update_p2sh_used( async def update_p2sh_used(
p2sh: P2SHScript, p2sh: P2SHWitness,
used: bool, used: bool,
db: Optional[Database] = None, db: Optional[Database] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,

View File

@@ -3,9 +3,7 @@ from datetime import datetime, timedelta
from typing import List, Optional from typing import List, Optional
from ..core import bolt11 as bolt11 from ..core import bolt11 as bolt11
from ..core.base import ( from ..core.base import HTLCWitness, Proof
Proof,
)
from ..core.db import Database from ..core.db import Database
from ..core.htlc import ( from ..core.htlc import (
HTLCSecret, HTLCSecret,
@@ -51,6 +49,6 @@ class WalletHTLC(SupportsDb):
async def add_htlc_preimage_to_proofs( async def add_htlc_preimage_to_proofs(
self, proofs: List[Proof], preimage: str self, proofs: List[Proof], preimage: str
) -> List[Proof]: ) -> List[Proof]:
for p, s in zip(proofs, preimage): for p in proofs:
p.htlcpreimage = s p.witness = HTLCWitness(preimage=preimage).json()
return proofs return proofs

View File

@@ -7,13 +7,14 @@ from loguru import logger
from ..core import bolt11 as bolt11 from ..core import bolt11 as bolt11
from ..core.base import ( from ..core.base import (
BlindedMessage, BlindedMessage,
P2PKWitness,
P2SHWitness,
Proof, Proof,
) )
from ..core.crypto.secp import PrivateKey from ..core.crypto.secp import PrivateKey
from ..core.db import Database from ..core.db import Database
from ..core.p2pk import ( from ..core.p2pk import (
P2PKSecret, P2PKSecret,
P2SHScript,
SigFlags, SigFlags,
sign_p2pk_sign, sign_p2pk_sign,
) )
@@ -44,7 +45,7 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig
txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode()
txin_signature_b64 = base64.urlsafe_b64encode(txin_signature).decode() txin_signature_b64 = base64.urlsafe_b64encode(txin_signature).decode()
p2shScript = P2SHScript( p2shScript = P2SHWitness(
script=txin_redeemScript_b64, script=txin_redeemScript_b64,
signature=txin_signature_b64, signature=txin_signature_b64,
address=str(txin_p2sh_address), address=str(txin_p2sh_address),
@@ -154,7 +155,7 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
""" """
p2pk_signatures = await self.sign_p2pk_outputs(outputs) p2pk_signatures = await self.sign_p2pk_outputs(outputs)
for o, s in zip(outputs, p2pk_signatures): for o, s in zip(outputs, p2pk_signatures):
o.p2pksigs = [s] o.witness = P2PKWitness(signatures=[s]).json()
return outputs return outputs
async def add_witnesses_to_outputs( async def add_witnesses_to_outputs(
@@ -201,7 +202,7 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
# attach unlock scripts to proofs # attach unlock scripts to proofs
for p in 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 return proofs
async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: 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 # attach unlock signatures to proofs
assert len(proofs) == len(p2pk_signatures), "wrong number of signatures" assert len(proofs) == len(p2pk_signatures), "wrong number of signatures"
for p, s in zip(proofs, p2pk_signatures): for p, s in zip(proofs, p2pk_signatures):
if p.p2pksigs: # if there are already signatures, append
p.p2pksigs.append(s) 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: else:
p.p2pksigs = [s] p.witness = P2PKWitness(signatures=[s]).json()
return proofs return proofs
async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]:

View File

@@ -400,10 +400,7 @@ class LedgerAPI(object):
"amount", "amount",
"secret", "secret",
"C", "C",
"p2shscript", "witness",
"p2pksigs",
"htlcpreimage",
"htlcsignature",
} }
return { return {
"outputs": ..., "outputs": ...,
@@ -472,7 +469,7 @@ class LedgerAPI(object):
def _meltrequest_include_fields(proofs: List[Proof]): def _meltrequest_include_fields(proofs: List[Proof]):
"""strips away fields from the model that aren't necessary for the /melt""" """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 { return {
"proofs": {i: proofs_include for i in range(len(proofs))}, "proofs": {i: proofs_include for i in range(len(proofs))},
"pr": ..., "pr": ...,

View File

@@ -6,7 +6,7 @@ from typing import List
import pytest import pytest
import pytest_asyncio 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.crypto.secp import PrivateKey
from cashu.core.htlc import HTLCSecret from cashu.core.htlc import HTLCSecret
from cashu.core.migrations import migrate_databases from cashu.core.migrations import migrate_databases
@@ -89,7 +89,7 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet):
# p2pk test # p2pk test
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
for p in send_proofs: for p in send_proofs:
p.htlcpreimage = preimage p.witness = HTLCWitness(preimage=preimage).json()
await wallet2.redeem(send_proofs) 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) await wallet1.mint(64, hash=invoice.hash)
preimage = "00000000000000000000000000000000" preimage = "00000000000000000000000000000000"
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # 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 # p2pk test
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
for p in send_proofs: for p in send_proofs:
p.htlcpreimage = preimage p.witness = HTLCWitness(preimage=preimage).json()
await assert_err( await assert_err(
wallet2.redeem(send_proofs), "Mint Error: HTLC preimage does not match" 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 # p2pk test
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
for p in send_proofs: for p in send_proofs:
p.htlcpreimage = preimage p.witness = HTLCWitness(preimage=preimage).json()
await assert_err( await assert_err(
wallet2.redeem(send_proofs), wallet2.redeem(send_proofs),
"Mint Error: HTLC no hash lock signatures provided.", "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) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures = await wallet1.sign_p2pk_proofs(send_proofs) signatures = await wallet1.sign_p2pk_proofs(send_proofs)
for p, s in zip(send_proofs, signatures): for p, s in zip(send_proofs, signatures):
p.htlcpreimage = preimage p.witness = HTLCWitness(
p.htlcsignature = s[:-5] + "11111" # wrong signature preimage=preimage, signature=s[:-5] + "11111"
).json() # wrong signature
await assert_err( await assert_err(
wallet2.redeem(send_proofs), 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) signatures = await wallet1.sign_p2pk_proofs(send_proofs)
for p, s in zip(send_proofs, signatures): for p, s in zip(send_proofs, signatures):
p.htlcpreimage = preimage p.witness = HTLCWitness(preimage=preimage, signature=s).json()
p.htlcsignature = s
await wallet2.redeem(send_proofs) 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) signatures = await wallet1.sign_p2pk_proofs(send_proofs)
for p, s in zip(send_proofs, signatures): for p, s in zip(send_proofs, signatures):
p.htlcpreimage = preimage p.witness = HTLCWitness(preimage=preimage, signature=s).json()
p.htlcsignature = s
# should error because we used wallet2 signatures for the hash lock # should error because we used wallet2 signatures for the hash lock
await assert_err( 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) signatures = await wallet1.sign_p2pk_proofs(send_proofs)
for p, s in zip(send_proofs, signatures): for p, s in zip(send_proofs, signatures):
p.htlcpreimage = preimage p.witness = HTLCWitness(
p.htlcsignature = s[:-5] + "11111" # wrong signature preimage=preimage, signature=s[:-5] + "11111"
).json() # wrong signature
# should error because we used wallet2 signatures for the hash lock # should error because we used wallet2 signatures for the hash lock
await assert_err( await assert_err(

View File

@@ -78,6 +78,21 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
await wallet2.redeem(send_proofs) 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 @pytest.mark.asyncio
async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64) invoice = await wallet1.request_mint(64)