diff --git a/cashu/core/nuts/nut14.py b/cashu/core/nuts/nut14.py new file mode 100644 index 0000000..5b7ce98 --- /dev/null +++ b/cashu/core/nuts/nut14.py @@ -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 + diff --git a/cashu/mint/conditions.py b/cashu/mint/conditions.py index d896639..cbb4e84 100644 --- a/cashu/mint/conditions.py +++ b/cashu/mint/conditions.py @@ -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 diff --git a/tests/mint/test_mint_htlc.py b/tests/mint/test_mint_htlc.py new file mode 100644 index 0000000..f6afdda --- /dev/null +++ b/tests/mint/test_mint_htlc.py @@ -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 diff --git a/tests/wallet/test_wallet_htlc.py b/tests/wallet/test_wallet_htlc.py index 8a2ab36..b8fb6b2 100644 --- a/tests/wallet/test_wallet_htlc.py +++ b/tests/wallet/test_wallet_htlc.py @@ -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