[Wallet/mint] P2PK with timelocks (#270)

* p2pk with nostr privatekey and timelocks

* add p2pk

* fix test

* fix test with custom secret

* sign whole split transaction

* p2pk signature now commits to entire secret and thus to a nonce

* use schnorr signatures

* revamp P2SH and P2PK with new Secret model

* test p2pk

* add comments

* add nostr private key to tests

* fix nostr receive

* make format

* test redemption after timelock

* refactor Server.serialize()

* sign sha256(secret)

* add optional refund pubkey that triggers after timelock

* use nostr private key for now (including nsec parser)

* use nostr private key and fix tests

* bump version to 0.12.2
This commit is contained in:
callebtc
2023-07-02 01:56:05 +02:00
committed by GitHub
parent 4beaf8ff41
commit 01d498309b
15 changed files with 548 additions and 231 deletions

View File

@@ -1,5 +1,7 @@
import asyncio
import json
import math
import time
from typing import Dict, List, Literal, Optional, Set, Union
from loguru import logger
@@ -12,13 +14,16 @@ from ..core.base import (
MintKeyset,
MintKeysets,
Proof,
Secret,
SecretKind,
)
from ..core.crypto import b_dhke
from ..core.crypto.keys import derive_pubkey, random_hash
from ..core.crypto.secp import PublicKey
from ..core.db import Connection, Database
from ..core.helpers import fee_reserve, sum_proofs
from ..core.script import verify_script
from ..core.p2pk import verify_p2pk_signature
from ..core.script import verify_bitcoin_script
from ..core.settings import settings
from ..core.split import amount_split
from ..lightning.base import Wallet
@@ -185,7 +190,7 @@ class Ledger:
"""Verifies that a secret is present and is not too long (DOS prevention)."""
if proof.secret is None or proof.secret == "":
raise Exception("no secret in proof.")
if len(proof.secret) > 64:
if len(proof.secret) > 512:
raise Exception("secret too long.")
return True
@@ -209,35 +214,90 @@ class Ledger:
C = PublicKey(bytes.fromhex(proof.C), raw=True)
return b_dhke.verify(private_key_amount, C, proof.secret)
def _verify_script(self, idx: int, proof: Proof) -> bool:
def _verify_spending_conditions(self, proof: Proof) -> bool:
"""
Verify bitcoin script in proof.script commited to by <address> in proof.secret.
proof.secret format: P2SH:<address>:<secret>
Verify spending conditions:
Condition: P2SH - Witnesses proof.p2shscript
Condition: P2PK - Witness: proof.p2pksig
"""
# if no script is given
if (
proof.script is None
or proof.script.script is None
or proof.script.signature is None
):
if len(proof.secret.split("P2SH:")) == 2:
# secret indicates a script but no script is present
return False
else:
# secret indicates no script, so treat script as valid
# P2SH
try:
secret = Secret.deserialize(proof.secret)
except Exception as e:
# secret is not a spending condition so we treat is a normal secret
return True
if secret.kind == SecretKind.P2SH:
# check if timelock is in the past
now = time.time()
if secret.timelock and secret.timelock < now:
logger.trace(f"p2sh timelock ran out ({secret.timelock}<{now}).")
return True
# execute and verify P2SH
txin_p2sh_address, valid = verify_script(
proof.script.script, proof.script.signature
)
if valid:
logger.trace(f"p2sh timelock still active ({secret.timelock}>{now}).")
if (
proof.p2shscript is None
or proof.p2shscript.script is None
or proof.p2shscript.signature is None
):
# no script present although secret indicates one
raise Exception("no script in proof.")
# execute and verify P2SH
txin_p2sh_address, valid = verify_bitcoin_script(
proof.p2shscript.script, proof.p2shscript.signature
)
if not valid:
raise Exception("script invalid.")
# check if secret commits to script address
# format: P2SH:<address>:<secret>
assert len(proof.secret.split(":")) == 3, "secret format wrong."
assert proof.secret.split(":")[1] == str(
assert secret.data == str(
txin_p2sh_address
), f"secret does not contain correct P2SH address: {proof.secret.split(':')[1]} is not {txin_p2sh_address}."
return valid
), f"secret does not contain correct P2SH address: {secret.data} is not {txin_p2sh_address}."
return True
# P2PK
if secret.kind == SecretKind.P2PK:
# check if timelock is in the past
now = time.time()
if secret.timelock and secret.timelock < now:
logger.trace(f"p2pk timelock ran out ({secret.timelock}<{now}).")
# check tags if a refund pubkey is present.
# If yes, we demand the signature to be from the refund pubkey
if secret.tags and secret.tags.get_tag("refund"):
signature_pubkey = secret.tags.get_tag("refund")
else:
# if no refund pubkey is present and the timelock has expired
# the token can be spent by anyone
return True
else:
# the timelock is still active, therefore we demand the signature
# to be from the pubkey in the data field
signature_pubkey = secret.data
logger.trace(f"p2pk timelock still active ({secret.timelock}>{now}).")
# now we check the signature
if not proof.p2pksig:
# no signature present although secret indicates one
raise Exception("no p2pk signature in proof.")
# we parse the secret as a P2PK commitment
# assert len(proof.secret.split(":")) == 5, "p2pk secret format invalid."
# check signature proof.p2pksig against pubkey
# we expect the signature to be on the pubkey (=message) itself
assert signature_pubkey, "no signature pubkey present."
assert verify_p2pk_signature(
message=secret.serialize().encode("utf-8"),
pubkey=PublicKey(bytes.fromhex(signature_pubkey), raw=True),
signature=bytes.fromhex(proof.p2pksig),
), "p2pk signature invalid."
logger.trace(proof.p2pksig)
logger.trace("p2pk signature valid.")
return True
# no spending contition
return True
def _verify_outputs(
self, total: int, amount: int, outputs: List[BlindedMessage]
@@ -509,7 +569,7 @@ class Ledger:
Exception: BDHKE verification failed.
"""
# Verify scripts
if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]):
if not all([self._verify_spending_conditions(p) for p in proofs]):
raise Exception("script validation failed.")
# Verify secret criteria
if not all([self._verify_secret_criteria(p) for p in proofs]):