mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 18:44:20 +01:00
Refactor secret conditions (#350)
* refactor spending conditions and add comments * fix kind enum deserialization
This commit is contained in:
@@ -132,17 +132,12 @@ class Proof(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def p2pksigs(self) -> List[str]:
|
def p2pksigs(self) -> List[str]:
|
||||||
assert self.witness, "Witness is missing"
|
assert self.witness, "Witness is missing for p2pk signature"
|
||||||
return P2PKWitness.from_witness(self.witness).signatures
|
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
|
@property
|
||||||
def htlcpreimage(self) -> Union[str, None]:
|
def htlcpreimage(self) -> Union[str, None]:
|
||||||
assert self.witness, "Witness is missing"
|
assert self.witness, "Witness is missing for htlc preimage"
|
||||||
return HTLCWitness.from_witness(self.witness).preimage
|
return HTLCWitness.from_witness(self.witness).preimage
|
||||||
|
|
||||||
|
|
||||||
@@ -162,7 +157,7 @@ class BlindedMessage(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def p2pksigs(self) -> List[str]:
|
def p2pksigs(self) -> List[str]:
|
||||||
assert self.witness, "Witness is missing"
|
assert self.witness, "Witness missing in output"
|
||||||
return P2PKWitness.from_witness(self.witness).signatures
|
return P2PKWitness.from_witness(self.witness).signatures
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from .secret import Secret, SecretKind
|
|||||||
class HTLCSecret(Secret):
|
class HTLCSecret(Secret):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_secret(cls, secret: Secret):
|
def from_secret(cls, secret: Secret):
|
||||||
assert secret.kind == SecretKind.HTLC, "Secret is not a HTLC secret"
|
assert SecretKind(secret.kind) == SecretKind.HTLC, "Secret is not a HTLC secret"
|
||||||
# NOTE: exclude tags in .dict() because it doesn't deserialize it properly
|
# NOTE: exclude tags in .dict() because it doesn't deserialize it properly
|
||||||
# need to add it back in manually with tags=secret.tags
|
# need to add it back in manually with tags=secret.tags
|
||||||
return cls(**secret.dict(exclude={"tags"}), tags=secret.tags)
|
return cls(**secret.dict(exclude={"tags"}), tags=secret.tags)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
|
from enum import Enum
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -8,23 +9,26 @@ from .crypto.secp import PrivateKey, PublicKey
|
|||||||
from .secret import Secret, SecretKind
|
from .secret import Secret, SecretKind
|
||||||
|
|
||||||
|
|
||||||
class SigFlags:
|
class SigFlags(Enum):
|
||||||
SIG_INPUTS = ( # require signatures only on the inputs (default signature flag)
|
# require signatures only on the inputs (default signature flag)
|
||||||
"SIG_INPUTS"
|
SIG_INPUTS = "SIG_INPUTS"
|
||||||
)
|
# require signatures on inputs and outputs
|
||||||
SIG_ALL = "SIG_ALL" # require signatures on inputs and outputs
|
SIG_ALL = "SIG_ALL"
|
||||||
|
|
||||||
|
|
||||||
class P2PKSecret(Secret):
|
class P2PKSecret(Secret):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_secret(cls, secret: Secret):
|
def from_secret(cls, secret: Secret):
|
||||||
assert secret.kind == SecretKind.P2PK, "Secret is not a P2PK secret"
|
assert SecretKind(secret.kind) == SecretKind.P2PK, "Secret is not a P2PK secret"
|
||||||
# NOTE: exclude tags in .dict() because it doesn't deserialize it properly
|
# NOTE: exclude tags in .dict() because it doesn't deserialize it properly
|
||||||
# need to add it back in manually with tags=secret.tags
|
# need to add it back in manually with tags=secret.tags
|
||||||
return cls(**secret.dict(exclude={"tags"}), tags=secret.tags)
|
return cls(**secret.dict(exclude={"tags"}), tags=secret.tags)
|
||||||
|
|
||||||
def get_p2pk_pubkey_from_secret(self) -> List[str]:
|
def get_p2pk_pubkey_from_secret(self) -> List[str]:
|
||||||
"""Gets the P2PK pubkey from a Secret depending on the locktime
|
"""Gets the P2PK pubkey from a Secret depending on the locktime.
|
||||||
|
|
||||||
|
If locktime is passed, only the refund pubkeys are returned.
|
||||||
|
Else, the pubkeys in the data field and in the 'pubkeys' tag are returned.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
secret (Secret): P2PK Secret in ecash token
|
secret (Secret): P2PK Secret in ecash token
|
||||||
@@ -54,8 +58,9 @@ class P2PKSecret(Secret):
|
|||||||
return int(locktime) if locktime else None
|
return int(locktime) if locktime else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sigflag(self) -> Union[None, str]:
|
def sigflag(self) -> Union[None, SigFlags]:
|
||||||
return self.tags.get_tag("sigflag")
|
sigflag = self.tags.get_tag("sigflag")
|
||||||
|
return SigFlags(sigflag) if sigflag else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def n_sigs(self) -> Union[None, int]:
|
def n_sigs(self) -> Union[None, int]:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -7,7 +8,7 @@ from pydantic import BaseModel
|
|||||||
from .crypto.secp import PrivateKey
|
from .crypto.secp import PrivateKey
|
||||||
|
|
||||||
|
|
||||||
class SecretKind:
|
class SecretKind(Enum):
|
||||||
P2PK = "P2PK"
|
P2PK = "P2PK"
|
||||||
HTLC = "HTLC"
|
HTLC = "HTLC"
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,184 @@ from ..core.secret import Secret, SecretKind
|
|||||||
|
|
||||||
|
|
||||||
class LedgerSpendingConditions:
|
class LedgerSpendingConditions:
|
||||||
|
def _verify_p2pk_spending_conditions(self, proof: Proof, secret: Secret) -> bool:
|
||||||
|
"""
|
||||||
|
Verify P2PK spending condition for a single input.
|
||||||
|
|
||||||
|
We return True:
|
||||||
|
- if the secret is not a P2PKSecret spending condition
|
||||||
|
- if the locktime has passed and no refund pubkey is present
|
||||||
|
|
||||||
|
We raise an exception:
|
||||||
|
- if the pubkeys in the secret are not unique
|
||||||
|
- if no signatures are present
|
||||||
|
- if the signatures are not unique
|
||||||
|
- if n_sigs is not positive
|
||||||
|
- if n_sigs is larger than the number of provided signatures
|
||||||
|
- if no valid signatures are present
|
||||||
|
- if the signature threshold is not met
|
||||||
|
"""
|
||||||
|
if SecretKind(secret.kind) != SecretKind.P2PK:
|
||||||
|
# not a P2PK secret
|
||||||
|
return True
|
||||||
|
|
||||||
|
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||||
|
|
||||||
|
# extract pubkeys that we require signatures from depending on whether the
|
||||||
|
# locktime has passed (refund) or not (pubkeys in secret.data and in tags)
|
||||||
|
# This is implemented in get_p2pk_pubkey_from_secret()
|
||||||
|
pubkeys = p2pk_secret.get_p2pk_pubkey_from_secret()
|
||||||
|
# we will get an empty list if the locktime has passed and no refund pubkey is present
|
||||||
|
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=p2pk_secret.serialize().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:
|
||||||
|
"""
|
||||||
|
Verify HTLC spending condition for a single input.
|
||||||
|
|
||||||
|
We return True:
|
||||||
|
- if the secret is not a HTLCSecret spending condition
|
||||||
|
|
||||||
|
We first verify the time lock. If the locktime has passed, we require
|
||||||
|
a valid signature if a 'refund' pubkey is present. If it isn't present,
|
||||||
|
anyone can spend.
|
||||||
|
|
||||||
|
We return True:
|
||||||
|
- if 'refund' pubkeys are present and a valid signature is provided for one of them
|
||||||
|
We raise an exception:
|
||||||
|
- if 'refund' but no valid signature is present
|
||||||
|
|
||||||
|
|
||||||
|
We then verify the hash lock. We require a valid preimage. We require a valid
|
||||||
|
signature if 'pubkeys' are present. If they aren't present, anyone who provides
|
||||||
|
a valid preimage can spend.
|
||||||
|
|
||||||
|
We raise an exception:
|
||||||
|
- if no preimage is provided
|
||||||
|
- if preimage does not match the hash lock in the secret
|
||||||
|
|
||||||
|
We return True:
|
||||||
|
- if 'pubkeys' are present and a valid signature is provided for one of them
|
||||||
|
|
||||||
|
We raise an exception:
|
||||||
|
- if 'pubkeys' are present but no valid signature is provided
|
||||||
|
"""
|
||||||
|
|
||||||
|
if SecretKind(secret.kind) != SecretKind.HTLC:
|
||||||
|
# not a P2PK secret
|
||||||
|
return True
|
||||||
|
htlc_secret = HTLCSecret.from_secret(secret)
|
||||||
|
|
||||||
|
# time lock
|
||||||
|
# check if locktime is in the past
|
||||||
|
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=htlc_secret.serialize().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
|
||||||
|
return True
|
||||||
|
|
||||||
|
# hash lock
|
||||||
|
assert proof.htlcpreimage, TransactionError("no HTLC preimage provided")
|
||||||
|
|
||||||
|
# first we check whether a correct preimage was included
|
||||||
|
if not hashlib.sha256(
|
||||||
|
bytes.fromhex(proof.htlcpreimage)
|
||||||
|
).digest() == bytes.fromhex(htlc_secret.data):
|
||||||
|
raise TransactionError("HTLC preimage does not match.")
|
||||||
|
|
||||||
|
# then we check whether a signature is 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(
|
||||||
|
message=htlc_secret.serialize().encode("utf-8"),
|
||||||
|
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
|
||||||
|
signature=bytes.fromhex(signature),
|
||||||
|
):
|
||||||
|
# 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
|
||||||
|
return True
|
||||||
|
|
||||||
def _verify_input_spending_conditions(self, proof: Proof) -> bool:
|
def _verify_input_spending_conditions(self, proof: Proof) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify spending conditions:
|
Verify spending conditions:
|
||||||
Condition: P2PK - Witness: proof.p2pksigs
|
Condition: P2PK - Checks if signature in proof.witness is valid for pubkey in proof.secret
|
||||||
Condition: HTLC - Witness: proof.htlcpreimage, proof.htlcsignature
|
Condition: HTLC - Checks if preimage in proof.witness is valid for hash in proof.secret
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -35,134 +208,118 @@ class LedgerSpendingConditions:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# P2PK
|
# P2PK
|
||||||
if secret.kind == SecretKind.P2PK:
|
if SecretKind(secret.kind) == SecretKind.P2PK:
|
||||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
return self._verify_p2pk_spending_conditions(proof, secret)
|
||||||
# check if locktime is in the past
|
|
||||||
pubkeys = p2pk_secret.get_p2pk_pubkey_from_secret()
|
|
||||||
assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique."
|
|
||||||
logger.trace(f"pubkeys: {pubkeys}")
|
|
||||||
# we will get an empty list if the locktime has passed and no refund pubkey is present
|
|
||||||
if not pubkeys:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# now we check the signature
|
# HTLC
|
||||||
if not proof.p2pksigs:
|
if SecretKind(secret.kind) == SecretKind.HTLC:
|
||||||
# no signature present although secret indicates one
|
return self._verify_htlc_spending_conditions(proof, secret)
|
||||||
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
|
# no spending condition present
|
||||||
if len(set(proof.p2pksigs)) != len(proof.p2pksigs):
|
return True
|
||||||
raise TransactionError("p2pk signatures must be unique.")
|
|
||||||
|
|
||||||
# we parse the secret as a P2PK commitment
|
# ------ output spending conditions ------
|
||||||
# assert len(proof.secret.split(":")) == 5, "p2pk secret format invalid."
|
|
||||||
|
|
||||||
# INPUTS: check signatures proof.p2pksigs against pubkey
|
def _verify_output_p2pk_spending_conditions(
|
||||||
|
self, proofs: List[Proof], outputs: List[BlindedMessage]
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
If sigflag==SIG_ALL in proof.secret, check if outputs
|
||||||
|
contain valid signatures for pubkeys in proof.secret.
|
||||||
|
|
||||||
|
We return True
|
||||||
|
- if not all proof.secret are Secret spending condition
|
||||||
|
- if not all secrets are P2PKSecret spending condition
|
||||||
|
- if not all signature.sigflag are SIG_ALL
|
||||||
|
|
||||||
|
We raise an exception:
|
||||||
|
- if not all pubkeys in all secrets are the same
|
||||||
|
- if not all n_sigs in all secrets are the same
|
||||||
|
- if not all signatures in all outputs are unique
|
||||||
|
- if not all signatures in all outputs are valid
|
||||||
|
- if no valid signatures are present
|
||||||
|
- if the signature threshold is not met
|
||||||
|
|
||||||
|
We return True if we successfully validated the spending condition.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
secrets_generic = [Secret.deserialize(p.secret) for p in proofs]
|
||||||
|
p2pk_secrets = [
|
||||||
|
P2PKSecret.from_secret(secret) for secret in secrets_generic
|
||||||
|
]
|
||||||
|
except Exception:
|
||||||
|
# secret is not a spending condition so we treat is a normal secret
|
||||||
|
return True
|
||||||
|
|
||||||
|
# check if all secrets are P2PK
|
||||||
|
# NOTE: This is redundant, because P2PKSecret.from_secret() already checks for the kind
|
||||||
|
# Leaving it in for explicitness
|
||||||
|
if not all(
|
||||||
|
[SecretKind(secret.kind) == SecretKind.P2PK for secret in p2pk_secrets]
|
||||||
|
):
|
||||||
|
# not all secrets are P2PK
|
||||||
|
return True
|
||||||
|
|
||||||
|
# check if all secrets are sigflag==SIG_ALL
|
||||||
|
if not all([secret.sigflag == SigFlags.SIG_ALL for secret in p2pk_secrets]):
|
||||||
|
# not all secrets have sigflag==SIG_ALL
|
||||||
|
return True
|
||||||
|
|
||||||
|
# extract all pubkeys and n_sigs from secrets
|
||||||
|
pubkeys_per_proof = [
|
||||||
|
secret.get_p2pk_pubkey_from_secret() for secret in p2pk_secrets
|
||||||
|
]
|
||||||
|
n_sigs_per_proof = [secret.n_sigs for secret in p2pk_secrets]
|
||||||
|
|
||||||
|
# all pubkeys and n_sigs must be the same
|
||||||
|
assert (
|
||||||
|
len(set([tuple(pubs_output) for pubs_output in pubkeys_per_proof])) == 1
|
||||||
|
), "pubkeys in all proofs must match."
|
||||||
|
assert len(set(n_sigs_per_proof)) == 1, "n_sigs in all proofs must match."
|
||||||
|
|
||||||
|
# TODO: add limit for maximum number of pubkeys
|
||||||
|
|
||||||
|
# validation successful
|
||||||
|
|
||||||
|
pubkeys: List[str] = pubkeys_per_proof[0]
|
||||||
|
# if n_sigs is None, we set it to 1
|
||||||
|
n_sigs: int = n_sigs_per_proof[0] or 1
|
||||||
|
|
||||||
|
logger.trace(f"pubkeys: {pubkeys}")
|
||||||
|
|
||||||
|
# loop over all outputs and check if the signatures are valid for pubkeys with a threshold of n_sig
|
||||||
|
for output in outputs:
|
||||||
# we expect the signature to be on the pubkey (=message) itself
|
# we expect the signature to be on the pubkey (=message) itself
|
||||||
n_sigs_required = p2pk_secret.n_sigs or 1
|
p2pksigs = output.p2pksigs
|
||||||
assert n_sigs_required > 0, "n_sigs must be positive."
|
assert p2pksigs, "no signatures in output."
|
||||||
|
# TODO: add limit for maximum number of signatures
|
||||||
|
|
||||||
# check if enough signatures are present
|
# we check whether any signature is duplicate
|
||||||
assert len(proof.p2pksigs) >= n_sigs_required, (
|
assert len(set(p2pksigs)) == len(
|
||||||
f"not enough signatures provided: {len(proof.p2pksigs)} <"
|
p2pksigs
|
||||||
f" {n_sigs_required}."
|
), "duplicate signatures in output."
|
||||||
)
|
|
||||||
|
|
||||||
n_valid_sigs_per_output = 0
|
n_valid_sigs_per_output = 0
|
||||||
# loop over all signatures in output
|
# loop over all signatures in output
|
||||||
for input_sig in proof.p2pksigs:
|
for sig in p2pksigs:
|
||||||
for pubkey in pubkeys:
|
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(
|
if verify_p2pk_signature(
|
||||||
message=p2pk_secret.serialize().encode("utf-8"),
|
message=output.B_.encode("utf-8"),
|
||||||
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
|
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
|
||||||
signature=bytes.fromhex(input_sig),
|
signature=bytes.fromhex(sig),
|
||||||
):
|
):
|
||||||
n_valid_sigs_per_output += 1
|
n_valid_sigs_per_output += 1
|
||||||
logger.trace(
|
assert n_valid_sigs_per_output, "no valid signature provided for output."
|
||||||
f"p2pk signature on input is valid: {input_sig} on"
|
assert (
|
||||||
f" {pubkey}."
|
n_valid_sigs_per_output >= n_sigs
|
||||||
)
|
), f"signature threshold not met. {n_valid_sigs_per_output} < {n_sigs}."
|
||||||
continue
|
|
||||||
else:
|
|
||||||
logger.trace(
|
|
||||||
f"p2pk signature on input is invalid: {input_sig} on"
|
|
||||||
f" {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(
|
logger.trace(
|
||||||
f"{n_valid_sigs_per_output} of {n_sigs_required} valid signatures"
|
f"{n_valid_sigs_per_output} of {n_sigs} valid signatures found."
|
||||||
" found."
|
|
||||||
)
|
)
|
||||||
|
logger.trace(p2pksigs)
|
||||||
logger.trace(proof.p2pksigs)
|
logger.trace("p2pk signatures on output is valid.")
|
||||||
logger.trace("p2pk signature on inputs is valid.")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
# HTLC
|
|
||||||
if secret.kind == SecretKind.HTLC:
|
|
||||||
htlc_secret = HTLCSecret.from_secret(secret)
|
|
||||||
# time lock
|
|
||||||
# check if locktime is in the past
|
|
||||||
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=htlc_secret.serialize().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
|
|
||||||
return True
|
|
||||||
|
|
||||||
# hash lock
|
|
||||||
assert proof.htlcpreimage, TransactionError("no HTLC preimage provided")
|
|
||||||
|
|
||||||
# first we check whether a correct preimage was included
|
|
||||||
if not hashlib.sha256(
|
|
||||||
bytes.fromhex(proof.htlcpreimage)
|
|
||||||
).digest() == bytes.fromhex(htlc_secret.data):
|
|
||||||
raise TransactionError("HTLC preimage does not match.")
|
|
||||||
|
|
||||||
# then we check whether a signature is 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(
|
|
||||||
message=htlc_secret.serialize().encode("utf-8"),
|
|
||||||
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
|
|
||||||
signature=bytes.fromhex(signature),
|
|
||||||
):
|
|
||||||
# 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
|
|
||||||
return True
|
|
||||||
|
|
||||||
# no spending condition present
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _verify_output_spending_conditions(
|
def _verify_output_spending_conditions(
|
||||||
@@ -170,86 +327,7 @@ class LedgerSpendingConditions:
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify spending conditions:
|
Verify spending conditions:
|
||||||
Condition: P2PK - Witness: output.p2pksigs
|
Condition: P2PK - If sigflag==SIG_ALL in proof.secret, check if outputs contain valid signatures for pubkeys in proof.secret.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# P2PK
|
return self._verify_output_p2pk_spending_conditions(proofs, outputs)
|
||||||
pubkeys_per_proof = []
|
|
||||||
n_sigs = []
|
|
||||||
for proof in proofs:
|
|
||||||
try:
|
|
||||||
secret = P2PKSecret.deserialize(proof.secret)
|
|
||||||
# get all p2pk pubkeys from secrets
|
|
||||||
pubkeys_per_proof.append(secret.get_p2pk_pubkey_from_secret())
|
|
||||||
# get signature threshold from secrets
|
|
||||||
n_sigs.append(secret.n_sigs)
|
|
||||||
except Exception:
|
|
||||||
# secret is not a spending condition so we treat is a normal secret
|
|
||||||
return True
|
|
||||||
# for all proofs all pubkeys must be the same
|
|
||||||
assert (
|
|
||||||
len(set([tuple(pubs_output) for pubs_output in pubkeys_per_proof])) == 1
|
|
||||||
), "pubkeys in all proofs must match."
|
|
||||||
pubkeys = pubkeys_per_proof[0]
|
|
||||||
if not pubkeys:
|
|
||||||
# no pubkeys present
|
|
||||||
return True
|
|
||||||
|
|
||||||
logger.trace(f"pubkeys: {pubkeys}")
|
|
||||||
# TODO: add limit for maximum number of pubkeys
|
|
||||||
|
|
||||||
# for all proofs all n_sigs must be the same
|
|
||||||
assert len(set(n_sigs)) == 1, "n_sigs in all proofs must match."
|
|
||||||
n_sigs_required = n_sigs[0] or 1
|
|
||||||
|
|
||||||
# first we check if all secrets are P2PK
|
|
||||||
if not all(
|
|
||||||
[Secret.deserialize(p.secret).kind == SecretKind.P2PK for p in proofs]
|
|
||||||
):
|
|
||||||
# not all secrets are P2PK
|
|
||||||
return True
|
|
||||||
|
|
||||||
# now we check if any of the secrets has sigflag==SIG_ALL
|
|
||||||
if not any(
|
|
||||||
[
|
|
||||||
P2PKSecret.deserialize(p.secret).sigflag == SigFlags.SIG_ALL
|
|
||||||
for p in proofs
|
|
||||||
]
|
|
||||||
):
|
|
||||||
# no secret has sigflag==SIG_ALL
|
|
||||||
return True
|
|
||||||
|
|
||||||
# loop over all outputs and check if the signatures are valid for pubkeys with a threshold of n_sig
|
|
||||||
for output in outputs:
|
|
||||||
# we expect the signature to be on the pubkey (=message) itself
|
|
||||||
assert output.p2pksigs, "no signatures in output."
|
|
||||||
# TODO: add limit for maximum number of signatures
|
|
||||||
|
|
||||||
# we check whether any signature is duplicate
|
|
||||||
assert len(set(output.p2pksigs)) == len(
|
|
||||||
output.p2pksigs
|
|
||||||
), "duplicate signatures in output."
|
|
||||||
|
|
||||||
n_valid_sigs_per_output = 0
|
|
||||||
# loop over all signatures in output
|
|
||||||
for output_sig in output.p2pksigs:
|
|
||||||
for pubkey in pubkeys:
|
|
||||||
if verify_p2pk_signature(
|
|
||||||
message=output.B_.encode("utf-8"),
|
|
||||||
pubkey=PublicKey(bytes.fromhex(pubkey), raw=True),
|
|
||||||
signature=bytes.fromhex(output_sig),
|
|
||||||
):
|
|
||||||
n_valid_sigs_per_output += 1
|
|
||||||
assert n_valid_sigs_per_output, "no valid signature provided for output."
|
|
||||||
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(output.p2pksigs)
|
|
||||||
logger.trace("p2pk signatures on output is valid.")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class WalletHTLC(SupportsDb):
|
|||||||
tags["pubkeys"] = hashlock_pubkey
|
tags["pubkeys"] = hashlock_pubkey
|
||||||
|
|
||||||
return HTLCSecret(
|
return HTLCSecret(
|
||||||
kind=SecretKind.HTLC,
|
kind=SecretKind.HTLC.value,
|
||||||
data=preimage_hash,
|
data=preimage_hash,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,12 +49,14 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
|||||||
tags["locktime"] = str(
|
tags["locktime"] = str(
|
||||||
int((datetime.now() + timedelta(seconds=locktime_seconds)).timestamp())
|
int((datetime.now() + timedelta(seconds=locktime_seconds)).timestamp())
|
||||||
)
|
)
|
||||||
tags["sigflag"] = SigFlags.SIG_ALL if sig_all else SigFlags.SIG_INPUTS
|
tags["sigflag"] = (
|
||||||
|
SigFlags.SIG_ALL.value if sig_all else SigFlags.SIG_INPUTS.value
|
||||||
|
)
|
||||||
if n_sigs > 1:
|
if n_sigs > 1:
|
||||||
tags["n_sigs"] = str(n_sigs)
|
tags["n_sigs"] = str(n_sigs)
|
||||||
logger.debug(f"After tags: {tags}")
|
logger.debug(f"After tags: {tags}")
|
||||||
return P2PKSecret(
|
return P2PKSecret(
|
||||||
kind=SecretKind.P2PK,
|
kind=SecretKind.P2PK.value,
|
||||||
data=pubkey,
|
data=pubkey,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
)
|
)
|
||||||
@@ -182,7 +184,9 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
|||||||
return proofs
|
return proofs
|
||||||
logger.debug("Spending conditions detected.")
|
logger.debug("Spending conditions detected.")
|
||||||
# P2PK signatures
|
# P2PK signatures
|
||||||
if all([Secret.deserialize(p.secret).kind == SecretKind.P2PK for p in proofs]):
|
if all(
|
||||||
|
[Secret.deserialize(p.secret).kind == SecretKind.P2PK.value for p in proofs]
|
||||||
|
):
|
||||||
logger.debug("P2PK redemption detected.")
|
logger.debug("P2PK redemption detected.")
|
||||||
proofs = await self.add_p2pk_witnesses_to_proofs(proofs)
|
proofs = await self.add_p2pk_witnesses_to_proofs(proofs)
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
|
|||||||
preimage = "00000000000000000000000000000000"
|
preimage = "00000000000000000000000000000000"
|
||||||
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||||
# p2pk test
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||||
for p in send_proofs:
|
for p in send_proofs:
|
||||||
assert HTLCSecret.deserialize(p.secret).data == preimage_hash
|
assert HTLCSecret.deserialize(p.secret).data == preimage_hash
|
||||||
@@ -86,7 +85,6 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet):
|
|||||||
preimage = "00000000000000000000000000000000"
|
preimage = "00000000000000000000000000000000"
|
||||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||||
# p2pk test
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||||
for p in send_proofs:
|
for p in send_proofs:
|
||||||
p.witness = HTLCWitness(preimage=preimage).json()
|
p.witness = HTLCWitness(preimage=preimage).json()
|
||||||
@@ -102,7 +100,6 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet)
|
|||||||
secret = await wallet1.create_htlc_lock(
|
secret = await wallet1.create_htlc_lock(
|
||||||
preimage=preimage[:-5] + "11111"
|
preimage=preimage[:-5] + "11111"
|
||||||
) # wrong preimage
|
) # wrong preimage
|
||||||
# p2pk test
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||||
for p in send_proofs:
|
for p in send_proofs:
|
||||||
p.witness = HTLCWitness(preimage=preimage).json()
|
p.witness = HTLCWitness(preimage=preimage).json()
|
||||||
@@ -121,7 +118,6 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
|
|||||||
secret = await wallet1.create_htlc_lock(
|
secret = await wallet1.create_htlc_lock(
|
||||||
preimage=preimage, hashlock_pubkey=pubkey_wallet1
|
preimage=preimage, hashlock_pubkey=pubkey_wallet1
|
||||||
)
|
)
|
||||||
# p2pk test
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||||
for p in send_proofs:
|
for p in send_proofs:
|
||||||
p.witness = HTLCWitness(preimage=preimage).json()
|
p.witness = HTLCWitness(preimage=preimage).json()
|
||||||
@@ -141,8 +137,6 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet
|
|||||||
secret = await wallet1.create_htlc_lock(
|
secret = await wallet1.create_htlc_lock(
|
||||||
preimage=preimage, hashlock_pubkey=pubkey_wallet1
|
preimage=preimage, hashlock_pubkey=pubkey_wallet1
|
||||||
)
|
)
|
||||||
|
|
||||||
# p2pk test
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||||
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
||||||
for p, s in zip(send_proofs, signatures):
|
for p, s in zip(send_proofs, signatures):
|
||||||
@@ -166,7 +160,6 @@ async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wall
|
|||||||
secret = await wallet1.create_htlc_lock(
|
secret = await wallet1.create_htlc_lock(
|
||||||
preimage=preimage, hashlock_pubkey=pubkey_wallet1
|
preimage=preimage, hashlock_pubkey=pubkey_wallet1
|
||||||
)
|
)
|
||||||
# p2pk test
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||||
|
|
||||||
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
||||||
@@ -192,7 +185,6 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
|
|||||||
locktime_seconds=5,
|
locktime_seconds=5,
|
||||||
locktime_pubkey=pubkey_wallet1,
|
locktime_pubkey=pubkey_wallet1,
|
||||||
)
|
)
|
||||||
# p2pk test
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||||
|
|
||||||
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
||||||
@@ -226,7 +218,6 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
|
|||||||
locktime_seconds=5,
|
locktime_seconds=5,
|
||||||
locktime_pubkey=pubkey_wallet1,
|
locktime_pubkey=pubkey_wallet1,
|
||||||
)
|
)
|
||||||
# p2pk test
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||||
|
|
||||||
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
signatures = await wallet1.sign_p2pk_proofs(send_proofs)
|
||||||
|
|||||||
Reference in New Issue
Block a user