#!/usr/bin/env python3 """Test P2SPKH: address generation, send, spend, migration, and decodescript. End-to-end test of Pay to Schnorr Public Key Hash. """ from test_framework.descriptors import descsum_create from test_framework.key import compute_xonly_pubkey from test_framework.test_framework import LimenkaTestFramework from test_framework.util import assert_equal, assert_greater_than, assert_raises_rpc_error from test_framework.wallet_util import generate_keypair from test_framework.messages import hash256 class P2SPKHTest(LimenkaTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): node = self.nodes[0] default_w = node.get_wallet_rpc(self.default_wallet_name) # Fund the default wallet with mature coinbase outputs self.generate(node, 101) assert_equal(default_w.getbalance(), 50) self._test_address_generation(node, default_w) self._test_send_and_spend(node, default_w) self._test_decodescript(node, default_w) self._test_migration(node, default_w) self._test_fundrawtransaction(node, default_w) self._test_multiple_spends(node, default_w) def _create_spk_wallet(self, node, wallet_name, use_xpub=True): """Create a wallet with a spk() descriptor and return (wallet_rpc, address).""" node.createwallet(wallet_name=wallet_name, blank=True) wallet = node.get_wallet_rpc(wallet_name) if use_xpub: from test_framework.extendedkey import ExtendedPrivateKey xprv = ExtendedPrivateKey.generate() desc_pay = descsum_create("spk(" + xprv.to_string() + "/87h/1h/0h/0/*)") desc_change = descsum_create("spk(" + xprv.to_string() + "/87h/1h/0h/1/*)") else: privkey_ec, pubkey = generate_keypair(compressed=True, wif=False) privkey_b = privkey_ec.get_bytes() xonly_pub, _ = compute_xonly_pubkey(privkey_b) desc_pay = descsum_create("spk(" + xonly_pub.hex() + ")") desc_change = descsum_create("spk(" + xonly_pub.hex() + ")") wallet.importdescriptors([ {"desc": desc_pay, "active": True, "timestamp": "now"}, {"desc": desc_change, "active": True, "timestamp": "now", "internal": True}, ]) if use_xpub: addr = wallet.getnewaddress(address_type="p2spkh") else: addrs = wallet.deriveaddresses(desc_pay) addr = addrs[0] info = wallet.getaddressinfo(addr) assert info['iswitness'], "P2SPKH should be witness" assert_equal(info.get('witness_version'), 3) return wallet, addr def _test_address_generation(self, node, default_w): """Test that spk() descriptor produces valid P2SPKH addresses.""" wallet, addr = self._create_spk_wallet(node, "spk_addr", use_xpub=True) self.log.info(f"P2SPKH address: {addr}") # Bech32m v3 address prefix. v0='q', v1='p', v2='z', v3='r' assert addr.startswith(("bcrt1r", "tb1r", "bc1r")), f"Expected v3 bech32m prefix, got {addr}" # Verify we can decode and re-derive info = wallet.getaddressinfo(addr) spk_hex = info['scriptPubKey'] assert len(spk_hex) == 68, f"ScriptPubKey should be 34 bytes (68 hex), got {len(spk_hex)}" # OP_3 (0x53) + OP_PUSH32 (0x20) + 32-byte hash assert spk_hex[:4] == "5320", f"Expected 0x53 0x20, got {spk_hex[:4]}" # Verify ranged wallets produce unique addresses addr2 = wallet.getnewaddress(address_type="p2spkh") assert addr != addr2 self.log.info("Address generation: PASS") def _test_send_and_spend(self, node, default_w): """Test sending to a P2SPKH address and spending from it.""" wallet, addr = self._create_spk_wallet(node, "spk_spend", use_xpub=True) self.log.info(f"P2SPKH spend test address: {addr}") # Send 10 BTC to P2SPKH address txid = default_w.sendtoaddress(addr, 10) self.generate(node, 1) # Verify the P2SPKH wallet sees the balance assert_equal(wallet.getbalance(), 10) utxos = wallet.listunspent() assert_equal(len(utxos), 1) assert_equal(utxos[0]['amount'], 10) # Verify output type is recognized tx = default_w.gettransaction(txid) assert tx['confirmations'] > 0 # Spend 5 BTC from P2SPKH to a new default-wallet address dest = default_w.getnewaddress() spend_txid = wallet.sendtoaddress(dest, 5) self.generate(node, 1) assert_greater_than(wallet.getbalance(), 4.9) # accounts for fee # Verify the spend transaction confirms spend_tx = wallet.gettransaction(spend_txid) assert_greater_than(spend_tx['confirmations'], 0) self.log.info("Send and spend: PASS") def _test_decodescript(self, node, default_w): """Test that decodescript recognizes P2SPKH scriptPubKeys.""" wallet, addr = self._create_spk_wallet(node, "spk_decode", use_xpub=False) info = wallet.getaddressinfo(addr) spk_hex = info['scriptPubKey'] decoded = node.decodescript(spk_hex) assert_equal(decoded['type'], 'witness_v3_spkhash') assert 'address' in decoded self.log.info("Decodescript: PASS") def _test_migration(self, node, default_w): """Test sweeping taproot UTXOs to P2SPKH (migration path).""" wallet, addr = self._create_spk_wallet(node, "spk_migrate", use_xpub=True) # Create a P2TR (taproot) address in the default wallet and fund it taproot_addr = default_w.getnewaddress(address_type="bech32m") txid = default_w.sendtoaddress(taproot_addr, 1) self.generate(node, 1) # Now sweep ALL funds from default wallet to P2SPKH address sweep_result = default_w.sendall(recipients=[addr], fee_rate=1) self.generate(node, 1) assert_equal(sweep_result['complete'], True) # The P2SPKH wallet should have received funds spk_balance = wallet.getbalance() assert_greater_than(spk_balance, 0) # The sweep moved the 1 BTC taproot UTXO plus any remaining coinbase # Construct a raw P2SPKH descriptor send (without using wallet descriptor) dest = wallet.getnewaddress(address_type="p2spkh") txid2 = default_w.sendtoaddress(dest, 1) assert txid2 is not None self.log.info("Migration sweep: PASS") def _test_fundrawtransaction(self, node, default_w): """Test fundrawtransaction with P2SPKH outputs.""" wallet, addr = self._create_spk_wallet(node, "spk_fund", use_xpub=True) # Fund the wallet with some BTC default_w.sendtoaddress(addr, 5) self.generate(node, 1) assert_equal(wallet.getbalance(), 5) # Create a raw transaction sending from P2SPKH dest = default_w.getnewaddress() raw = wallet.createrawtransaction([], [{dest: 2}]) funded = wallet.fundrawtransaction(raw) signed = wallet.signrawtransactionwithwallet(funded['hex']) assert signed['complete'] txid = wallet.sendrawtransaction(signed['hex']) self.generate(node, 1) tx = wallet.gettransaction(txid) assert_greater_than(tx['confirmations'], 0) self.log.info("fundrawtransaction: PASS") def _test_multiple_spends(self, node, default_w): """Test multiple P2SPKH inputs in one transaction.""" wallet, addr = self._create_spk_wallet(node, "spk_multi", use_xpub=True) # Send two separate UTXOs to the P2SPKH wallet addr2 = wallet.getnewaddress(address_type="p2spkh") addr3 = wallet.getnewaddress(address_type="p2spkh") default_w.sendtoaddress(addr, 3) default_w.sendtoaddress(addr2, 3) default_w.sendtoaddress(addr3, 3) self.generate(node, 1) assert_equal(wallet.getbalance(), 9) # Spend all three UTXOs in one transaction unspent = wallet.listunspent() assert_equal(len(unspent), 3) dest = default_w.getnewaddress() raw = wallet.createrawtransaction( [{"txid": u["txid"], "vout": u["vout"]} for u in unspent], {dest: 8.999} ) funded = wallet.fundrawtransaction(raw) signed = wallet.signrawtransactionwithwallet(funded['hex']) assert signed['complete'] txid = wallet.sendrawtransaction(signed['hex']) self.generate(node, 1) tx = wallet.gettransaction(txid) assert_greater_than(tx['confirmations'], 0) self.log.info("Multiple spends: PASS") if __name__ == '__main__': P2SPKHTest(__file__).main()