Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2WP: Allow other elements chains as parent chains for pegs #362

Merged
merged 3 commits into from
Sep 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions qa/pull-tester/rpc-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
'signed_blockchain.py',
'initial_reissuance_token.py',
'feature_blocksign.py',
'feature_fedpeg.py',
'default_asset_name.py',
'assetdir.py',

Expand Down
340 changes: 340 additions & 0 deletions qa/rpc-tests/feature_fedpeg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
#!/usr/bin/env python3

from decimal import Decimal
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,
rpc_auth_pair,
rpc_port,
start_node,
start_nodes,
stop_node,
)

# Sync mempool, make a block, sync blocks
def sync_all(sidechain, sidechain2, makeblock=True):
block = ""
timeout = 20
while len(sidechain.getrawmempool()) != len(sidechain2.getrawmempool()):
time.sleep(1)
timeout -= 1
if timeout == 0:
raise Exception("Peg-in has failed to propagate.")
if makeblock:
block = sidechain2.generate(1)
while sidechain.getblockcount() != sidechain2.getblockcount():
time.sleep(1)
timeout -= 1
if timeout == 0:
raise Exception("Blocks are not propagating.")
return block

def get_new_unconfidential_address(node):
addr = node.getnewaddress()
val_addr = node.validateaddress(addr)
if 'unconfidential' in val_addr:
return val_addr['unconfidential']
return val_addr['address']

class FedPegTest(BitcoinTestFramework):

def __init__(self):
super().__init__()
self.setup_clean_chain = True
self.num_nodes = 4

def setup_network(self, split=False):

# Parent chain args
self.extra_args = [[
# '-printtoconsole',
'-validatepegin=0',
'-anyonecanspendaremine',
'-initialfreecoins=2100000000000000',
]] * 2

self.nodes = start_nodes(2, self.options.tmpdir, self.extra_args[:2], chain='parent')
connect_nodes_bi(self.nodes, 0, 1)
self.parentgenesisblockhash = self.nodes[0].getblockhash(0)
print('parentgenesisblockhash', self.parentgenesisblockhash)
parent_pegged_asset = self.nodes[0].getsidechaininfo()['pegged_asset']

# Sidechain args
parent_chain_signblockscript = '51'
for n in range(2):
rpc_u, rpc_p = rpc_auth_pair(n)
self.extra_args.append([
# '-printtoconsole',
'-parentgenesisblockhash=%s' % self.parentgenesisblockhash,
'-validatepegin=1',
'-anyonecanspendaremine=0',
'-initialfreecoins=0',
'-peginconfirmationdepth=10',
'-mainchainrpchost=127.0.0.1',
'-mainchainrpcport=%s' % rpc_port(n),
'-mainchainrpcuser=%s' % rpc_u,
'-mainchainrpcpassword=%s' % rpc_p,
'-parentpubkeyprefix=235',
'-parentscriptprefix=75',
'-con_parent_chain_signblockscript=%s' % parent_chain_signblockscript,
'-con_parent_pegged_asset=%s' % parent_pegged_asset,
])
self.nodes.append(start_node(n + 2, self.options.tmpdir, self.extra_args[n + 2], chain='sidechain'))

connect_nodes_bi(self.nodes, 2, 3)
self.is_network_split = True
self.sync_all()

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 = parent.validateaddress(addrs["mainchain_address"])
print('addrs', addrs)
print('addr', addr)
txid1 = parent.sendtoaddress(addrs["mainchain_address"], 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('ERROR:', e.error)
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:
assert("Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"])

# Should fail due to non-witness
try:
pegtxid = sidechain.claimpegin(raw, proof, get_new_unconfidential_address(parent))
raise Exception("Peg-in with non-matching claim_script should fail.")
except JSONRPCException as e:
print(e.error["message"])
assert("Given or recovered script is not a witness program." in e.error["message"])

# # Should fail due to non-matching wallet address
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this here because it should fail due to this if it would not fail due to non-witness (clause above)?

This is very good for clarity, but it doesn't really test if it would fail in that case..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's simply the same test as in https://github.com/ElementsProject/elements/blob/elements-0.14.1/qa/rpc-tests/pegging.py#L233 but commented because I didn't manage to make it work, note here there's a similar one above "Should fail due to non-witness", perhaps that's enough.
Perhaps I should just remove that commented code, but then I will forget about it.
@instagibbs thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this functionality supposed to be working?

