From 2a4680697491eea0325ce17335033bd247ad1a4a Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 12 Dec 2018 12:20:09 +0100 Subject: [PATCH] Integration test for pegins and pegouts --- test/functional/feature_fedpeg.py | 419 ++++++++++++++++++ .../test_framework/test_framework.py | 8 +- test/functional/test_framework/test_node.py | 1 + test/functional/test_runner.py | 2 + 4 files changed, 428 insertions(+), 2 deletions(-) create mode 100755 test/functional/feature_fedpeg.py diff --git a/test/functional/feature_fedpeg.py b/test/functional/feature_fedpeg.py new file mode 100755 index 00000000000..d44ef38c7d6 --- /dev/null +++ b/test/functional/feature_fedpeg.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 + +from decimal import Decimal +import os +import json +import time + +from test_framework.authproxy import JSONRPCException +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + connect_nodes_bi, + get_auth_cookie, + get_datadir_path, + rpc_port, + p2p_port, +) + +def get_new_unconfidential_address(node): + addr = node.getnewaddress() + val_addr = node.getaddressinfo(addr) + if 'unconfidential' in val_addr: + return val_addr['unconfidential'] + return val_addr['address'] + +class FedPegTest(BitcoinTestFramework): + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 4 + + def add_options(self, parser): + parser.add_argument("--parent_binpath", dest="parent_binpath", default="", + help="Use a different binary for launching nodes") + parser.add_argument("--parent_bitcoin", dest="parent_bitcoin", default=False, action="store_true", + help="Parent nodes are Bitcoin") + + def setup_network(self, split=False): + if self.options.parent_bitcoin and self.options.parent_binpath == "": + raise Exception("Can't run with --parent_bitcoin without specifying --parent_binpath") + + self.nodes = [] + # Setup parent nodes + parent_chain = "regtest" if self.options.parent_bitcoin else "parent" + parent_binary = [self.options.parent_binpath] if self.options.parent_binpath != "" else None + for n in range(2): + extra_args = [ + "-printtoconsole=0", + "-port="+str(p2p_port(n)), + "-rpcport="+str(rpc_port(n)) + ] + if self.options.parent_bitcoin: + extra_args.extend([ + "-addresstype=legacy", # To make sure bitcoind gives back p2pkh no matter version + ]) + else: + extra_args.extend([ + "-validatepegin=0", + "-anyonecanspendaremine", + #TODO(rebase) add with initialfreecoins + #"-initialfreecoins=2100000000000000", + ]) + + self.add_nodes(1, [extra_args], chain=[parent_chain], binary=parent_binary) + self.start_node(n) + print("Node {} started".format(n)) + + connect_nodes_bi(self.nodes, 0, 1) + self.parentgenesisblockhash = self.nodes[0].getblockhash(0) + print('parentgenesisblockhash', self.parentgenesisblockhash) + #TODO(rebase) CA + #if not self.options.parent_bitcoin: + # parent_pegged_asset = self.nodes[0].getsidechaininfo()['pegged_asset'] + + # Setup sidechain nodes + self.fedpeg_script = "512103dff4923d778550cc13ce0d887d737553b4b58f4e8e886507fc39f5e447b2186451ae" + for n in range(2): + extra_args = [ + "-printtoconsole=0", + "-port="+str(p2p_port(2+n)), + "-rpcport="+str(rpc_port(2+n)), + '-parentgenesisblockhash=%s' % self.parentgenesisblockhash, + '-validatepegin=1', + '-fedpegscript=%s' % self.fedpeg_script, + '-anyonecanspendaremine=0', + '-minrelaytxfee=0', + '-blockmintxfee=0', + #TODO(rebase) initialfreecoins + #'-initialfreecoins=0', + '-peginconfirmationdepth=10', + '-mainchainrpchost=127.0.0.1', + '-mainchainrpcport=%s' % rpc_port(n), + '-recheckpeginblockinterval=15', # Long enough to allow failure and repair before timeout + ] + if not self.options.parent_bitcoin: + extra_args.extend([ + #TODO(rebase) should the defaults be put back to elements-specific ones? + #'-parentpubkeyprefix=235', + #'-parentscriptprefix=75', + '-parentpubkeyprefix=111', + '-parentscriptprefix=196', + #TODO(rebase) signed blocks + #'-con_parent_chain_signblockscript=51', + #TODO(rebase) CA + #'-con_parent_pegged_asset=%s' % parent_pegged_asset, + ]) + + # Use rpcuser auth only for first parent. + if n==0: + # Extract username and password from cookie file and use directly. + datadir = get_datadir_path(self.options.tmpdir, n) + rpc_u, rpc_p = get_auth_cookie(datadir, parent_chain) + extra_args.extend([ + '-mainchainrpcuser=%s' % rpc_u, + '-mainchainrpcpassword=%s' % rpc_p, + ]) + else: + # Need to specify where to find parent cookie file + datadir = get_datadir_path(self.options.tmpdir, n) + extra_args.append('-mainchainrpccookiefile='+datadir+"/" + parent_chain + "/.cookie") + + self.add_nodes(1, [extra_args], chain=["sidechain"]) + self.start_node(2+n) + print("Node {} started".format(2+n)) + + # We only connect the same-chain nodes, so sync_all works correctly + connect_nodes_bi(self.nodes, 2, 3) + self.node_groups = [[self.nodes[0], self.nodes[1]], [self.nodes[2], self.nodes[3]]] + self.sync_all(self.node_groups) + print("Setting up network done") + + def test_pegout(self, parent_chain_addr, sidechain): + pegout_txid = sidechain.sendtomainchain(parent_chain_addr, 1) + raw_pegout = sidechain.getrawtransaction(pegout_txid, True) + assert 'vout' in raw_pegout and len(raw_pegout['vout']) > 0 + pegout_tested = False + for output in raw_pegout['vout']: + scriptPubKey = output['scriptPubKey'] + if 'type' in scriptPubKey and scriptPubKey['type'] == 'nulldata': + assert ('pegout_hex' in scriptPubKey and 'pegout_asm' in scriptPubKey and 'pegout_type' in scriptPubKey and + 'pegout_chain' in scriptPubKey and 'pegout_reqSigs' in scriptPubKey and 'pegout_addresses' in scriptPubKey) + assert scriptPubKey['pegout_chain'] == self.parentgenesisblockhash + assert scriptPubKey['pegout_reqSigs'] == 1 + assert parent_chain_addr in scriptPubKey['pegout_addresses'] + pegout_tested = True + break + assert pegout_tested + + def run_test(self): + parent = self.nodes[0] + parent2 = self.nodes[1] + sidechain = self.nodes[2] + sidechain2 = self.nodes[3] + + parent.generate(101) + sidechain.generate(101) + + addrs = sidechain.getpeginaddress() + addr = addrs["mainchain_address"] + print('addrs', addrs) + print(parent.getaddressinfo(addr)) + txid1 = parent.sendtoaddress(addr, 24) + # 10+2 confirms required to get into mempool and confirm + parent.generate(1) + time.sleep(2) + proof = parent.gettxoutproof([txid1]) + + raw = parent.getrawtransaction(txid1) + print('raw', parent.getrawtransaction(txid1, True)) + + print("Attempting peg-in") + # First attempt fails the consensus check but gives useful result + try: + pegtxid = sidechain.claimpegin(raw, proof) + raise Exception("Peg-in should not be mature enough yet, need another block.") + except JSONRPCException as e: + print('RPC ERROR:', e.error['message']) + assert("Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"]) + + # Second attempt simply doesn't hit mempool bar + parent.generate(10) + try: + pegtxid = sidechain.claimpegin(raw, proof) + raise Exception("Peg-in should not be mature enough yet, need another block.") + except JSONRPCException as e: + print('RPC ERROR:', e.error['message']) + assert("Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"]) + + try: + pegtxid = sidechain.createrawpegin(raw, proof, 'AEIOU') + raise Exception("Peg-in with non-hex claim_script should fail.") + except JSONRPCException as e: + print('RPC ERROR:', e.error['message']) + assert("Given claim_script is not hex." in e.error["message"]) + + # Should fail due to non-matching wallet address + try: + scriptpubkey = sidechain.getaddressinfo(get_new_unconfidential_address(sidechain))["scriptPubKey"] + pegtxid = sidechain.claimpegin(raw, proof, scriptpubkey) + raise Exception("Peg-in with non-matching claim_script should fail.") + except JSONRPCException as e: + print('RPC ERROR:', e.error['message']) + assert("Given claim_script does not match the given Bitcoin transaction." in e.error["message"]) + + # 12 confirms allows in mempool + parent.generate(1) + # Should succeed via wallet lookup for address match, and when given + raw_pegin = sidechain.createrawpegin(raw, proof)['hex'] + signed_pegin = sidechain.signrawtransactionwithwallet(raw_pegin) + + pegtxid1 = sidechain.claimpegin(raw, proof) + + # Will invalidate the block that confirms this transaction later + self.sync_all(self.node_groups) + blockhash = sidechain2.generate(1) + self.sync_all(self.node_groups) + sidechain.generate(5) + + tx1 = sidechain.gettransaction(pegtxid1) + + print('tx1', tx1) + if "confirmations" in tx1 and tx1["confirmations"] == 6: + print("Peg-in is confirmed: Success!") + else: + raise Exception("Peg-in confirmation has failed.") + + # Look at pegin fields + decoded = sidechain.decoderawtransaction(tx1["hex"]) + assert decoded["vin"][0]["is_pegin"] == True + assert len(decoded["vin"][0]["pegin_witness"]) > 0 + #TODO(rebase) CT assert fee here too + # Check that there's sufficient fee for the peg-in + #vsize = decoded["vsize"] + #fee_output = decoded["vout"][1] + #fallbackfee_pervbyte = Decimal("0.00001")/Decimal("1000") + #print("fee_output", fee_output) + #assert fee_output["scriptPubKey"]["type"] == "fee" + #assert fee_output["value"] >= fallbackfee_pervbyte*vsize + + # Quick reorg checks of pegs + sidechain.invalidateblock(blockhash[0]) + if sidechain.gettransaction(pegtxid1)["confirmations"] != 0: + raise Exception("Peg-in didn't unconfirm after invalidateblock call.") + # Re-enters block + sidechain.generate(1) + if sidechain.gettransaction(pegtxid1)["confirmations"] != 1: + raise Exception("Peg-in should have one confirm on side block.") + sidechain.reconsiderblock(blockhash[0]) + if sidechain.gettransaction(pegtxid1)["confirmations"] != 6: + raise Exception("Peg-in should be back to 6 confirms.") + + # Do multiple claims in mempool + n_claims = 6 + + print("Flooding mempool with many small claims") + pegtxs = [] + sidechain.generate(101) + + # Do mixture of raw peg-in and automatic peg-in tx construction + # where raw creation is done on another node + for i in range(n_claims): + addrs = sidechain.getpeginaddress() + txid = parent.sendtoaddress(addrs["mainchain_address"], 1) + parent.generate(1) + proof = parent.gettxoutproof([txid]) + raw = parent.getrawtransaction(txid) + if i % 2 == 0: + parent.generate(11) + pegtxs += [sidechain.claimpegin(raw, proof)] + else: + # The raw API doesn't check for the additional 2 confirmation buffer + # So we only get 10 confirms then send off. Miners will add to block anyways. + + # Don't mature whole way yet to test signing immature peg-in input + parent.generate(8) + # Wallet in sidechain2 gets funds instead of sidechain + raw_pegin = sidechain2.createrawpegin(raw, proof, addrs["claim_script"])["hex"] + # First node should also be able to make a valid transaction with or without 3rd arg + # since this wallet originated the claim_script itself + sidechain.createrawpegin(raw, proof, addrs["claim_script"]) + sidechain.createrawpegin(raw, proof) + signed_pegin = sidechain.signrawtransactionwithwallet(raw_pegin) + assert(signed_pegin["complete"]) + assert("warning" in signed_pegin) # warning for immature peg-in + # fully mature them now + parent.generate(1) + pegtxs += [sidechain.sendrawtransaction(signed_pegin["hex"])] + + self.sync_all(self.node_groups) + sidechain2.generate(1) + for i, pegtxid in enumerate(pegtxs): + if i % 2 == 0: + tx = sidechain.gettransaction(pegtxid) + else: + tx = sidechain2.gettransaction(pegtxid) + if "confirmations" not in tx or tx["confirmations"] == 0: + raise Exception("Peg-in confirmation has failed.") + + print("Test pegout") + self.test_pegout(get_new_unconfidential_address(parent), sidechain) + + print("Test pegout P2SH") + parent_chain_addr = get_new_unconfidential_address(parent) + parent_pubkey = parent.getaddressinfo(parent_chain_addr)["pubkey"] + parent_chain_p2sh_addr = parent.createmultisig(1, [parent_pubkey])["address"] + self.test_pegout(parent_chain_p2sh_addr, sidechain) + + print("Test pegout Garbage") + parent_chain_addr = "garbage" + try: + self.test_pegout(parent_chain_addr, sidechain) + raise Exception("A garbage address should fail.") + except JSONRPCException as e: + assert("Invalid Bitcoin address" in e.error["message"]) + + print("Test pegout Garbage valid") + prev_txid = sidechain.sendtoaddress(sidechain.getnewaddress(), 1) + sidechain.generate(1) + pegout_chain = 'a' * 64 + pegout_hex = 'b' * 500 + inputs = [{"txid": prev_txid, "vout": 0}] + outputs = {"vdata": [pegout_chain, pegout_hex]} + rawtx = sidechain.createrawtransaction(inputs, outputs) + raw_pegout = sidechain.decoderawtransaction(rawtx) + + assert 'vout' in raw_pegout and len(raw_pegout['vout']) > 0 + pegout_tested = False + for output in raw_pegout['vout']: + scriptPubKey = output['scriptPubKey'] + if 'type' in scriptPubKey and scriptPubKey['type'] == 'nulldata': + assert ('pegout_hex' in scriptPubKey and 'pegout_asm' in scriptPubKey and 'pegout_type' in scriptPubKey and + 'pegout_chain' in scriptPubKey and 'pegout_reqSigs' not in scriptPubKey and 'pegout_addresses' not in scriptPubKey) + assert scriptPubKey['pegout_type'] == 'nonstandard' + assert scriptPubKey['pegout_chain'] == pegout_chain + assert scriptPubKey['pegout_hex'] == pegout_hex + pegout_tested = True + break + assert pegout_tested + + print ("Now test failure to validate peg-ins based on intermittant bitcoind rpc failure") + self.stop_node(1) + txid = parent.sendtoaddress(addr, 1) + parent.generate(12) + proof = parent.gettxoutproof([txid]) + raw = parent.getrawtransaction(txid) + stuck_peg = sidechain.claimpegin(raw, proof) + sidechain.generate(1) + print("Waiting to ensure block is being rejected by sidechain2") + time.sleep(5) + + assert(sidechain.getblockcount() != sidechain2.getblockcount()) + + print("Restarting parent2") + self.start_node(1) + parent2 = self.nodes[1] + connect_nodes_bi(self.nodes, 0, 1) + + # Don't make a block, race condition when pegin-invalid block + # is awaiting further validation, nodes reject subsequent blocks + # even ones they create + print("Now waiting for node to re-evaluate peg-in witness failed block... should take a few seconds") + self.sync_all(self.node_groups) + print("Completed!\n") + print("Now send funds out in two stages, partial, and full") + some_btc_addr = get_new_unconfidential_address(parent) + #TODO(rebase) CA bitcoin balances + #bal_1 = sidechain.getwalletinfo()["balance"]["bitcoin"] + bal_1 = sidechain.getwalletinfo()["balance"] + try: + sidechain.sendtomainchain(some_btc_addr, bal_1 + 1) + raise Exception("Sending out too much; should have failed") + except JSONRPCException as e: + assert("Insufficient funds" in e.error["message"]) + + #assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) + assert(sidechain.getwalletinfo()["balance"] == bal_1) + try: + sidechain.sendtomainchain(some_btc_addr+"b", bal_1 - 1) + raise Exception("Sending to invalid address; should have failed") + except JSONRPCException as e: + assert("Invalid Bitcoin address" in e.error["message"]) + + #assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) + assert(sidechain.getwalletinfo()["balance"] == bal_1) + try: + sidechain.sendtomainchain("1Nro9WkpaKm9axmcfPVp79dAJU1Gx7VmMZ", bal_1 - 1) + raise Exception("Sending to mainchain address when should have been testnet; should have failed") + except JSONRPCException as e: + assert("Invalid Bitcoin address" in e.error["message"]) + + #assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) + assert(sidechain.getwalletinfo()["balance"] == bal_1) + + peg_out_txid = sidechain.sendtomainchain(some_btc_addr, 1) + + peg_out_details = sidechain.decoderawtransaction(sidechain.getrawtransaction(peg_out_txid)) + # peg-out, change + #TODO(rebase) CA/CT fee output + #assert(len(peg_out_details["vout"]) == 3) + assert(len(peg_out_details["vout"]) == 2) + found_pegout_value = False + for output in peg_out_details["vout"]: + if "value" in output and output["value"] == 1: + found_pegout_value = True + assert(found_pegout_value) + + #bal_2 = sidechain.getwalletinfo()["balance"]["bitcoin"] + bal_2 = sidechain.getwalletinfo()["balance"] + # Make sure balance went down + assert(bal_2 + 1 < bal_1) + + sidechain.sendtomainchain(some_btc_addr, bal_2, True) + + #TODO(rebase) CA bitcoin balance + #assert("bitcoin" not in sidechain.getwalletinfo()["balance"]) + + print('Success!') + +if __name__ == '__main__': + FedPegTest().main() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index ffc3340c369..df380dfeffb 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -254,7 +254,7 @@ def run_test(self): # Public helper methods. These can be accessed by the subclass test scripts. - def add_nodes(self, num_nodes, extra_args=None, *, rpchost=None, binary=None): + def add_nodes(self, num_nodes, extra_args=None, *, rpchost=None, chain=None, binary=None): """Instantiate TestNode objects""" if self.bind_to_localhost_only: extra_confs = [["bind=127.0.0.1"]] * num_nodes @@ -264,11 +264,15 @@ def add_nodes(self, num_nodes, extra_args=None, *, rpchost=None, binary=None): extra_args = [[]] * num_nodes if binary is None: binary = [self.options.bitcoind] * num_nodes + if chain is None: + chain = [self.chain] * num_nodes assert_equal(len(extra_confs), num_nodes) assert_equal(len(extra_args), num_nodes) assert_equal(len(binary), num_nodes) + assert_equal(len(chain), num_nodes) for i in range(num_nodes): - self.nodes.append(TestNode(i, get_datadir_path(self.options.tmpdir, i), self.chain, rpchost=rpchost, timewait=self.rpc_timewait, bitcoind=binary[i], bitcoin_cli=self.options.bitcoincli, mocktime=self.mocktime, coverage_dir=self.options.coveragedir, extra_conf=extra_confs[i], extra_args=extra_args[i], use_cli=self.options.usecli)) + numnode = len(self.nodes) + self.nodes.append(TestNode(numnode, get_datadir_path(self.options.tmpdir, numnode), chain[i], rpchost=rpchost, timewait=self.rpc_timewait, bitcoind=binary[i], bitcoin_cli=self.options.bitcoincli, mocktime=self.mocktime, coverage_dir=self.options.coveragedir, extra_conf=extra_confs[i], extra_args=extra_args[i], use_cli=self.options.usecli)) def start_node(self, i, *args, **kwargs): """Start a bitcoind""" diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index d400a50bd2e..f41b2a5c9c1 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -76,6 +76,7 @@ def __init__(self, i, datadir, chain, *, rpchost, timewait, bitcoind, bitcoin_cl self.args = [ self.binary, "-datadir=" + self.datadir, + "-chain=" + self.chain, "-logtimemicros", "-debug", "-debugexclude=libevent", diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index d709ddbdbcc..cb875e5a8a3 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -57,6 +57,8 @@ BASE_SCRIPTS = [ # Scripts that are run by the travis build process. + # vv First elements tests vv + 'feature_fedpeg.py', # Longest test should go first, to favor running tests in parallel 'mempool_packages.py', 'feature_maxuploadtarget.py',