Integration test for pegins and pegouts

This commit is contained in:
Steven Roose 2018-12-12 12:20:09 +01:00
parent d8885400fb
commit d358364a36
No known key found for this signature in database
GPG key ID: 7FC91380BB4CE800
4 changed files with 430 additions and 2 deletions

421
test/functional/feature_fedpeg.py Executable file
View file

@ -0,0 +1,421 @@
#!/usr/bin/env python3
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",
"-initialfreecoins=2100000000000000",
"-anyonecanspendaremine",
"-signblockscript=51", # OP_TRUE
])
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',
'-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',
'-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)
assert ('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)
assert ('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)
sidechain.claimpegin(raw, proof) # stuck peg
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"])
assert(sidechain.getwalletinfo()["balance"] == 0)
print('Success!')
# Manually stop sidechains first, then the parent chains.
self.stop_node(2)
self.stop_node(3)
self.stop_node(0)
self.stop_node(1)
if __name__ == '__main__':
FedPegTest().main()

View file

@ -254,7 +254,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
# 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 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
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"""

View file

@ -76,6 +76,7 @@ class TestNode():
self.args = [
self.binary,
"-datadir=" + self.datadir,
"-chain=" + self.chain,
"-logtimemicros",
"-debug",
"-debugexclude=libevent",

View file

@ -57,6 +57,8 @@ TRAVIS_TIMEOUT_DURATION = 20 * 60
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',