2026-03-09 11:11:41 +01:00
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
|
|
|
|
|
|
from src.p2sh import generate_p2sh_multisig
|
|
|
|
|
|
|
|
|
|
SECRETSCAN_URL = "https://secretscan.org/Bitcoin?address={}"
|
|
|
|
|
|
|
|
|
|
NETWORKS = ['mainnet', 'testnet', 'regtest']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_fields():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
assert set(result.keys()) == {'network', 'script_type', 'm', 'n', 'redeem_script_hex', 'participants', 'address'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_script_type():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
assert result['script_type'] == 'p2sh-multisig'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_address_mainnet():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
assert result['address'].startswith('3')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_address_testnet():
|
|
|
|
|
result = generate_p2sh_multisig('testnet', m=2, n=3)
|
|
|
|
|
assert result['address'].startswith('2')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_address_regtest():
|
|
|
|
|
result = generate_p2sh_multisig('regtest', m=2, n=3)
|
|
|
|
|
assert result['address'].startswith('2')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_participants_count():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
assert len(result['participants']) == 3
|
|
|
|
|
assert result['m'] == 2
|
|
|
|
|
assert result['n'] == 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_participant_fields():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
for p in result['participants']:
|
|
|
|
|
assert 'private_key_hex' in p
|
|
|
|
|
assert 'private_key_wif' in p
|
|
|
|
|
assert 'public_key_hex' in p
|
|
|
|
|
assert len(p['private_key_hex']) == 64
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_redeem_script_not_empty():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
assert len(result['redeem_script_hex']) > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_1of1():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=1, n=1)
|
|
|
|
|
assert result['m'] == 1
|
|
|
|
|
assert result['n'] == 1
|
|
|
|
|
assert len(result['participants']) == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_unique_addresses():
|
|
|
|
|
r1 = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
r2 = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
assert r1['address'] != r2['address']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_print_secretscan(capsys):
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
url = SECRETSCAN_URL.format(result['address'])
|
|
|
|
|
print(f"\n[P2SH] Address: {result['address']}")
|
|
|
|
|
print(f"[P2SH] Verify on SecretScan: {url}")
|
|
|
|
|
captured = capsys.readouterr()
|
|
|
|
|
assert 'secretscan.org' in captured.out
|
2026-03-10 09:40:01 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Error cases ---
|
|
|
|
|
|
|
|
|
|
def test_p2sh_invalid_network_raises():
|
|
|
|
|
import pytest
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
generate_p2sh_multisig('invalid_net', m=2, n=3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_m_greater_than_n_raises():
|
|
|
|
|
import pytest
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
generate_p2sh_multisig('mainnet', m=3, n=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_m_zero_raises():
|
|
|
|
|
import pytest
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
generate_p2sh_multisig('mainnet', m=0, n=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_n_greater_than_16_raises():
|
|
|
|
|
import pytest
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
generate_p2sh_multisig('mainnet', m=1, n=17)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Redeem script structure ---
|
|
|
|
|
|
|
|
|
|
def test_p2sh_redeem_script_is_valid_hex():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
hex_str = result['redeem_script_hex']
|
|
|
|
|
assert all(c in '0123456789abcdef' for c in hex_str)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_redeem_script_starts_with_op_m():
|
|
|
|
|
# OP_2 = 0x52, OP_3 = 0x53
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
redeem = bytes.fromhex(result['redeem_script_hex'])
|
|
|
|
|
assert redeem[0] == 0x52 # OP_2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_redeem_script_ends_with_op_checkmultisig():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3)
|
|
|
|
|
redeem = bytes.fromhex(result['redeem_script_hex'])
|
|
|
|
|
assert redeem[-1] == 0xAE # OP_CHECKMULTISIG
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Edge cases ---
|
|
|
|
|
|
|
|
|
|
def test_p2sh_3of5():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=3, n=5)
|
|
|
|
|
assert result['m'] == 3
|
|
|
|
|
assert result['n'] == 5
|
|
|
|
|
assert len(result['participants']) == 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_1of16():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=1, n=16)
|
|
|
|
|
assert result['m'] == 1
|
|
|
|
|
assert result['n'] == 16
|
|
|
|
|
assert len(result['participants']) == 16
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- BIP67: sorted vs unsorted pubkeys produce different redeem scripts ---
|
|
|
|
|
|
|
|
|
|
def test_p2sh_sort_pubkeys_affects_redeem_script():
|
|
|
|
|
import random
|
|
|
|
|
random.seed(42)
|
|
|
|
|
# Generate with and without sorting; addresses will differ
|
|
|
|
|
r_sorted = generate_p2sh_multisig('mainnet', m=2, n=3, sort_pubkeys=True)
|
|
|
|
|
r_unsorted = generate_p2sh_multisig('mainnet', m=2, n=3, sort_pubkeys=False)
|
|
|
|
|
# Both produce valid addresses
|
|
|
|
|
assert r_sorted['address'].startswith('3')
|
|
|
|
|
assert r_unsorted['address'].startswith('3')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_compressed_pubkeys_33_bytes():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3, compressed=True)
|
|
|
|
|
for p in result['participants']:
|
|
|
|
|
assert len(bytes.fromhex(p['public_key_hex'])) == 33
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_p2sh_uncompressed_pubkeys_65_bytes():
|
|
|
|
|
result = generate_p2sh_multisig('mainnet', m=2, n=3, compressed=False)
|
|
|
|
|
for p in result['participants']:
|
|
|
|
|
assert len(bytes.fromhex(p['public_key_hex'])) == 65
|