Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc_p2p_avalanche_voting.py
Show All 23 Lines | |||||
class AvalancheTest(BitcoinTestFramework): | class AvalancheTest(BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
self.num_nodes = 2 | self.num_nodes = 2 | ||||
self.extra_args = [ | self.extra_args = [ | ||||
[ | [ | ||||
'-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", | ||||
], | ], | ||||
[ | [ | ||||
'-avaproofstakeutxodustthreshold=1000000', | "-avaproofstakeutxodustthreshold=1000000", | ||||
'-avaproofstakeutxoconfirmations=1', | "-avaproofstakeutxoconfirmations=1", | ||||
'-avacooldown=0', | "-avacooldown=0", | ||||
'-avaminquorumstake=0', | "-avaminquorumstake=0", | ||||
'-avaminavaproofsnodecount=0', | "-avaminavaproofsnodecount=0", | ||||
'-noparkdeepreorg', | "-noparkdeepreorg", | ||||
'-whitelist=noban@127.0.0.1', | "-whitelist=noban@127.0.0.1", | ||||
], | ], | ||||
] | ] | ||||
self.supports_cli = False | self.supports_cli = False | ||||
self.rpc_timeout = 120 | self.rpc_timeout = 120 | ||||
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 | ||||
# Generate many block and poll for them. | # Generate many block and poll for them. | ||||
self.generate(node, 100 - node.getblockcount()) | self.generate(node, 100 - node.getblockcount()) | ||||
fork_node = self.nodes[1] | fork_node = self.nodes[1] | ||||
# Get the key so we can verify signatures. | # Get the key so we can verify signatures. | ||||
avakey = ECPubKey() | avakey = ECPubKey() | ||||
Show All 11 Lines | def run_test(self): | ||||
# Verify signature. | # Verify signature. | ||||
assert avakey.verify_schnorr(response.sig, r.get_hash()) | assert avakey.verify_schnorr(response.sig, r.get_hash()) | ||||
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])) | ||||
assert_response( | assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, best_block_hash)]) | ||||
[AvalancheVote(AvalancheVoteError.ACCEPTED, best_block_hash)]) | |||||
self.log.info("Poll for a selection of blocks...") | self.log.info("Poll for a selection of blocks...") | ||||
various_block_hashes = [ | various_block_hashes = [ | ||||
int(node.getblockhash(0), 16), | int(node.getblockhash(0), 16), | ||||
int(node.getblockhash(1), 16), | int(node.getblockhash(1), 16), | ||||
int(node.getblockhash(10), 16), | int(node.getblockhash(10), 16), | ||||
int(node.getblockhash(25), 16), | int(node.getblockhash(25), 16), | ||||
int(node.getblockhash(42), 16), | int(node.getblockhash(42), 16), | ||||
int(node.getblockhash(96), 16), | int(node.getblockhash(96), 16), | ||||
int(node.getblockhash(99), 16), | int(node.getblockhash(99), 16), | ||||
int(node.getblockhash(100), 16), | int(node.getblockhash(100), 16), | ||||
] | ] | ||||
poll_node.send_poll(various_block_hashes) | poll_node.send_poll(various_block_hashes) | ||||
assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, h) | assert_response( | ||||
for h in various_block_hashes]) | [ | ||||
AvalancheVote(AvalancheVoteError.ACCEPTED, h) | |||||
for h in various_block_hashes | |||||
] | |||||
) | |||||
self.log.info( | self.log.info("Poll for a selection of blocks, but some are now invalid...") | ||||
"Poll for a selection of blocks, but some are now invalid...") | |||||
invalidated_block = node.getblockhash(76) | invalidated_block = node.getblockhash(76) | ||||
node.invalidateblock(invalidated_block) | node.invalidateblock(invalidated_block) | ||||
# We need to send the coin to a new address in order to make sure we do | # We need to send the coin to a new address in order to make sure we do | ||||
# not regenerate the same block. | # not regenerate the same block. | ||||
self.generatetoaddress(node, 26, ADDRS[0], sync_fun=self.no_op) | self.generatetoaddress(node, 26, ADDRS[0], sync_fun=self.no_op) | ||||
node.reconsiderblock(invalidated_block) | node.reconsiderblock(invalidated_block) | ||||
poll_node.send_poll(various_block_hashes) | poll_node.send_poll(various_block_hashes) | ||||
assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, h) for h in various_block_hashes[:5]] + | assert_response( | ||||
[AvalancheVote(AvalancheVoteError.FORK, h) for h in various_block_hashes[-3:]]) | [ | ||||
AvalancheVote(AvalancheVoteError.ACCEPTED, h) | |||||
for h in various_block_hashes[:5] | |||||
] | |||||
+ [ | |||||
AvalancheVote(AvalancheVoteError.FORK, h) | |||||
for h in various_block_hashes[-3:] | |||||
] | |||||
) | |||||
self.log.info("Poll for unknown blocks...") | self.log.info("Poll for unknown blocks...") | ||||
various_block_hashes = [ | various_block_hashes = [ | ||||
int(node.getblockhash(0), 16), | int(node.getblockhash(0), 16), | ||||
int(node.getblockhash(25), 16), | int(node.getblockhash(25), 16), | ||||
int(node.getblockhash(42), 16), | int(node.getblockhash(42), 16), | ||||
various_block_hashes[5], | various_block_hashes[5], | ||||
various_block_hashes[6], | various_block_hashes[6], | ||||
various_block_hashes[7], | various_block_hashes[7], | ||||
random.randrange(1 << 255, (1 << 256) - 1), | random.randrange(1 << 255, (1 << 256) - 1), | ||||
random.randrange(1 << 255, (1 << 256) - 1), | random.randrange(1 << 255, (1 << 256) - 1), | ||||
random.randrange(1 << 255, (1 << 256) - 1), | random.randrange(1 << 255, (1 << 256) - 1), | ||||
] | ] | ||||
poll_node.send_poll(various_block_hashes) | poll_node.send_poll(various_block_hashes) | ||||
assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, h) for h in various_block_hashes[:3]] + | assert_response( | ||||
[AvalancheVote(AvalancheVoteError.FORK, h) for h in various_block_hashes[3:6]] + | [ | ||||
[AvalancheVote(AvalancheVoteError.UNKNOWN, h) for h in various_block_hashes[-3:]]) | AvalancheVote(AvalancheVoteError.ACCEPTED, h) | ||||
for h in various_block_hashes[:3] | |||||
] | |||||
+ [ | |||||
AvalancheVote(AvalancheVoteError.FORK, h) | |||||
for h in various_block_hashes[3:6] | |||||
] | |||||
+ [ | |||||
AvalancheVote(AvalancheVoteError.UNKNOWN, h) | |||||
for h in various_block_hashes[-3:] | |||||
] | |||||
) | |||||
self.log.info("Trigger polling from the node...") | self.log.info("Trigger polling from the node...") | ||||
# Now that we have a peer, we should start polling for the tip. | # Now that we have a peer, we should start polling for the tip. | ||||
hash_tip = int(node.getbestblockhash(), 16) | hash_tip = int(node.getbestblockhash(), 16) | ||||
self.wait_until(lambda: can_find_inv_in_poll(quorum, hash_tip)) | self.wait_until(lambda: can_find_inv_in_poll(quorum, hash_tip)) | ||||
# Make sure the fork node has synced the blocks | # Make sure the fork node has synced the blocks | ||||
Show All 39 Lines | def run_test(self): | ||||
self.log.info("Answer all polls to park...") | self.log.info("Answer all polls to park...") | ||||
self.generate(node, 1, sync_fun=self.no_op) | self.generate(node, 1, sync_fun=self.no_op) | ||||
tip_to_park = node.getbestblockhash() | tip_to_park = node.getbestblockhash() | ||||
assert tip_to_park != fork_tip | assert tip_to_park != fork_tip | ||||
def has_parked_tip(tip_park): | def has_parked_tip(tip_park): | ||||
hash_tip_park = int(tip_park, 16) | hash_tip_park = int(tip_park, 16) | ||||
can_find_inv_in_poll(quorum, | can_find_inv_in_poll(quorum, hash_tip_park, AvalancheVoteError.PARKED) | ||||
hash_tip_park, AvalancheVoteError.PARKED) | |||||
for tip in node.getchaintips(): | for tip in node.getchaintips(): | ||||
if tip["hash"] == tip_park: | if tip["hash"] == tip_park: | ||||
return tip["status"] == "parked" | return tip["status"] == "parked" | ||||
return False | return False | ||||
# Because everybody answers no, the node will park that block. | # Because everybody answers no, the node will park that block. | ||||
with node.assert_debug_log([f"Avalanche rejected block {tip_to_park}"]): | with node.assert_debug_log([f"Avalanche rejected block {tip_to_park}"]): | ||||
self.wait_until(lambda: has_parked_tip(tip_to_park)) | self.wait_until(lambda: has_parked_tip(tip_to_park)) | ||||
assert_equal(node.getbestblockhash(), fork_tip) | assert_equal(node.getbestblockhash(), fork_tip) | ||||
# Voting yes will switch to accepting the block. | # Voting yes will switch to accepting the block. | ||||
with node.assert_debug_log([f"Avalanche accepted block {tip_to_park}"]): | with node.assert_debug_log([f"Avalanche accepted block {tip_to_park}"]): | ||||
self.wait_until(lambda: has_accepted_tip(tip_to_park)) | self.wait_until(lambda: has_accepted_tip(tip_to_park)) | ||||
# Answer no again and switch back to rejecting the block. | # Answer no again and switch back to rejecting the block. | ||||
with node.assert_debug_log([f"Avalanche rejected block {tip_to_park}"]): | with node.assert_debug_log([f"Avalanche rejected block {tip_to_park}"]): | ||||
self.wait_until(lambda: has_parked_tip(tip_to_park)) | self.wait_until(lambda: has_parked_tip(tip_to_park)) | ||||
assert_equal(node.getbestblockhash(), fork_tip) | assert_equal(node.getbestblockhash(), fork_tip) | ||||
# Vote a few more times until the block gets invalidated | # Vote a few more times until the block gets invalidated | ||||
hash_tip_park = int(tip_to_park, 16) | hash_tip_park = int(tip_to_park, 16) | ||||
with node.wait_for_debug_log( | with node.wait_for_debug_log( | ||||
[f"Avalanche invalidated block {tip_to_park}".encode()], | [f"Avalanche invalidated block {tip_to_park}".encode()], | ||||
chatty_callable=lambda: can_find_inv_in_poll(quorum, | chatty_callable=lambda: can_find_inv_in_poll( | ||||
hash_tip_park, AvalancheVoteError.PARKED) | quorum, hash_tip_park, AvalancheVoteError.PARKED | ||||
), | |||||
): | ): | ||||
pass | pass | ||||
# Mine on the current chaintip to trigger polling and so we don't reorg | # Mine on the current chaintip to trigger polling and so we don't reorg | ||||
old_fork_tip = fork_tip | old_fork_tip = fork_tip | ||||
fork_tip = self.generate(fork_node, 2, sync_fun=self.no_op)[-1] | fork_tip = self.generate(fork_node, 2, sync_fun=self.no_op)[-1] | ||||
# Manually unparking the invalidated block will reset finalization. | # Manually unparking the invalidated block will reset finalization. | ||||
Show All 18 Lines | def run_test(self): | ||||
self.log.info("Verify finalization sticks...") | self.log.info("Verify finalization sticks...") | ||||
chain_head = fork_tip | chain_head = fork_tip | ||||
self.log.info("...for a chain 1 block long...") | self.log.info("...for a chain 1 block long...") | ||||
# Create a new fork at the chaintip | # Create a new fork at the chaintip | ||||
fork_node.invalidateblock(chain_head) | fork_node.invalidateblock(chain_head) | ||||
# We need to send the coin to a new address in order to make sure we do | # We need to send the coin to a new address in order to make sure we do | ||||
# not regenerate the same block. | # not regenerate the same block. | ||||
blocks = self.generatetoaddress( | blocks = self.generatetoaddress(fork_node, 1, ADDRS[1], sync_fun=self.no_op) | ||||
fork_node, 1, ADDRS[1], sync_fun=self.no_op) | |||||
chain_head = blocks[0] | chain_head = blocks[0] | ||||
fork_tip = blocks[0] | fork_tip = blocks[0] | ||||
# node does not attempt to connect alternate chaintips so it is not | # node does not attempt to connect alternate chaintips so it is not | ||||
# parked. We check for an inactive valid header instead. | # parked. We check for an inactive valid header instead. | ||||
def valid_headers_block(blockhash): | def valid_headers_block(blockhash): | ||||
for tip in node.getchaintips(): | for tip in node.getchaintips(): | ||||
if tip["hash"] == blockhash: | if tip["hash"] == blockhash: | ||||
assert tip["status"] != "active" | assert tip["status"] != "active" | ||||
return tip["status"] == "valid-headers" | return tip["status"] == "valid-headers" | ||||
return False | return False | ||||
self.wait_until(lambda: valid_headers_block(fork_tip)) | self.wait_until(lambda: valid_headers_block(fork_tip)) | ||||
# sanity check | # sanity check | ||||
hash_to_find = int(fork_tip, 16) | hash_to_find = int(fork_tip, 16) | ||||
poll_node.send_poll([hash_to_find]) | poll_node.send_poll([hash_to_find]) | ||||
assert_response([AvalancheVote(AvalancheVoteError.FORK, hash_to_find)]) | assert_response([AvalancheVote(AvalancheVoteError.FORK, hash_to_find)]) | ||||
# Try some longer fork chains | # Try some longer fork chains | ||||
for numblocks in range(2, len(ADDRS)): | for numblocks in range(2, len(ADDRS)): | ||||
self.log.info(f"...for a chain {numblocks} blocks long...") | self.log.info(f"...for a chain {numblocks} blocks long...") | ||||
# Create a new fork N blocks deep | # Create a new fork N blocks deep | ||||
fork_node.invalidateblock(chain_head) | fork_node.invalidateblock(chain_head) | ||||
# We need to send the coin to a new address in order to make sure we do | # We need to send the coin to a new address in order to make sure we do | ||||
# not regenerate the same block. | # not regenerate the same block. | ||||
blocks = self.generatetoaddress( | blocks = self.generatetoaddress( | ||||
fork_node, numblocks, ADDRS[numblocks], sync_fun=self.no_op) | fork_node, numblocks, ADDRS[numblocks], sync_fun=self.no_op | ||||
) | |||||
chain_head = blocks[0] | chain_head = blocks[0] | ||||
fork_tip = blocks[-1] | fork_tip = blocks[-1] | ||||
# node should park the block if attempting to connect it because | # node should park the block if attempting to connect it because | ||||
# its tip is finalized | # its tip is finalized | ||||
self.wait_until(lambda: parked_block(fork_tip)) | self.wait_until(lambda: parked_block(fork_tip)) | ||||
# sanity check | # sanity check | ||||
hash_to_find = int(fork_tip, 16) | hash_to_find = int(fork_tip, 16) | ||||
poll_node.send_poll([hash_to_find]) | poll_node.send_poll([hash_to_find]) | ||||
assert_response( | assert_response([AvalancheVote(AvalancheVoteError.PARKED, hash_to_find)]) | ||||
[AvalancheVote(AvalancheVoteError.PARKED, hash_to_find)]) | |||||
self.log.info( | self.log.info("Check the node is discouraging unexpected avaresponses.") | ||||
"Check the node is discouraging unexpected avaresponses.") | |||||
with node.assert_debug_log( | with node.assert_debug_log( | ||||
['Misbehaving', 'peer=1', 'unexpected-ava-response']): | ["Misbehaving", "peer=1", "unexpected-ava-response"] | ||||
): | |||||
# unknown voting round | # unknown voting round | ||||
poll_node.send_avaresponse( | poll_node.send_avaresponse( | ||||
avaround=2**32 - 1, votes=[], privkey=poll_node.delegated_privkey) | avaround=2**32 - 1, votes=[], privkey=poll_node.delegated_privkey | ||||
) | |||||
if __name__ == '__main__': | if __name__ == "__main__": | ||||
AvalancheTest().main() | AvalancheTest().main() |