diff --git a/test/functional/abc_p2p_avalanche_policy_minerfund.py b/test/functional/abc_p2p_avalanche_policy_minerfund.py index 100223ee2..a6b0a11cd 100755 --- a/test/functional/abc_p2p_avalanche_policy_minerfund.py +++ b/test/functional/abc_p2p_avalanche_policy_minerfund.py @@ -1,221 +1,195 @@ #!/usr/bin/env python3 # Copyright (c) 2023 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the resolution of miner fund changes via avalanche.""" import random -from test_framework.avatools import get_ava_p2p_interface +from test_framework.avatools import can_find_inv_in_poll, get_ava_p2p_interface from test_framework.blocktools import create_block, create_coinbase from test_framework.cashaddr import decode from test_framework.messages import ( XEC, AvalancheVote, AvalancheVoteError, CTxOut, ToHex, ) from test_framework.script import OP_EQUAL, OP_HASH160, CScript from test_framework.test_framework import BitcoinTestFramework from test_framework.txtools import pad_tx from test_framework.util import assert_equal MINER_FUND_RATIO = 8 MINER_FUND_ADDR = 'ecregtest:prfhcnyqnl5cgrnmlfmms675w93ld7mvvq9jcw0zsn' OTHER_MINER_FUND_ADDR = 'ecregtest:pqv2r67sgz3qumufap3h2uuj0zfmnzuv8v38gtrh5v' QUORUM_NODE_COUNT = 16 WELLINGTON_ACTIVATION_TIME = 2100000600 class AvalancheMinerFundTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [ [ '-enableminerfund', '-avaproofstakeutxodustthreshold=1000000', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', '-whitelist=noban@127.0.0.1', f'-wellingtonactivationtime={WELLINGTON_ACTIVATION_TIME}', ], ] def run_test(self): node = self.nodes[0] # Build a fake quorum of nodes. def get_quorum(): return [get_ava_p2p_interface(self, node) for _ in range(0, QUORUM_NODE_COUNT)] # Pick one node from the quorum for polling. quorum = get_quorum() poll_node = quorum[0] assert node.getavalancheinfo()['ready_to_poll'] is True # Activate Wellington address = node.get_deterministic_priv_key().address node.setmocktime(WELLINGTON_ACTIVATION_TIME) self.generatetoaddress(node, nblocks=6, address=address) assert_equal( node.getblockchaininfo()['mediantime'], WELLINGTON_ACTIVATION_TIME) # Get block reward coinbase = node.getblock(node.getbestblockhash(), 2)['tx'][0] block_reward = sum([vout['value'] for vout in coinbase['vout']]) policy_miner_fund_amount = int( block_reward * XEC * MINER_FUND_RATIO / 100) - def can_find_block_in_poll(hash, resp=AvalancheVoteError.ACCEPTED): - found_hash = False - for n in quorum: - poll = n.get_avapoll_if_available() - - # That node has not received a poll - if poll is None: - continue - - # We got a poll, check for the hash and repond - votes = [] - for inv in poll.invs: - # Vote yes to everything - r = AvalancheVoteError.ACCEPTED - - # Look for what we expect - if inv.hash == hash: - r = resp - found_hash = True - - votes.append(AvalancheVote(r, inv.hash)) - - n.send_avaresponse(poll.round, votes, n.delegated_privkey) - - return found_hash - def has_accepted_tip(tip_expected): hash_tip_final = int(tip_expected, 16) - can_find_block_in_poll(hash_tip_final) + can_find_inv_in_poll(quorum, hash_tip_final) return node.getbestblockhash() == tip_expected def has_finalized_tip(tip_expected): hash_tip_final = int(tip_expected, 16) - can_find_block_in_poll(hash_tip_final) + can_find_inv_in_poll(quorum, hash_tip_final) return node.isfinalblock(tip_expected) def create_cb_pay_to_address(address, miner_fund_amount): # Build a coinbase with no miner fund cb = create_coinbase(node.getblockcount() + 1) # Keep only the block reward output cb.vout = cb.vout[:1] # Change the block reward to account for the miner fund cb.vout[0].nValue = int(block_reward * XEC - miner_fund_amount) # Add the miner fund output if address and miner_fund_amount > 0: _, _, script_hash = decode(address) cb.vout.append(CTxOut(nValue=miner_fund_amount, scriptPubKey=CScript( [OP_HASH160, script_hash, OP_EQUAL]))) pad_tx(cb) cb.calc_sha256() return cb def assert_response(expected): response = poll_node.wait_for_avaresponse() r = response.response assert_equal(r.cooldown, 0) votes = r.votes assert_equal(len(votes), len(expected)) for i in range(0, len(votes)): assert_equal(repr(votes[i]), repr(expected[i])) def new_block(tip, miner_fund_addr, miner_fund_amount): # Create a new block paying to the specified miner fund cb = create_cb_pay_to_address(miner_fund_addr, miner_fund_amount) block = create_block(int(tip, 16), cb, node.getblock(tip)[ 'time'] + 1, version=4) block.solve() node.submitblock(ToHex(block)) # Check the current tip is what we expect 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 assert_equal(node.getbestblockhash(), expected_tip) # Poll and check the node votes what we expect poll_node.send_poll([block.sha256]) expected_vote = AvalancheVoteError.ACCEPTED if matches_policy else AvalancheVoteError.PARKED assert_response([AvalancheVote(expected_vote, block.sha256)]) # Vote yes on this block until the node accepts it self.wait_until(lambda: has_accepted_tip(block.hash)) assert_equal(node.getbestblockhash(), block.hash) poll_node.send_poll([block.sha256]) assert_response( [AvalancheVote(AvalancheVoteError.ACCEPTED, block.sha256)]) return block # Base cases that we always want to test cases = [ # Normal miner fund as set by policy (MINER_FUND_ADDR, policy_miner_fund_amount), # Miner fund address changed but all else equal (OTHER_MINER_FUND_ADDR, policy_miner_fund_amount), # Pay no miner fund at all (None, 0), ] # Add some more random cases for _ in range(0, 10): addr = MINER_FUND_ADDR if random.randrange( 0, 2) else OTHER_MINER_FUND_ADDR amount = random.randrange(0, policy_miner_fund_amount * 2) cases.append((addr, amount)) # Shuffle the test cases so we get varied test coverage on the first # post-activation block over many test runs. random.shuffle(cases) for addr, amount in cases: self.log.info( f"Miner fund test case: address: {addr}, fund amount: {amount}") new_block(node.getbestblockhash(), addr, amount) # Check a rejection case tip = node.getbestblockhash() self.log.info("Miner fund rejection test case") reject = new_block( tip, OTHER_MINER_FUND_ADDR, policy_miner_fund_amount).hash reject_hash = int(reject, 16) with node.wait_for_debug_log( [f"Avalanche invalidated block {reject}".encode()], - chatty_callable=lambda: can_find_block_in_poll(reject_hash, AvalancheVoteError.PARKED)): + chatty_callable=lambda: can_find_inv_in_poll(quorum, reject_hash, AvalancheVoteError.PARKED)): pass # 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 assert_equal(node.getbestblockhash(), tip) # Tip should finalize self.wait_until(lambda: has_finalized_tip(tip)) # Check tip height for sanity assert_equal( node.getblockcount(), QUORUM_NODE_COUNT + 6 + len(cases) + 1) if __name__ == '__main__': AvalancheMinerFundTest().main() diff --git a/test/functional/abc_p2p_avalanche_proof_voting.py b/test/functional/abc_p2p_avalanche_proof_voting.py index 3f40861d1..8b062c1be 100755 --- a/test/functional/abc_p2p_avalanche_proof_voting.py +++ b/test/functional/abc_p2p_avalanche_proof_voting.py @@ -1,514 +1,489 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the resolution of conflicting proofs via avalanche.""" import time from test_framework.avatools import ( avalanche_proof_from_hex, + can_find_inv_in_poll, create_coinbase_stakes, gen_proof, get_ava_p2p_interface, get_proof_ids, ) from test_framework.key import ECKey, ECPubKey from test_framework.messages import ( MSG_AVA_PROOF, AvalancheProofVoteResponse, AvalancheVote, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, try_rpc, uint256_hex, ) from test_framework.wallet_util import bytes_to_wif QUORUM_NODE_COUNT = 16 class AvalancheProofVotingTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.avaproof_stake_utxo_confirmations = 1 self.conflicting_proof_cooldown = 100 self.peer_replacement_cooldown = 2000 self.extra_args = [ [ '-avaproofstakeutxodustthreshold=1000000', f'-avaproofstakeutxoconfirmations={self.avaproof_stake_utxo_confirmations}', f'-avalancheconflictingproofcooldown={self.conflicting_proof_cooldown}', f'-avalanchepeerreplacementcooldown={self.peer_replacement_cooldown}', '-avacooldown=0', '-avastalevotethreshold=140', '-avastalevotefactor=1', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', '-whitelist=noban@127.0.0.1', ], ] self.supports_cli = False # Build a fake quorum of nodes. def get_quorum(self, node): return [get_ava_p2p_interface(self, node, stake_utxo_confirmations=self.avaproof_stake_utxo_confirmations) for _ in range(0, QUORUM_NODE_COUNT)] - def can_find_proof_in_poll(self, hash, response): - found_hash = False - for n in self.quorum: - poll = n.get_avapoll_if_available() - - # That node has not received a poll - if poll is None: - continue - - # We got a poll, check for the hash and repond - votes = [] - for inv in poll.invs: - # Vote yes to everything - r = AvalancheProofVoteResponse.ACTIVE - - # Look for what we expect - if inv.hash == hash: - r = response - found_hash = True - - votes.append(AvalancheVote(r, inv.hash)) - - n.send_avaresponse(poll.round, votes, n.delegated_privkey) - - return found_hash - @staticmethod def send_proof(from_peer, proof_hex): proof = avalanche_proof_from_hex(proof_hex) from_peer.send_avaproof(proof) return proof.proofid def send_and_check_for_polling(self, peer, proof_hex, response=AvalancheProofVoteResponse.ACTIVE): proofid = self.send_proof(peer, proof_hex) - self.wait_until(lambda: self.can_find_proof_in_poll(proofid, response)) + self.wait_until(lambda: can_find_inv_in_poll(self.quorum, proofid, response)) def build_conflicting_proof(self, node, sequence): return node.buildavalancheproof( sequence, 0, self.privkey_wif, self.conflicting_stakes) def wait_for_invalidated_proof(self, node, proofid): def invalidate_proof(proofid): self.wait_until( - lambda: self.can_find_proof_in_poll( - proofid, response=AvalancheProofVoteResponse.REJECTED)) + lambda: can_find_inv_in_poll(self.quorum, + proofid, response=AvalancheProofVoteResponse.REJECTED)) return try_rpc(-8, "Proof not found", node.getrawavalancheproof, uint256_hex(proofid)) with node.assert_debug_log( [f"Avalanche invalidated proof {uint256_hex(proofid)}"], ["Failed to reject proof"] ): self.wait_until(lambda: invalidate_proof(proofid)) def wait_for_finalized_proof(self, node, proofid): def finalize_proof(proofid): - self.can_find_proof_in_poll( - proofid, response=AvalancheProofVoteResponse.ACTIVE) + can_find_inv_in_poll(self.quorum, + proofid, response=AvalancheProofVoteResponse.ACTIVE) return node.getrawavalancheproof( uint256_hex(proofid)).get("finalized", False) with node.assert_debug_log([f"Avalanche finalized proof {uint256_hex(proofid)}"]): self.wait_until(lambda: finalize_proof(proofid)) def run_test(self): node = self.nodes[0] privkey = ECKey() privkey.generate() self.privkey_wif = bytes_to_wif(privkey.get_bytes()) self.quorum = self.get_quorum(node) addrkey0 = node.get_deterministic_priv_key() blockhash = self.generatetoaddress( node, 9, addrkey0.address, sync_fun=self.no_op) self.conflicting_stakes = create_coinbase_stakes( node, blockhash[5:9], addrkey0.key) self.poll_tests(node) self.update_tests(node) self.vote_tests(node) self.stale_proof_tests(node) self.maturity_poll_tests(node) def poll_tests(self, node): # Disable the peer replacement cooldown for this test self.restart_node( 0, extra_args=self.extra_args[0] + ['-avalanchepeerreplacementcooldown=0']) self.quorum = self.get_quorum(node) proof_seq10 = self.build_conflicting_proof(node, 10) proof_seq20 = self.build_conflicting_proof(node, 20) proof_seq30 = self.build_conflicting_proof(node, 30) proof_seq40 = self.build_conflicting_proof(node, 40) no_stake = node.buildavalancheproof( 200, 2000000000, self.privkey_wif, [] ) # Get the key so we can verify signatures. avakey = ECPubKey() avakey.set(bytes.fromhex(node.getavalanchekey())) self.log.info("Trigger polling from the node...") peer = get_ava_p2p_interface(self, node) mock_time = int(time.time()) node.setmocktime(mock_time) self.log.info("Check we poll for valid proof") self.send_and_check_for_polling(peer, proof_seq30) proofid_seq30 = avalanche_proof_from_hex(proof_seq30).proofid self.wait_for_finalized_proof(node, proofid_seq30) self.log.info( "Check we don't poll for subsequent proofs if the cooldown is not elapsed, proof not the favorite") with node.assert_debug_log(["Not polling the avalanche proof (cooldown-not-elapsed)"]): peer.send_avaproof(avalanche_proof_from_hex(proof_seq20)) self.log.info( "Check we don't poll for subsequent proofs if the cooldown is not elapsed, proof is the favorite") with node.assert_debug_log(["Not polling the avalanche proof (cooldown-not-elapsed)"]): peer.send_avaproof(avalanche_proof_from_hex(proof_seq40)) self.log.info( "Check we poll for conflicting proof if the proof is not the favorite") mock_time += self.conflicting_proof_cooldown node.setmocktime(mock_time) self.send_and_check_for_polling( peer, proof_seq20, response=AvalancheProofVoteResponse.REJECTED) # Continue to vote until the proof is invalidated proofid_seq20 = avalanche_proof_from_hex(proof_seq20).proofid self.wait_for_invalidated_proof(node, proofid_seq20) self.log.info( "Check we poll for conflicting proof if the proof is the favorite") mock_time += self.conflicting_proof_cooldown node.setmocktime(mock_time) self.send_and_check_for_polling(peer, proof_seq40) mock_time += self.conflicting_proof_cooldown node.setmocktime(mock_time) self.log.info("Check we don't poll for proofs that get rejected") with node.assert_debug_log(["Not polling the avalanche proof (rejected-proof)"]): peer.send_avaproof(avalanche_proof_from_hex(proof_seq10)) self.log.info("Check we don't poll for invalid proofs and get banned") with node.assert_debug_log(["Misbehaving", "invalid-proof"]): peer.send_avaproof(avalanche_proof_from_hex(no_stake)) def update_tests(self, node): # Restart the node to get rid of in-flight requests self.restart_node(0) self.quorum = self.get_quorum(node) peer = get_ava_p2p_interface(self, node) mock_time = int(time.time()) node.setmocktime(mock_time) proof_seq30 = self.build_conflicting_proof(node, 30) proof_seq40 = self.build_conflicting_proof(node, 40) proof_seq50 = self.build_conflicting_proof(node, 50) proofid_seq30 = avalanche_proof_from_hex(proof_seq30).proofid proofid_seq40 = avalanche_proof_from_hex(proof_seq40).proofid proofid_seq50 = avalanche_proof_from_hex(proof_seq50).proofid node.sendavalancheproof(proof_seq40) self.wait_until(lambda: proofid_seq40 in get_proof_ids(node)) assert proofid_seq40 in get_proof_ids(node) assert proofid_seq30 not in get_proof_ids(node) self.log.info("Test proof acceptance") def accept_proof(proofid): - self.wait_until(lambda: self.can_find_proof_in_poll( - proofid, response=AvalancheProofVoteResponse.ACTIVE)) + self.wait_until(lambda: can_find_inv_in_poll(self.quorum, + proofid, response=AvalancheProofVoteResponse.ACTIVE)) return proofid in get_proof_ids(node) mock_time += self.conflicting_proof_cooldown node.setmocktime(mock_time) self.send_and_check_for_polling(peer, proof_seq30) # Let the quorum vote for it self.wait_until(lambda: accept_proof(proofid_seq30)) assert proofid_seq40 not in get_proof_ids(node) self.log.info("Test the peer replacement rate limit") self.wait_for_finalized_proof(node, proofid_seq30) # Not enough assert self.conflicting_proof_cooldown < self.peer_replacement_cooldown mock_time += self.conflicting_proof_cooldown node.setmocktime(mock_time) with node.assert_debug_log(["Not polling the avalanche proof (cooldown-not-elapsed)"]): self.send_proof(peer, proof_seq50) mock_time += self.peer_replacement_cooldown node.setmocktime(mock_time) self.log.info("Test proof rejection") self.send_proof(peer, proof_seq50) self.wait_until(lambda: proofid_seq50 in get_proof_ids(node)) assert proofid_seq40 not in get_proof_ids(node) def reject_proof(proofid): self.wait_until( - lambda: self.can_find_proof_in_poll( - proofid, response=AvalancheProofVoteResponse.REJECTED)) + lambda: can_find_inv_in_poll(self.quorum, + proofid, response=AvalancheProofVoteResponse.REJECTED)) return proofid not in get_proof_ids(node) with node.assert_debug_log( [f"Avalanche rejected proof {uint256_hex(proofid_seq50)}"], ["Failed to reject proof"] ): self.wait_until(lambda: reject_proof(proofid_seq50)) assert proofid_seq50 not in get_proof_ids(node) assert proofid_seq40 in get_proof_ids(node) self.log.info("Test proof invalidation") self.wait_for_invalidated_proof(node, proofid_seq50) self.log.info("The node will now ignore the invalid proof") for i in range(5): with node.assert_debug_log(["received: avaproof"]): self.send_proof(peer, proof_seq50) assert_raises_rpc_error(-8, "Proof not found", node.getrawavalancheproof, uint256_hex(proofid_seq50)) node.setmocktime(0) def vote_tests(self, node): self.avaproof_stake_utxo_confirmations = 2 self.restart_node(0, extra_args=['-avaproofstakeutxodustthreshold=1000000', f'-avaproofstakeutxoconfirmations={self.avaproof_stake_utxo_confirmations}', '-avacooldown=0', '-avalancheconflictingproofcooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', '-whitelist=noban@127.0.0.1', ]) self.get_quorum(node) ava_node = get_ava_p2p_interface( self, node, stake_utxo_confirmations=self.avaproof_stake_utxo_confirmations) # Generate coinbases to use for stakes stakes_key = node.get_deterministic_priv_key() blocks = self.generatetoaddress( node, 4, stakes_key.address, sync_fun=self.no_op) # Get the ava key so we can verify signatures. ava_key = ECPubKey() ava_key.set(bytes.fromhex(node.getavalanchekey())) def create_proof(stakes, sequence=10): proof = node.buildavalancheproof( sequence, 0, self.privkey_wif, stakes) proof_id = avalanche_proof_from_hex(proof).proofid return proof, proof_id # proof_0 is valid right now stakes_0 = create_coinbase_stakes(node, [blocks[0]], stakes_key.key) proof_0, proof_0_id = create_proof(stakes_0) # proof_1 is valid right now, and from different stakes stakes_1 = create_coinbase_stakes(node, [blocks[1]], stakes_key.key) proof_1, proof_1_id = create_proof(stakes_1) # proof_2 is immature because the stake UTXO is immature stakes_2 = create_coinbase_stakes(node, [blocks[3]], stakes_key.key) proof_2, proof_2_id = create_proof(stakes_2) # proof_3 conflicts with proof_0 and proof_1, but has a lower sequence stakes_3 = create_coinbase_stakes( node, [blocks[0], blocks[1]], stakes_key.key) proof_3, proof_3_id = create_proof(stakes_3, sequence=1) # proof_4 is invalid and should be rejected stakes_4 = create_coinbase_stakes(node, [blocks[2]], stakes_key.key) stakes_4[0]['amount'] -= 100000 proof_4, proof_4_id = create_proof(stakes_4) # Create a helper to issue a poll and validate the responses def poll_assert_response(expected): # Issue a poll for each proof self.log.info("Trigger polling from the node...") ava_node.send_poll( [proof_0_id, proof_1_id, proof_2_id, proof_3_id, proof_4_id], MSG_AVA_PROOF) response = ava_node.wait_for_avaresponse() r = response.response # Verify signature assert ava_key.verify_schnorr(response.sig, r.get_hash()) # Verify votes votes = r.votes assert_equal(len(votes), len(expected)) for i in range(0, len(votes)): assert_equal(repr(votes[i]), repr(expected[i])) # Check that all proofs start unknown poll_assert_response([ AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_0_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_1_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_2_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_3_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_4_id)]) # Send the first proof. Nodes should now respond that it's accepted node.sendavalancheproof(proof_0) poll_assert_response([ AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_0_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_1_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_2_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_3_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_4_id)]) # Send and check the 2nd proof. Nodes should now respond that it's # accepted node.sendavalancheproof(proof_1) poll_assert_response([ AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_0_id), AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_1_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_2_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_3_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_4_id)]) # The next proof should be rejected/put in the immature pool ava_node.send_proof(avalanche_proof_from_hex(proof_2)) poll_assert_response([ AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_0_id), AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_1_id), AvalancheVote(AvalancheProofVoteResponse.IMMATURE, proof_2_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_3_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_4_id)]) # The next proof should be rejected and marked as a conflicting proof assert_raises_rpc_error(-8, "conflicting-utxos", node.sendavalancheproof, proof_3) poll_assert_response([ AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_0_id), AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_1_id), AvalancheVote(AvalancheProofVoteResponse.IMMATURE, proof_2_id), AvalancheVote(AvalancheProofVoteResponse.CONFLICT, proof_3_id), AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proof_4_id)]) # The final proof should be permanently rejected for being completely # invalid ava_node.send_proof(avalanche_proof_from_hex(proof_4)) poll_assert_response([ AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_0_id), AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof_1_id), AvalancheVote(AvalancheProofVoteResponse.IMMATURE, proof_2_id), AvalancheVote(AvalancheProofVoteResponse.CONFLICT, proof_3_id), AvalancheVote(AvalancheProofVoteResponse.REJECTED, proof_4_id)]) def stale_proof_tests(self, node): # Restart the node to get rid of in-flight requests self.restart_node(0) self.quorum = self.get_quorum(node) peer = get_ava_p2p_interface(self, node) mock_time = int(time.time()) node.setmocktime(mock_time) proof_seq1 = self.build_conflicting_proof(node, 1) proof_seq2 = self.build_conflicting_proof(node, 2) proofid_seq1 = avalanche_proof_from_hex(proof_seq1).proofid proofid_seq2 = avalanche_proof_from_hex(proof_seq2).proofid node.sendavalancheproof(proof_seq2) self.wait_until(lambda: proofid_seq2 in get_proof_ids(node)) assert proofid_seq2 in get_proof_ids(node) assert proofid_seq1 not in get_proof_ids(node) mock_time += self.conflicting_proof_cooldown node.setmocktime(mock_time) self.send_and_check_for_polling( peer, proof_seq1, response=AvalancheProofVoteResponse.UNKNOWN) def vote_until_dropped(proofid): - self.can_find_proof_in_poll( - proofid, response=AvalancheProofVoteResponse.UNKNOWN) + can_find_inv_in_poll(self.quorum, + proofid, response=AvalancheProofVoteResponse.UNKNOWN) return try_rpc(-8, "Proof not found", node.getrawavalancheproof, uint256_hex(proofid)) with node.assert_debug_log([f"Avalanche stalled proof {uint256_hex(proofid_seq1)}"]): self.wait_until(lambda: vote_until_dropped(proofid_seq1)) # Verify that proof_seq2 was not replaced assert proofid_seq2 in get_proof_ids(node) assert proofid_seq1 not in get_proof_ids(node) # When polled, peer responds with expected votes for both proofs peer.send_poll([proofid_seq1, proofid_seq2], MSG_AVA_PROOF) response = peer.wait_for_avaresponse() assert repr(response.response.votes) == repr([ AvalancheVote(AvalancheProofVoteResponse.UNKNOWN, proofid_seq1), AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proofid_seq2)]) node.setmocktime(0) def maturity_poll_tests(self, node): # Restart the node with appropriate flags for this test self.avaproof_stake_utxo_confirmations = 2 self.restart_node(0, extra_args=[ '-avaproofstakeutxodustthreshold=1000000', f'-avaproofstakeutxoconfirmations={self.avaproof_stake_utxo_confirmations}', '-avalancheconflictingproofcooldown=0', '-avacooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', '-whitelist=noban@127.0.0.1', ]) self.quorum = self.get_quorum(node) peer = get_ava_p2p_interface( self, node, stake_utxo_confirmations=self.avaproof_stake_utxo_confirmations) _, immature_proof = gen_proof(self, node) self.log.info("Immature proofs are not polled") with node.assert_debug_log(["Not polling the avalanche proof (immature-proof)"]): peer.send_avaproof(immature_proof) self.log.info("Newly mature proofs are polled") self.generate(node, 1, sync_fun=self.no_op) self.send_and_check_for_polling(peer, immature_proof.serialize().hex()) if __name__ == '__main__': AvalancheProofVotingTest().main() diff --git a/test/functional/abc_p2p_avalanche_voting.py b/test/functional/abc_p2p_avalanche_voting.py index ceb423c7c..8696b42f3 100755 --- a/test/functional/abc_p2p_avalanche_voting.py +++ b/test/functional/abc_p2p_avalanche_voting.py @@ -1,336 +1,310 @@ #!/usr/bin/env python3 # Copyright (c) 2020-2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the resolution of forks via avalanche.""" import random -from test_framework.avatools import get_ava_p2p_interface +from test_framework.avatools import can_find_inv_in_poll, get_ava_p2p_interface from test_framework.key import ECPubKey from test_framework.messages import AvalancheVote, AvalancheVoteError from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal QUORUM_NODE_COUNT = 16 ADDRS = [ "ecregtest:pqv2r67sgz3qumufap3h2uuj0zfmnzuv8v38gtrh5v", "ecregtest:qqca3gh95tnjxqja7dt4kfdryyp0d2uss55p4myvzk", "ecregtest:qqzkkywqd9xyqgal27hc2wweu47392xywqz0pes57w", "ecregtest:qz7xgksy86wnenxf9t4hqc3lyvpjf6tpycfzk2wjml", "ecregtest:qq7dt5j42hvj8txm3jc66mp7x029txwp5cmuu4wmxq", "ecregtest:qrf5yf3t05hjlax0vl475t5nru29rwtegvzna37wyh", ] class AvalancheTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [ [ '-avaproofstakeutxodustthreshold=1000000', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', '-whitelist=noban@127.0.0.1', ], [ '-avaproofstakeutxodustthreshold=1000000', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', '-noparkdeepreorg', '-whitelist=noban@127.0.0.1', ], ] self.supports_cli = False self.rpc_timeout = 120 def run_test(self): node = self.nodes[0] # Build a fake quorum of nodes. def get_quorum(): return [get_ava_p2p_interface(self, node) for _ in range(0, QUORUM_NODE_COUNT)] # Pick one node from the quorum for polling. quorum = get_quorum() poll_node = quorum[0] assert node.getavalancheinfo()['ready_to_poll'] is True # Generate many block and poll for them. self.generate(node, 100 - node.getblockcount()) fork_node = self.nodes[1] # Get the key so we can verify signatures. avakey = ECPubKey() avakey.set(bytes.fromhex(node.getavalanchekey())) self.log.info("Poll for the chain tip...") best_block_hash = int(node.getbestblockhash(), 16) poll_node.send_poll([best_block_hash]) def assert_response(expected): response = poll_node.wait_for_avaresponse() r = response.response assert_equal(r.cooldown, 0) # Verify signature. assert avakey.verify_schnorr(response.sig, r.get_hash()) votes = r.votes assert_equal(len(votes), len(expected)) for i in range(0, len(votes)): assert_equal(repr(votes[i]), repr(expected[i])) assert_response( [AvalancheVote(AvalancheVoteError.ACCEPTED, best_block_hash)]) self.log.info("Poll for a selection of blocks...") various_block_hashes = [ int(node.getblockhash(0), 16), int(node.getblockhash(1), 16), int(node.getblockhash(10), 16), int(node.getblockhash(25), 16), int(node.getblockhash(42), 16), int(node.getblockhash(96), 16), int(node.getblockhash(99), 16), int(node.getblockhash(100), 16), ] poll_node.send_poll(various_block_hashes) assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, h) for h in various_block_hashes]) self.log.info( "Poll for a selection of blocks, but some are now invalid...") invalidated_block = node.getblockhash(76) node.invalidateblock(invalidated_block) # We need to send the coin to a new address in order to make sure we do # not regenerate the same block. self.generatetoaddress(node, 26, ADDRS[0], sync_fun=self.no_op) node.reconsiderblock(invalidated_block) poll_node.send_poll(various_block_hashes) assert_response([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...") various_block_hashes = [ int(node.getblockhash(0), 16), int(node.getblockhash(25), 16), int(node.getblockhash(42), 16), various_block_hashes[5], various_block_hashes[6], 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), ] poll_node.send_poll(various_block_hashes) assert_response([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...") - def can_find_block_in_poll(hash, resp=AvalancheVoteError.ACCEPTED): - found_hash = False - for n in quorum: - poll = n.get_avapoll_if_available() - - # That node has not received a poll - if poll is None: - continue - - # We got a poll, check for the hash and repond - votes = [] - for inv in poll.invs: - # Vote yes to everything - r = AvalancheVoteError.ACCEPTED - - # Look for what we expect - if inv.hash == hash: - r = resp - found_hash = True - - votes.append(AvalancheVote(r, inv.hash)) - - n.send_avaresponse(poll.round, votes, n.delegated_privkey) - - return found_hash - # Now that we have a peer, we should start polling for the tip. hash_tip = int(node.getbestblockhash(), 16) - self.wait_until(lambda: can_find_block_in_poll(hash_tip)) + self.wait_until(lambda: can_find_inv_in_poll(quorum, hash_tip)) # Make sure the fork node has synced the blocks self.sync_blocks([node, fork_node]) # Create a fork 2 blocks deep. This should trigger polling. fork_node.invalidateblock(fork_node.getblockhash(100)) self.generate(fork_node, 2, sync_fun=self.no_op) # Because the new tip is a deep reorg, the node will not accept it # right away, but poll for it. def parked_block(blockhash): for tip in node.getchaintips(): if tip["hash"] == blockhash: assert tip["status"] != "active" return tip["status"] == "parked" return False fork_tip = fork_node.getbestblockhash() self.wait_until(lambda: parked_block(fork_tip)) self.log.info("Answer all polls to finalize...") def has_accepted_tip(tip_expected): hash_tip_accept = int(tip_expected, 16) - can_find_block_in_poll(hash_tip_accept) + can_find_inv_in_poll(quorum, hash_tip_accept) return node.getbestblockhash() == tip_expected # Because everybody answers yes, the node will accept that block. with node.assert_debug_log([f"Avalanche accepted block {fork_tip}"]): self.wait_until(lambda: has_accepted_tip(fork_tip)) def has_finalized_tip(tip_expected): hash_tip_final = int(tip_expected, 16) - can_find_block_in_poll(hash_tip_final) + can_find_inv_in_poll(quorum, hash_tip_final) return node.isfinalblock(tip_expected) # And continuing to answer yes finalizes the block. with node.assert_debug_log([f"Avalanche finalized block {fork_tip}"]): self.wait_until(lambda: has_finalized_tip(fork_tip)) assert_equal(node.getbestblockhash(), fork_tip) self.log.info("Answer all polls to park...") self.generate(node, 1, sync_fun=self.no_op) tip_to_park = node.getbestblockhash() assert tip_to_park != fork_tip def has_parked_tip(tip_park): hash_tip_park = int(tip_park, 16) - can_find_block_in_poll( - hash_tip_park, AvalancheVoteError.PARKED) + can_find_inv_in_poll(quorum, + hash_tip_park, AvalancheVoteError.PARKED) for tip in node.getchaintips(): if tip["hash"] == tip_park: return tip["status"] == "parked" return False # Because everybody answers no, the node will park that block. with node.assert_debug_log([f"Avalanche rejected block {tip_to_park}"]): self.wait_until(lambda: has_parked_tip(tip_to_park)) assert_equal(node.getbestblockhash(), fork_tip) # Voting yes will switch to accepting the block. with node.assert_debug_log([f"Avalanche accepted block {tip_to_park}"]): self.wait_until(lambda: has_accepted_tip(tip_to_park)) # Answer no again and switch back to rejecting the block. with node.assert_debug_log([f"Avalanche rejected block {tip_to_park}"]): self.wait_until(lambda: has_parked_tip(tip_to_park)) assert_equal(node.getbestblockhash(), fork_tip) # Vote a few more times until the block gets invalidated hash_tip_park = int(tip_to_park, 16) with node.wait_for_debug_log( [f"Avalanche invalidated block {tip_to_park}".encode()], - chatty_callable=lambda: can_find_block_in_poll( - hash_tip_park, AvalancheVoteError.PARKED) + chatty_callable=lambda: can_find_inv_in_poll(quorum, + hash_tip_park, AvalancheVoteError.PARKED) ): pass # Mine on the current chaintip to trigger polling and so we don't reorg old_fork_tip = fork_tip fork_tip = self.generate(fork_node, 2, sync_fun=self.no_op)[-1] # Manually unparking the invalidated block will reset finalization. node.unparkblock(tip_to_park) assert not node.isfinalblock(old_fork_tip) # Wait until the new tip is finalized self.sync_blocks([node, fork_node]) self.wait_until(lambda: has_finalized_tip(fork_tip)) assert_equal(node.getbestblockhash(), fork_tip) # Manually parking the finalized chaintip will reset finalization. node.parkblock(fork_tip) assert not node.isfinalblock(fork_tip) # Trigger polling and finalize a new tip to setup for the next test. node.unparkblock(fork_tip) fork_tip = self.generate(fork_node, 1)[-1] self.wait_until(lambda: has_finalized_tip(fork_tip)) assert_equal(node.getbestblockhash(), fork_tip) self.log.info("Verify finalization sticks...") chain_head = fork_tip self.log.info("...for a chain 1 block long...") # Create a new fork at the chaintip fork_node.invalidateblock(chain_head) # We need to send the coin to a new address in order to make sure we do # not regenerate the same block. blocks = self.generatetoaddress( fork_node, 1, ADDRS[1], sync_fun=self.no_op) chain_head = blocks[0] fork_tip = blocks[0] # node does not attempt to connect alternate chaintips so it is not # parked. We check for an inactive valid header instead. def valid_headers_block(blockhash): for tip in node.getchaintips(): if tip["hash"] == blockhash: assert tip["status"] != "active" return tip["status"] == "valid-headers" return False self.wait_until(lambda: valid_headers_block(fork_tip)) # sanity check hash_to_find = int(fork_tip, 16) poll_node.send_poll([hash_to_find]) assert_response([AvalancheVote(AvalancheVoteError.FORK, hash_to_find)]) # Try some longer fork chains for numblocks in range(2, len(ADDRS)): self.log.info(f"...for a chain {numblocks} blocks long...") # Create a new fork N blocks deep fork_node.invalidateblock(chain_head) # We need to send the coin to a new address in order to make sure we do # not regenerate the same block. blocks = self.generatetoaddress( fork_node, numblocks, ADDRS[numblocks], sync_fun=self.no_op) chain_head = blocks[0] fork_tip = blocks[-1] # node should park the block if attempting to connect it because # its tip is finalized self.wait_until(lambda: parked_block(fork_tip)) # sanity check hash_to_find = int(fork_tip, 16) poll_node.send_poll([hash_to_find]) assert_response( [AvalancheVote(AvalancheVoteError.PARKED, hash_to_find)]) self.log.info( "Check the node is discouraging unexpected avaresponses.") with node.assert_debug_log( ['Misbehaving', 'peer=1', 'unexpected-ava-response']): # unknown voting round poll_node.send_avaresponse( round=2**32 - 1, votes=[], privkey=poll_node.delegated_privkey) if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_rpc_isfinal.py b/test/functional/abc_rpc_isfinal.py index da8d812ed..b3d6afebb 100755 --- a/test/functional/abc_rpc_isfinal.py +++ b/test/functional/abc_rpc_isfinal.py @@ -1,232 +1,200 @@ #!/usr/bin/env python3 # Copyright (c) 2022 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the isfinalxxx RPCS.""" import random from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.authproxy import JSONRPCException -from test_framework.avatools import AvaP2PInterface +from test_framework.avatools import AvaP2PInterface, can_find_inv_in_poll from test_framework.blocktools import create_block, create_coinbase -from test_framework.messages import ( - AvalancheVote, - AvalancheVoteError, - CBlockHeader, - msg_headers, -) +from test_framework.messages import CBlockHeader, msg_headers from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error, uint256_hex QUORUM_NODE_COUNT = 16 class AvalancheIsFinalTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [ [ '-avaproofstakeutxodustthreshold=1000000', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0', '-avaminquorumstake=0', '-avaminavaproofsnodecount=0', ] ] def run_test(self): node = self.nodes[0] tip = node.getbestblockhash() assert_raises_rpc_error( -1, "Avalanche is not ready to poll yet.", self.nodes[0].isfinalblock, tip, ) assert_raises_rpc_error( -1, "Avalanche is not ready to poll yet.", self.nodes[0].isfinaltransaction, node.getblock(tip)['tx'][0], tip, ) # Build a fake quorum of nodes. def get_quorum(): return [node.add_p2p_connection(AvaP2PInterface(self, node)) for _ in range(0, QUORUM_NODE_COUNT)] # Pick one node from the quorum for polling. quorum = get_quorum() def is_quorum_established(): return node.getavalancheinfo()['ready_to_poll'] is True self.wait_until(is_quorum_established) - def can_find_block_in_poll( - blockhash, resp=AvalancheVoteError.ACCEPTED): - found_hash = False - for n in quorum: - poll = n.get_avapoll_if_available() - - # That node has not received a poll - if poll is None: - continue - - # We got a poll, check for the hash and repond - votes = [] - for inv in poll.invs: - # Vote yes to everything - r = AvalancheVoteError.ACCEPTED - - # Look for what we expect - if inv.hash == int(blockhash, 16): - r = resp - found_hash = True - - votes.append(AvalancheVote(r, inv.hash)) - - n.send_avaresponse(poll.round, votes, n.delegated_privkey) - - return found_hash - blockhash = self.generate(node, 1, sync_fun=self.no_op)[0] cb_txid = node.getblock(blockhash)['tx'][0] assert not node.isfinalblock(blockhash) assert not node.isfinaltransaction(cb_txid, blockhash) def is_finalblock(blockhash): - can_find_block_in_poll(blockhash) + can_find_inv_in_poll(quorum, int(blockhash, 16)) return node.isfinalblock(blockhash) with node.assert_debug_log([f"Avalanche finalized block {blockhash}"]): self.wait_until(lambda: is_finalblock(blockhash)) assert node.isfinaltransaction(cb_txid, blockhash) self.log.info("Check block ancestors are finalized as well") tip_height = node.getblockheader(blockhash)['height'] for height in range(0, tip_height): hash = node.getblockhash(height) assert node.isfinalblock(hash) txid = node.getblock(hash)['tx'][0] assert node.isfinaltransaction(txid, hash) if self.is_wallet_compiled(): self.log.info("Check mempool transactions are not finalized") # Mature some utxos tip = self.generate(node, 100, sync_fun=self.no_op)[-1] wallet_txid = node.sendtoaddress( ADDRESS_ECREG_UNSPENDABLE, 1_000_000) assert wallet_txid in node.getrawmempool() assert_raises_rpc_error( -5, "No such transaction found in the provided block.", node.isfinaltransaction, wallet_txid, tip, ) self.log.info( "A transaction is only finalized if the containing block is finalized") tip = self.generate(node, 1, sync_fun=self.no_op)[0] assert wallet_txid not in node.getrawmempool() assert not node.isfinaltransaction(wallet_txid, tip) self.wait_until(lambda: is_finalblock(tip)) assert node.isfinaltransaction(wallet_txid, tip) # Needs -txindex assert_raises_rpc_error( -5, "No such transaction. Use -txindex or provide a block hash to enable blockchain transaction queries.", node.isfinaltransaction, wallet_txid, ) self.log.info( "Repeat with -txindex so we don't need the blockhash") self.restart_node(0, self.extra_args[0] + ['-txindex']) quorum = get_quorum() self.wait_until(is_quorum_established) # Try to raise a -txindex not synced yet error. This is not # guaranteed because syncing is fast! try: node.isfinaltransaction( uint256_hex(random.randint(0, 2**256 - 1)), ) except JSONRPCException as e: assert_equal(e.error['code'], -5) if e.error['message'] == "No such mempool or blockchain transaction.": # If we got a regular "not found" error, the txindex should # have synced. assert node.getindexinfo()['txindex']['synced'] is True else: # Otherwise we might have successfully raised before the # indexer completed. Checking the status now is useless as # the indexer might have completed the synchronization in # the meantime and the status is no longer relevant. assert e.error['message'] == "No such transaction. Blockchain transactions are still in the process of being indexed." else: assert False, "The isfinaltransaction RPC call did not throw as expected." self.wait_until(lambda: node.getindexinfo()[ 'txindex']['synced'] is True) self.wait_until(lambda: is_finalblock(tip)) assert node.isfinaltransaction(wallet_txid) wallet_txid = node.sendtoaddress( ADDRESS_ECREG_UNSPENDABLE, 1_000_000) assert wallet_txid in node.getrawmempool() assert not node.isfinaltransaction(wallet_txid) assert_raises_rpc_error( -5, "No such mempool or blockchain transaction.", node.isfinaltransaction, uint256_hex(random.randint(0, 2**256 - 1)), ) self.log.info("Check unknown item") for _ in range(10): assert_raises_rpc_error( -8, "Block not found", node.isfinalblock, uint256_hex(random.randint(0, 2**256 - 1)), ) assert_raises_rpc_error( -8, "Block not found", node.isfinaltransaction, uint256_hex(random.randint(0, 2**256 - 1)), uint256_hex(random.randint(0, 2**256 - 1)), ) tip = node.getbestblockhash() height = node.getblockcount() + 1 time = node.getblock(tip)['time'] + 1 block = create_block(int(tip, 16), create_coinbase(height), time) block.solve() peer = node.add_p2p_connection(AvaP2PInterface()) msg = msg_headers() msg.headers = [CBlockHeader(block)] peer.send_message(msg) self.wait_until(lambda: node.getchaintips()[0]['height'] == height) assert_raises_rpc_error( -1, "Block data not downloaded yet.", node.isfinaltransaction, uint256_hex(random.randint(0, 2**256 - 1)), uint256_hex(block.sha256), ) if __name__ == '__main__': AvalancheIsFinalTest().main() diff --git a/test/functional/test_framework/avatools.py b/test/functional/test_framework/avatools.py index 8f01c3b10..fae11c440 100644 --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -1,430 +1,459 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for avalanche tests.""" import random import struct from typing import TYPE_CHECKING, Any, Dict, List, Optional from .authproxy import JSONRPCException from .key import ECKey from .messages import ( MSG_AVA_PROOF, MSG_BLOCK, NODE_AVALANCHE, NODE_NETWORK, AvalancheDelegation, AvalancheProof, AvalancheResponse, + AvalancheVote, + AvalancheVoteError, CInv, CTransaction, FromHex, TCPAvalancheResponse, ToHex, calculate_shortid, hash256, msg_avahello, msg_avapoll, msg_avaproof, msg_avaproofs, msg_notfound, msg_tcpavaresponse, ) from .p2p import P2PInterface, p2p_lock if TYPE_CHECKING: from .test_framework import BitcoinTestFramework from .test_node import TestNode from .util import satoshi_round, uint256_hex, wait_until_helper from .wallet_util import bytes_to_wif def avalanche_proof_from_hex(proof_hex: str) -> AvalancheProof: return FromHex(AvalancheProof(), proof_hex) def create_coinbase_stakes( node: TestNode, blockhashes: List[str], priv_key: str, amount: Optional[str] = None) -> List[Dict[str, Any]]: """Returns a list of dictionaries representing stakes, in a format compatible with the buildavalancheproof RPC, using only coinbase transactions. :param node: Test node used to get the block and coinbase data. :param blockhashes: List of block hashes, whose coinbase tx will be used as a stake. :param priv_key: Private key controlling the coinbase UTXO :param amount: If specified, this overwrites the amount information in the coinbase dicts. """ blocks = [node.getblock(h, 2) for h in blockhashes] coinbases = [ { 'height': b['height'], 'txid': b['tx'][0]['txid'], 'n': 0, 'value': b['tx'][0]['vout'][0]['value'], } for b in blocks ] return [{ 'txid': coinbase['txid'], 'vout': coinbase['n'], 'amount': amount or coinbase['value'], 'height': coinbase['height'], 'iscoinbase': True, 'privatekey': priv_key, } for coinbase in coinbases] def get_utxos_in_blocks(node: TestNode, blockhashes: List[str]) -> List[Dict]: """Return all UTXOs in the specified list of blocks. """ utxos = filter( lambda u: node.gettransaction(u["txid"])["blockhash"] in blockhashes, node.listunspent()) return list(utxos) def create_stakes( test_framework: 'BitcoinTestFramework', node: TestNode, blockhashes: List[str], count: int, sync_fun=None,) -> List[Dict[str, Any]]: """ Create a list of stakes by splitting existing UTXOs from a specified list of blocks into 10 new coins. This function can generate more valid stakes than `get_coinbase_stakes` does, because on the regtest chain halving happens every 150 blocks so the coinbase amount is below the dust threshold after only 900 blocks. :param node: Test node used to generate blocks and send transactions :param blockhashes: List of block hashes whose UTXOs will be split. :param count: Number of stakes to return. """ assert 10 * len(blockhashes) >= count utxos = get_utxos_in_blocks(node, blockhashes) addresses = [node.getnewaddress() for _ in range(10)] private_keys = {addr: node.dumpprivkey(addr) for addr in addresses} for u in utxos: inputs = [{"txid": u["txid"], "vout": u["vout"]}] outputs = { addr: satoshi_round(u['amount'] / 10) for addr in addresses} raw_tx = node.createrawtransaction(inputs, outputs) ctx = FromHex(CTransaction(), raw_tx) ctx.vout[0].nValue -= node.calculate_fee(ctx) signed_tx = node.signrawtransactionwithwallet(ToHex(ctx))["hex"] node.sendrawtransaction(signed_tx) # confirm the transactions new_blocks = [] while node.getmempoolinfo()['size'] > 0: new_blocks += test_framework.generate( node, 1, sync_fun=test_framework.no_op if sync_fun is None else sync_fun) utxos = get_utxos_in_blocks(node, new_blocks) stakes = [] # cache block heights heights = {} for utxo in utxos[:count]: blockhash = node.gettransaction(utxo["txid"])["blockhash"] if blockhash not in heights: heights[blockhash] = node.getblock(blockhash, 1)["height"] stakes.append({ 'txid': utxo['txid'], 'vout': utxo['vout'], 'amount': utxo['amount'], 'iscoinbase': utxo['label'] == "coinbase", 'height': heights[blockhash], 'privatekey': private_keys[utxo["address"]], }) return stakes def get_proof_ids(node): return [int(peer['proofid'], 16) for peer in node.getavalanchepeerinfo()] def wait_for_proof(node, proofid_hex, expect_status="boundToPeer", timeout=60): """ Wait for the proof to be known by the node. The expect_status is checked once after the proof is found and can be one of the following: "immature", "boundToPeer", "conflicting" or "finalized". """ ret = {} def proof_found(): nonlocal ret try: ret = node.getrawavalancheproof(proofid_hex) return True except JSONRPCException: return False wait_until_helper(proof_found, timeout=timeout) assert ret.get(expect_status, False) is True class NoHandshakeAvaP2PInterface(P2PInterface): """P2PInterface with avalanche capabilities""" def __init__(self): self.round = 0 self.avahello = None self.avaresponses = [] self.avapolls = [] self.nodeid: Optional[int] = None super().__init__() def peer_connect(self, *args, **kwargs): create_conn = super().peer_connect(*args, **kwargs) # Save the nonce and extra entropy so they can be reused later. self.local_nonce = self.on_connection_send_msg.nNonce self.local_extra_entropy = self.on_connection_send_msg.nExtraEntropy return create_conn def peer_accept_connection(self, *args, **kwargs): create_conn = super().peer_accept_connection(*args, **kwargs) # Save the nonce and extra entropy so they can be reused later. self.local_nonce = self.on_connection_send_msg.nNonce self.local_extra_entropy = self.on_connection_send_msg.nExtraEntropy return create_conn def on_version(self, message): super().on_version(message) # Save the nonce and extra entropy so they can be reused later. self.remote_nonce = message.nNonce self.remote_extra_entropy = message.nExtraEntropy def on_avaresponse(self, message): self.avaresponses.append(message.response) def on_avapoll(self, message): self.avapolls.append(message.poll) def on_avahello(self, message): assert self.avahello is None self.avahello = message def send_avaresponse(self, round, votes, privkey): response = AvalancheResponse(round, 0, votes) sig = privkey.sign_schnorr(response.get_hash()) msg = msg_tcpavaresponse() msg.response = TCPAvalancheResponse(response, sig) self.send_message(msg) def wait_for_avaresponse(self, timeout=5): self.wait_until( lambda: len(self.avaresponses) > 0, timeout=timeout) with p2p_lock: return self.avaresponses.pop(0) def send_poll(self, hashes, type=MSG_BLOCK): msg = msg_avapoll() msg.poll.round = self.round self.round += 1 for h in hashes: msg.poll.invs.append(CInv(type, h)) self.send_message(msg) def send_proof(self, proof): msg = msg_avaproof() msg.proof = proof self.send_message(msg) def get_avapoll_if_available(self): with p2p_lock: return self.avapolls.pop(0) if len(self.avapolls) > 0 else None def wait_for_avahello(self, timeout=5): self.wait_until( lambda: self.avahello is not None, timeout=timeout) with p2p_lock: return self.avahello def build_avahello(self, delegation: AvalancheDelegation, delegated_privkey: ECKey) -> msg_avahello: local_sighash = hash256( delegation.getid() + struct.pack(" 0: self.send_message(msg_notfound(not_found)) def get_ava_p2p_interface_no_handshake( node: TestNode, services=NODE_NETWORK | NODE_AVALANCHE) -> NoHandshakeAvaP2PInterface: """Build and return a NoHandshakeAvaP2PInterface connected to the specified TestNode. """ n = NoHandshakeAvaP2PInterface() node.add_p2p_connection( n, services=services) n.wait_for_verack() n.nodeid = node.getpeerinfo()[-1]['id'] return n def get_ava_p2p_interface( test_framework: 'BitcoinTestFramework', node: TestNode, services=NODE_NETWORK | NODE_AVALANCHE, stake_utxo_confirmations=1, sync_fun=None,) -> AvaP2PInterface: """Build and return an AvaP2PInterface connected to the specified TestNode. """ n = AvaP2PInterface(test_framework, node) # Make sure the proof utxos are mature if stake_utxo_confirmations > 1: test_framework.generate( node, stake_utxo_confirmations - 1, sync_fun=test_framework.no_op if sync_fun is None else sync_fun) assert node.verifyavalancheproof(n.proof.serialize().hex()) proofid_hex = uint256_hex(n.proof.proofid) node.add_p2p_connection(n, services=services) n.nodeid = node.getpeerinfo()[-1]['id'] def avapeer_connected(): node_list = [] try: node_list = node.getavalanchepeerinfo(proofid_hex)[0]['node_list'] except BaseException: pass return n.nodeid in node_list wait_until_helper(avapeer_connected, timeout=5) return n def gen_proof(test_framework, node, coinbase_utxos=1, expiry=0, sync_fun=None): blockhashes = test_framework.generate( node, coinbase_utxos, sync_fun=test_framework.no_op if sync_fun is None else sync_fun) privkey = ECKey() privkey.generate() stakes = create_coinbase_stakes( node, blockhashes, node.get_deterministic_priv_key().key) proof_hex = node.buildavalancheproof( 42, expiry, bytes_to_wif(privkey.get_bytes()), stakes) return privkey, avalanche_proof_from_hex(proof_hex) def build_msg_avaproofs(proofs: List[AvalancheProof], prefilled_proofs: Optional[List[AvalancheProof]] = None, key_pair: Optional[List[int]] = None) -> msg_avaproofs: if key_pair is None: key_pair = [random.randint(0, 2**64 - 1)] * 2 msg = msg_avaproofs() msg.key0 = key_pair[0] msg.key1 = key_pair[1] msg.prefilled_proofs = prefilled_proofs or [] msg.shortids = [ calculate_shortid( msg.key0, msg.key1, proof.proofid) for proof in proofs] return msg + + +def can_find_inv_in_poll(quorum, hash, response=AvalancheVoteError.ACCEPTED): + found_hash = False + for n in quorum: + poll = n.get_avapoll_if_available() + + # That node has not received a poll + if poll is None: + continue + + # We got a poll, check for the hash and repond + votes = [] + for inv in poll.invs: + # Vote yes to everything + r = AvalancheVoteError.ACCEPTED + + # Look for what we expect + if inv.hash == hash: + r = response + found_hash = True + + votes.append(AvalancheVote(r, inv.hash)) + + n.send_avaresponse(poll.round, votes, n.delegated_privkey) + + return found_hash