mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-09 11:44:20 +01:00
SIG_ALL signature flag for P2PK (#735)
* n_sigs_refund working, tests added * update requirements * wip sigall * wip * sigall works * add signatures for refund * add mint p2pk tests * add more p2pk tests * fix tests * sign htlc pubkeys as well * fix htlc and add new test * fix regtest * fix new tests with deprecated * remove asserts * comments * new wallet p2pk tests * getting there * add more tests * fixes * refactor htlc and p2pk validation * reduce code * melt with sigall * fix htlcs * fix deprecated api tests * Update cashu/mint/conditions.py Co-authored-by: lollerfirst <43107113+lollerfirst@users.noreply.github.com> * refactor sigall validation --------- Co-authored-by: lollerfirst <43107113+lollerfirst@users.noreply.github.com>
This commit is contained in:
@@ -215,7 +215,7 @@ def pay_onchain(address: str, sats: int) -> str:
|
||||
return run_cmd(cmd)
|
||||
|
||||
|
||||
async def pay_if_regtest(bolt11: str):
|
||||
async def pay_if_regtest(bolt11: str) -> None:
|
||||
if is_regtest:
|
||||
pay_real_invoice(bolt11)
|
||||
if is_fake:
|
||||
|
||||
@@ -2,6 +2,7 @@ import pytest
|
||||
|
||||
from cashu.core.base import TokenV3, TokenV4, Unit
|
||||
from cashu.core.helpers import calculate_number_of_blank_outputs
|
||||
from cashu.core.secret import Secret, SecretKind, Tags
|
||||
from cashu.core.split import amount_split
|
||||
from cashu.wallet.helpers import deserialize_token_from_string
|
||||
|
||||
@@ -262,3 +263,52 @@ def test_parse_token_v3_v4_base64_keyset_id():
|
||||
# this token can not be serialized to V4
|
||||
token = deserialize_token_from_string(token_v3_base64_keyset_serialized)
|
||||
assert isinstance(token, TokenV3)
|
||||
|
||||
|
||||
def test_secret_equality():
|
||||
assert Secret(
|
||||
kind=SecretKind.P2PK.value, data="asd", tags=Tags([["asd", "wasd"], ["mew"]])
|
||||
) == Secret(
|
||||
kind=SecretKind.P2PK.value, data="asd", tags=Tags([["asd", "wasd"], ["mew"]])
|
||||
)
|
||||
|
||||
|
||||
def test_secret_set_dict():
|
||||
d = dict()
|
||||
s = Secret(
|
||||
kind=SecretKind.P2PK.value,
|
||||
data="asd",
|
||||
tags=Tags([["asd", "wasd"], ["mew"]]),
|
||||
nonce="abcd",
|
||||
)
|
||||
s2 = Secret(
|
||||
kind=SecretKind.P2PK.value,
|
||||
data="asd",
|
||||
tags=Tags([["asd", "wasd"], ["mew"]]),
|
||||
nonce="efgh",
|
||||
)
|
||||
# test set
|
||||
assert len(set([s, s2])) == 1
|
||||
# test dict
|
||||
d[s] = "test"
|
||||
assert d[s] == "test"
|
||||
assert (
|
||||
d[
|
||||
Secret(
|
||||
kind=SecretKind.P2PK.value,
|
||||
data="asd",
|
||||
tags=Tags([["asd", "wasd"], ["mew"]]),
|
||||
)
|
||||
]
|
||||
== "test"
|
||||
)
|
||||
assert (
|
||||
d[
|
||||
Secret(
|
||||
kind=SecretKind.P2PK.value,
|
||||
data="asd",
|
||||
tags=Tags([["asd", "wasd"], ["mew"]]),
|
||||
)
|
||||
]
|
||||
== "test"
|
||||
)
|
||||
|
||||
@@ -159,7 +159,7 @@ async def test_api_keyset_keys_old_keyset_id(ledger: Ledger):
|
||||
settings.debug_mint_only_deprecated,
|
||||
reason="settings.debug_mint_only_deprecated is set",
|
||||
)
|
||||
async def test_split(ledger: Ledger, wallet: Wallet):
|
||||
async def test_swap(ledger: Ledger, wallet: Wallet):
|
||||
mint_quote = await wallet.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
323
tests/test_mint_p2pk.py
Normal file
323
tests/test_mint_p2pk.py
Normal file
@@ -0,0 +1,323 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import P2PKWitness
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(ledger: Ledger):
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_inputs_require_sigall_detection(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test the ledger function that detects if any inputs require SIG_ALL."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create two proofs: one with SIG_INPUTS and one with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
|
||||
# Create a proof with SIG_INPUTS
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
_, send_proofs_inputs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
|
||||
# Create a new mint quote for the second mint operation
|
||||
mint_quote_2 = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote_2.request)
|
||||
await ledger.get_mint_quote(mint_quote_2.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote_2.quote)
|
||||
|
||||
# Create a proof with SIG_ALL
|
||||
secret_lock_all = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, send_proofs_all = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_all
|
||||
)
|
||||
|
||||
# Test that _inputs_require_sigall correctly detects SIG_ALL flag
|
||||
assert not ledger._inputs_require_sigall(
|
||||
send_proofs_inputs
|
||||
), "Should not detect SIG_ALL"
|
||||
assert ledger._inputs_require_sigall(send_proofs_all), "Should detect SIG_ALL"
|
||||
|
||||
# Test with a mixed list of proofs (should detect SIG_ALL if any proof has it)
|
||||
mixed_proofs = send_proofs_inputs + send_proofs_all
|
||||
assert ledger._inputs_require_sigall(
|
||||
mixed_proofs
|
||||
), "Should detect SIG_ALL in mixed list"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_verify_p2pk_signature_validation(
|
||||
wallet1: Wallet1, ledger: Ledger
|
||||
):
|
||||
"""Test the signature validation for P2PK inputs."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Create locked tokens
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Sign the tokens
|
||||
signed_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
assert len(signed_proofs) > 0, "Should have signed proofs"
|
||||
|
||||
# Verify that a valid witness was added to the proofs
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness is not None, "Proof should have a witness"
|
||||
witness = P2PKWitness.from_witness(proof.witness)
|
||||
assert len(witness.signatures) > 0, "Witness should have a signature"
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# The swap should succeed because the signatures are valid
|
||||
promises = await ledger.swap(proofs=signed_proofs, outputs=outputs)
|
||||
assert len(promises) == len(
|
||||
outputs
|
||||
), "Should have the same number of promises as outputs"
|
||||
|
||||
# Test for a failure
|
||||
# Create a fake witness with an incorrect signature
|
||||
fake_signature = "0" * 128 # Just a fake 64-byte hex string
|
||||
for proof in send_proofs:
|
||||
proof.witness = P2PKWitness(signatures=[fake_signature]).json()
|
||||
|
||||
# The swap should fail because the signatures are invalid
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"signature threshold not met",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_verify_incorrect_signature(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test rejection of incorrect signatures for P2PK inputs."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Create locked tokens
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create a fake witness with an incorrect signature
|
||||
fake_signature = "0" * 128 # Just a fake 64-byte hex string
|
||||
for proof in send_proofs:
|
||||
proof.witness = P2PKWitness(signatures=[fake_signature]).json()
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# The swap should fail because the signatures are invalid
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"signature threshold not met",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_verify_sigall_validation(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test validation of SIG_ALL signature that covers both inputs and outputs."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
|
||||
# Create locked tokens
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Create the message to sign (all inputs + all outputs)
|
||||
message_to_sign = "".join([p.secret for p in send_proofs] + [o.B_ for o in outputs])
|
||||
|
||||
# Sign the message with the wallet's private key
|
||||
signature = wallet1.schnorr_sign_message(message_to_sign)
|
||||
|
||||
# Add the signature to the first proof only (as required for SIG_ALL)
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[signature]).json()
|
||||
|
||||
# The swap should succeed because the SIG_ALL signature is valid
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(
|
||||
outputs
|
||||
), "Should have the same number of promises as outputs"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_verify_incorrect_sigall_signature(
|
||||
wallet1: Wallet1, ledger: Ledger
|
||||
):
|
||||
"""Test rejection of incorrect SIG_ALL signatures."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
|
||||
# Create locked tokens
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Create a fake witness with an incorrect signature
|
||||
fake_signature = "0" * 128 # Just a fake 64-byte hex string
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[fake_signature]).json()
|
||||
|
||||
# The swap should fail because the SIG_ALL signature is invalid
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"signature threshold not met",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_swap_p2pk_without_signature(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test ledger swap with p2pk locked tokens without providing signatures."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# Create a p2pk lock with wallet's own public key
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Attempt to swap WITHOUT adding signatures - this should fail
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"Witness is missing for p2pk signature",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ledger_swap_p2pk_with_signature(wallet1: Wallet1, ledger: Ledger):
|
||||
"""Test ledger swap with p2pk locked tokens with proper signatures."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await ledger.get_mint_quote(mint_quote.quote)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# Create a p2pk lock with wallet's own public key
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Generate outputs for the swap
|
||||
output_amounts = [32]
|
||||
secrets, rs, derivation_paths = await wallet1.generate_n_secrets(
|
||||
len(output_amounts)
|
||||
)
|
||||
outputs, rs = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Sign the p2pk inputs before sending to the ledger
|
||||
signed_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
# Extract signed proofs and put them back in the send_proofs list
|
||||
signed_proofs_secrets = [p.secret for p in signed_proofs]
|
||||
for p in send_proofs:
|
||||
if p.secret in signed_proofs_secrets:
|
||||
send_proofs[send_proofs.index(p)] = signed_proofs[
|
||||
signed_proofs_secrets.index(p.secret)
|
||||
]
|
||||
|
||||
# Now swap with signatures - this should succeed
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
|
||||
# Verify the result
|
||||
assert len(promises) == len(outputs)
|
||||
assert [p.amount for p in promises] == [o.amount for o in outputs]
|
||||
635
tests/test_mint_p2pk_comprehensive.py
Normal file
635
tests/test_mint_p2pk_comprehensive.py
Normal file
@@ -0,0 +1,635 @@
|
||||
import copy
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import BlindedMessage, P2PKWitness
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import P2PKSecret, SigFlags
|
||||
from cashu.core.secret import Secret, SecretKind, Tags
|
||||
from cashu.mint.ledger import Ledger
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(ledger: Ledger):
|
||||
wallet1 = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet1_p2pk_comprehensive",
|
||||
name="wallet1",
|
||||
)
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(ledger: Ledger):
|
||||
wallet2 = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet2_p2pk_comprehensive",
|
||||
name="wallet2",
|
||||
)
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
await wallet2.load_mint()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet3(ledger: Ledger):
|
||||
wallet3 = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="test_data/wallet3_p2pk_comprehensive",
|
||||
name="wallet3",
|
||||
)
|
||||
await migrate_databases(wallet3.db, migrations)
|
||||
await wallet3.load_mint()
|
||||
yield wallet3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_inputs_basic(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test basic P2PK with SIG_INPUTS."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Verify wallet1 has tokens
|
||||
assert wallet1.balance == 64
|
||||
|
||||
# Create locked tokens from wallet1 to wallet2
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Verify that sent tokens have P2PK secrets with SIG_INPUTS flag
|
||||
for proof in send_proofs:
|
||||
p2pk_secret = Secret.deserialize(proof.secret)
|
||||
assert p2pk_secret.kind == SecretKind.P2PK.value
|
||||
assert P2PKSecret.from_secret(p2pk_secret).sigflag == SigFlags.SIG_INPUTS
|
||||
|
||||
# Try to redeem without signatures (should fail)
|
||||
unsigned_proofs = copy.deepcopy(send_proofs)
|
||||
for proof in unsigned_proofs:
|
||||
proof.witness = None
|
||||
await assert_err(
|
||||
ledger.swap(
|
||||
proofs=unsigned_proofs, outputs=await create_test_outputs(wallet2, 16)
|
||||
),
|
||||
"Witness is missing for p2pk signature",
|
||||
)
|
||||
|
||||
# Redeem with proper signatures
|
||||
signed_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)
|
||||
assert all(p.witness is not None for p in signed_proofs)
|
||||
|
||||
# Now swap should succeed
|
||||
outputs = await create_test_outputs(wallet2, 16)
|
||||
promises = await ledger.swap(proofs=signed_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all_valid(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test P2PK with SIG_ALL where the signature covers both inputs and outputs."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create locked tokens with SIG_ALL
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, sig_all=True)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Verify that sent tokens have P2PK secrets with SIG_ALL flag
|
||||
for proof in send_proofs:
|
||||
p2pk_secret = Secret.deserialize(proof.secret)
|
||||
assert p2pk_secret.kind == SecretKind.P2PK.value
|
||||
assert P2PKSecret.from_secret(p2pk_secret).sigflag == SigFlags.SIG_ALL
|
||||
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet2, 16)
|
||||
|
||||
# Create a message from concatenated inputs and outputs
|
||||
message_to_sign = "".join([p.secret for p in send_proofs] + [o.B_ for o in outputs])
|
||||
|
||||
# Sign with wallet2's private key
|
||||
signature = wallet2.schnorr_sign_message(message_to_sign)
|
||||
|
||||
# Add the signature to the first proof only (since it's SIG_ALL)
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[signature]).json()
|
||||
|
||||
# Swap should succeed
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all_invalid(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test P2PK with SIG_ALL where the signature is invalid."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create locked tokens with SIG_ALL
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, sig_all=True)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet2, 16)
|
||||
|
||||
# Add an invalid signature
|
||||
fake_signature = "0" * 128 # Just a fake 64-byte hex string
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[fake_signature]).json()
|
||||
|
||||
# Swap should fail
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs), "signature threshold not met"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all_mixed(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test that attempting to use mixed SIG_ALL and SIG_INPUTS proofs fails."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(128)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(128, quote_id=mint_quote.quote)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet2, 32) # 16 + 16
|
||||
|
||||
# Create a proof with SIG_ALL
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
secret_lock_all = await wallet1.create_p2pk_lock(pubkey_wallet2, sig_all=True)
|
||||
_, proofs_sig_all = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_all
|
||||
)
|
||||
# sign proofs_sig_all
|
||||
signed_proofs_sig_all = wallet2.add_witness_swap_sig_all(proofs_sig_all, outputs)
|
||||
|
||||
# Mint more tokens to wallet1 for the SIG_INPUTS test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a proof with SIG_INPUTS
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey_wallet2, sig_all=False)
|
||||
_, proofs_sig_inputs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
# sign proofs_sig_inputs
|
||||
signed_proofs_sig_inputs = wallet2.sign_p2pk_sig_inputs(proofs_sig_inputs)
|
||||
|
||||
# Combine the proofs
|
||||
mixed_proofs = signed_proofs_sig_all + signed_proofs_sig_inputs
|
||||
|
||||
# Add an invalid signature to the SIG_ALL proof
|
||||
mixed_proofs[0].witness = P2PKWitness(signatures=["0" * 128]).json()
|
||||
|
||||
# Try to use the mixed proofs (should fail)
|
||||
await assert_err(
|
||||
ledger.swap(proofs=mixed_proofs, outputs=outputs),
|
||||
"not all secrets are equal.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_2_of_3(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with 2-of-3 multisig."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(6400)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(6400, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys from all wallets
|
||||
pubkey1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey2 = await wallet2.create_p2pk_pubkey()
|
||||
pubkey3 = await wallet3.create_p2pk_pubkey()
|
||||
|
||||
# Create 2-of-3 multisig tokens locked to all three wallets
|
||||
tags = Tags([["pubkeys", pubkey2, pubkey3]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey1, tags=tags, n_sigs=2)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Sign with wallet1 (first signature)
|
||||
signed_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
# Try to redeem with only 1 signature (should fail)
|
||||
await assert_err(
|
||||
ledger.swap(proofs=signed_proofs, outputs=outputs),
|
||||
"not enough pubkeys (3) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
# Mint new tokens for the second test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create new locked tokens
|
||||
_, send_proofs2 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Sign with wallet1 (first signature)
|
||||
signed_proofs2 = wallet1.sign_p2pk_sig_inputs(send_proofs2)
|
||||
|
||||
# Add signature from wallet2 (second signature)
|
||||
signed_proofs2 = wallet2.sign_p2pk_sig_inputs(signed_proofs2)
|
||||
|
||||
# Now redemption should succeed with 2 of 3 signatures
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
promises = await ledger.swap(proofs=signed_proofs2, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
# Mint new tokens for the third test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create new locked tokens
|
||||
_, send_proofs3 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Alternative: sign with wallet1 and wallet3
|
||||
signed_proofs3 = wallet1.sign_p2pk_sig_inputs(send_proofs3)
|
||||
signed_proofs3 = wallet3.sign_p2pk_sig_inputs(signed_proofs3)
|
||||
|
||||
# This should also succeed
|
||||
# Create outputs for redemption
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
promises2 = await ledger.swap(proofs=signed_proofs3, outputs=outputs)
|
||||
assert len(promises2) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_timelock(wallet1: Wallet, wallet2: Wallet, ledger: Ledger):
|
||||
"""Test P2PK with a timelock that expires."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create tokens with a 2-second timelock
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
# Set a past timestamp to ensure test works consistently
|
||||
past_time = int(time.time()) - 10
|
||||
tags = Tags([["locktime", str(past_time)]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, tags=tags)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Store current time to check if locktime passed
|
||||
locktime = 0
|
||||
for proof in send_proofs:
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||
locktime = p2pk_secret.locktime
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Verify that current time is past the locktime
|
||||
assert locktime is not None, "Locktime should not be None"
|
||||
assert (
|
||||
int(time.time()) > locktime
|
||||
), f"Current time ({int(time.time())}) should be greater than locktime ({locktime})"
|
||||
|
||||
# Try to redeem without signature after locktime (should succeed)
|
||||
unsigned_proofs = copy.deepcopy(send_proofs)
|
||||
for proof in unsigned_proofs:
|
||||
proof.witness = None
|
||||
|
||||
promises = await ledger.swap(proofs=unsigned_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_timelock_with_refund_before_locktime(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with a timelock and refund pubkeys before locktime."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # Receiver
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey() # Refund key
|
||||
|
||||
# Create tokens with a 2-second timelock and refund key
|
||||
future_time = int(time.time()) + 60 # 60 seconds in the future
|
||||
refund_tags = Tags([["refund", pubkey_wallet3], ["locktime", str(future_time)]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, tags=refund_tags)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Try to redeem without any signature before locktime (should fail)
|
||||
unsigned_proofs = copy.deepcopy(send_proofs)
|
||||
for proof in unsigned_proofs:
|
||||
proof.witness = None
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=unsigned_proofs, outputs=outputs),
|
||||
"Witness is missing for p2pk signature",
|
||||
)
|
||||
|
||||
# Try to redeem with refund key signature before locktime (should fail)
|
||||
refund_signed_proofs = wallet3.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=refund_signed_proofs, outputs=outputs),
|
||||
"signature threshold not met", # Refund key can't be used before locktime
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_timelock_with_receiver_signature(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with a timelock and refund pubkeys with receiver signature."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # Receiver
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey() # Refund key
|
||||
|
||||
# Create tokens with a 2-second timelock and refund key
|
||||
future_time = int(time.time()) + 60 # 60 seconds in the future
|
||||
refund_tags = Tags([["refund", pubkey_wallet3], ["locktime", str(future_time)]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2, tags=refund_tags)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Try to redeem with the correct receiver signature (should succeed)
|
||||
receiver_signed_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
promises = await ledger.swap(proofs=receiver_signed_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_timelock_with_refund_after_locktime(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with a timelock and refund pubkeys after locktime."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # Receiver
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey() # Refund key
|
||||
|
||||
# Create tokens with a past timestamp for locktime testing
|
||||
past_time = int(time.time()) - 10 # 10 seconds in the past
|
||||
refund_tags_past = Tags([["refund", pubkey_wallet3], ["locktime", str(past_time)]])
|
||||
secret_lock_past = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=refund_tags_past
|
||||
)
|
||||
_, send_proofs3 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_past
|
||||
)
|
||||
|
||||
# Try to redeem with refund key after locktime (should succeed)
|
||||
refund_signed_proofs2 = wallet3.sign_p2pk_sig_inputs(send_proofs3)
|
||||
|
||||
# This should work because locktime has passed and refund key is used
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
promises2 = await ledger.swap(proofs=refund_signed_proofs2, outputs=outputs)
|
||||
assert len(promises2) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_n_sigs_refund(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test P2PK with a timelock and multiple refund pubkeys with n_sigs_refund."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # Receiver
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # Refund key 1
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey() # Refund key 2
|
||||
|
||||
# Create tokens with a future timelock and 2-of-2 refund requirement
|
||||
future_time = int(time.time()) + 60 # 60 seconds in the future
|
||||
refund_tags = Tags(
|
||||
[
|
||||
["refund", pubkey_wallet2, pubkey_wallet3],
|
||||
["n_sigs_refund", "2"],
|
||||
["locktime", str(future_time)],
|
||||
]
|
||||
)
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet1, tags=refund_tags)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Mint new tokens for receiver test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create new locked tokens
|
||||
_, send_proofs2 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Try to redeem with receiver key (should succeed before locktime)
|
||||
receiver_signed_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs2)
|
||||
promises = await ledger.swap(proofs=receiver_signed_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
# Mint new tokens for the refund test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create tokens with a past locktime for refund testing
|
||||
past_time = int(time.time()) - 10 # 10 seconds in the past
|
||||
refund_tags_past = Tags(
|
||||
[
|
||||
["refund", pubkey_wallet2, pubkey_wallet3],
|
||||
["n_sigs_refund", "2"],
|
||||
["locktime", str(past_time)],
|
||||
]
|
||||
)
|
||||
secret_lock_past = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet1, tags=refund_tags_past
|
||||
)
|
||||
_, send_proofs3 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_past
|
||||
)
|
||||
|
||||
# Try to redeem with only one refund key signature (should fail)
|
||||
refund_signed_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs3)
|
||||
|
||||
await assert_err(
|
||||
ledger.swap(proofs=refund_signed_proofs, outputs=outputs),
|
||||
"not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
# Mint new tokens for the final test
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create tokens with same past locktime
|
||||
_, send_proofs4 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_past
|
||||
)
|
||||
|
||||
# Add both refund signatures
|
||||
refund_signed_proofs2 = wallet2.sign_p2pk_sig_inputs(send_proofs4)
|
||||
refund_signed_proofs2 = wallet3.sign_p2pk_sig_inputs(refund_signed_proofs2)
|
||||
|
||||
# Now it should succeed with 2-of-2 refund signatures
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
promises2 = await ledger.swap(proofs=refund_signed_proofs2, outputs=outputs)
|
||||
assert len(promises2) == len(outputs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_invalid_pubkey_check(
|
||||
wallet1: Wallet, wallet2: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test that an invalid public key is properly rejected."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create an invalid pubkey string (too short)
|
||||
invalid_pubkey = "03aaff"
|
||||
|
||||
# Try to create a P2PK lock with invalid pubkey
|
||||
# This should fail in create_p2pk_lock, but if it doesn't, let's handle it gracefully
|
||||
try:
|
||||
secret_lock = await wallet1.create_p2pk_lock(invalid_pubkey)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Verify it fails during validation
|
||||
await assert_err(
|
||||
ledger.swap(proofs=send_proofs, outputs=outputs),
|
||||
"failed to deserialize pubkey", # Generic error for pubkey issues
|
||||
)
|
||||
except Exception as e:
|
||||
# If it fails during creation, that's fine too
|
||||
assert (
|
||||
"pubkey" in str(e).lower() or "key" in str(e).lower()
|
||||
), f"Expected error about invalid public key, got: {str(e)}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_sig_all_with_multiple_pubkeys(
|
||||
wallet1: Wallet, wallet2: Wallet, wallet3: Wallet, ledger: Ledger
|
||||
):
|
||||
"""Test SIG_ALL combined with multiple pubkeys/n_sigs."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys
|
||||
pubkey1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey2 = await wallet2.create_p2pk_pubkey()
|
||||
pubkey3 = await wallet3.create_p2pk_pubkey()
|
||||
|
||||
# Create tokens with SIG_ALL and 2-of-3 multisig
|
||||
tags = Tags([["pubkeys", pubkey2, pubkey3]])
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey1, tags=tags, n_sigs=2, sig_all=True
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create outputs
|
||||
outputs = await create_test_outputs(wallet1, 16)
|
||||
|
||||
# Create message to sign (all inputs + all outputs)
|
||||
message_to_sign = "".join([p.secret for p in send_proofs] + [o.B_ for o in outputs])
|
||||
|
||||
# Sign with wallet1's key
|
||||
signature1 = wallet1.schnorr_sign_message(message_to_sign)
|
||||
|
||||
# Sign with wallet2's key
|
||||
signature2 = wallet2.schnorr_sign_message(message_to_sign)
|
||||
|
||||
# Add both signatures to the first proof only (SIG_ALL)
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[signature1, signature2]).json()
|
||||
|
||||
# This should succeed with 2 valid signatures
|
||||
promises = await ledger.swap(proofs=send_proofs, outputs=outputs)
|
||||
assert len(promises) == len(outputs)
|
||||
|
||||
|
||||
async def create_test_outputs(wallet: Wallet, amount: int) -> List[BlindedMessage]:
|
||||
"""Helper to create blinded outputs for testing."""
|
||||
output_amounts = [amount]
|
||||
secrets, rs, _ = await wallet.generate_n_secrets(len(output_amounts))
|
||||
outputs, _ = wallet._construct_outputs(output_amounts, secrets, rs)
|
||||
return outputs
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import hashlib
|
||||
import secrets
|
||||
from typing import List
|
||||
@@ -10,6 +11,8 @@ from cashu.core.base import HTLCWitness, Proof
|
||||
from cashu.core.crypto.secp import PrivateKey
|
||||
from cashu.core.htlc import HTLCSecret
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import SigFlags
|
||||
from cashu.core.secret import SecretKind
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
@@ -107,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(
|
||||
wallet2.redeem(send_proofs), "Mint Error: HTLC preimage does not match"
|
||||
wallet1.redeem(send_proofs), "Mint Error: HTLC preimage does not match"
|
||||
)
|
||||
|
||||
|
||||
@@ -143,7 +146,7 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet
|
||||
preimage=preimage, hashlock_pubkeys=[pubkey_wallet1]
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
signatures = wallet1.sign_proofs(send_proofs)
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(
|
||||
preimage=preimage, signatures=[f"{s[:-5]}11111"]
|
||||
@@ -151,7 +154,7 @@ async def test_htlc_redeem_with_wrong_signature(wallet1: Wallet, wallet2: Wallet
|
||||
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"Mint Error: signature threshold not met",
|
||||
)
|
||||
|
||||
|
||||
@@ -168,11 +171,11 @@ async def test_htlc_redeem_with_correct_signature(wallet1: Wallet, wallet2: Wall
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures = wallet1.sign_proofs(send_proofs)
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s]).json()
|
||||
|
||||
await wallet2.redeem(send_proofs)
|
||||
await wallet1.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -191,8 +194,8 @@ async def test_htlc_redeem_with_2_of_1_signatures(wallet1: Wallet, wallet2: Wall
|
||||
)
|
||||
_, 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)
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
|
||||
|
||||
@@ -215,8 +218,8 @@ async def test_htlc_redeem_with_2_of_2_signatures(wallet1: Wallet, wallet2: Wall
|
||||
)
|
||||
_, 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)
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
|
||||
|
||||
@@ -241,8 +244,8 @@ async def test_htlc_redeem_with_2_of_2_signatures_with_duplicate_pubkeys(
|
||||
)
|
||||
_, 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)
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s1, s2 in zip(send_proofs, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[s1, s2]).json()
|
||||
|
||||
@@ -270,14 +273,14 @@ async def test_htlc_redeem_with_3_of_3_signatures_but_only_2_provided(
|
||||
)
|
||||
_, 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)
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(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.",
|
||||
"Mint Error: not enough pubkeys (2) or signatures (2) present for n_sigs (3).",
|
||||
)
|
||||
|
||||
|
||||
@@ -303,8 +306,8 @@ async def test_htlc_redeem_with_2_of_3_signatures_with_2_valid_and_1_invalid_pro
|
||||
)
|
||||
_, 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)
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(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()
|
||||
@@ -312,39 +315,6 @@ async def test_htlc_redeem_with_2_of_3_signatures_with_2_valid_and_1_invalid_pro
|
||||
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
|
||||
):
|
||||
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"
|
||||
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
|
||||
@@ -364,14 +334,14 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_correct_signature(
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures = wallet1.sign_proofs(send_proofs)
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
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: no valid signature provided for input.",
|
||||
"Mint Error: signature threshold not met",
|
||||
)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
@@ -394,11 +364,12 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
locktime_seconds=2,
|
||||
locktime_pubkeys=[pubkey_wallet1],
|
||||
locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2],
|
||||
locktime_n_sigs=2,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
signatures = wallet1.sign_proofs(send_proofs)
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
p.witness = HTLCWitness(
|
||||
preimage=preimage, signatures=[f"{s[:-5]}11111"]
|
||||
@@ -407,12 +378,175 @@ async def test_htlc_redeem_hashlock_wrong_signature_timelock_wrong_signature(
|
||||
# should error because we used wallet2 signatures for the hash lock
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
# should fail since lock time has passed and we provided a wrong signature for timelock
|
||||
# should fail since lock time has passed and we provided not enough signatures for the timelock locktime_n_sigs
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"Mint Error: signature threshold not met. 1 < 2.",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_redeem_timelock_2_of_2_signatures(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Testing the 2-of-2 timelock (refund) signature case."""
|
||||
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"
|
||||
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_wallet2],
|
||||
locktime_seconds=2,
|
||||
locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2],
|
||||
locktime_n_sigs=2,
|
||||
)
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
send_proofs_copy = send_proofs.copy()
|
||||
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(send_proofs)
|
||||
for p, s in zip(send_proofs, signatures):
|
||||
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: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
# locktime has passed
|
||||
|
||||
# should fail. lock time has passed but we provided only wallet1 signature for timelock, we need 2 though
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
# let's add the second signature
|
||||
send_proofs_copy = wallet2.sign_p2pk_sig_inputs(send_proofs_copy)
|
||||
|
||||
# now we can redeem it
|
||||
await wallet1.redeem(send_proofs_copy)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_sigall_behavior(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Test HTLC with SIG_ALL flag, requiring signatures on both inputs and outputs."""
|
||||
# Mint tokens for testing
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Setup HTLC parameters
|
||||
preimage = "00000000000000000000000000000000"
|
||||
# preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
|
||||
# Create HTLC lock with SIG_ALL flag
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=preimage,
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
hashlock_n_sigs=1,
|
||||
)
|
||||
|
||||
# Modify the secret to use SIG_ALL
|
||||
secret.tags["sigflag"] = SigFlags.SIG_ALL.value
|
||||
|
||||
# Send tokens with this HTLC lock
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
|
||||
# verify sigflag is SIG_ALL
|
||||
assert HTLCSecret.from_secret(secret).kind == SecretKind.HTLC.value
|
||||
assert HTLCSecret.from_secret(secret).sigflag == SigFlags.SIG_ALL
|
||||
|
||||
# first redeem fails because no preimage
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs), "Mint Error: no HTLC preimage provided"
|
||||
)
|
||||
|
||||
# we add the preimage to the proof
|
||||
for p in send_proofs:
|
||||
p.witness = HTLCWitness(preimage=preimage).json()
|
||||
|
||||
# Should succeed, redeem adds signatures to the proof
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_htlc_n_sigs_refund_locktime(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Test HTLC with n_sigs_refund parameter requiring multiple signatures for refund after locktime."""
|
||||
# Create a third wallet for the third signature
|
||||
wallet3 = await Wallet.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_htlc_3", "wallet3"
|
||||
)
|
||||
await migrate_databases(wallet3.db, migrations)
|
||||
wallet3.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet3.load_mint()
|
||||
|
||||
# Mint tokens for testing
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Setup parameters
|
||||
preimage = "00000000000000000000000000000000"
|
||||
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"
|
||||
|
||||
# Create HTLC with:
|
||||
# 1. Timelock in the past
|
||||
# 2. Three refund pubkeys with 2-of-3 signature requirement
|
||||
secret = await wallet1.create_htlc_lock(
|
||||
preimage=wrong_preimage, # this ensures we can't redeem via preimage
|
||||
hashlock_pubkeys=[pubkey_wallet2],
|
||||
locktime_seconds=-200000,
|
||||
locktime_pubkeys=[pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
|
||||
locktime_n_sigs=2, # require 2 of 3 signatures for refund
|
||||
)
|
||||
|
||||
# # Send tokens with this lock
|
||||
_, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 8, secret_lock=secret)
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
|
||||
# # First, try correct preimage but should fail as we're using wrong preimage hash
|
||||
# for p in send_proofs:
|
||||
# p.witness = HTLCWitness(preimage=preimage).json()
|
||||
|
||||
# await assert_err(
|
||||
# wallet2.redeem(send_proofs), "Mint Error: HTLC preimage does not match"
|
||||
# )
|
||||
|
||||
# # Wait for locktime to pass
|
||||
# await asyncio.sleep(2)
|
||||
|
||||
# Try redeeming with only 1 signature after locktime
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs_copy)
|
||||
for p, sig in zip(send_proofs_copy, signatures1):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[sig]).json()
|
||||
|
||||
# Should fail because we need 2 signatures
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs_copy),
|
||||
"Mint Error: not enough pubkeys (3) or signatures (1) present for n_sigs (2)",
|
||||
)
|
||||
|
||||
# Make a fresh copy and add 2 signatures
|
||||
send_proofs_copy2 = copy.deepcopy(send_proofs)
|
||||
signatures1 = wallet1.signatures_proofs_sig_inputs(send_proofs_copy2)
|
||||
signatures2 = wallet2.signatures_proofs_sig_inputs(send_proofs_copy2)
|
||||
|
||||
for p, sig1, sig2 in zip(send_proofs_copy2, signatures1, signatures2):
|
||||
p.witness = HTLCWitness(preimage=preimage, signatures=[sig1, sig2]).json()
|
||||
|
||||
# Should succeed with 2 of 3 signatures after locktime
|
||||
await wallet1.redeem(send_proofs_copy2)
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import secrets
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from coincurve import PrivateKey as CoincurvePrivateKey
|
||||
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.base import P2PKWitness, Proof
|
||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import SigFlags
|
||||
from cashu.core.secret import Tags
|
||||
from cashu.core.p2pk import P2PKSecret, SigFlags
|
||||
from cashu.core.secret import Secret, SecretKind, Tags
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
@@ -121,7 +123,7 @@ async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wal
|
||||
wallet2.private_key = PrivateKey() # wrong private key
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
@@ -145,7 +147,7 @@ async def test_p2pk_short_locktime_receive_with_wrong_private_key(
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# should succeed because even with the wrong private key we
|
||||
@@ -160,7 +162,8 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
assert garbage_pubkey
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
@@ -175,7 +178,7 @@ async def test_p2pk_locktime_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet
|
||||
# and locktime has not passed
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# we can now redeem because of the refund locktime
|
||||
@@ -189,7 +192,8 @@ async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2:
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
garbage_pubkey_2 = PrivateKey().pubkey
|
||||
assert garbage_pubkey
|
||||
assert garbage_pubkey_2
|
||||
@@ -206,13 +210,13 @@ async def test_p2pk_locktime_with_wrong_refund_pubkey(wallet1: Wallet, wallet2:
|
||||
# and locktime has not passed
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# we still can't redeem it because we used garbage_pubkey_2 as a refund pubkey
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs_copy),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
@@ -226,7 +230,8 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
assert garbage_pubkey
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
@@ -241,15 +246,65 @@ async def test_p2pk_locktime_with_second_refund_pubkey(
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
# receiver side: can't redeem since we used a garbage pubkey
|
||||
# and locktime has not passed
|
||||
# WALLET WILL ADD A SIGNATURE BECAUSE IT SEES ITS REFUND PUBKEY (it adds a signature even though the locktime hasn't passed)
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: no valid signature provided for input.",
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# we can now redeem because of the refund locktime
|
||||
await wallet1.redeem(send_proofs_copy)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_2_of_2_refund_pubkeys(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
"""Testing the case where we expect a 2-of-2 signature from the refund pubkeys"""
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey() # receiver side
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
|
||||
# sender side
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
assert garbage_pubkey
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
locktime_seconds=2, # locktime
|
||||
tags=Tags(
|
||||
[["refund", pubkey_wallet2, pubkey_wallet1], ["n_sigs_refund", "2"]],
|
||||
), # multiple refund pubkeys
|
||||
) # sender side
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# we need to copy the send_proofs because the redeem function
|
||||
# modifies the send_proofs in place by adding the signatures
|
||||
send_proofs_copy = copy.deepcopy(send_proofs)
|
||||
send_proofs_copy2 = copy.deepcopy(send_proofs)
|
||||
# receiver side: can't redeem since we used a garbage pubkey
|
||||
# and locktime has not passed
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# now is the refund time, but we can't redeem it because we need 2 signatures
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs_copy),
|
||||
"not enough pubkeys (2) or signatures (1) present for n_sigs (2)",
|
||||
)
|
||||
|
||||
# let's add the second signature
|
||||
send_proofs_copy2 = wallet2.sign_p2pk_sig_inputs(send_proofs_copy2)
|
||||
|
||||
# now we can redeem it
|
||||
await wallet1.redeem(send_proofs_copy2)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_2_of_2(wallet1: Wallet, wallet2: Wallet):
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
@@ -267,7 +322,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 = wallet1.add_signature_witnesses_to_proofs(send_proofs)
|
||||
send_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
# here we add the signatures of wallet2
|
||||
await wallet2.redeem(send_proofs)
|
||||
|
||||
@@ -289,10 +344,65 @@ 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 = wallet2.add_signature_witnesses_to_proofs(send_proofs)
|
||||
send_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)
|
||||
# wallet does not add a second signature if it finds its own signature already in the witness
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_multisig_two_signatures_same_pubkey(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
# we generate two different signatures from the same private key
|
||||
mint_quote = await wallet2.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet2.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=2
|
||||
)
|
||||
|
||||
_, send_proofs = await wallet2.swap_to_send(
|
||||
wallet2.proofs, 1, secret_lock=secret_lock
|
||||
)
|
||||
assert len(send_proofs) == 1
|
||||
proof = send_proofs[0]
|
||||
# create coincurve private key so we can sign the message
|
||||
coincurve_privatekey2 = CoincurvePrivateKey(
|
||||
bytes.fromhex(wallet2.private_key.serialize())
|
||||
)
|
||||
# check if private keys are the same
|
||||
assert coincurve_privatekey2.to_hex() == wallet2.private_key.serialize()
|
||||
|
||||
msg = hashlib.sha256(proof.secret.encode("utf-8")).digest()
|
||||
coincurve_signature = coincurve_privatekey2.sign_schnorr(msg)
|
||||
|
||||
# add signatures of wallet2 – this is a duplicate signature
|
||||
send_proofs = wallet2.sign_p2pk_sig_inputs(send_proofs)
|
||||
|
||||
# the signatures from coincurve are not the same as the ones from wallet2
|
||||
assert coincurve_signature.hex() != proof.p2pksigs[0]
|
||||
|
||||
# verify both signatures:
|
||||
assert PublicKey(bytes.fromhex(pubkey_wallet2), raw=True).schnorr_verify(
|
||||
msg, bytes.fromhex(proof.p2pksigs[0]), None, raw=True
|
||||
)
|
||||
assert PublicKey(bytes.fromhex(pubkey_wallet2), raw=True).schnorr_verify(
|
||||
msg, coincurve_signature, None, raw=True
|
||||
)
|
||||
|
||||
# add coincurve signature, and the wallet2 signature will be added during .redeem
|
||||
send_proofs[0].witness = P2PKWitness(signatures=[coincurve_signature.hex()]).json()
|
||||
|
||||
# here we add the signatures of wallet2
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs), "Mint Error: signatures must be unique."
|
||||
wallet2.redeem(send_proofs), "Mint Error: signature threshold not met. 1 < 2."
|
||||
)
|
||||
|
||||
|
||||
@@ -313,7 +423,7 @@ async def test_p2pk_multisig_quorum_not_met_1_of_2(wallet1: Wallet, wallet2: Wal
|
||||
)
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough signatures provided: 1 < 2.",
|
||||
"Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2)",
|
||||
)
|
||||
|
||||
|
||||
@@ -324,21 +434,26 @@ async def test_p2pk_multisig_quorum_not_met_2_of_3(wallet1: Wallet, wallet2: Wal
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
garbage_priv = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
garbage_pubkey = garbage_priv.pubkey
|
||||
assert garbage_pubkey
|
||||
assert pubkey_wallet1 != pubkey_wallet2
|
||||
# p2pk test
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey_wallet2, tags=Tags([["pubkeys", pubkey_wallet1]]), n_sigs=3
|
||||
pubkey_wallet2,
|
||||
tags=Tags([["pubkeys", pubkey_wallet1, garbage_pubkey.serialize().hex()]]),
|
||||
n_sigs=3,
|
||||
)
|
||||
|
||||
# create locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# add signatures of wallet1
|
||||
send_proofs = wallet1.add_signature_witnesses_to_proofs(send_proofs)
|
||||
send_proofs = wallet1.sign_p2pk_sig_inputs(send_proofs)
|
||||
# here we add the signatures of wallet2
|
||||
await assert_err(
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough signatures provided: 2 < 3.",
|
||||
"Mint Error: not enough pubkeys (3) or signatures (2) present for n_sigs (3)",
|
||||
)
|
||||
|
||||
|
||||
@@ -380,10 +495,9 @@ async def test_p2pk_multisig_with_wrong_first_private_key(
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
# add signatures of wallet1
|
||||
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."
|
||||
wallet2.redeem(send_proofs),
|
||||
"Mint Error: not enough pubkeys (2) or signatures (1) present for n_sigs (2).",
|
||||
)
|
||||
|
||||
|
||||
@@ -412,7 +526,7 @@ async def test_secret_initialized_with_tags(wallet1: Wallet):
|
||||
pubkey = PrivateKey().pubkey
|
||||
assert pubkey
|
||||
secret = await wallet1.create_p2pk_lock(
|
||||
pubkey=pubkey.serialize().hex(),
|
||||
data=pubkey.serialize().hex(),
|
||||
tags=tags,
|
||||
)
|
||||
assert secret.locktime == 100
|
||||
@@ -425,7 +539,7 @@ async def test_secret_initialized_with_arguments(wallet1: Wallet):
|
||||
pubkey = PrivateKey().pubkey
|
||||
assert pubkey
|
||||
secret = await wallet1.create_p2pk_lock(
|
||||
pubkey=pubkey.serialize().hex(),
|
||||
data=pubkey.serialize().hex(),
|
||||
locktime_seconds=100,
|
||||
n_sigs=3,
|
||||
sig_all=True,
|
||||
@@ -434,3 +548,157 @@ async def test_secret_initialized_with_arguments(wallet1: Wallet):
|
||||
assert secret.locktime > 1689000000
|
||||
assert secret.n_sigs == 3
|
||||
assert secret.sigflag == SigFlags.SIG_ALL
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_verify_is_p2pk_input(wallet1: Wallet1):
|
||||
"""Test the wallet correctly identifies P2PK inputs."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock with wallet's own public key
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Now get a proof and check if it's detected as P2PK
|
||||
proof = send_proofs[0]
|
||||
|
||||
# This tests the internal method that recognizes a P2PK input
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
assert secret.kind == SecretKind.P2PK.value, "Secret should be of kind P2PK"
|
||||
|
||||
# We can verify that we can convert it to a P2PKSecret
|
||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||
assert p2pk_secret.data == pubkey, "P2PK secret data should contain the pubkey"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_verify_p2pk_sigflag_is_sig_inputs(wallet1: Wallet1):
|
||||
"""Test the wallet correctly identifies the SIG_INPUTS flag."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock with SIG_INPUTS (default)
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Check if sigflag is correctly identified as SIG_INPUTS
|
||||
proof = send_proofs[0]
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||
|
||||
assert p2pk_secret.sigflag == SigFlags.SIG_INPUTS, "Sigflag should be SIG_INPUTS"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_verify_p2pk_sigflag_is_sig_all(wallet1: Wallet1):
|
||||
"""Test the wallet correctly identifies the SIG_ALL flag."""
|
||||
# Mint tokens to the wallet
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a p2pk lock with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
|
||||
# Use swap_to_send to create p2pk locked proofs
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 32, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Check if sigflag is correctly identified as SIG_ALL
|
||||
proof = send_proofs[0]
|
||||
secret = Secret.deserialize(proof.secret)
|
||||
p2pk_secret = P2PKSecret.from_secret(secret)
|
||||
|
||||
assert p2pk_secret.sigflag == SigFlags.SIG_ALL, "Sigflag should be SIG_ALL"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_p2pk_locktime_with_3_of_3_refund_pubkeys(
|
||||
wallet1: Wallet, wallet2: Wallet
|
||||
):
|
||||
"""Testing the case where we expect a 3-of-3 signature from the refund pubkeys"""
|
||||
# Create a third wallet for this test
|
||||
wallet3 = await Wallet.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_3", "wallet3"
|
||||
)
|
||||
await migrate_databases(wallet3.db, migrations)
|
||||
wallet3.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet3.load_mint()
|
||||
|
||||
# Get tokens and create public keys for all wallets
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
pubkey_wallet1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
|
||||
pubkey_wallet3 = await wallet3.create_p2pk_pubkey()
|
||||
|
||||
# Create an unspendable lock with refund conditions requiring 3 signatures
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
assert garbage_pubkey
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
|
||||
locktime_seconds=2, # locktime
|
||||
tags=Tags(
|
||||
[
|
||||
["refund", pubkey_wallet1, pubkey_wallet2, pubkey_wallet3],
|
||||
["n_sigs_refund", "3"],
|
||||
],
|
||||
), # multiple refund pubkeys with required 3 signatures
|
||||
)
|
||||
|
||||
# Send tokens with this lock
|
||||
_, send_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 8, secret_lock=secret_lock
|
||||
)
|
||||
|
||||
# Create copies for different test scenarios
|
||||
send_proofs_copy1 = copy.deepcopy(send_proofs)
|
||||
send_proofs_copy2 = copy.deepcopy(send_proofs)
|
||||
|
||||
# Verify tokens can't be redeemed before locktime
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs),
|
||||
"Mint Error: signature threshold not met. 0 < 1.",
|
||||
)
|
||||
|
||||
# Wait for locktime to expire
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Try with only 1 signature (wallet1) - should fail
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs_copy1),
|
||||
"not enough pubkeys (3) or signatures (1) present for n_sigs (3)",
|
||||
)
|
||||
|
||||
# Add second signature (wallet2)
|
||||
send_proofs_copy2 = wallet2.sign_p2pk_sig_inputs(send_proofs_copy2)
|
||||
|
||||
# Try with 2 signatures - should still fail
|
||||
await assert_err(
|
||||
wallet1.redeem(send_proofs_copy2),
|
||||
"not enough pubkeys (3) or signatures (2) present for n_sigs (3)",
|
||||
)
|
||||
|
||||
# Add the third signature (wallet3)
|
||||
send_proofs_copy2 = wallet3.sign_p2pk_sig_inputs(send_proofs_copy2)
|
||||
|
||||
# Now with 3 signatures it should succeed
|
||||
await wallet1.redeem(send_proofs_copy2)
|
||||
|
||||
468
tests/test_wallet_p2pk_methods.py
Normal file
468
tests/test_wallet_p2pk_methods.py
Normal file
@@ -0,0 +1,468 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import P2PKWitness
|
||||
from cashu.core.crypto.secp import PrivateKey
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.p2pk import P2PKSecret, SigFlags
|
||||
from cashu.core.secret import SecretKind, Tags
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from tests.conftest import SERVER_ENDPOINT
|
||||
from tests.helpers import pay_if_regtest
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
if msg not in str(exc.args[0]):
|
||||
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
|
||||
return
|
||||
raise Exception(f"Expected error: {msg}, got no error")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1():
|
||||
wallet1 = await Wallet.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_methods_1", "wallet1"
|
||||
)
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
yield wallet1
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2():
|
||||
wallet2 = await Wallet.with_db(
|
||||
SERVER_ENDPOINT, "test_data/wallet_p2pk_methods_2", "wallet2"
|
||||
)
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet2.load_mint()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_p2pk_lock_default(wallet1: Wallet):
|
||||
"""Test creating a P2PK lock with default parameters."""
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
|
||||
# Verify created lock properties
|
||||
assert isinstance(secret_lock, P2PKSecret)
|
||||
assert secret_lock.kind == SecretKind.P2PK.value
|
||||
assert secret_lock.data == pubkey
|
||||
assert secret_lock.locktime is None
|
||||
assert secret_lock.sigflag == SigFlags.SIG_INPUTS
|
||||
assert secret_lock.n_sigs == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_p2pk_lock_with_options(wallet1: Wallet):
|
||||
"""Test creating a P2PK lock with all options specified."""
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(
|
||||
pubkey,
|
||||
locktime_seconds=3600,
|
||||
sig_all=True,
|
||||
n_sigs=2,
|
||||
tags=Tags([["custom_tag", "custom_value"]]),
|
||||
)
|
||||
|
||||
# Verify created lock properties
|
||||
assert isinstance(secret_lock, P2PKSecret)
|
||||
assert secret_lock.kind == SecretKind.P2PK.value
|
||||
assert secret_lock.data == pubkey
|
||||
assert secret_lock.locktime is not None
|
||||
assert secret_lock.sigflag == SigFlags.SIG_ALL
|
||||
assert secret_lock.n_sigs == 2
|
||||
assert secret_lock.tags.get_tag("custom_tag") == "custom_value"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_signatures_proofs_sig_inputs(wallet1: Wallet):
|
||||
"""Test signing proofs with the private key."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create locked proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 32, secret_lock=secret_lock)
|
||||
|
||||
# Test signatures_proofs_sig_inputs
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(proofs)
|
||||
|
||||
# Verify signatures were created
|
||||
assert len(signatures) == len(proofs)
|
||||
assert all(isinstance(sig, str) for sig in signatures)
|
||||
assert all(len(sig) == 128 for sig in signatures) # 64-byte hex signatures
|
||||
|
||||
# Verify the signatures are valid
|
||||
for proof, signature in zip(proofs, signatures):
|
||||
message = proof.secret.encode("utf-8")
|
||||
sig_bytes = bytes.fromhex(signature)
|
||||
# Make sure wallet has a pubkey
|
||||
assert wallet1.private_key.pubkey is not None
|
||||
assert wallet1.private_key.pubkey.schnorr_verify(
|
||||
hashlib.sha256(message).digest(), sig_bytes, None, raw=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schnorr_sign_message(wallet1: Wallet):
|
||||
"""Test signing an arbitrary message."""
|
||||
# Define a test message
|
||||
message = "test message to sign"
|
||||
|
||||
# Sign the message
|
||||
signature = wallet1.schnorr_sign_message(message)
|
||||
|
||||
# Verify signature format
|
||||
assert isinstance(signature, str)
|
||||
assert len(signature) == 128 # 64-byte hex signature
|
||||
|
||||
# Verify signature is valid
|
||||
sig_bytes = bytes.fromhex(signature)
|
||||
# Make sure wallet has a pubkey
|
||||
assert wallet1.private_key.pubkey is not None
|
||||
assert wallet1.private_key.pubkey.schnorr_verify(
|
||||
hashlib.sha256(message.encode("utf-8")).digest(), sig_bytes, None, raw=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inputs_require_sigall_detection(wallet1: Wallet):
|
||||
"""Test detection of SIG_ALL flag in proof inputs."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create proofs with SIG_INPUTS
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
_, proofs_sig_inputs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
|
||||
# Create proofs with SIG_ALL
|
||||
mint_quote_2 = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote_2.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote_2.quote)
|
||||
secret_lock_all = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, proofs_sig_all = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_all
|
||||
)
|
||||
|
||||
# Test detection of SIG_ALL
|
||||
assert not wallet1._inputs_require_sigall(proofs_sig_inputs)
|
||||
assert wallet1._inputs_require_sigall(proofs_sig_all)
|
||||
|
||||
# Test mixed list of proofs
|
||||
mixed_proofs = proofs_sig_inputs + proofs_sig_all
|
||||
assert wallet1._inputs_require_sigall(mixed_proofs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_witness_swap_sig_all(wallet1: Wallet):
|
||||
"""Test adding a witness to the first proof for SIG_ALL."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create proofs with SIG_ALL
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 16, secret_lock=secret_lock)
|
||||
|
||||
# Create some outputs
|
||||
output_amounts = [16]
|
||||
secrets, rs, _ = await wallet1.generate_n_secrets(len(output_amounts))
|
||||
outputs, _ = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Add witness
|
||||
signed_proofs = wallet1.add_witness_swap_sig_all(proofs, outputs)
|
||||
|
||||
# Verify the first proof has a witness
|
||||
assert signed_proofs[0].witness is not None
|
||||
witness = P2PKWitness.from_witness(signed_proofs[0].witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
# Verify the signature includes both inputs and outputs
|
||||
message_to_sign = "".join([p.secret for p in proofs] + [o.B_ for o in outputs])
|
||||
signature = wallet1.schnorr_sign_message(message_to_sign)
|
||||
assert witness.signatures[0] == signature
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sign_proofs_inplace_swap(wallet1: Wallet):
|
||||
"""Test signing proofs in place for a swap operation."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create SIG_ALL proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 16, secret_lock=secret_lock)
|
||||
|
||||
# Create some outputs
|
||||
output_amounts = [16]
|
||||
secrets, rs, _ = await wallet1.generate_n_secrets(len(output_amounts))
|
||||
outputs, _ = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# Sign proofs
|
||||
signed_proofs = wallet1.sign_proofs_inplace_swap(proofs, outputs)
|
||||
|
||||
# Verify the first proof has a witness with a signature
|
||||
assert signed_proofs[0].witness is not None
|
||||
witness = P2PKWitness.from_witness(signed_proofs[0].witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_signatures_to_proofs(wallet1: Wallet):
|
||||
"""Test adding signatures to proofs."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create P2PK proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 16, secret_lock=secret_lock)
|
||||
|
||||
# Generate signatures
|
||||
signatures = wallet1.signatures_proofs_sig_inputs(proofs)
|
||||
|
||||
# Add signatures to proofs
|
||||
signed_proofs = wallet1.add_signatures_to_proofs(proofs, signatures)
|
||||
|
||||
# Verify signatures were added to the proofs
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness is not None
|
||||
witness = P2PKWitness.from_witness(proof.witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
# Test adding same signatures to already signed proofs (should not duplicate)
|
||||
signed_proofs = wallet1.add_signatures_to_proofs(signed_proofs, signatures)
|
||||
|
||||
# Verify the signatures were not duplicated
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness
|
||||
witness = P2PKWitness.from_witness(proof.witness)
|
||||
# Should still have 1 signature because duplicates aren't added
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_proofs_locked_to_our_pubkey(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Test filtering proofs locked to our public key."""
|
||||
# Mint tokens to wallet1
|
||||
mint_quote = await wallet1.request_mint(640)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(640, quote_id=mint_quote.quote)
|
||||
|
||||
# Get pubkeys for both wallets
|
||||
pubkey1 = await wallet1.create_p2pk_pubkey()
|
||||
pubkey2 = await wallet2.create_p2pk_pubkey()
|
||||
|
||||
# Create proofs locked to wallet1's pubkey
|
||||
secret_lock1 = await wallet1.create_p2pk_lock(pubkey1)
|
||||
_, proofs1 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock1
|
||||
)
|
||||
|
||||
# Create proofs locked to wallet2's pubkey
|
||||
secret_lock2 = await wallet1.create_p2pk_lock(pubkey2)
|
||||
_, proofs2 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock2
|
||||
)
|
||||
|
||||
# Create proofs with multiple pubkeys
|
||||
secret_lock3 = await wallet1.create_p2pk_lock(
|
||||
pubkey1, tags=Tags([["pubkeys", pubkey2]])
|
||||
)
|
||||
_, proofs3 = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock3
|
||||
)
|
||||
|
||||
# Sign the proofs to avoid witness errors
|
||||
signed_proofs1 = wallet1.sign_p2pk_sig_inputs(proofs1)
|
||||
signed_proofs2 = wallet2.sign_p2pk_sig_inputs(proofs2)
|
||||
signed_proofs3 = wallet1.sign_p2pk_sig_inputs(proofs3)
|
||||
signed_proofs3 = wallet2.sign_p2pk_sig_inputs(signed_proofs3)
|
||||
|
||||
# Ensure pubkeys are available
|
||||
assert wallet1.private_key.pubkey is not None
|
||||
assert wallet2.private_key.pubkey is not None
|
||||
|
||||
# Filter using wallet1
|
||||
filtered1 = wallet1.filter_proofs_locked_to_our_pubkey(
|
||||
signed_proofs1 + signed_proofs2 + signed_proofs3
|
||||
)
|
||||
# wallet1 should find proofs1 and proofs3
|
||||
assert len(filtered1) == len(signed_proofs1) + len(signed_proofs3)
|
||||
|
||||
# Filter using wallet2
|
||||
filtered2 = wallet2.filter_proofs_locked_to_our_pubkey(
|
||||
signed_proofs1 + signed_proofs2 + signed_proofs3
|
||||
)
|
||||
# wallet2 should find proofs2 and proofs3
|
||||
assert len(filtered2) == len(signed_proofs2) + len(signed_proofs3)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sign_p2pk_sig_inputs(wallet1: Wallet):
|
||||
"""Test signing P2PK SIG_INPUTS proofs."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a mix of P2PK and non-P2PK proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
|
||||
# Regular proofs (not P2PK)
|
||||
_, regular_proofs = await wallet1.swap_to_send(wallet1.proofs, 16)
|
||||
|
||||
# P2PK SIG_INPUTS proofs
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
_, p2pk_input_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
|
||||
# P2PK SIG_ALL proofs - these won't be signed by sign_p2pk_sig_inputs
|
||||
secret_lock_all = await wallet1.create_p2pk_lock(pubkey, sig_all=True)
|
||||
_, p2pk_all_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_all
|
||||
)
|
||||
|
||||
# P2PK locked to a different pubkey - these won't be signed
|
||||
garbage_pubkey_p = PrivateKey().pubkey
|
||||
assert garbage_pubkey_p is not None
|
||||
garbage_pubkey = garbage_pubkey_p.serialize().hex()
|
||||
secret_lock_other = await wallet1.create_p2pk_lock(garbage_pubkey)
|
||||
_, p2pk_other_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_other
|
||||
)
|
||||
|
||||
# Mix all proofs
|
||||
mixed_proofs = (
|
||||
regular_proofs + p2pk_input_proofs + p2pk_all_proofs + p2pk_other_proofs
|
||||
)
|
||||
|
||||
# Sign the mixed proofs
|
||||
signed_proofs = wallet1.sign_p2pk_sig_inputs(mixed_proofs)
|
||||
|
||||
# Only P2PK SIG_INPUTS proofs locked to our pubkey should be signed
|
||||
assert len(signed_proofs) == len(p2pk_input_proofs)
|
||||
|
||||
# Verify the signatures were added
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness is not None
|
||||
witness = P2PKWitness.from_witness(proof.witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_witnesses_sig_inputs(wallet1: Wallet):
|
||||
"""Test adding witnesses to P2PK SIG_INPUTS proofs."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Create a mix of P2PK and non-P2PK proofs
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
|
||||
# Regular proofs (not P2PK)
|
||||
_, regular_proofs = await wallet1.swap_to_send(wallet1.proofs, 16)
|
||||
|
||||
# P2PK SIG_INPUTS proofs
|
||||
secret_lock_inputs = await wallet1.create_p2pk_lock(pubkey, sig_all=False)
|
||||
_, p2pk_input_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_inputs
|
||||
)
|
||||
|
||||
# Mix all proofs and make a copy for comparison
|
||||
mixed_proofs = regular_proofs + p2pk_input_proofs
|
||||
mixed_proofs_copy = copy.deepcopy(mixed_proofs)
|
||||
|
||||
# Add witnesses to the proofs
|
||||
signed_proofs = wallet1.add_witnesses_sig_inputs(mixed_proofs)
|
||||
|
||||
# Verify that only P2PK proofs have witnesses added
|
||||
for i, (orig_proof, signed_proof) in enumerate(
|
||||
zip(mixed_proofs_copy, signed_proofs)
|
||||
):
|
||||
if i < len(regular_proofs):
|
||||
# Regular proofs should be unchanged
|
||||
assert signed_proof.witness == orig_proof.witness
|
||||
else:
|
||||
# P2PK proofs should have witnesses added
|
||||
assert signed_proof.witness is not None
|
||||
witness = P2PKWitness.from_witness(signed_proof.witness)
|
||||
assert len(witness.signatures) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edge_cases(wallet1: Wallet, wallet2: Wallet):
|
||||
"""Test various edge cases for the WalletP2PK methods."""
|
||||
# Mint tokens
|
||||
mint_quote = await wallet1.request_mint(64)
|
||||
await pay_if_regtest(mint_quote.request)
|
||||
await wallet1.mint(64, quote_id=mint_quote.quote)
|
||||
|
||||
# Case 1: Empty list of proofs
|
||||
assert wallet1.signatures_proofs_sig_inputs([]) == []
|
||||
assert wallet1.add_signatures_to_proofs([], []) == []
|
||||
assert wallet1.filter_proofs_locked_to_our_pubkey([]) == []
|
||||
assert wallet1.sign_p2pk_sig_inputs([]) == []
|
||||
assert wallet1.add_witnesses_sig_inputs([]) == []
|
||||
|
||||
# Case 2: Mismatched number of proofs and signatures
|
||||
pubkey = await wallet1.create_p2pk_pubkey()
|
||||
secret_lock = await wallet1.create_p2pk_lock(pubkey)
|
||||
_, proofs = await wallet1.swap_to_send(wallet1.proofs, 16, secret_lock=secret_lock)
|
||||
assert len(proofs) == 1
|
||||
# Create fake signatures but we have only one proof - this should fail
|
||||
signatures = ["fake_signature1", "fake_signature2"]
|
||||
assert len(signatures) != len(proofs)
|
||||
|
||||
# This should raise an assertion error
|
||||
with pytest.raises(AssertionError, match="wrong number of signatures"):
|
||||
wallet1.add_signatures_to_proofs(proofs, signatures)
|
||||
|
||||
# Case 3: SIG_ALL with proofs locked to different public keys
|
||||
assert wallet1.private_key.pubkey is not None
|
||||
garbage_pubkey = PrivateKey().pubkey
|
||||
assert garbage_pubkey is not None
|
||||
secret_lock_other = await wallet1.create_p2pk_lock(
|
||||
garbage_pubkey.serialize().hex(), sig_all=True
|
||||
)
|
||||
_, other_proofs = await wallet1.swap_to_send(
|
||||
wallet1.proofs, 16, secret_lock=secret_lock_other
|
||||
)
|
||||
|
||||
output_amounts = [16]
|
||||
secrets, rs, _ = await wallet1.generate_n_secrets(len(output_amounts))
|
||||
outputs, _ = wallet1._construct_outputs(output_amounts, secrets, rs)
|
||||
|
||||
# wallet1 shouldn't add signatures because proofs are locked to a different pubkey
|
||||
signed_proofs = wallet1.add_witness_swap_sig_all(other_proofs, outputs)
|
||||
# Check each proof for None witness
|
||||
for proof in signed_proofs:
|
||||
assert proof.witness is None
|
||||
Reference in New Issue
Block a user