# try:
# pegtxid = sidechain.claimpegin(raw, proof, get_new_unconfidential_address(sidechain))
# raise Exception("Peg-in with non-matching claim_script should fail.")
# except JSONRPCException as e:
# print(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
pegtxid1 = sidechain.claimpegin(raw, proof)

# Will invalidate the block that confirms this transaction later
sync_all(parent, parent2)
blockhash = sync_all(sidechain, sidechain2)
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
# 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")
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 many claims in mempool
n_claims = 5

print("Flooding mempool with many small claims")
pegtxs = []
sidechain.generate(101)

for i in range(n_claims):
addrs = sidechain.getpeginaddress()
txid = parent.sendtoaddress(addrs["mainchain_address"], 1)
parent.generate(12)
proof = parent.gettxoutproof([txid])
raw = parent.getrawtransaction(txid)
pegtxs += [sidechain.claimpegin(raw, proof)]

sync_all(parent, parent2)
sync_all(sidechain, sidechain2)

sidechain2.generate(1)
for pegtxid in pegtxs:
tx = sidechain.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.validateaddress(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")
stop_node(self.nodes[1], 1)
txid = parent.sendtoaddress(addrs["mainchain_address"], 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.nodes[1] = start_node(1, self.options.tmpdir, self.extra_args[1], chain='parent')
parent2 = self.nodes[1]
connect_nodes_bi(self.nodes, 0, 1)
time.sleep(5)

# Don't make a block, race condition when pegin-invalid block
# is awaiting further validation, nodes reject subsequent blocks
# even ones they create
sync_all(sidechain, sidechain2, makeblock=False)
print("Now send funds out in two stages, partial, and full")
some_btc_addr = get_new_unconfidential_address(parent)
bal_1 = sidechain.getwalletinfo()["balance"]["bitcoin"]
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)
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)
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)

peg_out_txid = sidechain.sendtomainchain(some_btc_addr, 1)

peg_out_details = sidechain.decoderawtransaction(sidechain.getrawtransaction(peg_out_txid))
# peg-out, change
assert(len(peg_out_details["vout"]) == 3)
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"]
# Make sure balance went down
assert(bal_2 + 1 < bal_1)

sidechain.sendtomainchain(some_btc_addr, bal_2, True)

assert("bitcoin" not in sidechain.getwalletinfo()["balance"])

print('Success!')

if __name__ == '__main__':
FedPegTest().main()
9 changes: 4 additions & 5 deletions qa/rpc-tests/test_framework/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ def initialize_datadir(dirname, n):
os.makedirs(datadir)
rpc_u, rpc_p = rpc_auth_pair(n)
with open(os.path.join(datadir, "elements.conf"), 'w', encoding='utf8') as f:
f.write("regtest=1\n")
f.write("rpcuser=" + rpc_u + "\n")
f.write("rpcpassword=" + rpc_p + "\n")
f.write("port="+str(p2p_port(n))+"\n")
Expand Down Expand Up @@ -334,14 +333,14 @@ def _rpchost_to_args(rpchost):
rv += ['-rpcport=' + rpcport]
return rv

def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, chain='elementsregtest'):
"""
Start a bitcoind and return RPC connection to it
"""
datadir = os.path.join(dirname, "node"+str(i))
if binary is None:
binary = os.getenv("ELEMENTSD", "elementsd")
args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-mocktime="+str(get_mocktime()) ]
args = [ binary, '-chain='+chain, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-mocktime="+str(get_mocktime()) ]
if extra_args is not None: args.extend(extra_args)
bitcoind_processes[i] = subprocess.Popen(args)
if os.getenv("PYTHON_DEBUG", ""):
Expand All @@ -357,7 +356,7 @@ def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=

return proxy

def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, chain='elementsregtest'):
"""
Start multiple bitcoinds, return RPC connections to them
"""
Expand All @@ -366,7 +365,7 @@ def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None
rpcs = []
try:
for i in range(num_nodes):
rpcs.append(start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i]))
rpcs.append(start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i], chain=chain))
except: # If one node failed to start, stop the others
stop_nodes(rpcs)
raise
Expand Down
Loading