Refactor conditions and fix HTLC multisig (#643)

* refactor conditions and fix htlc multisig

* restore db/write.py

* safer check for P2PK secrets for SIG_ALL

* comment cleanup
This commit is contained in:
callebtc
2024-10-22 13:02:45 +01:00
committed by GitHub
parent d12a8d1bde
commit 09d007ec88
9 changed files with 349 additions and 177 deletions

View File

@@ -95,21 +95,7 @@ class ProofState(LedgerEvent):
class HTLCWitness(BaseModel): class HTLCWitness(BaseModel):
preimage: Optional[str] = None preimage: Optional[str] = None
signature: Optional[str] = None signatures: Optional[List[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 @classmethod
def from_witness(cls, witness: str): def from_witness(cls, witness: str):
@@ -206,10 +192,15 @@ class Proof(BaseModel):
return P2PKWitness.from_witness(self.witness).signatures return P2PKWitness.from_witness(self.witness).signatures
@property @property
def htlcpreimage(self) -> Union[str, None]: def htlcpreimage(self) -> str | None:
assert self.witness, "Witness is missing for htlc preimage" assert self.witness, "Witness is missing for htlc preimage"
return HTLCWitness.from_witness(self.witness).preimage return HTLCWitness.from_witness(self.witness).preimage
@property
def htlcsigs(self) -> List[str] | None:
assert self.witness, "Witness is missing for htlc signatures"
return HTLCWitness.from_witness(self.witness).signatures
class Proofs(BaseModel): class Proofs(BaseModel):
# NOTE: not used in Pydantic validation # NOTE: not used in Pydantic validation
@@ -647,6 +638,7 @@ class WalletKeyset:
int(amount): PublicKey(bytes.fromhex(hex_key), raw=True) int(amount): PublicKey(bytes.fromhex(hex_key), raw=True)
for amount, hex_key in dict(json.loads(serialized)).items() for amount, hex_key in dict(json.loads(serialized)).items()
} }
return cls( return cls(
id=row["id"], id=row["id"],
unit=row["unit"], unit=row["unit"],

View File

@@ -1,8 +1,16 @@
from enum import Enum
from typing import Union from typing import Union
from .secret import Secret, SecretKind from .secret import Secret, SecretKind
class SigFlags(Enum):
# require signatures only on the inputs (default signature flag)
SIG_INPUTS = "SIG_INPUTS"
# require signatures on inputs and outputs
SIG_ALL = "SIG_ALL"
class HTLCSecret(Secret): class HTLCSecret(Secret):
@classmethod @classmethod
def from_secret(cls, secret: Secret): def from_secret(cls, secret: Secret):
@@ -15,3 +23,13 @@ class HTLCSecret(Secret):
def locktime(self) -> Union[None, int]: def locktime(self) -> Union[None, int]:
locktime = self.tags.get_tag("locktime") locktime = self.tags.get_tag("locktime")
return int(locktime) if locktime else None return int(locktime) if locktime else None
@property
def sigflag(self) -> Union[None, SigFlags]:
sigflag = self.tags.get_tag("sigflag")
return SigFlags(sigflag) if sigflag else None
@property
def n_sigs(self) -> Union[None, int]:
n_sigs = self.tags.get_tag("n_sigs")
return int(n_sigs) if n_sigs else None

View File

@@ -68,18 +68,16 @@ class P2PKSecret(Secret):
return int(n_sigs) if n_sigs else None return int(n_sigs) if n_sigs else None
def sign_p2pk_sign(message: bytes, private_key: PrivateKey) -> bytes: def schnorr_sign(message: bytes, private_key: PrivateKey) -> bytes:
# ecdsa version
# signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message))
signature = private_key.schnorr_sign( signature = private_key.schnorr_sign(
hashlib.sha256(message).digest(), None, raw=True hashlib.sha256(message).digest(), None, raw=True
) )
return signature return signature
def verify_p2pk_signature(message: bytes, pubkey: PublicKey, signature: bytes) -> bool: def verify_schnorr_signature(
# ecdsa version message: bytes, pubkey: PublicKey, signature: bytes
# return pubkey.ecdsa_verify(message, pubkey.ecdsa_deserialize(signature)) ) -> bool:
return pubkey.schnorr_verify( return pubkey.schnorr_verify(
hashlib.sha256(message).digest(), signature, None, raw=True hashlib.sha256(message).digest(), signature, None, raw=True
) )

View File

@@ -4,7 +4,7 @@ from typing import List
from loguru import logger from loguru import logger
from ..core.base import BlindedMessage, HTLCWitness, Proof from ..core.base import BlindedMessage, Proof
from ..core.crypto.secp import PublicKey from ..core.crypto.secp import PublicKey
from ..core.errors import ( from ..core.errors import (
TransactionError, TransactionError,
@@ -13,7 +13,7 @@ from ..core.htlc import HTLCSecret
from ..core.p2pk import ( from ..core.p2pk import (
P2PKSecret, P2PKSecret,
SigFlags, SigFlags,
verify_p2pk_signature, verify_schnorr_signature,
) )
from ..core.secret import Secret, SecretKind from ..core.secret import Secret, SecretKind
@@ -50,63 +50,10 @@ class LedgerSpendingConditions:
if not pubkeys: if not pubkeys:
return True return True
assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique." return self._verify_secret_signatures(
logger.trace(f"pubkeys: {pubkeys}") proof, pubkeys, proof.p2pksigs, p2pk_secret.n_sigs
# verify that signatures are present
if not proof.p2pksigs:
# no signature present although secret indicates one
logger.error(f"no p2pk signatures in proof: {proof.p2pksigs}")
raise TransactionError("no p2pk signatures in proof.")
# we make sure that there are no duplicate signatures
if len(set(proof.p2pksigs)) != len(proof.p2pksigs):
raise TransactionError("p2pk signatures must be unique.")
# we parse the secret as a P2PK commitment
# assert len(proof.secret.split(":")) == 5, "p2pk secret format invalid."
# INPUTS: check signatures proof.p2pksigs against pubkey
# we expect the signature to be on the pubkey (=message) itself
n_sigs_required = p2pk_secret.n_sigs or 1
assert n_sigs_required > 0, "n_sigs must be positive."
# check if enough signatures are present
assert (
len(proof.p2pksigs) >= n_sigs_required
), f"not enough signatures provided: {len(proof.p2pksigs)} < {n_sigs_required}."
n_valid_sigs_per_output = 0
# loop over all signatures in output
for input_sig in proof.p2pksigs:
for pubkey in pubkeys:
logger.trace(f"verifying signature {input_sig} by pubkey {pubkey}.")
logger.trace(f"Message: {p2pk_secret.serialize().encode('utf-8')}")
if verify_p2pk_signature(
message=proof.secret.encode("utf-8"),
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
signature=bytes.fromhex(input_sig),
):
n_valid_sigs_per_output += 1
logger.trace(
f"p2pk signature on input is valid: {input_sig} on {pubkey}."
)
# check if we have enough valid signatures
assert n_valid_sigs_per_output, "no valid signature provided for input."
assert n_valid_sigs_per_output >= n_sigs_required, (
f"signature threshold not met. {n_valid_sigs_per_output} <"
f" {n_sigs_required}."
) )
logger.trace(
f"{n_valid_sigs_per_output} of {n_sigs_required} valid signatures found."
)
logger.trace(proof.p2pksigs)
logger.trace("p2pk signature on inputs is valid.")
return True
def _verify_htlc_spending_conditions(self, proof: Proof, secret: Secret) -> bool: def _verify_htlc_spending_conditions(self, proof: Proof, secret: Secret) -> bool:
""" """
Verify HTLC spending condition for a single input. Verify HTLC spending condition for a single input.
@@ -149,18 +96,9 @@ 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.witness, TransactionError("no HTLC refund signature.") return self._verify_secret_signatures(
signature = HTLCWitness.from_witness(proof.witness).signature proof, refund_pubkeys, proof.p2pksigs, htlc_secret.n_sigs
assert signature, TransactionError("no HTLC refund signature provided") )
for pubkey in refund_pubkeys:
if verify_p2pk_signature(
message=proof.secret.encode("utf-8"),
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
signature=bytes.fromhex(signature),
):
# a signature matches
return True
raise TransactionError("HTLC refund signatures did not match.")
# no pubkeys given in secret, anyone can spend # no pubkeys given in secret, anyone can spend
return True return True
@@ -173,23 +111,74 @@ class LedgerSpendingConditions:
).digest() == bytes.fromhex(htlc_secret.data): ).digest() == bytes.fromhex(htlc_secret.data):
raise TransactionError("HTLC preimage does not match.") raise TransactionError("HTLC preimage does not match.")
# then we check whether a signature is required # then we check whether signatures are required
hashlock_pubkeys = htlc_secret.tags.get_tag_all("pubkeys") hashlock_pubkeys = htlc_secret.tags.get_tag_all("pubkeys")
if hashlock_pubkeys: if not hashlock_pubkeys:
assert proof.witness, TransactionError("no HTLC hash lock signature.") # no pubkeys given in secret, anyone can spend
signature = HTLCWitness.from_witness(proof.witness).signature return True
assert signature, TransactionError("HTLC no hash lock signatures provided.")
for pubkey in hashlock_pubkeys: return self._verify_secret_signatures(
if verify_p2pk_signature( proof, hashlock_pubkeys, proof.htlcsigs or [], htlc_secret.n_sigs
)
def _verify_secret_signatures(
self,
proof: Proof,
pubkeys: List[str],
signatures: List[str],
n_sigs_required: int | None = 1,
) -> bool:
assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique."
logger.trace(f"pubkeys: {pubkeys}")
# verify that signatures are present
if not signatures:
# no signature present although secret indicates one
logger.error(f"no signatures in proof: {proof}")
raise TransactionError("no signatures in proof.")
# we make sure that there are no duplicate signatures
if len(set(signatures)) != len(signatures):
raise TransactionError("signatures must be unique.")
# INPUTS: check signatures against pubkey
# we expect the signature to be on the pubkey (=message) itself
n_sigs_required = n_sigs_required or 1
assert n_sigs_required > 0, "n_sigs must be positive."
# check if enough signatures are present
assert (
len(signatures) >= n_sigs_required
), f"not enough signatures provided: {len(signatures)} < {n_sigs_required}."
n_valid_sigs_per_output = 0
# loop over all signatures in input
for input_sig in signatures:
for pubkey in pubkeys:
logger.trace(f"verifying signature {input_sig} by pubkey {pubkey}.")
logger.trace(f"Message: {proof.secret}")
if verify_schnorr_signature(
message=proof.secret.encode("utf-8"), message=proof.secret.encode("utf-8"),
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
signature=bytes.fromhex(signature), signature=bytes.fromhex(input_sig),
): ):
# a signature matches n_valid_sigs_per_output += 1
return True logger.trace(
# none of the pubkeys had a match f"signature on input is valid: {input_sig} on {pubkey}."
raise TransactionError("HTLC hash lock signatures did not match.") )
# no pubkeys were included, anyone can spend
# check if we have enough valid signatures
assert n_valid_sigs_per_output, "no valid signature provided for input."
assert n_valid_sigs_per_output >= n_sigs_required, (
f"signature threshold not met. {n_valid_sigs_per_output} <"
f" {n_sigs_required}."
)
logger.trace(
f"{n_valid_sigs_per_output} of {n_sigs_required} valid signatures found."
)
logger.trace("p2pk signature on inputs is valid.")
return True return True
def _verify_input_spending_conditions(self, proof: Proof) -> bool: def _verify_input_spending_conditions(self, proof: Proof) -> bool:
@@ -304,7 +293,7 @@ class LedgerSpendingConditions:
# loop over all signatures in output # loop over all signatures in output
for sig in p2pksigs: for sig in p2pksigs:
for pubkey in pubkeys: for pubkey in pubkeys:
if verify_p2pk_signature( if verify_schnorr_signature(
message=bytes.fromhex(output.B_), message=bytes.fromhex(output.B_),
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
signature=bytes.fromhex(sig), signature=bytes.fromhex(sig),

View File

@@ -1,6 +1,6 @@
import hashlib import hashlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Optional from typing import List
from ..core.base import HTLCWitness, Proof from ..core.base import HTLCWitness, Proof
from ..core.db import Database from ..core.db import Database
@@ -17,27 +17,31 @@ class WalletHTLC(SupportsDb):
async def create_htlc_lock( async def create_htlc_lock(
self, self,
*, *,
preimage: Optional[str] = None, preimage: str | None = None,
preimage_hash: Optional[str] = None, preimage_hash: str | None = None,
hashlock_pubkey: Optional[str] = None, hashlock_pubkeys: List[str] | None = None,
locktime_seconds: Optional[int] = None, hashlock_n_sigs: int | None = None,
locktime_pubkey: Optional[str] = None, locktime_seconds: int | None = None,
locktime_pubkeys: List[str] | None = None,
) -> HTLCSecret: ) -> HTLCSecret:
tags = Tags() tags = Tags()
if locktime_seconds: if locktime_seconds:
tags["locktime"] = str( tags["locktime"] = str(
int((datetime.now() + timedelta(seconds=locktime_seconds)).timestamp()) int((datetime.now() + timedelta(seconds=locktime_seconds)).timestamp())
) )
if locktime_pubkey: if locktime_pubkeys:
tags["refund"] = locktime_pubkey tags["refund"] = locktime_pubkeys
if not preimage_hash and preimage: if not preimage_hash and preimage:
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
assert preimage_hash, "preimage_hash or preimage must be provided" assert preimage_hash, "preimage_hash or preimage must be provided"
if hashlock_pubkey: if hashlock_pubkeys:
tags["pubkeys"] = hashlock_pubkey tags["pubkeys"] = hashlock_pubkeys
if hashlock_n_sigs:
tags["n_sigs"] = str(hashlock_n_sigs)
return HTLCSecret( return HTLCSecret(
kind=SecretKind.HTLC.value, kind=SecretKind.HTLC.value,

View File

@@ -13,7 +13,7 @@ from ..core.db import Database
from ..core.p2pk import ( from ..core.p2pk import (
P2PKSecret, P2PKSecret,
SigFlags, SigFlags,
sign_p2pk_sign, schnorr_sign,
) )
from ..core.secret import Secret, SecretKind, Tags from ..core.secret import Secret, SecretKind, Tags
from .protocols import SupportsDb, SupportsPrivateKey from .protocols import SupportsDb, SupportsPrivateKey
@@ -21,7 +21,7 @@ from .protocols import SupportsDb, SupportsPrivateKey
class WalletP2PK(SupportsPrivateKey, SupportsDb): class WalletP2PK(SupportsPrivateKey, SupportsDb):
db: Database db: Database
private_key: Optional[PrivateKey] = None private_key: PrivateKey
# ---------- P2PK ---------- # ---------- P2PK ----------
async def create_p2pk_pubkey(self): async def create_p2pk_pubkey(self):
@@ -61,10 +61,15 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
tags=tags, tags=tags,
) )
async def sign_p2pk_proofs(self, proofs: List[Proof]) -> List[str]: def sign_proofs(self, proofs: List[Proof]) -> List[str]:
assert ( """Signs proof secrets with the private key of the wallet.
self.private_key
), "No private key set in settings. Set NOSTR_PRIVATE_KEY in .env" Args:
proofs (List[Proof]): Proofs to sign
Returns:
List[str]: List of signatures for each proof
"""
private_key = self.private_key private_key = self.private_key
assert private_key.pubkey assert private_key.pubkey
logger.trace( logger.trace(
@@ -76,7 +81,7 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
logger.trace(f"Signing message: {proof.secret}") logger.trace(f"Signing message: {proof.secret}")
signatures = [ signatures = [
sign_p2pk_sign( schnorr_sign(
message=proof.secret.encode("utf-8"), message=proof.secret.encode("utf-8"),
private_key=private_key, private_key=private_key,
).hex() ).hex()
@@ -85,21 +90,18 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
logger.debug(f"Signatures: {signatures}") logger.debug(f"Signatures: {signatures}")
return signatures return signatures
async def sign_p2pk_outputs(self, outputs: List[BlindedMessage]) -> List[str]: def sign_outputs(self, outputs: List[BlindedMessage]) -> List[str]:
assert (
self.private_key
), "No private key set in settings. Set NOSTR_PRIVATE_KEY in .env"
private_key = self.private_key private_key = self.private_key
assert private_key.pubkey assert private_key.pubkey
return [ return [
sign_p2pk_sign( schnorr_sign(
message=bytes.fromhex(output.B_), message=bytes.fromhex(output.B_),
private_key=private_key, private_key=private_key,
).hex() ).hex()
for output in outputs for output in outputs
] ]
async def add_p2pk_witnesses_to_outputs( def add_signature_witnesses_to_outputs(
self, outputs: List[BlindedMessage] self, outputs: List[BlindedMessage]
) -> List[BlindedMessage]: ) -> List[BlindedMessage]:
"""Takes a list of outputs and adds a P2PK signatures to each. """Takes a list of outputs and adds a P2PK signatures to each.
@@ -108,12 +110,12 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
Returns: Returns:
List[BlindedMessage]: Outputs with P2PK signatures added List[BlindedMessage]: Outputs with P2PK signatures added
""" """
p2pk_signatures = await self.sign_p2pk_outputs(outputs) p2pk_signatures = self.sign_outputs(outputs)
for o, s in zip(outputs, p2pk_signatures): for o, s in zip(outputs, p2pk_signatures):
o.witness = P2PKWitness(signatures=[s]).json() o.witness = P2PKWitness(signatures=[s]).json()
return outputs return outputs
async def add_witnesses_to_outputs( def add_witnesses_to_outputs(
self, proofs: List[Proof], outputs: List[BlindedMessage] self, proofs: List[Proof], outputs: List[BlindedMessage]
) -> List[BlindedMessage]: ) -> List[BlindedMessage]:
"""Adds witnesses to outputs if the inputs (proofs) indicate an appropriate signature flag """Adds witnesses to outputs if the inputs (proofs) indicate an appropriate signature flag
@@ -127,25 +129,24 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
# first we check whether all tokens have serialized secrets as their secret # first we check whether all tokens have serialized secrets as their secret
try: try:
for p in proofs: for p in proofs:
Secret.deserialize(p.secret) secret = Secret.deserialize(p.secret)
except Exception: except Exception:
# if not, we do not add witnesses (treat as regular token secret) # if not, we do not add witnesses (treat as regular token secret)
return outputs return outputs
# if any of the proofs provided require SIG_ALL, we must provide it # if any of the proofs provided is P2PK and requires SIG_ALL, we must signatures to all outputs
if any( if any(
[ [
P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL secret.kind == SecretKind.P2PK.value
and P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL
for p in proofs for p in proofs
] ]
): ):
outputs = await self.add_p2pk_witnesses_to_outputs(outputs) outputs = self.add_signature_witnesses_to_outputs(outputs)
return outputs return outputs
async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: def add_signature_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]:
p2pk_signatures = await self.sign_p2pk_proofs(proofs) p2pk_signatures = self.sign_proofs(proofs)
logger.debug(f"Unlock signatures for {len(proofs)} proofs: {p2pk_signatures}")
logger.debug(f"Proofs: {proofs}")
# 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):
@@ -157,14 +158,14 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
p.witness = P2PKWitness(signatures=[s]).json() p.witness = P2PKWitness(signatures=[s]).json()
return proofs return proofs
async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]:
"""Adds witnesses to proofs for P2PK redemption. """Adds witnesses to proofs for P2PK redemption.
This method parses the secret of each proof and determines the correct This method parses the secret of each proof and determines the correct
witness type and adds it to the proof if we have it available. witness type and adds it to the proof if we have it available.
Note: In order for this method to work, all proofs must have the same secret type. Note: In order for this method to work, all proofs must have the same secret type.
For P2PK, we use an individual signature for each token in proofs. For P2PK and HTLC, we use an individual signature for each token in proofs.
Args: Args:
proofs (List[Proof]): List of proofs to add witnesses to proofs (List[Proof]): List of proofs to add witnesses to
@@ -172,22 +173,22 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
Returns: Returns:
List[Proof]: List of proofs with witnesses added List[Proof]: List of proofs with witnesses added
""" """
# iterate through proofs and produce witnesses for each
# first we check whether all tokens have serialized secrets as their secret # first we check whether all tokens have serialized secrets as their secret
try: try:
for p in proofs: for p in proofs:
Secret.deserialize(p.secret) secret = Secret.deserialize(p.secret)
except Exception: except Exception:
# if not, we do not add witnesses (treat as regular token secret) # if not, we do not add witnesses (treat as regular token secret)
return proofs return proofs
logger.debug("Spending conditions detected.") logger.debug("Spending conditions detected.")
# P2PK signatures # check if all secrets are either P2PK or HTLC
if all( if all([secret.kind == SecretKind.P2PK.value for p in proofs]):
[Secret.deserialize(p.secret).kind == SecretKind.P2PK.value for p in proofs] proofs = self.add_signature_witnesses_to_proofs(proofs)
):
logger.debug("P2PK redemption detected.") # if all([secret.kind == SecretKind.HTLC.value for p in proofs]):
proofs = await self.add_p2pk_witnesses_to_proofs(proofs) # for p in proofs:
# htlc_secret = HTLCSecret.deserialize(p.secret)
# if htlc_secret.tags.get_tag("pubkeys"):
# p = self.add_signature_witnesses_to_proofs([p])[0]
return proofs return proofs

View File

@@ -668,7 +668,7 @@ class Wallet(
proofs = copy.copy(proofs) proofs = copy.copy(proofs)
# potentially add witnesses to unlock provided proofs (if they indicate one) # potentially add witnesses to unlock provided proofs (if they indicate one)
proofs = await self.add_witnesses_to_proofs(proofs) proofs = self.add_witnesses_to_proofs(proofs)
input_fees = self.get_fees_for_proofs(proofs) input_fees = self.get_fees_for_proofs(proofs)
logger.debug(f"Input fees: {input_fees}") logger.debug(f"Input fees: {input_fees}")
@@ -700,7 +700,7 @@ class Wallet(
outputs, rs = self._construct_outputs(amounts, secrets, rs, self.keyset_id) outputs, rs = self._construct_outputs(amounts, secrets, rs, self.keyset_id)
# potentially add witnesses to outputs based on what requirement the proofs indicate # potentially add witnesses to outputs based on what requirement the proofs indicate
outputs = await self.add_witnesses_to_outputs(proofs, outputs) outputs = self.add_witnesses_to_outputs(proofs, outputs)
# Call swap API # Call swap API
promises = await super().split(proofs, outputs) promises = await super().split(proofs, outputs)

View File

@@ -120,14 +120,14 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock( secret = await wallet1.create_htlc_lock(
preimage=preimage, hashlock_pubkey=pubkey_wallet1 preimage=preimage, hashlock_pubkeys=[pubkey_wallet1]
) )
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
for p in send_proofs: for p in send_proofs:
p.witness = HTLCWitness(preimage=preimage).json() 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: no signatures in proof.",
) )
@@ -140,18 +140,18 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock( secret = await wallet1.create_htlc_lock(
preimage=preimage, hashlock_pubkey=pubkey_wallet1 preimage=preimage, hashlock_pubkeys=[pubkey_wallet1]
) )
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures = await wallet1.sign_p2pk_proofs(send_proofs) signatures = wallet1.sign_proofs(send_proofs)
for p, s in zip(send_proofs, signatures): for p, s in zip(send_proofs, signatures):
p.witness = HTLCWitness( p.witness = HTLCWitness(
preimage=preimage, signature=f"{s[:-5]}11111" preimage=preimage, signatures=[f"{s[:-5]}11111"]
).json() # wrong signature ).json() # wrong signature
await assert_err( await assert_err(
wallet2.redeem(send_proofs), wallet2.redeem(send_proofs),
"Mint Error: HTLC hash lock signatures did not match.", "Mint Error: no valid signature provided for input.",
) )
@@ -164,17 +164,187 @@ async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wall
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock( secret = await wallet1.create_htlc_lock(
preimage=preimage, hashlock_pubkey=pubkey_wallet1 preimage=preimage, hashlock_pubkeys=[pubkey_wallet1]
) )
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures = await wallet1.sign_p2pk_proofs(send_proofs) signatures = wallet1.sign_proofs(send_proofs)
for p, s in zip(send_proofs, signatures): for p, s in zip(send_proofs, signatures):
p.witness = HTLCWitness(preimage=preimage, signature=s).json() p.witness = HTLCWitness(preimage=preimage, signatures=[s]).json()
await wallet2.redeem(send_proofs) await wallet2.redeem(send_proofs)
@pytest.mark.asyncio
async def test_htlc_redeem_with_2_of_1_signatures(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64)
await pay_if_regtest(invoice.bolt11)
await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(
preimage=preimage,
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
hashlock_n_sigs=1,
)
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures1 = wallet1.sign_proofs(send_proofs)
signatures2 = wallet2.sign_proofs(send_proofs)
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
await wallet2.redeem(send_proofs)
@pytest.mark.asyncio
async def test_htlc_redeem_with_2_of_2_signatures(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64)
await pay_if_regtest(invoice.bolt11)
await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(
preimage=preimage,
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
hashlock_n_sigs=2,
)
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures1 = wallet1.sign_proofs(send_proofs)
signatures2 = wallet2.sign_proofs(send_proofs)
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
await wallet2.redeem(send_proofs)
@pytest.mark.asyncio
async def test_htlc_redeem_with_2_of_2_signatures_with_duplicate_pubkeys(
wallet1: Wallet, wallet2: Wallet
):
invoice = await wallet1.request_mint(64)
await pay_if_regtest(invoice.bolt11)
await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = pubkey_wallet1
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(
preimage=preimage,
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
hashlock_n_sigs=2,
)
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures1 = wallet1.sign_proofs(send_proofs)
signatures2 = wallet2.sign_proofs(send_proofs)
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
await assert_err(
wallet2.redeem(send_proofs),
"Mint Error: pubkeys must be unique.",
)
@pytest.mark.asyncio
async def test_htlc_redeem_with_3_of_3_signatures_but_only_2_provided(
wallet1: Wallet, wallet2: Wallet
):
invoice = await wallet1.request_mint(64)
await pay_if_regtest(invoice.bolt11)
await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(
preimage=preimage,
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2],
hashlock_n_sigs=3,
)
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures1 = wallet1.sign_proofs(send_proofs)
signatures2 = wallet2.sign_proofs(send_proofs)
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
await assert_err(
wallet2.redeem(send_proofs),
"Mint Error: not enough signatures provided: 2 < 3.",
)
@pytest.mark.asyncio
async def test_htlc_redeem_with_2_of_3_signatures_with_2_valid_and_1_invalid_provided(
wallet1: Wallet, wallet2: Wallet
):
invoice = await wallet1.request_mint(64)
await pay_if_regtest(invoice.bolt11)
await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
privatekey_wallet3 = PrivateKey(secrets.token_bytes(32), raw=True)
assert privatekey_wallet3.pubkey
pubkey_wallet3 = privatekey_wallet3.pubkey.serialize().hex()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(
preimage=preimage,
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
hashlock_n_sigs=2,
)
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures1 = wallet1.sign_proofs(send_proofs)
signatures2 = wallet2.sign_proofs(send_proofs)
signatures3 = [f"{s[:-5]}11111" for s in signatures1] # wrong signature
for p, s1, s2, s3 in zip(send_proofs, signatures1, signatures2, signatures3):
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2, s3]).json()
await wallet2.redeem(send_proofs)
@pytest.mark.asyncio
async def test_htlc_redeem_with_3_of_3_signatures_with_2_valid_and_1_invalid_provided(
wallet1: Wallet, wallet2: Wallet
):
invoice = await wallet1.request_mint(64)
await pay_if_regtest(invoice.bolt11)
await wallet1.mint(64, id=invoice.id)
preimage = "00000000000000000000000000000000"
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
privatekey_wallet3 = PrivateKey(secrets.token_bytes(32), raw=True)
assert privatekey_wallet3.pubkey
pubkey_wallet3 = privatekey_wallet3.pubkey.serialize().hex()
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock(
preimage=preimage,
hashlock_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
hashlock_n_sigs=3,
)
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures1 = wallet1.sign_proofs(send_proofs)
signatures2 = wallet2.sign_proofs(send_proofs)
signatures3 = [f"{s[:-5]}11111" for s in signatures1] # wrong signature
for p, s1, s2, s3 in zip(send_proofs, signatures1, signatures2, signatures3):
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2, s3]).json()
await assert_err(
wallet2.redeem(send_proofs), "Mint Error: signature threshold not met. 2 < 3."
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature( async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
wallet1: Wallet, wallet2: Wallet wallet1: Wallet, wallet2: Wallet
@@ -188,20 +358,20 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock( secret = await wallet1.create_htlc_lock(
preimage=preimage, preimage=preimage,
hashlock_pubkey=pubkey_wallet2, hashlock_pubkeys=[pubkey_wallet2],
locktime_seconds=2, locktime_seconds=2,
locktime_pubkey=pubkey_wallet1, locktime_pubkeys=[pubkey_wallet1],
) )
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures = await wallet1.sign_p2pk_proofs(send_proofs) signatures = wallet1.sign_proofs(send_proofs)
for p, s in zip(send_proofs, signatures): for p, s in zip(send_proofs, signatures):
p.witness = HTLCWitness(preimage=preimage, signature=s).json() p.witness = HTLCWitness(preimage=preimage, signatures=[s]).json()
# 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(
wallet1.redeem(send_proofs), wallet1.redeem(send_proofs),
"Mint Error: HTLC hash lock signatures did not match.", "Mint Error: no valid signature provided for input.",
) )
await asyncio.sleep(2) await asyncio.sleep(2)
@@ -222,27 +392,27 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() # preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
secret = await wallet1.create_htlc_lock( secret = await wallet1.create_htlc_lock(
preimage=preimage, preimage=preimage,
hashlock_pubkey=pubkey_wallet2, hashlock_pubkeys=[pubkey_wallet2],
locktime_seconds=2, locktime_seconds=2,
locktime_pubkey=pubkey_wallet1, locktime_pubkeys=[pubkey_wallet1],
) )
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret) _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
signatures = await wallet1.sign_p2pk_proofs(send_proofs) signatures = wallet1.sign_proofs(send_proofs)
for p, s in zip(send_proofs, signatures): for p, s in zip(send_proofs, signatures):
p.witness = HTLCWitness( p.witness = HTLCWitness(
preimage=preimage, signature=f"{s[:-5]}11111" preimage=preimage, signatures=[f"{s[:-5]}11111"]
).json() # wrong signature ).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(
wallet1.redeem(send_proofs), wallet1.redeem(send_proofs),
"Mint Error: HTLC hash lock signatures did not match.", "Mint Error: no valid signature provided for input.",
) )
await asyncio.sleep(2) await asyncio.sleep(2)
# should fail since lock time has passed and we provided a wrong signature for timelock # should fail since lock time has passed and we provided a wrong signature for timelock
await assert_err( await assert_err(
wallet1.redeem(send_proofs), wallet1.redeem(send_proofs),
"Mint Error: HTLC refund signatures did not match.", "Mint Error: no valid signature provided for input.",
) )

View File

@@ -267,7 +267,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
wallet1.proofs, 8, secret_lock=secret_lock wallet1.proofs, 8, secret_lock=secret_lock
) )
# add signatures of wallet1 # add signatures of wallet1
send_proofs = await wallet1.add_p2pk_witnesses_to_proofs(send_proofs) send_proofs = wallet1.add_signature_witnesses_to_proofs(send_proofs)
# here we add the signatures of wallet2 # here we add the signatures of wallet2
await wallet2.redeem(send_proofs) await wallet2.redeem(send_proofs)
@@ -289,10 +289,10 @@ async def test_p2pk_multisig_duplicate_signature(wallet1: Wallet, wallet2: Walle
wallet1.proofs, 8, secret_lock=secret_lock wallet1.proofs, 8, secret_lock=secret_lock
) )
# add signatures of wallet2 this is a duplicate signature # add signatures of wallet2 this is a duplicate signature
send_proofs = await wallet2.add_p2pk_witnesses_to_proofs(send_proofs) send_proofs = wallet2.add_signature_witnesses_to_proofs(send_proofs)
# here we add the signatures of wallet2 # here we add the signatures of wallet2
await assert_err( await assert_err(
wallet2.redeem(send_proofs), "Mint Error: p2pk signatures must be unique." wallet2.redeem(send_proofs), "Mint Error: signatures must be unique."
) )
@@ -334,7 +334,7 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal
wallet1.proofs, 8, secret_lock=secret_lock wallet1.proofs, 8, secret_lock=secret_lock
) )
# add signatures of wallet1 # add signatures of wallet1
send_proofs = await wallet1.add_p2pk_witnesses_to_proofs(send_proofs) send_proofs = wallet1.add_signature_witnesses_to_proofs(send_proofs)
# here we add the signatures of wallet2 # here we add the signatures of wallet2
await assert_err( await assert_err(
wallet2.redeem(send_proofs), wallet2.redeem(send_proofs),
@@ -381,7 +381,7 @@ async def test_p2pk_multisig_with_wrong_first_private_key(
wallet1.proofs, 8, secret_lock=secret_lock wallet1.proofs, 8, secret_lock=secret_lock
) )
# add signatures of wallet1 # add signatures of wallet1
send_proofs = await wallet1.add_p2pk_witnesses_to_proofs(send_proofs) send_proofs = wallet1.add_signature_witnesses_to_proofs(send_proofs)
await assert_err( await assert_err(
wallet2.redeem(send_proofs), "Mint Error: signature threshold not met. 1 < 2." wallet2.redeem(send_proofs), "Mint Error: signature threshold not met. 1 < 2."
) )