Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc_p2p_avalanche_policy_minerfund.py
Show All 15 Lines | from test_framework.messages import ( | ||||
ToHex, | ToHex, | ||||
) | ) | ||||
from test_framework.script import OP_EQUAL, OP_HASH160, CScript | from test_framework.script import OP_EQUAL, OP_HASH160, CScript | ||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.txtools import pad_tx | from test_framework.txtools import pad_tx | ||||
from test_framework.util import assert_equal | from test_framework.util import assert_equal | ||||
MINER_FUND_RATIO = 8 | MINER_FUND_RATIO = 8 | ||||
MINER_FUND_ADDR = 'ecregtest:prfhcnyqnl5cgrnmlfmms675w93ld7mvvq9jcw0zsn' | MINER_FUND_ADDR = "ecregtest:prfhcnyqnl5cgrnmlfmms675w93ld7mvvq9jcw0zsn" | ||||
OTHER_MINER_FUND_ADDR = 'ecregtest:pqv2r67sgz3qumufap3h2uuj0zfmnzuv8v38gtrh5v' | OTHER_MINER_FUND_ADDR = "ecregtest:pqv2r67sgz3qumufap3h2uuj0zfmnzuv8v38gtrh5v" | ||||
QUORUM_NODE_COUNT = 16 | QUORUM_NODE_COUNT = 16 | ||||
WELLINGTON_ACTIVATION_TIME = 2100000600 | WELLINGTON_ACTIVATION_TIME = 2100000600 | ||||
class AvalancheMinerFundTest(BitcoinTestFramework): | class AvalancheMinerFundTest(BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
self.num_nodes = 1 | self.num_nodes = 1 | ||||
self.extra_args = [ | self.extra_args = [ | ||||
[ | [ | ||||
'-enableminerfund', | "-enableminerfund", | ||||
'-avaproofstakeutxodustthreshold=1000000', | "-avaproofstakeutxodustthreshold=1000000", | ||||
'-avaproofstakeutxoconfirmations=1', | "-avaproofstakeutxoconfirmations=1", | ||||
'-avacooldown=0', | "-avacooldown=0", | ||||
'-avaminquorumstake=0', | "-avaminquorumstake=0", | ||||
'-avaminavaproofsnodecount=0', | "-avaminavaproofsnodecount=0", | ||||
'-whitelist=noban@127.0.0.1', | "-whitelist=noban@127.0.0.1", | ||||
f'-wellingtonactivationtime={WELLINGTON_ACTIVATION_TIME}', | f"-wellingtonactivationtime={WELLINGTON_ACTIVATION_TIME}", | ||||
], | ], | ||||
] | ] | ||||
def run_test(self): | def run_test(self): | ||||
node = self.nodes[0] | node = self.nodes[0] | ||||
# Build a fake quorum of nodes. | # Build a fake quorum of nodes. | ||||
def get_quorum(): | def get_quorum(): | ||||
return [get_ava_p2p_interface(self, node) | return [ | ||||
for _ in range(0, QUORUM_NODE_COUNT)] | get_ava_p2p_interface(self, node) for _ in range(0, QUORUM_NODE_COUNT) | ||||
] | |||||
# Pick one node from the quorum for polling. | # Pick one node from the quorum for polling. | ||||
quorum = get_quorum() | quorum = get_quorum() | ||||
poll_node = quorum[0] | poll_node = quorum[0] | ||||
assert node.getavalancheinfo()['ready_to_poll'] is True | assert node.getavalancheinfo()["ready_to_poll"] is True | ||||
# Activate Wellington | # Activate Wellington | ||||
address = node.get_deterministic_priv_key().address | address = node.get_deterministic_priv_key().address | ||||
node.setmocktime(WELLINGTON_ACTIVATION_TIME) | node.setmocktime(WELLINGTON_ACTIVATION_TIME) | ||||
self.generatetoaddress(node, nblocks=6, address=address) | self.generatetoaddress(node, nblocks=6, address=address) | ||||
assert_equal( | assert_equal(node.getblockchaininfo()["mediantime"], WELLINGTON_ACTIVATION_TIME) | ||||
node.getblockchaininfo()['mediantime'], | |||||
WELLINGTON_ACTIVATION_TIME) | |||||
# Get block reward | # Get block reward | ||||
coinbase = node.getblock(node.getbestblockhash(), 2)['tx'][0] | coinbase = node.getblock(node.getbestblockhash(), 2)["tx"][0] | ||||
block_reward = sum([vout['value'] for vout in coinbase['vout']]) | block_reward = sum([vout["value"] for vout in coinbase["vout"]]) | ||||
policy_miner_fund_amount = int( | policy_miner_fund_amount = int(block_reward * XEC * MINER_FUND_RATIO / 100) | ||||
block_reward * XEC * MINER_FUND_RATIO / 100) | |||||
def has_accepted_tip(tip_expected): | def has_accepted_tip(tip_expected): | ||||
hash_tip_final = int(tip_expected, 16) | hash_tip_final = int(tip_expected, 16) | ||||
can_find_inv_in_poll(quorum, hash_tip_final) | can_find_inv_in_poll(quorum, hash_tip_final) | ||||
return node.getbestblockhash() == tip_expected | return node.getbestblockhash() == tip_expected | ||||
def has_finalized_tip(tip_expected): | def has_finalized_tip(tip_expected): | ||||
hash_tip_final = int(tip_expected, 16) | hash_tip_final = int(tip_expected, 16) | ||||
can_find_inv_in_poll(quorum, hash_tip_final) | can_find_inv_in_poll(quorum, hash_tip_final) | ||||
return node.isfinalblock(tip_expected) | return node.isfinalblock(tip_expected) | ||||
def create_cb_pay_to_address(address, miner_fund_amount): | def create_cb_pay_to_address(address, miner_fund_amount): | ||||
# Build a coinbase with no miner fund | # Build a coinbase with no miner fund | ||||
cb = create_coinbase(node.getblockcount() + 1) | cb = create_coinbase(node.getblockcount() + 1) | ||||
# Keep only the block reward output | # Keep only the block reward output | ||||
cb.vout = cb.vout[:1] | cb.vout = cb.vout[:1] | ||||
# Change the block reward to account for the miner fund | # Change the block reward to account for the miner fund | ||||
cb.vout[0].nValue = int(block_reward * XEC - miner_fund_amount) | cb.vout[0].nValue = int(block_reward * XEC - miner_fund_amount) | ||||
# Add the miner fund output | # Add the miner fund output | ||||
if address and miner_fund_amount > 0: | if address and miner_fund_amount > 0: | ||||
_, _, script_hash = decode(address) | _, _, script_hash = decode(address) | ||||
cb.vout.append(CTxOut(nValue=miner_fund_amount, scriptPubKey=CScript( | cb.vout.append( | ||||
[OP_HASH160, script_hash, OP_EQUAL]))) | CTxOut( | ||||
nValue=miner_fund_amount, | |||||
scriptPubKey=CScript([OP_HASH160, script_hash, OP_EQUAL]), | |||||
) | |||||
) | |||||
pad_tx(cb) | pad_tx(cb) | ||||
cb.calc_sha256() | cb.calc_sha256() | ||||
return cb | return cb | ||||
def assert_response(expected): | def assert_response(expected): | ||||
response = poll_node.wait_for_avaresponse() | response = poll_node.wait_for_avaresponse() | ||||
r = response.response | r = response.response | ||||
assert_equal(r.cooldown, 0) | assert_equal(r.cooldown, 0) | ||||
votes = r.votes | votes = r.votes | ||||
assert_equal(len(votes), len(expected)) | assert_equal(len(votes), len(expected)) | ||||
for i in range(0, len(votes)): | for i in range(0, len(votes)): | ||||
assert_equal(repr(votes[i]), repr(expected[i])) | assert_equal(repr(votes[i]), repr(expected[i])) | ||||
def new_block(tip, miner_fund_addr, miner_fund_amount): | def new_block(tip, miner_fund_addr, miner_fund_amount): | ||||
# Create a new block paying to the specified miner fund | # Create a new block paying to the specified miner fund | ||||
cb = create_cb_pay_to_address(miner_fund_addr, miner_fund_amount) | cb = create_cb_pay_to_address(miner_fund_addr, miner_fund_amount) | ||||
block = create_block(int(tip, 16), cb, node.getblock(tip)[ | block = create_block( | ||||
'time'] + 1, version=4) | int(tip, 16), cb, node.getblock(tip)["time"] + 1, version=4 | ||||
) | |||||
block.solve() | block.solve() | ||||
node.submitblock(ToHex(block)) | node.submitblock(ToHex(block)) | ||||
# Check the current tip is what we expect | # Check the current tip is what we expect | ||||
matches_policy = miner_fund_addr == MINER_FUND_ADDR and miner_fund_amount >= policy_miner_fund_amount | matches_policy = ( | ||||
miner_fund_addr == MINER_FUND_ADDR | |||||
and miner_fund_amount >= policy_miner_fund_amount | |||||
) | |||||
expected_tip = block.hash if matches_policy else tip | expected_tip = block.hash if matches_policy else tip | ||||
assert_equal(node.getbestblockhash(), expected_tip) | assert_equal(node.getbestblockhash(), expected_tip) | ||||
# Poll and check the node votes what we expect | # Poll and check the node votes what we expect | ||||
poll_node.send_poll([block.sha256]) | poll_node.send_poll([block.sha256]) | ||||
expected_vote = AvalancheVoteError.ACCEPTED if matches_policy else AvalancheVoteError.PARKED | expected_vote = ( | ||||
AvalancheVoteError.ACCEPTED | |||||
if matches_policy | |||||
else AvalancheVoteError.PARKED | |||||
) | |||||
assert_response([AvalancheVote(expected_vote, block.sha256)]) | assert_response([AvalancheVote(expected_vote, block.sha256)]) | ||||
# Vote yes on this block until the node accepts it | # Vote yes on this block until the node accepts it | ||||
self.wait_until(lambda: has_accepted_tip(block.hash)) | self.wait_until(lambda: has_accepted_tip(block.hash)) | ||||
assert_equal(node.getbestblockhash(), block.hash) | assert_equal(node.getbestblockhash(), block.hash) | ||||
poll_node.send_poll([block.sha256]) | poll_node.send_poll([block.sha256]) | ||||
assert_response( | assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, block.sha256)]) | ||||
[AvalancheVote(AvalancheVoteError.ACCEPTED, block.sha256)]) | |||||
return block | return block | ||||
# Base cases that we always want to test | # Base cases that we always want to test | ||||
cases = [ | cases = [ | ||||
# Normal miner fund as set by policy | # Normal miner fund as set by policy | ||||
(MINER_FUND_ADDR, policy_miner_fund_amount), | (MINER_FUND_ADDR, policy_miner_fund_amount), | ||||
# Miner fund address changed but all else equal | # Miner fund address changed but all else equal | ||||
(OTHER_MINER_FUND_ADDR, policy_miner_fund_amount), | (OTHER_MINER_FUND_ADDR, policy_miner_fund_amount), | ||||
# Pay no miner fund at all | # Pay no miner fund at all | ||||
(None, 0), | (None, 0), | ||||
] | ] | ||||
# Add some more random cases | # Add some more random cases | ||||
for _ in range(0, 10): | for _ in range(0, 10): | ||||
addr = MINER_FUND_ADDR if random.randrange( | addr = MINER_FUND_ADDR if random.randrange(0, 2) else OTHER_MINER_FUND_ADDR | ||||
0, 2) else OTHER_MINER_FUND_ADDR | |||||
amount = random.randrange(0, policy_miner_fund_amount * 2) | amount = random.randrange(0, policy_miner_fund_amount * 2) | ||||
cases.append((addr, amount)) | cases.append((addr, amount)) | ||||
# Shuffle the test cases so we get varied test coverage on the first | # Shuffle the test cases so we get varied test coverage on the first | ||||
# post-activation block over many test runs. | # post-activation block over many test runs. | ||||
random.shuffle(cases) | random.shuffle(cases) | ||||
for addr, amount in cases: | for addr, amount in cases: | ||||
self.log.info( | self.log.info( | ||||
f"Miner fund test case: address: {addr}, fund amount: {amount}") | f"Miner fund test case: address: {addr}, fund amount: {amount}" | ||||
) | |||||
new_block(node.getbestblockhash(), addr, amount) | new_block(node.getbestblockhash(), addr, amount) | ||||
# Check a rejection case | # Check a rejection case | ||||
tip = node.getbestblockhash() | tip = node.getbestblockhash() | ||||
self.log.info("Miner fund rejection test case") | self.log.info("Miner fund rejection test case") | ||||
reject = new_block( | reject = new_block(tip, OTHER_MINER_FUND_ADDR, policy_miner_fund_amount).hash | ||||
tip, | |||||
OTHER_MINER_FUND_ADDR, | |||||
policy_miner_fund_amount).hash | |||||
reject_hash = int(reject, 16) | reject_hash = int(reject, 16) | ||||
with node.wait_for_debug_log( | with node.wait_for_debug_log( | ||||
[f"Avalanche invalidated block {reject}".encode()], | [f"Avalanche invalidated block {reject}".encode()], | ||||
chatty_callable=lambda: can_find_inv_in_poll(quorum, reject_hash, AvalancheVoteError.PARKED)): | chatty_callable=lambda: can_find_inv_in_poll( | ||||
quorum, reject_hash, AvalancheVoteError.PARKED | |||||
), | |||||
): | |||||
pass | pass | ||||
# Build a block on the accepted tip and the chain continues as normal | # Build a block on the accepted tip and the chain continues as normal | ||||
tip = new_block(tip, MINER_FUND_ADDR, policy_miner_fund_amount).hash | tip = new_block(tip, MINER_FUND_ADDR, policy_miner_fund_amount).hash | ||||
assert_equal(node.getbestblockhash(), tip) | assert_equal(node.getbestblockhash(), tip) | ||||
# Tip should finalize | # Tip should finalize | ||||
self.wait_until(lambda: has_finalized_tip(tip)) | self.wait_until(lambda: has_finalized_tip(tip)) | ||||
# Check tip height for sanity | # Check tip height for sanity | ||||
assert_equal( | assert_equal(node.getblockcount(), QUORUM_NODE_COUNT + 6 + len(cases) + 1) | ||||
node.getblockcount(), | |||||
QUORUM_NODE_COUNT + | |||||
6 + | |||||
len(cases) + | |||||
1) | |||||
if __name__ == '__main__': | if __name__ == "__main__": | ||||
AvalancheMinerFundTest().main() | AvalancheMinerFundTest().main() |