mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-19 10:04:19 +01:00
refactor: HTLC spending conditions (#803)
* refactor nut14 * format * tests * update tests * more tests * format * adjust wallet tests -> 32 bytes preimages * format
This commit is contained in:
37
cashu/core/nuts/nut14.py
Normal file
37
cashu/core/nuts/nut14.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import time
|
||||
from hashlib import sha256
|
||||
|
||||
from ..base import Proof
|
||||
from ..errors import TransactionError
|
||||
from ..htlc import HTLCSecret
|
||||
from ..secret import Secret, SecretKind
|
||||
|
||||
|
||||
def verify_htlc_spending_conditions(
|
||||
proof: Proof,
|
||||
) -> bool:
|
||||
"""
|
||||
Verifies an HTLC spending condition.
|
||||
Either the preimage is provided or the locktime has passed and a refund is requested.
|
||||
"""
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
if not secret.kind or secret.kind != SecretKind.HTLC.value:
|
||||
raise TransactionError("not an HTLC secret.")
|
||||
htlc_secret = HTLCSecret.from_secret(secret)
|
||||
# hash lock
|
||||
if not proof.htlcpreimage:
|
||||
raise TransactionError("no HTLC preimage provided")
|
||||
# verify correct preimage (the hashlock) if the locktime hasn't passed
|
||||
now = time.time()
|
||||
if not htlc_secret.locktime or htlc_secret.locktime > now:
|
||||
try:
|
||||
if len(proof.htlcpreimage) != 64:
|
||||
raise TransactionError("HTLC preimage must be 64 characters hex.")
|
||||
if not sha256(
|
||||
bytes.fromhex(proof.htlcpreimage)
|
||||
).digest() == bytes.fromhex(htlc_secret.data):
|
||||
raise TransactionError("HTLC preimage does not match.")
|
||||
except ValueError:
|
||||
raise TransactionError("invalid preimage for HTLC: not a hex string.")
|
||||
return True
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import hashlib
|
||||
import time
|
||||
from typing import List, Optional, Union
|
||||
|
||||
@@ -10,6 +9,7 @@ from ..core.errors import (
|
||||
TransactionError,
|
||||
)
|
||||
from ..core.htlc import HTLCSecret
|
||||
from ..core.nuts.nut14 import verify_htlc_spending_conditions
|
||||
from ..core.p2pk import (
|
||||
P2PKSecret,
|
||||
SigFlags,
|
||||
@@ -75,56 +75,6 @@ class LedgerSpendingConditions:
|
||||
message_to_sign, pubkeys, proof.p2pksigs, n_sigs
|
||||
)
|
||||
|
||||
def _verify_htlc_spending_conditions(
|
||||
self,
|
||||
proof: Proof,
|
||||
secret: HTLCSecret,
|
||||
message_to_sign: Optional[str] = None,
|
||||
) -> 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
|
||||
"""
|
||||
|
||||
htlc_secret = secret
|
||||
# hash lock
|
||||
if not proof.htlcpreimage:
|
||||
raise TransactionError("no HTLC preimage provided")
|
||||
# verify correct preimage (the hashlock) if the locktime hasn't passed
|
||||
now = time.time()
|
||||
if not htlc_secret.locktime or htlc_secret.locktime > now:
|
||||
if not hashlib.sha256(
|
||||
bytes.fromhex(proof.htlcpreimage)
|
||||
).digest() == bytes.fromhex(htlc_secret.data):
|
||||
raise TransactionError("HTLC preimage does not match.")
|
||||
return True
|
||||
|
||||
def _verify_p2pk_signatures(
|
||||
self,
|
||||
message_to_sign: str,
|
||||
@@ -213,7 +163,7 @@ class LedgerSpendingConditions:
|
||||
# HTLC
|
||||
if SecretKind(secret.kind) == SecretKind.HTLC:
|
||||
htlc_secret = HTLCSecret.from_secret(secret)
|
||||
self._verify_htlc_spending_conditions(proof, htlc_secret)
|
||||
verify_htlc_spending_conditions(proof)
|
||||
return self._verify_p2pk_sig_inputs(proof, htlc_secret)
|
||||
|
||||
# no spending condition present
|
||||
|
||||
88
tests/mint/test_mint_htlc.py
Normal file
88
tests/mint/test_mint_htlc.py
Normal file
@@ -0,0 +1,88 @@
|
||||
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.errors import TransactionError
|
||||
from cashu.core.nuts.nut14 import verify_htlc_spending_conditions
|
||||
|
||||
|
||||
def test_htlc():
|
||||
proof = Proof.from_dict({
|
||||
"amount": 0,
|
||||
"secret": "[\"HTLC\",{\"nonce\":\"66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925\",\"data\":\"4884fdaafea47c29fea7159d0daddd9c085d6200e1359e85bb81736af6b7c837\"}]",
|
||||
"C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
|
||||
"id": "009a1f293253e41e",
|
||||
"witness": "{\"preimage\":\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"}"
|
||||
})
|
||||
|
||||
print(f"{proof.secret = }")
|
||||
htlc_preimage = proof.htlcpreimage
|
||||
assert htlc_preimage
|
||||
|
||||
verify_htlc_spending_conditions(proof)
|
||||
|
||||
def test_htlc_case_insensitive():
|
||||
proof = Proof.from_dict({
|
||||
"amount": 0,
|
||||
"secret": "[\"HTLC\",{\"nonce\":\"66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925\",\"data\":\"4884fdaafea47c29fea7159d0daddd9c085d6200e1359e85bb81736af6b7c837\"}]",
|
||||
"C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
|
||||
"id": "009a1f293253e41e",
|
||||
"witness": "{\"preimage\":\"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF\"}"
|
||||
})
|
||||
|
||||
htlc_preimage = proof.htlcpreimage
|
||||
assert htlc_preimage
|
||||
|
||||
verify_htlc_spending_conditions(proof)
|
||||
|
||||
def test_invalid_preimage():
|
||||
proof = Proof.from_dict({
|
||||
"amount": 0,
|
||||
"secret": "[\"HTLC\",{\"nonce\":\"72996563049cc84daa2c3f31fd5c3d10770e69d6ebbb8da5b6d76db303dbae43\",\"data\":\"c2f480d4dda9f4522b9f6d590011636d904accfe59f12f9d66a0221c2558e3a2\"}]",
|
||||
"C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
|
||||
"id": "009a1f293253e41e",
|
||||
"witness": "{\"preimage\":\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"}"
|
||||
})
|
||||
|
||||
htlc_preimage = proof.htlcpreimage
|
||||
assert htlc_preimage
|
||||
|
||||
try:
|
||||
verify_htlc_spending_conditions(proof)
|
||||
assert False, "Expected a TransactionError"
|
||||
except TransactionError as e:
|
||||
assert "HTLC preimage does not match." in e.detail
|
||||
|
||||
def test_htlc_preimage_too_large():
|
||||
proof = Proof.from_dict({
|
||||
"amount": 0,
|
||||
"secret": "[\"HTLC\",{\"nonce\":\"66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925\",\"data\":\"c2f480d4dda9f4522b9f6d590011636d904accfe59f12f9d66a0221c2558e3a2\"}]",
|
||||
"C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
|
||||
"id": "009a1f293253e41e",
|
||||
"witness": "{\"preimage\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"}"
|
||||
})
|
||||
|
||||
htlc_preimage = proof.htlcpreimage
|
||||
assert htlc_preimage
|
||||
|
||||
try:
|
||||
verify_htlc_spending_conditions(proof)
|
||||
assert False, "Expected a TransactionError"
|
||||
except TransactionError as e:
|
||||
assert "HTLC preimage must be 64 characters hex." in e.detail
|
||||
|
||||
def test_htlc_nonhex_preimage():
|
||||
proof = Proof.from_dict({
|
||||
"amount": 0,
|
||||
"secret": "[\"HTLC\",{\"nonce\":\"66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925\",\"data\":\"72996563049cc84daa2c3f31fd5c3d10770e69d6ebbb8da5b6d76db303dbae43\"}]",
|
||||
"C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
|
||||
"id": "009a1f293253e41e",
|
||||
"witness": "{\"preimage\":\"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\"}"
|
||||
})
|
||||
|
||||
htlc_preimage = proof.htlcpreimage
|
||||
assert htlc_preimage
|
||||
|
||||
try:
|
||||
verify_htlc_spending_conditions(proof)
|
||||
assert False, "Expected a TransactionError"
|
||||
except TransactionError as e:
|
||||
assert "invalid preimage for HTLC: not a hex string." in e.detail
|
||||
@@ -63,7 +63,7 @@ async def test_create_htlc_secret(wallet1: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
assert secret.data == preimage_hash
|
||||
@@ -74,7 +74,7 @@ async def test_htlc_split(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
@@ -87,7 +87,7 @@ async def test_htlc_redeem_with_preimage(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(preimage=preimage)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
@@ -101,7 +101,7 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet)
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=f"{preimage[:-5]}11111"
|
||||
@@ -110,7 +110,7 @@ async def test_htlc_redeem_with_wrong_preimage(wallet1: Wallet, wallet2: Wallet)
|
||||
for p in send_proofs:
|
||||
p.witness = HTLCWitness(preimage=preimage).json()
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs), "Mint Error: HTLC preimage does not match"
|
||||
wallet1.redeem(send_proofs), "Mint Error: HTLC preimage does not match."
|
||||
)
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ async def test_htlc_redeem_with_no_signature(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
@@ -139,7 +139,7 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
@@ -163,7 +163,7 @@ async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wall
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
@@ -183,7 +183,7 @@ async def test_htlc_redeem_with_2_of_1_signatures(wallet1: Wallet, wallet2: Wall
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -207,7 +207,7 @@ async def test_htlc_redeem_with_2_of_2_signatures(wallet1: Wallet, wallet2: Wall
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -233,7 +233,7 @@ async def test_htlc_redeem_with_2_of_2_signatures_with_duplicate_pubkeys(
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = pubkey_wallet1
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -262,7 +262,7 @@ async def test_htlc_redeem_with_3_of_3_signatures_but_only_2_provided(
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -291,7 +291,7 @@ async def test_htlc_redeem_with_2_of_3_signatures_with_2_valid_and_1_invalid_pro
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
privatekey_wallet3 = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
@@ -322,7 +322,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -356,7 +356,7 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -395,7 +395,7 @@ async def test_htlc_redeem_timelock_2_of_2_signatures(wallet1: Wallet, wallet2:
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
@@ -444,7 +444,7 @@ async def test_htlc_sigall_behavior(wallet1: Wallet, wallet2: Wallet):
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Setup HTLC parameters
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
|
||||
@@ -495,13 +495,13 @@ async def test_htlc_n_sigs_refund_locktime(wallet1: Wallet, wallet2: Wallet):
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Setup parameters
|
||||
preimage = "00000000000000000000000000000000"
|
||||
preimage = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey()
|
||||
|
||||
# Wrong preimage - making it so we can only spend via locktime
|
||||
wrong_preimage = "11111111111111111111111111111111"
|
||||
wrong_preimage = "1111111111111111111111111111111111111111111111111111111111111111"
|
||||
|
||||
# Create HTLC with:
|
||||
# 1. Timelock in the past
|
||||
|
||||
Reference in New Issue
Block a user