Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc_p2p_avalanche_transaction_finalization.py
| # Copyright (c) 2025 The Bitcoin developers | # Copyright (c) 2025 The Bitcoin developers | ||||||||
| # Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||||||
| # file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||||||
| """Test avalanche transaction finalization.""" | """Test avalanche transaction finalization.""" | ||||||||
| from test_framework.avatools import can_find_inv_in_poll, get_ava_p2p_interface | from test_framework.avatools import can_find_inv_in_poll, get_ava_p2p_interface | ||||||||
| from test_framework.blocktools import COINBASE_MATURITY | from test_framework.blocktools import COINBASE_MATURITY | ||||||||
| from test_framework.messages import AvalancheTxVoteError | from test_framework.messages import AvalancheTxVoteError, AvalancheVote | ||||||||
| from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||||||
| from test_framework.util import assert_equal, uint256_hex | from test_framework.util import assert_equal, uint256_hex | ||||||||
| from test_framework.wallet import MiniWallet | from test_framework.wallet import MiniWallet | ||||||||
| QUORUM_NODE_COUNT = 16 | QUORUM_NODE_COUNT = 16 | ||||||||
| class AvalancheTransactionFinalizationTest(BitcoinTestFramework): | class AvalancheTransactionFinalizationTest(BitcoinTestFramework): | ||||||||
| Show All 21 Lines | def finalize_tip(self): | ||||||||
| int(tip, 16), | int(tip, 16), | ||||||||
| response=AvalancheTxVoteError.ACCEPTED, | response=AvalancheTxVoteError.ACCEPTED, | ||||||||
| other_response=AvalancheTxVoteError.UNKNOWN, | other_response=AvalancheTxVoteError.UNKNOWN, | ||||||||
| ) | ) | ||||||||
| return self.nodes[0].isfinalblock(tip) | return self.nodes[0].isfinalblock(tip) | ||||||||
| self.wait_until(vote_until_final) | self.wait_until(vote_until_final) | ||||||||
| def finalize_tip_and_check_no_finalized_tx_is_polled(self, finalized_txids): | |||||||||
| tip = self.nodes[0].getbestblockhash() | |||||||||
| def find_tip_in_poll_and_check_for_no_finalized_tx(): | |||||||||
| found_hash = False | |||||||||
| tip_int = int(tip, 16) | |||||||||
| 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 to everything but our searched inv | |||||||||
PiRKUnsubmitted Not Done Inline Actions
PiRK: | |||||||||
| r = AvalancheTxVoteError.UNKNOWN | |||||||||
| # Look for what we expect | |||||||||
| if inv.hash == tip_int: | |||||||||
| r = AvalancheTxVoteError.ACCEPTED | |||||||||
| found_hash = True | |||||||||
| votes.append(AvalancheVote(r, inv.hash)) | |||||||||
| # We found the tip, so we expect none of the finalized tx to be | |||||||||
| # present in the polled inventories | |||||||||
| if found_hash: | |||||||||
| for inv in poll.invs: | |||||||||
| assert inv.hash not in finalized_txids | |||||||||
| n.send_avaresponse(poll.round, votes, n.delegated_privkey) | |||||||||
| return found_hash | |||||||||
| def vote_until_final(): | |||||||||
| find_tip_in_poll_and_check_for_no_finalized_tx() | |||||||||
| return self.nodes[0].isfinalblock(tip) | |||||||||
| self.wait_until(vote_until_final) | |||||||||
| def finalize_tx(self, txid, other_response=AvalancheTxVoteError.ACCEPTED): | def finalize_tx(self, txid, other_response=AvalancheTxVoteError.ACCEPTED): | ||||||||
| def vote_until_final(): | def vote_until_final(): | ||||||||
| can_find_inv_in_poll( | can_find_inv_in_poll( | ||||||||
| self.quorum, | self.quorum, | ||||||||
| txid, | txid, | ||||||||
| response=AvalancheTxVoteError.ACCEPTED, | response=AvalancheTxVoteError.ACCEPTED, | ||||||||
| other_response=other_response, | other_response=other_response, | ||||||||
| ) | ) | ||||||||
| Show All 16 Lines | def test_simple_txs(self): | ||||||||
| int(self.wallet.send_self_transfer(from_node=node)["txid"], 16) | int(self.wallet.send_self_transfer(from_node=node)["txid"], 16) | ||||||||
| for _ in range(num_txs) | for _ in range(num_txs) | ||||||||
| ] | ] | ||||||||
| assert_equal(node.getmempoolinfo()["size"], num_txs) | assert_equal(node.getmempoolinfo()["size"], num_txs) | ||||||||
| for txid in txids: | for txid in txids: | ||||||||
| self.finalize_tx(txid) | self.finalize_tx(txid) | ||||||||
| # Mine one more block, wait for it to be finalized while checking none | |||||||||
| # of the finalized txs are part of the polled items | |||||||||
| self.generate(self.wallet, 1) | |||||||||
| self.finalize_tip_and_check_no_finalized_tx_is_polled(txids) | |||||||||
| def test_chained_txs(self): | def test_chained_txs(self): | ||||||||
| self.log.info("Check the finalization of chained txs") | self.log.info("Check the finalization of chained txs") | ||||||||
| node = self.nodes[0] | node = self.nodes[0] | ||||||||
| # Make some valid chained txs | # Make some valid chained txs | ||||||||
| num_txs = 5 | num_txs = 5 | ||||||||
| self.generate(self.wallet, num_txs) | self.generate(self.wallet, num_txs) | ||||||||
| Show All 9 Lines | def test_chained_txs(self): | ||||||||
| int(txids[-2], 16), other_response=AvalancheTxVoteError.UNKNOWN | int(txids[-2], 16), other_response=AvalancheTxVoteError.UNKNOWN | ||||||||
| ) | ) | ||||||||
| # The ancestors should all be final as well | # The ancestors should all be final as well | ||||||||
| assert all(node.isfinaltransaction(txid) for txid in txids[:-1]) | assert all(node.isfinaltransaction(txid) for txid in txids[:-1]) | ||||||||
| # But not the descendant | # But not the descendant | ||||||||
| assert not node.isfinaltransaction(txids[-1]) | assert not node.isfinaltransaction(txids[-1]) | ||||||||
| # Mine one more block, wait for it to be finalized while checking none | |||||||||
| # of the finalized txs are part of the polled items | |||||||||
| self.generate(self.wallet, 1) | |||||||||
| self.finalize_tip_and_check_no_finalized_tx_is_polled(txids[:-1]) | |||||||||
| def test_diamond_txs(self): | def test_diamond_txs(self): | ||||||||
| self.log.info("Check the finalization of diamond shaped tx chains") | self.log.info("Check the finalization of diamond shaped tx chains") | ||||||||
| node = self.nodes[0] | node = self.nodes[0] | ||||||||
| # --> tx2 -- | # --> tx2 -- | ||||||||
| # / \ | # / \ | ||||||||
| # tx1 --> tx4 | # tx1 --> tx4 | ||||||||
| Show All 26 Lines | def test_diamond_txs(self): | ||||||||
| self.finalize_tx( | self.finalize_tx( | ||||||||
| int(txids[-2], 16), other_response=AvalancheTxVoteError.UNKNOWN | int(txids[-2], 16), other_response=AvalancheTxVoteError.UNKNOWN | ||||||||
| ) | ) | ||||||||
| assert all(node.isfinaltransaction(txid) for txid in txids[:-1]) | assert all(node.isfinaltransaction(txid) for txid in txids[:-1]) | ||||||||
| # But not tx5 | # But not tx5 | ||||||||
| assert not node.isfinaltransaction(txids[-1]) | assert not node.isfinaltransaction(txids[-1]) | ||||||||
| # Mine one more block, wait for it to be finalized while checking none | |||||||||
| # of the finalized txs are part of the polled items | |||||||||
| self.generate(self.wallet, 1) | |||||||||
| self.finalize_tip_and_check_no_finalized_tx_is_polled(txids[:-1]) | |||||||||
| def run_test(self): | def run_test(self): | ||||||||
| def get_quorum(): | def get_quorum(): | ||||||||
| return [ | return [ | ||||||||
| get_ava_p2p_interface(self, self.nodes[0]) | get_ava_p2p_interface(self, self.nodes[0]) | ||||||||
| for _ in range(0, QUORUM_NODE_COUNT) | for _ in range(0, QUORUM_NODE_COUNT) | ||||||||
| ] | ] | ||||||||
| self.quorum = get_quorum() | self.quorum = get_quorum() | ||||||||
| Show All 23 Lines | |||||||||