neobytes/qa/rpc-tests/dip3-deterministicmns.py

568 lines
22 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# Copyright (c) 2015-2018 The Dash Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# Test deterministic masternodes
#
from test_framework.blocktools import create_block, create_coinbase, get_masternode_payment
from test_framework.mininode import CTransaction, ToHex, FromHex, CTxOut, COIN, CCbTx
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import *
class Masternode(object):
pass
class DIP3Test(BitcoinTestFramework):
def __init__(self):
super().__init__()
self.num_initial_mn = 11 # Should be >= 11 to make sure quorums are not always the same MNs
self.num_nodes = 1 + self.num_initial_mn + 2 # +1 for controller, +1 for mn-qt, +1 for mn created after dip3 activation
self.setup_clean_chain = True
self.extra_args = ["-budgetparams=240:100:240"]
self.extra_args += ["-sporkkey=cP4EKFyJsHT39LDqgdcB43Y3YXjNyjb5Fuas1GQSeAtjnZWmZEQK"]
def setup_network(self):
disable_mocktime()
self.start_controller_node()
self.is_network_split = False
def start_controller_node(self, extra_args=None):
print("starting controller node")
if self.nodes is None:
self.nodes = [None]
args = self.extra_args
if extra_args is not None:
args += extra_args
self.nodes[0] = start_node(0, self.options.tmpdir, extra_args=args)
for i in range(1, self.num_nodes):
if i < len(self.nodes) and self.nodes[i] is not None:
connect_nodes_bi(self.nodes, 0, i)
def stop_controller_node(self):
print("stopping controller node")
stop_node(self.nodes[0], 0)
def restart_controller_node(self):
self.stop_controller_node()
self.start_controller_node()
def run_test(self):
print("funding controller node")
while self.nodes[0].getbalance() < (self.num_initial_mn + 3) * 1000:
self.nodes[0].generate(1) # generate enough for collaterals
print("controller node has {} dash".format(self.nodes[0].getbalance()))
# Make sure we're below block 432 (which activates dip3)
print("testing rejection of ProTx before dip3 activation")
assert(self.nodes[0].getblockchaininfo()['blocks'] < 432)
mns = []
# prepare mn which should still be accepted later when dip3 activates
print("creating collateral for mn-before-dip3")
before_dip3_mn = self.prepare_mn(self.nodes[0], 1, 'mn-before-dip3')
self.create_mn_collateral(self.nodes[0], before_dip3_mn)
mns.append(before_dip3_mn)
# block 500 starts enforcing DIP3 MN payments
while self.nodes[0].getblockcount() < 498:
self.nodes[0].generate(1)
print("mining final block for DIP3 activation")
self.nodes[0].generate(1)
# We have hundreds of blocks to sync here, give it more time
print("syncing blocks for all nodes")
sync_blocks(self.nodes, timeout=120)
# DIP3 has activated here
self.register_mn(self.nodes[0], before_dip3_mn)
self.start_mn(before_dip3_mn)
print("registering MNs")
for i in range(0, self.num_initial_mn):
mn = self.prepare_mn(self.nodes[0], i + 2, "mn-%d" % i)
mns.append(mn)
# start a few MNs before they are registered and a few after they are registered
start = (i % 3) == 0
if start:
self.start_mn(mn)
2018-10-25 16:29:50 +02:00
# let a few of the protx MNs refer to the existing collaterals
fund = (i % 2) == 0
2018-10-25 16:29:50 +02:00
if fund:
print("register_fund %s" % mn.alias)
self.register_fund_mn(self.nodes[0], mn)
2018-10-25 16:29:50 +02:00
else:
print("create_collateral %s" % mn.alias)
self.create_mn_collateral(self.nodes[0], mn)
print("register %s" % mn.alias)
self.register_mn(self.nodes[0], mn)
self.nodes[0].generate(1)
if not start:
self.start_mn(mn)
self.sync_all()
self.assert_mnlists(mns)
print("testing instant send")
self.test_instantsend(10, 3)
print("test that MNs disappear from the list when the ProTx collateral is spent")
spend_mns_count = 3
mns_tmp = [] + mns
dummy_txins = []
for i in range(spend_mns_count):
dummy_txin = self.spend_mn_collateral(mns[i], with_dummy_input_output=True)
dummy_txins.append(dummy_txin)
self.nodes[0].generate(1)
self.sync_all()
mns_tmp.remove(mns[i])
2018-10-25 16:29:50 +02:00
self.assert_mnlists(mns_tmp)
print("test that reverting the blockchain on a single node results in the mnlist to be reverted as well")
for i in range(spend_mns_count):
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
mns_tmp.append(mns[spend_mns_count - 1 - i])
2018-10-25 16:29:50 +02:00
self.assert_mnlist(self.nodes[0], mns_tmp)
print("cause a reorg with a double spend and check that mnlists are still correct on all nodes")
self.mine_double_spend(self.nodes[0], dummy_txins, self.nodes[0].getnewaddress(), use_mnmerkleroot_from_tip=True)
self.nodes[0].generate(spend_mns_count)
self.sync_all()
2018-10-25 16:29:50 +02:00
self.assert_mnlists(mns_tmp)
print("test mn payment enforcement with deterministic MNs")
for i in range(20):
node = self.nodes[i % len(self.nodes)]
self.test_invalid_mn_payment(node)
node.generate(1)
self.sync_all()
print("testing ProUpServTx")
for mn in mns:
self.test_protx_update_service(mn)
print("testing P2SH/multisig for payee addresses")
multisig = self.nodes[0].createmultisig(1, [self.nodes[0].getnewaddress(), self.nodes[0].getnewaddress()])['address']
self.update_mn_payee(mns[0], multisig)
found_multisig_payee = False
for i in range(len(mns)):
bt = self.nodes[0].getblocktemplate()
expected_payee = bt['masternode'][0]['payee']
expected_amount = bt['masternode'][0]['amount']
self.nodes[0].generate(1)
self.sync_all()
if expected_payee == multisig:
block = self.nodes[0].getblock(self.nodes[0].getbestblockhash())
cbtx = self.nodes[0].getrawtransaction(block['tx'][0], 1)
for out in cbtx['vout']:
if 'addresses' in out['scriptPubKey']:
if expected_payee in out['scriptPubKey']['addresses'] and out['valueSat'] == expected_amount:
found_multisig_payee = True
assert(found_multisig_payee)
print("testing reusing of collaterals for replaced MNs")
for i in range(0, 5):
mn = mns[i]
# a few of these will actually refer to old ProRegTx internal collaterals,
# which should work the same as external collaterals
new_mn = self.prepare_mn(self.nodes[0], mn.idx, mn.alias)
new_mn.collateral_address = mn.collateral_address
new_mn.collateral_txid = mn.collateral_txid
new_mn.collateral_vout = mn.collateral_vout
self.register_mn(self.nodes[0], new_mn)
mns[i] = new_mn
self.nodes[0].generate(1)
self.sync_all()
self.assert_mnlists(mns)
print("restarting MN %s" % new_mn.alias)
self.stop_node(new_mn.idx)
self.start_mn(new_mn)
self.sync_all()
print("testing instant send with replaced MNs")
self.test_instantsend(10, 3, timeout=20)
print("testing simple PoSe")
self.assert_mnlists(mns)
self.nodes[0].spork('SPORK_17_QUORUM_DKG_ENABLED', 0)
self.wait_for_sporks()
height = self.nodes[0].getblockcount()
skip_count = 24 - (height % 24)
if skip_count != 0:
self.nodes[0].generate(skip_count)
for i in range(len(mns), len(mns) - 2, -1):
mn = mns[len(mns) - 1]
mns.remove(mn)
self.stop_node(mn.idx)
self.nodes.remove(mn.node)
punished = False
banned = False
t = time.time()
while (not punished or not banned) and (time.time() - t) < 120:
time.sleep(1)
# 10 blocks until we can mine the dummy commitment
for j in range(10):
self.nodes[0].generate(1)
self.sync_all()
time.sleep(0.5)
info = self.nodes[0].protx('info', mn.protx_hash)
if not punished:
if info['state']['PoSePenalty'] > 0:
punished = True
if not banned:
if info['state']['PoSeBanHeight'] != -1:
banned = True
# Fast-forward to next DKG session
self.nodes[0].generate(24 - (self.nodes[0].getblockcount() % 24))
self.sync_all()
assert(punished and banned)
def prepare_mn(self, node, idx, alias):
mn = Masternode()
mn.idx = idx
mn.alias = alias
mn.is_protx = True
mn.p2p_port = p2p_port(mn.idx)
blsKey = node.bls('generate')
mn.fundsAddr = node.getnewaddress()
mn.ownerAddr = node.getnewaddress()
mn.operatorAddr = blsKey['public']
mn.votingAddr = mn.ownerAddr
mn.blsMnkey = blsKey['secret']
return mn
def create_mn_collateral(self, node, mn):
mn.collateral_address = node.getnewaddress()
mn.collateral_txid = node.sendtoaddress(mn.collateral_address, 1000)
mn.collateral_vout = -1
node.generate(1)
rawtx = node.getrawtransaction(mn.collateral_txid, 1)
for txout in rawtx['vout']:
if txout['value'] == Decimal(1000):
mn.collateral_vout = txout['n']
break
assert(mn.collateral_vout != -1)
# register a protx MN and also fund it (using collateral inside ProRegTx)
def register_fund_mn(self, node, mn):
node.sendtoaddress(mn.fundsAddr, 1000.001)
2018-10-25 16:29:50 +02:00
mn.collateral_address = node.getnewaddress()
mn.rewards_address = node.getnewaddress()
2018-10-25 16:29:50 +02:00
mn.protx_hash = node.protx('register_fund', mn.collateral_address, '127.0.0.1:%d' % mn.p2p_port, mn.ownerAddr, mn.operatorAddr, mn.votingAddr, 0, mn.rewards_address, mn.fundsAddr)
2018-10-25 16:29:50 +02:00
mn.collateral_txid = mn.protx_hash
mn.collateral_vout = -1
2018-10-25 16:29:50 +02:00
rawtx = node.getrawtransaction(mn.collateral_txid, 1)
for txout in rawtx['vout']:
if txout['value'] == Decimal(1000):
mn.collateral_vout = txout['n']
break
assert(mn.collateral_vout != -1)
2018-10-25 16:29:50 +02:00
# create a protx MN which refers to an existing collateral
def register_mn(self, node, mn):
node.sendtoaddress(mn.fundsAddr, 0.001)
2018-10-25 16:29:50 +02:00
mn.rewards_address = node.getnewaddress()
mn.protx_hash = node.protx('register', mn.collateral_txid, mn.collateral_vout, '127.0.0.1:%d' % mn.p2p_port, mn.ownerAddr, mn.operatorAddr, mn.votingAddr, 0, mn.rewards_address, mn.fundsAddr)
node.generate(1)
2018-10-25 16:29:50 +02:00
def start_mn(self, mn):
while len(self.nodes) <= mn.idx:
self.nodes.append(None)
extra_args = ['-masternode=1', '-masternodeblsprivkey=%s' % mn.blsMnkey]
n = start_node(mn.idx, self.options.tmpdir, self.extra_args + extra_args, redirect_stderr=True)
self.nodes[mn.idx] = n
for i in range(0, self.num_nodes):
if i < len(self.nodes) and self.nodes[i] is not None and i != mn.idx:
connect_nodes_bi(self.nodes, mn.idx, i)
mn.node = self.nodes[mn.idx]
self.sync_all()
self.force_finish_mnsync(mn.node)
def spend_mn_collateral(self, mn, with_dummy_input_output=False):
return self.spend_input(mn.collateral_txid, mn.collateral_vout, 1000, with_dummy_input_output)
def update_mn_payee(self, mn, payee):
self.nodes[0].sendtoaddress(mn.fundsAddr, 0.001)
self.nodes[0].protx('update_registrar', mn.protx_hash, '', '', payee, mn.fundsAddr)
self.nodes[0].generate(1)
self.sync_all()
info = self.nodes[0].protx('info', mn.protx_hash)
assert(info['state']['payoutAddress'] == payee)
def test_protx_update_service(self, mn):
self.nodes[0].sendtoaddress(mn.fundsAddr, 0.001)
self.nodes[0].protx('update_service', mn.protx_hash, '127.0.0.2:%d' % mn.p2p_port, mn.blsMnkey, "", mn.fundsAddr)
self.nodes[0].generate(1)
self.sync_all()
for node in self.nodes:
2018-10-25 16:29:50 +02:00
protx_info = node.protx('info', mn.protx_hash)
mn_list = node.masternode('list')
2018-10-25 16:29:50 +02:00
assert_equal(protx_info['state']['addr'], '127.0.0.2:%d' % mn.p2p_port)
assert_equal(mn_list['%s-%d' % (mn.collateral_txid, mn.collateral_vout)]['address'], '127.0.0.2:%d' % mn.p2p_port)
# undo
self.nodes[0].protx('update_service', mn.protx_hash, '127.0.0.1:%d' % mn.p2p_port, mn.blsMnkey, "", mn.fundsAddr)
self.nodes[0].generate(1)
def force_finish_mnsync(self, node):
while True:
s = node.mnsync('next')
if s == 'sync updated to MASTERNODE_SYNC_FINISHED':
break
time.sleep(0.1)
def force_finish_mnsync_list(self, node):
if node.mnsync('status')['AssetName'] == 'MASTERNODE_SYNC_WAITING':
node.mnsync('next')
while True:
mnlist = node.masternode('list', 'status')
if len(mnlist) != 0:
time.sleep(0.5)
self.force_finish_mnsync(node)
return
time.sleep(0.1)
def test_instantsend(self, tx_count, repeat, timeout=20):
self.nodes[0].spork('SPORK_2_INSTANTSEND_ENABLED', 0)
self.wait_for_sporks()
# give all nodes some coins first
for i in range(tx_count):
outputs = {}
for node in self.nodes[1:]:
outputs[node.getnewaddress()] = 1
rawtx = self.nodes[0].createrawtransaction([], outputs)
rawtx = self.nodes[0].fundrawtransaction(rawtx)['hex']
rawtx = self.nodes[0].signrawtransaction(rawtx)['hex']
self.nodes[0].sendrawtransaction(rawtx)
self.nodes[0].generate(1)
self.sync_all()
for j in range(repeat):
for i in range(tx_count):
while True:
from_node_idx = random.randint(1, len(self.nodes) - 1)
from_node = self.nodes[from_node_idx]
if from_node is not None:
break
while True:
to_node_idx = random.randint(0, len(self.nodes) - 1)
to_node = self.nodes[to_node_idx]
if to_node is not None and from_node is not to_node:
break
to_address = to_node.getnewaddress()
txid = from_node.instantsendtoaddress(to_address, 0.1)
for node in self.nodes:
if node is not None:
self.wait_for_instant_lock(node, to_node_idx, txid, timeout=timeout)
self.nodes[0].generate(6)
self.sync_all()
def wait_for_instant_lock(self, node, node_idx, txid, timeout=10):
st = time.time()
while time.time() < st + timeout:
try:
tx = node.getrawtransaction(txid, 1)
except:
tx = None
if tx is None:
time.sleep(0.5)
continue
if tx['instantlock']:
return
time.sleep(0.5)
raise AssertionError("wait_for_instant_lock timed out for: {} on node {}".format(txid, node_idx))
2018-10-25 16:29:50 +02:00
def assert_mnlists(self, mns):
for node in self.nodes:
2018-10-25 16:29:50 +02:00
self.assert_mnlist(node, mns)
2018-10-25 16:29:50 +02:00
def assert_mnlist(self, node, mns):
if not self.compare_mnlist(node, mns):
expected = []
for mn in mns:
2018-10-25 16:29:50 +02:00
expected.append('%s-%d' % (mn.collateral_txid, mn.collateral_vout))
print('mnlist: ' + str(node.masternode('list', 'status')))
print('expected: ' + str(expected))
raise AssertionError("mnlists does not match provided mns")
def wait_for_sporks(self, timeout=30):
st = time.time()
while time.time() < st + timeout:
if self.compare_sporks():
return
time.sleep(0.5)
raise AssertionError("wait_for_sporks timed out")
def compare_sporks(self):
sporks = self.nodes[0].spork('show')
for node in self.nodes[1:]:
sporks2 = node.spork('show')
if sporks != sporks2:
return False
return True
2018-10-25 16:29:50 +02:00
def compare_mnlist(self, node, mns):
mnlist = node.masternode('list', 'status')
for mn in mns:
s = '%s-%d' % (mn.collateral_txid, mn.collateral_vout)
in_list = s in mnlist
2018-10-25 16:29:50 +02:00
if not in_list:
return False
mnlist.pop(s, None)
if len(mnlist) != 0:
return False
return True
def spend_input(self, txid, vout, amount, with_dummy_input_output=False):
# with_dummy_input_output is useful if you want to test reorgs with double spends of the TX without touching the actual txid/vout
address = self.nodes[0].getnewaddress()
txins = [
{'txid': txid, 'vout': vout}
]
targets = {address: amount}
dummy_txin = None
if with_dummy_input_output:
dummyaddress = self.nodes[0].getnewaddress()
unspent = self.nodes[0].listunspent(20)
for u in unspent:
if u['amount'] > Decimal(1):
dummy_txin = {'txid': u['txid'], 'vout': u['vout']}
txins.append(dummy_txin)
targets[dummyaddress] = float(u['amount'] - Decimal(0.0001))
break
rawtx = self.nodes[0].createrawtransaction(txins, targets)
rawtx = self.nodes[0].fundrawtransaction(rawtx)['hex']
rawtx = self.nodes[0].signrawtransaction(rawtx)['hex']
new_txid = self.nodes[0].sendrawtransaction(rawtx)
return dummy_txin
def mine_block(self, node, vtx=[], miner_address=None, mn_payee=None, mn_amount=None, use_mnmerkleroot_from_tip=False, expected_error=None):
bt = node.getblocktemplate()
height = bt['height']
tip_hash = bt['previousblockhash']
tip_block = node.getblock(tip_hash)
coinbasevalue = bt['coinbasevalue']
if miner_address is None:
miner_address = node.getnewaddress()
if mn_payee is None:
if isinstance(bt['masternode'], list):
mn_payee = bt['masternode'][0]['payee']
else:
mn_payee = bt['masternode']['payee']
# we can't take the masternode payee amount from the template here as we might have additional fees in vtx
# calculate fees that the block template included (we'll have to remove it from the coinbase as we won't
# include the template's transactions
bt_fees = 0
for tx in bt['transactions']:
bt_fees += tx['fee']
new_fees = 0
for tx in vtx:
in_value = 0
out_value = 0
for txin in tx.vin:
txout = node.gettxout("%064x" % txin.prevout.hash, txin.prevout.n, False)
in_value += int(txout['value'] * COIN)
for txout in tx.vout:
out_value += txout.nValue
new_fees += in_value - out_value
# fix fees
coinbasevalue -= bt_fees
coinbasevalue += new_fees
if mn_amount is None:
mn_amount = get_masternode_payment(height, coinbasevalue)
miner_amount = coinbasevalue - mn_amount
outputs = {miner_address: str(Decimal(miner_amount) / COIN)}
if mn_amount > 0:
outputs[mn_payee] = str(Decimal(mn_amount) / COIN)
coinbase = FromHex(CTransaction(), node.createrawtransaction([], outputs))
coinbase.vin = create_coinbase(height).vin
# We can't really use this one as it would result in invalid merkle roots for masternode lists
if len(bt['coinbase_payload']) != 0:
cbtx = FromHex(CCbTx(version=1), bt['coinbase_payload'])
if use_mnmerkleroot_from_tip:
if 'cbTx' in tip_block:
cbtx.merkleRootMNList = int(tip_block['cbTx']['merkleRootMNList'], 16)
else:
cbtx.merkleRootMNList = 0
coinbase.nVersion = 3
coinbase.nType = 5 # CbTx
coinbase.vExtraPayload = cbtx.serialize()
coinbase.calc_sha256()
block = create_block(int(tip_hash, 16), coinbase)
block.vtx += vtx
Implement and enforce DIP6 commitments (#2477) * Add LLMQ parameters to consensus params * Add DIP6 quorum commitment special TX * Implement CQuorumBlockProcessor which validates and handles commitments * Add quorum commitments to new blocks * Propagate QFCOMMITMENT messages to all nodes * Allow special transactions in blocks which have no inputs/outputs But only for TRANSACTION_QUORUM_COMMITMENT for now. * Add quorum commitments to self-crafted blocks in DIP3 tests * Add simple fork logic for current testnet This should avoid a fork on the current testnet. It only applies to the current chain which activated DIP3 at height 264000 and block 00000048e6e71d4bd90e7c456dcb94683ae832fcad13e1760d8283f7e89f332f. When we revert the chain to retest the DIP3 deployment, this fork logic can be removed again. * Use quorumVvecHash instead of quorumHash to make null commitments unique Implementation of https://github.com/dashpay/dips/pull/31 * Re-add quorum commitments after pruning mempool selected blocks * Refactor CQuorumBlockProcessor::ProcessBlock to have less nested if/else statements Also add BEGIN/END markers for temporary code. * Add comments/documentation to LLMQParams * Move code which determines if a commitment is required into IsCommitmentRequired This should make the code easier to read and also removes some duplication. The also changes the error types that are possible from 3 to 2 now. Instead of having "bad-qc-already-mined" and "bad-qc-not-mining-phase", there is only "bad-qc-not-allowed" now. * Use new parameter from consensus parames for the temporary fork
2018-11-23 15:42:09 +01:00
# Add quorum commitments from template
for tx in bt['transactions']:
tx2 = FromHex(CTransaction(), tx['data'])
if tx2.nType == 6:
block.vtx.append(tx2)
block.hashMerkleRoot = block.calc_merkle_root()
block.solve()
result = node.submitblock(ToHex(block))
if expected_error is not None and result != expected_error:
raise AssertionError('mining the block should have failed with error %s, but submitblock returned %s' % (expected_error, result))
elif expected_error is None and result is not None:
raise AssertionError('submitblock returned %s' % (result))
def mine_double_spend(self, node, txins, target_address, use_mnmerkleroot_from_tip=False):
amount = Decimal(0)
for txin in txins:
txout = node.gettxout(txin['txid'], txin['vout'], False)
amount += txout['value']
amount -= Decimal("0.001") # fee
rawtx = node.createrawtransaction(txins, {target_address: amount})
rawtx = node.signrawtransaction(rawtx)['hex']
tx = FromHex(CTransaction(), rawtx)
self.mine_block(node, [tx], use_mnmerkleroot_from_tip=use_mnmerkleroot_from_tip)
def test_invalid_mn_payment(self, node):
mn_payee = self.nodes[0].getnewaddress()
self.mine_block(node, mn_payee=mn_payee, expected_error='bad-cb-payee')
self.mine_block(node, mn_amount=1, expected_error='bad-cb-payee')
if __name__ == '__main__':
DIP3Test().main()