mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-22 19:34:18 +01:00
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:
@@ -95,21 +95,7 @@ class ProofState(LedgerEvent):
|
||||
|
||||
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
|
||||
signatures: Optional[List[str]] = None
|
||||
|
||||
@classmethod
|
||||
def from_witness(cls, witness: str):
|
||||
@@ -206,10 +192,15 @@ class Proof(BaseModel):
|
||||
return P2PKWitness.from_witness(self.witness).signatures
|
||||
|
||||
@property
|
||||
def htlcpreimage(self) -> Union[str, None]:
|
||||
def htlcpreimage(self) -> str | None:
|
||||
assert self.witness, "Witness is missing for htlc 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):
|
||||
# NOTE: not used in Pydantic validation
|
||||
@@ -647,6 +638,7 @@ class WalletKeyset:
|
||||
int(amount): PublicKey(bytes.fromhex(hex_key), raw=True)
|
||||
for amount, hex_key in dict(json.loads(serialized)).items()
|
||||
}
|
||||
|
||||
return cls(
|
||||
id=row["id"],
|
||||
unit=row["unit"],
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
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):
|
||||
@classmethod
|
||||
def from_secret(cls, secret: Secret):
|
||||
@@ -15,3 +23,13 @@ class HTLCSecret(Secret):
|
||||
def locktime(self) -> Union[None, int]:
|
||||
locktime = self.tags.get_tag("locktime")
|
||||
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
|
||||
|
||||
@@ -68,18 +68,16 @@ class P2PKSecret(Secret):
|
||||
return int(n_sigs) if n_sigs else None
|
||||
|
||||
|
||||
def sign_p2pk_sign(message: bytes, private_key: PrivateKey) -> bytes:
|
||||
# ecdsa version
|
||||
# signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message))
|
||||
def schnorr_sign(message: bytes, private_key: PrivateKey) -> bytes:
|
||||
signature = private_key.schnorr_sign(
|
||||
hashlib.sha256(message).digest(), None, raw=True
|
||||
)
|
||||
return signature
|
||||
|
||||
|
||||
def verify_p2pk_signature(message: bytes, pubkey: PublicKey, signature: bytes) -> bool:
|
||||
# ecdsa version
|
||||
# return pubkey.ecdsa_verify(message, pubkey.ecdsa_deserialize(signature))
|
||||
def verify_schnorr_signature(
|
||||
message: bytes, pubkey: PublicKey, signature: bytes
|
||||
) -> bool:
|
||||
return pubkey.schnorr_verify(
|
||||
hashlib.sha256(message).digest(), signature, None, raw=True
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import List
|
||||
|
||||
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.errors import (
|
||||
TransactionError,
|
||||
@@ -13,7 +13,7 @@ from ..core.htlc import HTLCSecret
|
||||
from ..core.p2pk import (
|
||||
P2PKSecret,
|
||||
SigFlags,
|
||||
verify_p2pk_signature,
|
||||
verify_schnorr_signature,
|
||||
)
|
||||
from ..core.secret import Secret, SecretKind
|
||||
|
||||
@@ -50,63 +50,10 @@ class LedgerSpendingConditions:
|
||||
if not pubkeys:
|
||||
return True
|
||||
|
||||
assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique."
|
||||
logger.trace(f"pubkeys: {pubkeys}")
|
||||
|
||||
# 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}."
|
||||
return self._verify_secret_signatures(
|
||||
proof, pubkeys, proof.p2pksigs, p2pk_secret.n_sigs
|
||||
)
|
||||
|
||||
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:
|
||||
"""
|
||||
Verify HTLC spending condition for a single input.
|
||||
@@ -149,18 +96,9 @@ 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.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=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.")
|
||||
return self._verify_secret_signatures(
|
||||
proof, refund_pubkeys, proof.p2pksigs, htlc_secret.n_sigs
|
||||
)
|
||||
# no pubkeys given in secret, anyone can spend
|
||||
return True
|
||||
|
||||
@@ -173,23 +111,74 @@ class LedgerSpendingConditions:
|
||||
).digest() == bytes.fromhex(htlc_secret.data):
|
||||
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")
|
||||
if hashlock_pubkeys:
|
||||
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(
|
||||
if not hashlock_pubkeys:
|
||||
# no pubkeys given in secret, anyone can spend
|
||||
return True
|
||||
|
||||
return self._verify_secret_signatures(
|
||||
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"),
|
||||
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
|
||||
signature=bytes.fromhex(signature),
|
||||
signature=bytes.fromhex(input_sig),
|
||||
):
|
||||
# a signature matches
|
||||
return True
|
||||
# none of the pubkeys had a match
|
||||
raise TransactionError("HTLC hash lock signatures did not match.")
|
||||
# no pubkeys were included, anyone can spend
|
||||
n_valid_sigs_per_output += 1
|
||||
logger.trace(
|
||||
f"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("p2pk signature on inputs is valid.")
|
||||
|
||||
return True
|
||||
|
||||
def _verify_input_spending_conditions(self, proof: Proof) -> bool:
|
||||
@@ -304,7 +293,7 @@ class LedgerSpendingConditions:
|
||||
# loop over all signatures in output
|
||||
for sig in p2pksigs:
|
||||
for pubkey in pubkeys:
|
||||
if verify_p2pk_signature(
|
||||
if verify_schnorr_signature(
|
||||
message=bytes.fromhex(output.B_),
|
||||
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
|
||||
signature=bytes.fromhex(sig),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from ..core.base import HTLCWitness, Proof
|
||||
from ..core.db import Database
|
||||
@@ -17,27 +17,31 @@ class WalletHTLC(SupportsDb):
|
||||
async def create_htlc_lock(
|
||||
self,
|
||||
*,
|
||||
preimage: Optional[str] = None,
|
||||
preimage_hash: Optional[str] = None,
|
||||
hashlock_pubkey: Optional[str] = None,
|
||||
locktime_seconds: Optional[int] = None,
|
||||
locktime_pubkey: Optional[str] = None,
|
||||
preimage: str | None = None,
|
||||
preimage_hash: str | None = None,
|
||||
hashlock_pubkeys: List[str] | None = None,
|
||||
hashlock_n_sigs: int | None = None,
|
||||
locktime_seconds: int | None = None,
|
||||
locktime_pubkeys: List[str] | None = None,
|
||||
) -> HTLCSecret:
|
||||
tags = Tags()
|
||||
if locktime_seconds:
|
||||
tags["locktime"] = str(
|
||||
int((datetime.now() + timedelta(seconds=locktime_seconds)).timestamp())
|
||||
)
|
||||
if locktime_pubkey:
|
||||
tags["refund"] = locktime_pubkey
|
||||
if locktime_pubkeys:
|
||||
tags["refund"] = locktime_pubkeys
|
||||
|
||||
if not preimage_hash and preimage:
|
||||
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
|
||||
assert preimage_hash, "preimage_hash or preimage must be provided"
|
||||
|
||||
if hashlock_pubkey:
|
||||
tags["pubkeys"] = hashlock_pubkey
|
||||
if hashlock_pubkeys:
|
||||
tags["pubkeys"] = hashlock_pubkeys
|
||||
|
||||
if hashlock_n_sigs:
|
||||
tags["n_sigs"] = str(hashlock_n_sigs)
|
||||
|
||||
return HTLCSecret(
|
||||
kind=SecretKind.HTLC.value,
|
||||
|
||||
@@ -13,7 +13,7 @@ from ..core.db import Database
|
||||
from ..core.p2pk import (
|
||||
P2PKSecret,
|
||||
SigFlags,
|
||||
sign_p2pk_sign,
|
||||
schnorr_sign,
|
||||
)
|
||||
from ..core.secret import Secret, SecretKind, Tags
|
||||
from .protocols import SupportsDb, SupportsPrivateKey
|
||||
@@ -21,7 +21,7 @@ from .protocols import SupportsDb, SupportsPrivateKey
|
||||
|
||||
class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
db: Database
|
||||
private_key: Optional[PrivateKey] = None
|
||||
private_key: PrivateKey
|
||||
# ---------- P2PK ----------
|
||||
|
||||
async def create_p2pk_pubkey(self):
|
||||
@@ -61,10 +61,15 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
async def sign_p2pk_proofs(self, proofs: List[Proof]) -> List[str]:
|
||||
assert (
|
||||
self.private_key
|
||||
), "No private key set in settings. Set NOSTR_PRIVATE_KEY in .env"
|
||||
def sign_proofs(self, proofs: List[Proof]) -> List[str]:
|
||||
"""Signs proof secrets with the private key of the wallet.
|
||||
|
||||
Args:
|
||||
proofs (List[Proof]): Proofs to sign
|
||||
|
||||
Returns:
|
||||
List[str]: List of signatures for each proof
|
||||
"""
|
||||
private_key = self.private_key
|
||||
assert private_key.pubkey
|
||||
logger.trace(
|
||||
@@ -76,7 +81,7 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
logger.trace(f"Signing message: {proof.secret}")
|
||||
|
||||
signatures = [
|
||||
sign_p2pk_sign(
|
||||
schnorr_sign(
|
||||
message=proof.secret.encode("utf-8"),
|
||||
private_key=private_key,
|
||||
).hex()
|
||||
@@ -85,21 +90,18 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
logger.debug(f"Signatures: {signatures}")
|
||||
return signatures
|
||||
|
||||
async def sign_p2pk_outputs(self, outputs: List[BlindedMessage]) -> List[str]:
|
||||
assert (
|
||||
self.private_key
|
||||
), "No private key set in settings. Set NOSTR_PRIVATE_KEY in .env"
|
||||
def sign_outputs(self, outputs: List[BlindedMessage]) -> List[str]:
|
||||
private_key = self.private_key
|
||||
assert private_key.pubkey
|
||||
return [
|
||||
sign_p2pk_sign(
|
||||
schnorr_sign(
|
||||
message=bytes.fromhex(output.B_),
|
||||
private_key=private_key,
|
||||
).hex()
|
||||
for output in outputs
|
||||
]
|
||||
|
||||
async def add_p2pk_witnesses_to_outputs(
|
||||
def add_signature_witnesses_to_outputs(
|
||||
self, outputs: List[BlindedMessage]
|
||||
) -> List[BlindedMessage]:
|
||||
"""Takes a list of outputs and adds a P2PK signatures to each.
|
||||
@@ -108,12 +110,12 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
Returns:
|
||||
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):
|
||||
o.witness = P2PKWitness(signatures=[s]).json()
|
||||
return outputs
|
||||
|
||||
async def add_witnesses_to_outputs(
|
||||
def add_witnesses_to_outputs(
|
||||
self, proofs: List[Proof], outputs: List[BlindedMessage]
|
||||
) -> List[BlindedMessage]:
|
||||
"""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
|
||||
try:
|
||||
for p in proofs:
|
||||
Secret.deserialize(p.secret)
|
||||
secret = Secret.deserialize(p.secret)
|
||||
except Exception:
|
||||
# if not, we do not add witnesses (treat as regular token secret)
|
||||
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(
|
||||
[
|
||||
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
|
||||
]
|
||||
):
|
||||
outputs = await self.add_p2pk_witnesses_to_outputs(outputs)
|
||||
outputs = self.add_signature_witnesses_to_outputs(outputs)
|
||||
return outputs
|
||||
|
||||
async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]:
|
||||
p2pk_signatures = await self.sign_p2pk_proofs(proofs)
|
||||
logger.debug(f"Unlock signatures for {len(proofs)} proofs: {p2pk_signatures}")
|
||||
logger.debug(f"Proofs: {proofs}")
|
||||
def add_signature_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]:
|
||||
p2pk_signatures = self.sign_proofs(proofs)
|
||||
# attach unlock signatures to proofs
|
||||
assert len(proofs) == len(p2pk_signatures), "wrong number of signatures"
|
||||
for p, s in zip(proofs, p2pk_signatures):
|
||||
@@ -157,14 +158,14 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
p.witness = P2PKWitness(signatures=[s]).json()
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
proofs (List[Proof]): List of proofs to add witnesses to
|
||||
@@ -172,22 +173,22 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
Returns:
|
||||
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
|
||||
try:
|
||||
for p in proofs:
|
||||
Secret.deserialize(p.secret)
|
||||
secret = Secret.deserialize(p.secret)
|
||||
except Exception:
|
||||
# if not, we do not add witnesses (treat as regular token secret)
|
||||
return proofs
|
||||
logger.debug("Spending conditions detected.")
|
||||
# P2PK signatures
|
||||
if all(
|
||||
[Secret.deserialize(p.secret).kind == SecretKind.P2PK.value for p in proofs]
|
||||
):
|
||||
logger.debug("P2PK redemption detected.")
|
||||
proofs = await self.add_p2pk_witnesses_to_proofs(proofs)
|
||||
# check if all secrets are either P2PK or HTLC
|
||||
if all([secret.kind == SecretKind.P2PK.value for p in proofs]):
|
||||
proofs = self.add_signature_witnesses_to_proofs(proofs)
|
||||
|
||||
# if all([secret.kind == SecretKind.HTLC.value for p in 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
|
||||
|
||||
@@ -668,7 +668,7 @@ class Wallet(
|
||||
proofs = copy.copy(proofs)
|
||||
|
||||
# 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)
|
||||
logger.debug(f"Input fees: {input_fees}")
|
||||
@@ -700,7 +700,7 @@ class Wallet(
|
||||
outputs, rs = self._construct_outputs(amounts, secrets, rs, self.keyset_id)
|
||||
|
||||
# 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
|
||||
promises = await super().split(proofs, outputs)
|
||||
|
||||
@@ -120,14 +120,14 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
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)
|
||||
for p in send_proofs:
|
||||
p.witness = HTLCWitness(preimage=preimage).json()
|
||||
await assert_err(
|
||||
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()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
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)
|
||||
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
||||
signatures = wallet1.sign_proofs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(
|
||||
preimage=preimage, signature=f"{s[:-5]}11111"
|
||||
preimage=preimage, signatures=[f"{s[:-5]}11111"]
|
||||
).json() # wrong signature
|
||||
|
||||
await assert_err(
|
||||
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()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
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)
|
||||
|
||||
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
||||
signatures = wallet1.sign_proofs(send_proofs)
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
|
||||
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()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkey=pubkey_wallet2,
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
locktime_seconds=2,
|
||||
locktime_pubkey=pubkey_wallet1,
|
||||
locktime_pubkeys=[pubkey_wallet1],
|
||||
)
|
||||
_, 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):
|
||||
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
|
||||
await assert_err(
|
||||
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)
|
||||
@@ -222,27 +392,27 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkey=pubkey_wallet2,
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
locktime_seconds=2,
|
||||
locktime_pubkey=pubkey_wallet1,
|
||||
locktime_pubkeys=[pubkey_wallet1],
|
||||
)
|
||||
_, 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):
|
||||
p.witness = HTLCWitness(
|
||||
preimage=preimage, signature=f"{s[:-5]}11111"
|
||||
preimage=preimage, signatures=[f"{s[:-5]}11111"]
|
||||
).json() # wrong signature
|
||||
|
||||
# should error because we used wallet2 signatures for the hash lock
|
||||
await assert_err(
|
||||
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)
|
||||
# should fail since lock time has passed and we provided a wrong signature for timelock
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: HTLC refund signatures did not match.",
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
)
|
||||
|
||||
@@ -267,7 +267,7 @@ async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# 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
|
||||
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
|
||||
)
|
||||
# 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
|
||||
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
|
||||
)
|
||||
# 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
|
||||
await assert_err(
|
||||
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
|
||||
)
|
||||
# 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(
|
||||
wallet2.redeem(send_proofs), "Mint Error: signature threshold not met. 1 < 2."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user