mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-13 12:33:42 +02:00
300 lines
16 KiB
Python
Executable file
300 lines
16 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2016 The Bitcoin Core developers
|
|
# Distributed under the MIT/X11 software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
from time import sleep
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import assert_equal, assert_raises_rpc_error, Decimal, assert_greater_than
|
|
|
|
'''
|
|
This test focuses on enforcement of PAK, not on transitioning lists
|
|
or RPC return values for PAK enforcement settings which is covered
|
|
in feature_dynafed.py
|
|
|
|
TODO: Test non-activated dynafed means no block enforcement of PAK
|
|
'''
|
|
|
|
class PAKTest (BitcoinTestFramework):
|
|
|
|
def set_test_params(self):
|
|
self.num_nodes = 3
|
|
self.setup_clean_chain = True
|
|
self.extra_args = [["-enforce_pak=1", "-evbparams=dynafed:-1:::", "-initialfreecoins=210000000000000", "-anyonecanspendaremine=1", "-parent_bech32_hrp=lol", "-pubkeyprefix=112", "-scriptprefix=197", "-con_connect_genesis_outputs=1"] for i in range(self.num_nodes)]
|
|
# First node doesn't enforce PAK, a "HF" of the other two nodes
|
|
# -acceptnonstdtxn=1 is required for first node as IsStandard will block larger pegout scripts if -enforce_pak=0
|
|
self.extra_args[0] = ["-acceptnonstdtxn=1"] + self.extra_args[0][1:]
|
|
|
|
def add_options(self, parser):
|
|
self.add_wallet_options(parser)
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
|
|
def run_test(self):
|
|
|
|
# Transitioning PAK lists and checking lists in RPC tested in feature_dynafed
|
|
|
|
self.log.info("Test wallet PAK")
|
|
|
|
# We will re-use the same xpub, but each wallet will create its own online pak
|
|
# so the lists will be incompatible, even if all else was synced
|
|
xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
|
|
xpub_desc = "pkh("+xpub+"/0/*)" # Transform this into a descriptor
|
|
init_results = []
|
|
info_results = []
|
|
for i in range(self.num_nodes):
|
|
if i == 0:
|
|
assert_raises_rpc_error(-8, "PAK enforcement is not enabled on this network.", self.nodes[i].initpegoutwallet, xpub)
|
|
init_results += [None]
|
|
info_results += [None]
|
|
continue
|
|
|
|
init_results += [ self.nodes[i].initpegoutwallet(xpub) ]
|
|
info_results += [ self.nodes[i].getwalletpakinfo() ]
|
|
assert_equal(init_results[i]["address_lookahead"], info_results[i]["address_lookahead"])
|
|
assert_equal(init_results[i]["pakentry"], info_results[i]["pakentry"])
|
|
assert_equal(init_results[i]["liquid_pak"], info_results[i]["liquid_pak"])
|
|
assert_equal(init_results[i]["liquid_pak_address"], info_results[i]["liquid_pak_address"])
|
|
assert_equal(info_results[i]["bitcoin_descriptor"], xpub_desc)
|
|
assert_equal(info_results[i]["bip32_counter"], "0")
|
|
validata = self.nodes[i].validateaddress(init_results[i]["address_lookahead"][0])
|
|
assert not validata["isvalid"]
|
|
assert validata["isvalid_parent"]
|
|
assert not validata["parent_address_info"]["isscript"]
|
|
assert not validata["parent_address_info"]["iswitness"]
|
|
|
|
# Use custom derivation counter values, check if stored correctly,
|
|
# address lookahead looks correct and that new liquid_pak was chosen
|
|
assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[1].initpegoutwallet, xpub, -1)
|
|
|
|
assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[1].initpegoutwallet, xpub, 1000000001)
|
|
|
|
# Make sure we can also prepend the key origin to the xpub.
|
|
self.nodes[1].initpegoutwallet("pkh([deadbeef/44h/0h/0h]"+xpub+"/0/*)")
|
|
new_init = self.nodes[1].initpegoutwallet(xpub, 2)
|
|
assert_equal(self.nodes[1].getwalletpakinfo()["bip32_counter"], "2")
|
|
assert_equal(new_init["address_lookahead"][0], init_results[1]["address_lookahead"][2])
|
|
assert new_init["liquid_pak"] != init_results[1]["liquid_pak"]
|
|
|
|
self.log.info("initpegoutwallet with null argument")
|
|
null_pak_init = self.nodes[2].initpegoutwallet(xpub, 5, None)
|
|
assert_equal(self.nodes[2].getwalletpakinfo()["bip32_counter"], "5")
|
|
assert null_pak_init["liquid_pak"]
|
|
|
|
# Restart and connect peers to check wallet persistence
|
|
self.stop_nodes()
|
|
self.start_nodes()
|
|
self.connect_nodes(0,1)
|
|
self.connect_nodes(1,2)
|
|
|
|
# Check PAK settings persistence in wallet across restart
|
|
restarted_info = self.nodes[1].getwalletpakinfo()
|
|
assert_equal(restarted_info["bitcoin_descriptor"], xpub_desc)
|
|
assert_equal(restarted_info["liquid_pak"], new_init["liquid_pak"])
|
|
assert_equal(restarted_info["bip32_counter"], "2")
|
|
|
|
# Compile list of extension space entries for pak enforcement
|
|
extension_space_proposal = []
|
|
for entry in init_results:
|
|
if entry is not None:
|
|
pakentry = entry["pakentry"]
|
|
extension_space_proposal += [pakentry[4:4+66]+pakentry[4+66+1:]]
|
|
|
|
self.log.info("Test mempool enforcement of PAK peg-outs")
|
|
|
|
# Transition to a pak list that only node 1 can peg-out to
|
|
WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
|
|
for _ in range(9):
|
|
block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":[extension_space_proposal[0]]})
|
|
assert_equal(self.nodes[1].submitblock(block), None)
|
|
|
|
self.sync_all()
|
|
assert_equal(self.nodes[0].getblockchaininfo()["extension_space"], [extension_space_proposal[0]])
|
|
|
|
# node 1 has wrong pak entry in wallet
|
|
assert_raises_rpc_error(-4, "Given online key is not in Pegout Authorization Key List", self.nodes[1].sendtomainchain, "", 1)
|
|
|
|
# put back init_info version that's in pak list
|
|
self.nodes[1].initpegoutwallet(xpub, 0, init_results[1]["liquid_pak"])
|
|
|
|
# Node 1 will now make a PAK peg-out, accepted in all mempools and blocks
|
|
pegout_info = self.nodes[1].sendtomainchain("", 1)
|
|
raw_node1_pegout = self.nodes[1].gettransaction(pegout_info["txid"])["hex"]
|
|
self.sync_all() # mempool sync
|
|
self.generatetoaddress(self.nodes[1], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
|
|
assert_greater_than(self.nodes[1].gettransaction(pegout_info["txid"])["confirmations"], 0)
|
|
|
|
# Re-org keep node 1 peg-out unconfirmed and transition to "full list"
|
|
# then check peg-out fails
|
|
|
|
# Invalidate back to block 1, then make 9 new blocks to hit transition
|
|
# If you roll back to genesis block p2p code gets flakey
|
|
num_block_rollback = self.nodes[1].getblockcount()-2
|
|
fork_hash = self.nodes[1].getblockhash(2)
|
|
for i in range(self.num_nodes):
|
|
self.nodes[i].invalidateblock(fork_hash)
|
|
self.sync_blocks()
|
|
|
|
for _ in range(num_block_rollback):
|
|
block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":extension_space_proposal})
|
|
self.nodes[1].submitblock(block)
|
|
|
|
self.sync_blocks()
|
|
|
|
# node 0 puts the peg-out back in its mempool, can't sync all
|
|
self.nodes[0].sendrawtransaction(raw_node1_pegout)
|
|
self.sync_mempools(self.nodes[1:])
|
|
|
|
# rejected in mempool
|
|
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[1].sendrawtransaction, raw_node1_pegout)
|
|
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[2].sendrawtransaction, raw_node1_pegout)
|
|
# node 0 tries to make bad block
|
|
wrong_pak_prop = self.nodes[0].getnewblockhex()
|
|
# rejected in blocks
|
|
assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[1].testproposedblock, wrong_pak_prop, True)
|
|
assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[2].testproposedblock, wrong_pak_prop, True)
|
|
|
|
self.log.info("Test various RPC arguments")
|
|
|
|
# Fail to peg-out too-small value
|
|
assert_raises_rpc_error(-8, "Invalid amount for send, must send more than 0.00100000 BTC", self.nodes[1].sendtomainchain, "", Decimal('0.0009'))
|
|
|
|
# Use wrong network's extended pubkey
|
|
mainnetxpub = "xpub6AATBi58516uxLogbuaG3jkom7x1qyDoZzMN2AePBuQnMFKUV9xC2BW9vXsFJ9rELsvbeGQcFWhtbyM4qDeijM22u3AaSiSYEvuMZkJqtLn"
|
|
assert_raises_rpc_error(-8, "'%s' is not a valid descriptor function" % mainnetxpub, self.nodes[1].initpegoutwallet, mainnetxpub)
|
|
|
|
# Test fixed online pubkey
|
|
init_info = self.nodes[1].initpegoutwallet(xpub)
|
|
init_info2 = self.nodes[1].initpegoutwallet(xpub, 0, init_info['liquid_pak'])
|
|
assert_equal(init_info, init_info2)
|
|
init_info3 = self.nodes[1].initpegoutwallet(xpub)
|
|
assert init_info != init_info3
|
|
|
|
# Test Descriptor PAK Support
|
|
|
|
# Non-supported descriptors
|
|
assert_raises_rpc_error(-8, "bitcoin_descriptor is not of any type supported: pkh(<xpub>), sh(wpkh(<xpub>)), wpkh(<xpub>), or <xpub>.", self.nodes[1].initpegoutwallet, "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)")
|
|
|
|
assert_raises_rpc_error(-8, "bitcoin_descriptor must be a ranged descriptor.", self.nodes[1].initpegoutwallet, "pkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)")
|
|
|
|
# Peg out with each new type, check that destination script matches
|
|
wpkh_desc = "wpkh("+xpub+"/0/*)"
|
|
# add a valid checksum
|
|
wpkh_desc = self.nodes[1].getdescriptorinfo(wpkh_desc)["descriptor"]
|
|
wpkh_info = self.nodes[1].initpegoutwallet(wpkh_desc)
|
|
wpkh_pak_info = self.nodes[1].getwalletpakinfo()
|
|
|
|
# Transition to wpkh entry list
|
|
wpkh_pak_entry = wpkh_info["pakentry"]
|
|
wpkh_pak_prop = [wpkh_pak_entry[4:4+66]+wpkh_pak_entry[4+66+1:]]
|
|
for _ in range(10):
|
|
block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":wpkh_pak_prop})
|
|
self.nodes[1].submitblock(block)
|
|
self.sync_blocks()
|
|
|
|
# Get some block subsidy and send off
|
|
self.generatetoaddress(self.nodes[1], 101, self.nodes[1].getnewaddress(), sync_fun=self.no_op)
|
|
wpkh_stmc = self.nodes[1].sendtomainchain("", 1)
|
|
wpkh_txid = wpkh_stmc['txid']
|
|
|
|
self.log.info("sendtomainchain with null argument")
|
|
verbose_stmc = self.nodes[1].sendtomainchain("", 1, None, True)
|
|
assert isinstance(verbose_stmc, dict)
|
|
assert 'txid' in verbose_stmc
|
|
assert 'fee_reason' in verbose_stmc
|
|
|
|
# Also check some basic return fields of sendtomainchain with pak
|
|
assert_equal(wpkh_stmc["bitcoin_address"], wpkh_info["address_lookahead"][0])
|
|
validata = self.nodes[1].validateaddress(wpkh_stmc["bitcoin_address"])
|
|
assert not validata["isvalid"]
|
|
assert validata["isvalid_parent"]
|
|
assert not validata["parent_address_info"]["isscript"]
|
|
assert validata["parent_address_info"]["iswitness"]
|
|
assert_equal(wpkh_pak_info["bip32_counter"], wpkh_stmc["bip32_counter"])
|
|
assert_equal(wpkh_pak_info["bitcoin_descriptor"], wpkh_stmc["bitcoin_descriptor"])
|
|
|
|
sh_wpkh_desc = "sh(wpkh("+xpub+"/0/1/*))"
|
|
sh_wpkh_info = self.nodes[1].initpegoutwallet(sh_wpkh_desc)
|
|
|
|
validata = self.nodes[1].validateaddress(sh_wpkh_info["address_lookahead"][0])
|
|
assert not validata["isvalid"]
|
|
assert validata["isvalid_parent"]
|
|
assert validata["parent_address_info"]["isscript"]
|
|
assert not validata["parent_address_info"]["iswitness"]
|
|
|
|
# Transition to sh_wpkh entry list
|
|
sh_wpkh_pak_entry = sh_wpkh_info["pakentry"]
|
|
sh_wpkh_pak_prop = [sh_wpkh_pak_entry[4:4+66]+sh_wpkh_pak_entry[4+66+1:]]
|
|
for _ in range(10):
|
|
block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":sh_wpkh_pak_prop})
|
|
self.nodes[1].submitblock(block)
|
|
self.sync_blocks()
|
|
|
|
self.generatetoaddress(self.nodes[1], 1, self.nodes[1].getnewaddress(), sync_fun=self.no_op)
|
|
sh_wpkh_txid = self.nodes[1].sendtomainchain("", 1)['txid']
|
|
|
|
# Make sure peg-outs look correct
|
|
wpkh_raw = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(wpkh_txid)['hex'])
|
|
sh_wpkh_raw = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(sh_wpkh_txid)['hex'])
|
|
|
|
peg_out_found = False
|
|
for output in wpkh_raw["vout"]:
|
|
if "pegout_address" in output["scriptPubKey"]:
|
|
if output["scriptPubKey"]["pegout_address"] == wpkh_info["address_lookahead"][0]:
|
|
peg_out_found = True
|
|
break
|
|
else:
|
|
raise Exception("Found unexpected peg-out output")
|
|
assert peg_out_found
|
|
|
|
peg_out_found = False
|
|
for output in sh_wpkh_raw["vout"]:
|
|
if "pegout_address" in output["scriptPubKey"]:
|
|
if output["scriptPubKey"]["pegout_address"] == sh_wpkh_info["address_lookahead"][0]:
|
|
peg_out_found = True
|
|
break
|
|
else:
|
|
raise Exception("Found unexpected peg-out output")
|
|
assert peg_out_found
|
|
|
|
# Make sure they all confirm
|
|
self.generatetoaddress(self.nodes[1], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
|
|
for tx_id in [wpkh_txid, sh_wpkh_txid]:
|
|
assert_greater_than(self.nodes[1].gettransaction(tx_id)["confirmations"], 0)
|
|
|
|
self.sync_blocks()
|
|
|
|
self.log.info("Test that pak-less pegouts are rejected")
|
|
|
|
# Last test of a pak-less peg-out failing to get into mempool/block
|
|
# Note it leaves a transaction in node 0's mempool, so sync_all cannot
|
|
# work after unless it's somehow booted.
|
|
|
|
# node 0 will now create a pegout, will fail to enter mempool of node 1 or 2
|
|
# since it's pak-less
|
|
pegout_address = "n3NkSZqoPMCQN5FENxUBw4qVATbytH6FDK"
|
|
nopak_pegout_txid = self.nodes[0].sendtomainchain(pegout_address, 1)
|
|
pegout_tx = self.nodes[0].gettransaction(nopak_pegout_txid,True,True)
|
|
assert_equal(pegout_tx["decoded"]["vout"][[d["value"] for d in pegout_tx["decoded"]["vout"]].index(1)]["scriptPubKey"]["pegout_address"],pegout_address)
|
|
raw_pakless_pegout = self.nodes[0].gettransaction(nopak_pegout_txid)["hex"]
|
|
sleep(1) # hacky fix for flaky timing where getrawmempool doesn't have nopak_pegout_txid yet
|
|
assert nopak_pegout_txid in self.nodes[0].getrawmempool()
|
|
|
|
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[1].sendrawtransaction, raw_pakless_pegout)
|
|
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[2].sendrawtransaction, raw_pakless_pegout)
|
|
|
|
# node 0 makes a block that includes the pakless pegout, rejected by others
|
|
# by consensus
|
|
bad_prop = self.nodes[0].getnewblockhex()
|
|
assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[1].testproposedblock, bad_prop, True)
|
|
assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[2].testproposedblock, bad_prop, True)
|
|
|
|
# Test that subtracting fee from output works
|
|
self.generatetoaddress(self.nodes[1], 101, self.nodes[1].getnewaddress(), sync_fun=self.no_op)
|
|
self.nodes[1].sendtomainchain("", self.nodes[1].getbalance()["bitcoin"], True)
|
|
assert_equal(self.nodes[1].getbalance()["bitcoin"], 0)
|
|
|
|
# TODO: create rawsendtomainchain to do transaction surgery for testing
|
|
|
|
if __name__ == '__main__':
|
|
PAKTest(__file__).main()
|