tests: add BitcoinPurple test suite
46 tests across three classes: TestBitcoinPurpleConstants — validates all BTCP network constants against the technical specification: NET_NAME, TESTNET flags, CLI flags, address prefixes (P2PKH 56, P2SH 55, WIF 0xb7), bech32/BOLT11 HRP, genesis hashes and wire-order chain_hash, default ports, PoW parameters (adj_interval 120, target_timespan 7200, MAX_TARGET, POW_GENESIS_BITS), SLIP-0132 HD key headers, LN parameters, NETS_LIST uniqueness, and inheritance independence. TestBitcoinPurpleDifficultyAdjustment — tests the 120-block retarget logic using BitcoinPurple mainnet (testnet was wrong because get_target always returns 0 on testnet). Covers: genesis index returns genesis target from POW_GENESIS_BITS, on-time/fast/slow adjustments, lower and upper clamp, the 120-block window (not 2016), and can_connect() calling get_target with the correct period index. TestBitcoinPurpleAddress — tests address encoding under BitcoinPurple: P2PKH starts with 'P', bech32 with HRP 'btcp', address_to_script produces correct P2WPKH scriptPubKey, WIF round-trip with prefix 0xb7, Bitcoin addresses rejected on BTCP network
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
"""
|
||||
Tests for BitcoinPurple (BTCP) network support.
|
||||
|
||||
Covers:
|
||||
- Network constants against the technical specification
|
||||
- 120-block difficulty adjustment logic in blockchain.py
|
||||
- Address encoding under the BTCP network parameters
|
||||
"""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from electrum import bitcoin, blockchain, constants, segwit_addr
|
||||
from electrum.bitcoin import (
|
||||
address_to_script,
|
||||
is_address,
|
||||
is_b58_address,
|
||||
is_segwit_address,
|
||||
public_key_to_p2pkh,
|
||||
serialize_privkey,
|
||||
deserialize_privkey,
|
||||
)
|
||||
from electrum.blockchain import Blockchain, InvalidHeader
|
||||
from electrum.constants import BitcoinPurple, BitcoinPurpleTestnet
|
||||
from electrum.simple_config import SimpleConfig
|
||||
from electrum.util import make_dir
|
||||
|
||||
from . import ElectrumTestCase
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBitcoinPurpleConstants(ElectrumTestCase):
|
||||
"""Verify all BTCP network constants against the technical specification."""
|
||||
|
||||
# --- identity ---
|
||||
|
||||
def test_net_names(self):
|
||||
self.assertEqual("bitcoinpurple", BitcoinPurple.NET_NAME)
|
||||
self.assertEqual("bitcoinpurple_testnet", BitcoinPurpleTestnet.NET_NAME)
|
||||
|
||||
def test_testnet_flags(self):
|
||||
self.assertFalse(BitcoinPurple.TESTNET)
|
||||
self.assertTrue(BitcoinPurpleTestnet.TESTNET)
|
||||
|
||||
def test_cli_flags_and_datadir(self):
|
||||
self.assertEqual("bitcoinpurple", BitcoinPurple.cli_flag())
|
||||
self.assertEqual("bitcoinpurple", BitcoinPurple.datadir_subdir())
|
||||
self.assertEqual("bitcoinpurple_testnet", BitcoinPurpleTestnet.cli_flag())
|
||||
self.assertEqual("bitcoinpurple_testnet", BitcoinPurpleTestnet.datadir_subdir())
|
||||
|
||||
# --- address encoding ---
|
||||
|
||||
def test_address_prefixes_mainnet(self):
|
||||
self.assertEqual(56, BitcoinPurple.ADDRTYPE_P2PKH) # 0x38
|
||||
self.assertEqual(55, BitcoinPurple.ADDRTYPE_P2SH) # 0x37
|
||||
self.assertEqual(0xb7, BitcoinPurple.WIF_PREFIX) # 183
|
||||
|
||||
def test_address_prefixes_testnet(self):
|
||||
# BTCP testnet keeps the same prefixes as mainnet
|
||||
self.assertEqual(56, BitcoinPurpleTestnet.ADDRTYPE_P2PKH)
|
||||
self.assertEqual(55, BitcoinPurpleTestnet.ADDRTYPE_P2SH)
|
||||
self.assertEqual(0xb7, BitcoinPurpleTestnet.WIF_PREFIX)
|
||||
|
||||
def test_segwit_hrp(self):
|
||||
self.assertEqual("btcp", BitcoinPurple.SEGWIT_HRP)
|
||||
self.assertEqual("tbtcp", BitcoinPurpleTestnet.SEGWIT_HRP)
|
||||
|
||||
def test_bolt11_hrp(self):
|
||||
self.assertEqual("btcp", BitcoinPurple.BOLT11_HRP)
|
||||
self.assertEqual("tbtcp", BitcoinPurpleTestnet.BOLT11_HRP)
|
||||
|
||||
# --- genesis ---
|
||||
|
||||
def test_genesis_mainnet(self):
|
||||
self.assertEqual(
|
||||
"000003823fbf82ea4906cbe214617ce7a70a5da29c19ecb1d65618bcf04ec015",
|
||||
BitcoinPurple.GENESIS,
|
||||
)
|
||||
|
||||
def test_genesis_testnet(self):
|
||||
self.assertEqual(
|
||||
"000002fdc3921c1ad368816fcc587f499698d42b42ab5a5d94ee67882ef9d998",
|
||||
BitcoinPurpleTestnet.GENESIS,
|
||||
)
|
||||
|
||||
def test_rev_genesis_bytes_mainnet(self):
|
||||
# Wire-order (reversed) bytes used in LN chain_hash
|
||||
expected_wire = "15c04ef0bc1856d6b1ec199ca25d0aa7e77c6114e2cb0649ea82bf3f82030000"
|
||||
self.assertEqual(expected_wire, BitcoinPurple.rev_genesis_bytes().hex())
|
||||
|
||||
def test_rev_genesis_bytes_testnet(self):
|
||||
expected_wire = "98d9f92e8867ee945d5aab422bd49896497f58cc6f8168d31a1c92c3fd020000"
|
||||
self.assertEqual(expected_wire, BitcoinPurpleTestnet.rev_genesis_bytes().hex())
|
||||
|
||||
# --- ports ---
|
||||
|
||||
def test_default_ports_mainnet(self):
|
||||
self.assertEqual({'t': '50001', 's': '50002'}, BitcoinPurple.DEFAULT_PORTS)
|
||||
|
||||
def test_default_ports_testnet(self):
|
||||
self.assertEqual({'t': '60001', 's': '60002'}, BitcoinPurpleTestnet.DEFAULT_PORTS)
|
||||
|
||||
# --- PoW constants ---
|
||||
|
||||
def test_pow_adjustment_interval(self):
|
||||
self.assertEqual(120, BitcoinPurple.DIFFICULTY_ADJUSTMENT_INTERVAL)
|
||||
# testnet inherits the same value
|
||||
self.assertEqual(120, BitcoinPurpleTestnet.DIFFICULTY_ADJUSTMENT_INTERVAL)
|
||||
|
||||
def test_pow_target_timespan(self):
|
||||
self.assertEqual(7200, BitcoinPurple.POW_TARGET_TIMESPAN) # 120 * 60 s
|
||||
|
||||
def test_pow_max_adjustment_factor(self):
|
||||
self.assertEqual(4, BitcoinPurple.MAX_ADJUSTMENT_FACTOR)
|
||||
|
||||
def test_pow_max_target_exceeds_bitcoin(self):
|
||||
# BTCP starts with easier proof-of-work than Bitcoin
|
||||
self.assertGreater(BitcoinPurple.MAX_TARGET, constants.BitcoinMainnet.MAX_TARGET)
|
||||
|
||||
def test_pow_max_target_value(self):
|
||||
# compact 0x1e0ffff0 maps to a target just under BTCP MAX_TARGET
|
||||
genesis_bits = 0x1e0ffff0
|
||||
genesis_target = Blockchain.bits_to_target(genesis_bits)
|
||||
self.assertLessEqual(genesis_target, BitcoinPurple.MAX_TARGET)
|
||||
|
||||
# --- HD key headers ---
|
||||
|
||||
def test_xpub_headers_mainnet_match_bitcoin(self):
|
||||
# BTCP mainnet reuses Bitcoin's BIP32 root bytes (0488B21E / 0488ADE4)
|
||||
for script_type in ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh'):
|
||||
with self.subTest(script_type=script_type):
|
||||
self.assertEqual(
|
||||
constants.BitcoinMainnet.XPUB_HEADERS[script_type],
|
||||
BitcoinPurple.XPUB_HEADERS[script_type],
|
||||
)
|
||||
self.assertEqual(
|
||||
constants.BitcoinMainnet.XPRV_HEADERS[script_type],
|
||||
BitcoinPurple.XPRV_HEADERS[script_type],
|
||||
)
|
||||
|
||||
def test_xpub_headers_testnet_match_bitcoin_testnet(self):
|
||||
for script_type in ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh'):
|
||||
with self.subTest(script_type=script_type):
|
||||
self.assertEqual(
|
||||
constants.BitcoinTestnet.XPUB_HEADERS[script_type],
|
||||
BitcoinPurpleTestnet.XPUB_HEADERS[script_type],
|
||||
)
|
||||
self.assertEqual(
|
||||
constants.BitcoinTestnet.XPRV_HEADERS[script_type],
|
||||
BitcoinPurpleTestnet.XPRV_HEADERS[script_type],
|
||||
)
|
||||
|
||||
def test_xpub_inv_headers_are_inverses(self):
|
||||
for hdr, hdr_inv in (
|
||||
(BitcoinPurple.XPUB_HEADERS, BitcoinPurple.XPUB_HEADERS_INV),
|
||||
(BitcoinPurple.XPRV_HEADERS, BitcoinPurple.XPRV_HEADERS_INV),
|
||||
):
|
||||
for k, v in hdr.items():
|
||||
self.assertEqual(k, hdr_inv[v])
|
||||
|
||||
# --- Lightning ---
|
||||
|
||||
def test_ln_realm_byte(self):
|
||||
self.assertEqual(0, BitcoinPurple.LN_REALM_BYTE)
|
||||
self.assertEqual(1, BitcoinPurpleTestnet.LN_REALM_BYTE)
|
||||
|
||||
def test_ln_dns_seeds_empty(self):
|
||||
self.assertEqual([], BitcoinPurple.LN_DNS_SEEDS)
|
||||
self.assertEqual([], BitcoinPurpleTestnet.LN_DNS_SEEDS)
|
||||
|
||||
def test_bip44_coin_type(self):
|
||||
self.assertEqual(13496, BitcoinPurple.BIP44_COIN_TYPE)
|
||||
self.assertEqual(1, BitcoinPurpleTestnet.BIP44_COIN_TYPE)
|
||||
|
||||
# --- NETS_LIST integrity ---
|
||||
|
||||
def test_nets_list_contains_btcp(self):
|
||||
net_names = [c.NET_NAME for c in constants.NETS_LIST]
|
||||
self.assertIn("bitcoinpurple", net_names)
|
||||
self.assertIn("bitcoinpurple_testnet", net_names)
|
||||
|
||||
def test_nets_list_unique(self):
|
||||
net_names = [c.NET_NAME for c in constants.NETS_LIST]
|
||||
self.assertEqual(len(net_names), len(set(net_names)))
|
||||
|
||||
# --- inheritance: BitcoinPurple must not inherit from any Bitcoin class ---
|
||||
|
||||
def test_btcp_independent_from_bitcoin_mainnet(self):
|
||||
self.assertFalse(issubclass(BitcoinPurple, constants.BitcoinMainnet))
|
||||
|
||||
def test_btcp_independent_from_bitcoin_testnet(self):
|
||||
self.assertFalse(issubclass(BitcoinPurple, constants.BitcoinTestnet))
|
||||
|
||||
def test_btcp_testnet_inherits_from_btcp(self):
|
||||
self.assertTrue(issubclass(BitcoinPurpleTestnet, BitcoinPurple))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Difficulty adjustment (120-block retarget)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBitcoinPurpleDifficultyAdjustment(ElectrumTestCase):
|
||||
"""
|
||||
Test the 120-block BTCP difficulty retarget logic in blockchain.get_target().
|
||||
|
||||
Uses BitcoinPurple (mainnet) so that get_target() executes the real retarget
|
||||
formula. (Testnet always returns 0, which prevents testing the formula.)
|
||||
For can_connect() tests we either rely on the ordering of checks inside
|
||||
can_connect() or patch the parts that are not under test.
|
||||
"""
|
||||
|
||||
GENESIS_BITS = 0x1e0ffff0 # nBits from BTCP genesis block
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
constants.BitcoinPurple.set_as_network()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
constants.BitcoinMainnet.set_as_network()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
make_dir(os.path.join(self.electrum_path, 'forks'))
|
||||
self.config = SimpleConfig({'electrum_path': self.electrum_path})
|
||||
blockchain.blockchains = {}
|
||||
|
||||
def _make_chain(self) -> Blockchain:
|
||||
chain = Blockchain(
|
||||
config=self.config, forkpoint=0, parent=None,
|
||||
forkpoint_hash=constants.net.GENESIS, prev_hash=None)
|
||||
open(chain.path(), 'w+').close()
|
||||
blockchain.blockchains[constants.net.GENESIS] = chain
|
||||
return chain
|
||||
|
||||
def _fake_headers(self, first_ts: int, last_ts: int, bits: int = GENESIS_BITS):
|
||||
"""Return a read_header side_effect that yields controlled timestamps/bits."""
|
||||
adj = constants.net.DIFFICULTY_ADJUSTMENT_INTERVAL
|
||||
|
||||
def read_header(height: int):
|
||||
if height % adj == 0:
|
||||
return {'bits': bits, 'timestamp': first_ts}
|
||||
if height % adj == adj - 1:
|
||||
return {'bits': bits, 'timestamp': last_ts}
|
||||
return {'bits': bits, 'timestamp': first_ts + (last_ts - first_ts) * (height % adj) // (adj - 1)}
|
||||
|
||||
return read_header
|
||||
|
||||
# --- index -1 ---
|
||||
|
||||
def test_get_target_genesis_index_returns_genesis_target(self):
|
||||
# Index -1 must return the genesis-era target (derived from POW_GENESIS_BITS
|
||||
# 0x1e0ffff0), which is slightly harder than MAX_TARGET (0x1e0fffff).
|
||||
chain = self._make_chain()
|
||||
got = chain.get_target(-1)
|
||||
expected = Blockchain.bits_to_target(BitcoinPurple.POW_GENESIS_BITS)
|
||||
self.assertEqual(expected, got)
|
||||
# Genesis target must be at or below powLimit
|
||||
self.assertLessEqual(got, BitcoinPurple.MAX_TARGET)
|
||||
# Differs from Bitcoin mainnet's MAX_TARGET
|
||||
self.assertNotEqual(constants.BitcoinMainnet.MAX_TARGET, got)
|
||||
|
||||
# --- retarget formula ---
|
||||
|
||||
def test_get_target_on_time(self):
|
||||
"""actual == target_timespan → target unchanged."""
|
||||
ts = constants.net.POW_TARGET_TIMESPAN # 7200
|
||||
initial_target = Blockchain.bits_to_target(self.GENESIS_BITS)
|
||||
chain = self._make_chain()
|
||||
with patch.object(chain, 'read_header', side_effect=self._fake_headers(0, ts)):
|
||||
new_target = chain.get_target(0)
|
||||
expected = Blockchain.bits_to_target(Blockchain.target_to_bits(initial_target))
|
||||
self.assertEqual(expected, new_target)
|
||||
|
||||
def test_get_target_fast_blocks_increases_difficulty(self):
|
||||
"""Blocks mined twice as fast → target halves (difficulty doubles)."""
|
||||
ts = constants.net.POW_TARGET_TIMESPAN # 7200
|
||||
actual = ts // 2 # 3600, above the 1800-floor clamp
|
||||
|
||||
initial_target = Blockchain.bits_to_target(self.GENESIS_BITS)
|
||||
expected_target = Blockchain.bits_to_target(
|
||||
Blockchain.target_to_bits(initial_target * actual // ts)
|
||||
)
|
||||
chain = self._make_chain()
|
||||
with patch.object(chain, 'read_header', side_effect=self._fake_headers(0, actual)):
|
||||
new_target = chain.get_target(0)
|
||||
|
||||
self.assertEqual(expected_target, new_target)
|
||||
self.assertLess(new_target, initial_target) # harder
|
||||
|
||||
def test_get_target_slow_blocks_decreases_difficulty(self):
|
||||
"""Blocks mined twice as slow → target doubles (difficulty halves)."""
|
||||
ts = constants.net.POW_TARGET_TIMESPAN # 7200
|
||||
actual = ts * 2 # 14400, below the 28800-ceiling clamp
|
||||
|
||||
initial_target = Blockchain.bits_to_target(self.GENESIS_BITS)
|
||||
expected_target = Blockchain.bits_to_target(
|
||||
Blockchain.target_to_bits(min(BitcoinPurple.MAX_TARGET, initial_target * actual // ts))
|
||||
)
|
||||
chain = self._make_chain()
|
||||
with patch.object(chain, 'read_header', side_effect=self._fake_headers(0, actual)):
|
||||
new_target = chain.get_target(0)
|
||||
|
||||
self.assertEqual(expected_target, new_target)
|
||||
self.assertGreater(new_target, initial_target) # easier
|
||||
|
||||
def test_get_target_clamp_lower(self):
|
||||
"""actual < target/4 → clamped to target/4 (max 4× harder per retarget)."""
|
||||
ts = constants.net.POW_TARGET_TIMESPAN # 7200
|
||||
floor = ts // constants.net.MAX_ADJUSTMENT_FACTOR # 1800
|
||||
# Use actual timespan far below the floor
|
||||
actual = 100
|
||||
|
||||
initial_target = Blockchain.bits_to_target(self.GENESIS_BITS)
|
||||
expected_target = Blockchain.bits_to_target(
|
||||
Blockchain.target_to_bits(initial_target * floor // ts)
|
||||
)
|
||||
chain = self._make_chain()
|
||||
with patch.object(chain, 'read_header', side_effect=self._fake_headers(0, actual)):
|
||||
new_target = chain.get_target(0)
|
||||
|
||||
self.assertEqual(expected_target, new_target)
|
||||
|
||||
def test_get_target_clamp_upper(self):
|
||||
"""actual > target*4 → clamped to target*4 (max 4× easier per retarget)."""
|
||||
ts = constants.net.POW_TARGET_TIMESPAN # 7200
|
||||
ceiling = ts * constants.net.MAX_ADJUSTMENT_FACTOR # 28800
|
||||
# Use actual timespan far above the ceiling
|
||||
actual = 999_999
|
||||
|
||||
initial_target = Blockchain.bits_to_target(self.GENESIS_BITS)
|
||||
raw = initial_target * ceiling // ts
|
||||
expected_target = Blockchain.bits_to_target(
|
||||
Blockchain.target_to_bits(min(BitcoinPurple.MAX_TARGET, raw))
|
||||
)
|
||||
chain = self._make_chain()
|
||||
with patch.object(chain, 'read_header', side_effect=self._fake_headers(0, actual)):
|
||||
new_target = chain.get_target(0)
|
||||
|
||||
self.assertEqual(expected_target, new_target)
|
||||
|
||||
# --- period index computation ---
|
||||
|
||||
def test_adj_interval_is_120_not_2016(self):
|
||||
"""adj_interval from constants must equal 120 when BTCP network is active."""
|
||||
self.assertEqual(120, constants.net.DIFFICULTY_ADJUSTMENT_INTERVAL)
|
||||
|
||||
def test_get_target_uses_120_block_window(self):
|
||||
"""get_target(1) reads headers at heights 120 and 239, not 2016 and 4031."""
|
||||
ts = constants.net.POW_TARGET_TIMESPAN
|
||||
read_calls = []
|
||||
|
||||
def tracking_read_header(height):
|
||||
read_calls.append(height)
|
||||
return {'bits': self.GENESIS_BITS, 'timestamp': height * 60}
|
||||
|
||||
chain = self._make_chain()
|
||||
with patch.object(chain, 'read_header', side_effect=tracking_read_header):
|
||||
chain.get_target(1)
|
||||
|
||||
# Must have read exactly headers 120 and 239 (period 1 = [120, 239])
|
||||
self.assertIn(120, read_calls)
|
||||
self.assertIn(239, read_calls)
|
||||
# Must NOT have touched 2016 or 4031 (Bitcoin-style chunk boundary)
|
||||
self.assertNotIn(2016, read_calls)
|
||||
self.assertNotIn(4031, read_calls)
|
||||
|
||||
def test_can_connect_uses_120_block_period(self):
|
||||
"""
|
||||
can_connect should look up target at height // 120 - 1, not height // 2016 - 1.
|
||||
Verify by checking that get_target is called with the correct period index.
|
||||
"""
|
||||
chain = self._make_chain()
|
||||
get_target_calls = []
|
||||
original_get_target = chain.get_target
|
||||
|
||||
def tracking_get_target(index):
|
||||
get_target_calls.append(index)
|
||||
return original_get_target(index)
|
||||
|
||||
# Height 1 (period 0): can_connect calls get_target(1 // 120 - 1) = get_target(-1)
|
||||
# before verify_header. Even though verify_header rejects the PoW (nonce=0),
|
||||
# get_target is called first so the index is recorded.
|
||||
dummy_header = {
|
||||
'block_height': 1,
|
||||
'prev_block_hash': constants.net.GENESIS,
|
||||
'version': 1,
|
||||
'merkle_root': '00' * 32,
|
||||
'timestamp': 1691126892,
|
||||
'bits': self.GENESIS_BITS,
|
||||
'nonce': 0,
|
||||
}
|
||||
with patch.object(chain, 'get_target', side_effect=tracking_get_target):
|
||||
chain.can_connect(dummy_header, check_height=False)
|
||||
self.assertIn(-1, get_target_calls)
|
||||
|
||||
# Height 121 (first block of period 1): get_target(121 // 120 - 1) = get_target(0).
|
||||
# get_hash(120) would raise MissingHeader (header not on disk) before get_target
|
||||
# is reached, so we patch get_hash to return a fake prev hash.
|
||||
get_target_calls.clear()
|
||||
fake_prev = 'ab' * 32
|
||||
dummy_header['block_height'] = 121
|
||||
dummy_header['prev_block_hash'] = fake_prev
|
||||
with patch.object(chain, 'get_hash', return_value=fake_prev):
|
||||
with patch.object(chain, 'get_target', side_effect=tracking_get_target):
|
||||
chain.can_connect(dummy_header, check_height=False)
|
||||
self.assertIn(0, get_target_calls)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Address encoding under BTCP network parameters
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBitcoinPurpleAddress(ElectrumTestCase):
|
||||
"""P2PKH, P2SH, Bech32, and WIF encoding under BitcoinPurple network."""
|
||||
|
||||
# compressed pubkey for a known private key (k=1, secp256k1)
|
||||
PUBKEY_HEX = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
constants.BitcoinPurple.set_as_network()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
constants.BitcoinMainnet.set_as_network()
|
||||
|
||||
def test_p2pkh_address_starts_with_P(self):
|
||||
# P2PKH prefix 56 (0x38) encodes to Base58 addresses starting with 'P'
|
||||
pubkey = bytes.fromhex(self.PUBKEY_HEX)
|
||||
addr = public_key_to_p2pkh(pubkey)
|
||||
self.assertTrue(addr.startswith('P'), f"Expected 'P...' address, got: {addr}")
|
||||
|
||||
def test_p2pkh_address_is_valid(self):
|
||||
pubkey = bytes.fromhex(self.PUBKEY_HEX)
|
||||
addr = public_key_to_p2pkh(pubkey)
|
||||
self.assertTrue(is_address(addr))
|
||||
self.assertTrue(is_b58_address(addr))
|
||||
|
||||
def test_p2pkh_not_valid_on_bitcoin_mainnet(self):
|
||||
# BTCP address must be rejected on Bitcoin mainnet (different prefix)
|
||||
pubkey = bytes.fromhex(self.PUBKEY_HEX)
|
||||
addr = public_key_to_p2pkh(pubkey)
|
||||
self.assertFalse(is_address(addr, net=constants.BitcoinMainnet))
|
||||
|
||||
def test_bech32_hrp_is_btcp(self):
|
||||
# A native SegWit witness-v0 address encoded with HRP 'btcp' must be
|
||||
# accepted as valid by the BTCP network and rejected by Bitcoin mainnet.
|
||||
witness_program = bytes(20) # all-zero 20-byte hash (P2WPKH)
|
||||
addr = segwit_addr.encode_segwit_address(BitcoinPurple.SEGWIT_HRP, 0, witness_program)
|
||||
self.assertIsNotNone(addr)
|
||||
self.assertTrue(is_segwit_address(addr))
|
||||
self.assertFalse(is_segwit_address(addr, net=constants.BitcoinMainnet))
|
||||
|
||||
def test_bech32_address_to_script(self):
|
||||
# address_to_script must produce a valid P2WPKH scriptPubKey for a BTCP bech32 addr
|
||||
witness_program = bytes(20)
|
||||
addr = segwit_addr.encode_segwit_address(BitcoinPurple.SEGWIT_HRP, 0, witness_program)
|
||||
script = address_to_script(addr)
|
||||
# P2WPKH scriptPubKey: OP_0 <20-byte-hash> = 0x0014 + 20 zero bytes
|
||||
self.assertEqual('0014' + '00' * 20, script.hex())
|
||||
|
||||
def test_wif_roundtrip(self):
|
||||
# serialize_privkey / deserialize_privkey must round-trip under BTCP prefix 0xb7
|
||||
raw_key = bytes(31) + bytes([1]) # 32-byte privkey with value 1
|
||||
wif = serialize_privkey(raw_key, True, 'p2pkh')
|
||||
txin_type, got_key, compressed = deserialize_privkey(wif)
|
||||
self.assertEqual(raw_key, got_key)
|
||||
self.assertTrue(compressed)
|
||||
|
||||
def test_bitcoin_address_not_valid_on_btcp(self):
|
||||
# A Bitcoin mainnet P2PKH address ('1...') must not pass is_address on BTCP
|
||||
btc_addr = "1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf" # Bitcoin genesis coinbase
|
||||
self.assertFalse(is_address(btc_addr))
|
||||
|
||||
def test_bitcoin_bech32_not_valid_on_btcp(self):
|
||||
# A Bitcoin mainnet bech32 address (HRP 'bc') must be rejected on BTCP
|
||||
btc_bech32 = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
|
||||
self.assertFalse(is_segwit_address(btc_bech32))
|
||||
Reference in New Issue
Block a user