mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
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:
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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": ...,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user