Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc_p2p_avalanche_transaction_voting.py
# Copyright (c) 2022 The Bitcoin developers | # Copyright (c) 2022 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 voting.""" | """Test avalanche transaction voting.""" | ||||
import random | import random | ||||
from decimal import Decimal | |||||
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 ( | from test_framework.blocktools import ( | ||||
COINBASE_MATURITY, | COINBASE_MATURITY, | ||||
create_block, | create_block, | ||||
create_coinbase, | create_coinbase, | ||||
make_conform_to_ctor, | make_conform_to_ctor, | ||||
) | ) | ||||
▲ Show 20 Lines • Show All 139 Lines • ▼ Show 20 Lines | def run_test(self): | ||||
[from_wallet_tx(orphan_tx)], | [from_wallet_tx(orphan_tx)], | ||||
node, | node, | ||||
success=False, | success=False, | ||||
reject_reason="bad-txns-inputs-missingorspent", | reject_reason="bad-txns-inputs-missingorspent", | ||||
) | ) | ||||
poll_node.send_poll([orphan_txid], MSG_TX) | poll_node.send_poll([orphan_txid], MSG_TX) | ||||
assert_response([AvalancheVote(AvalancheTxVoteError.ORPHAN, orphan_txid)]) | assert_response([AvalancheVote(AvalancheTxVoteError.ORPHAN, orphan_txid)]) | ||||
# Let's clean up the non transaction inventories from our avalanche polls | |||||
def has_finalized_proof(proofid): | |||||
can_find_inv_in_poll(quorum, proofid) | |||||
return node.getrawavalancheproof(uint256_hex(proofid))["finalized"] | |||||
for q in quorum: | |||||
self.wait_until(lambda: has_finalized_proof(q.proof.proofid)) | |||||
def has_finalized_block(block_hash): | |||||
can_find_inv_in_poll(quorum, int(block_hash, 16)) | |||||
return node.isfinalblock(block_hash) | |||||
tip = node.getbestblockhash() | |||||
self.wait_until(lambda: has_finalized_block(tip)) | |||||
self.log.info("Check the votes on conflicting transactions") | self.log.info("Check the votes on conflicting transactions") | ||||
utxo = wallet.get_utxo() | utxo = wallet.get_utxo() | ||||
mempool_tx = wallet.create_self_transfer(utxo_to_spend=utxo) | mempool_tx = wallet.create_self_transfer(utxo_to_spend=utxo) | ||||
peer.send_txs_and_test([from_wallet_tx(mempool_tx)], node, success=True) | peer.send_txs_and_test([from_wallet_tx(mempool_tx)], node, success=True) | ||||
assert mempool_tx["txid"] in node.getrawmempool() | assert mempool_tx["txid"] in node.getrawmempool() | ||||
conflicting_tx = wallet.create_self_transfer(utxo_to_spend=utxo) | conflicting_tx = wallet.create_self_transfer(utxo_to_spend=utxo) | ||||
conflicting_txid = int(conflicting_tx["txid"], 16) | conflicting_txid = int(conflicting_tx["txid"], 16) | ||||
peer.send_txs_and_test( | peer.send_txs_and_test( | ||||
[from_wallet_tx(conflicting_tx)], | [from_wallet_tx(conflicting_tx)], | ||||
node, | node, | ||||
success=False, | success=False, | ||||
reject_reason="txn-mempool-conflict", | reject_reason="txn-mempool-conflict", | ||||
) | ) | ||||
poll_node.send_poll([conflicting_txid], MSG_TX) | poll_node.send_poll([conflicting_txid], MSG_TX) | ||||
assert_response( | assert_response( | ||||
[AvalancheVote(AvalancheTxVoteError.CONFLICTING, conflicting_txid)] | [AvalancheVote(AvalancheTxVoteError.CONFLICTING, conflicting_txid)] | ||||
) | ) | ||||
self.log.info("Check the node polls for transactions added to the mempool") | self.log.info("Check the node polls for transactions added to the mempool") | ||||
# Let's clean up the non transaction inventories from our avalanche polls | |||||
def has_finalized_proof(proofid): | |||||
can_find_inv_in_poll(quorum, proofid) | |||||
return node.getrawavalancheproof(uint256_hex(proofid))["finalized"] | |||||
for q in quorum: | |||||
self.wait_until(lambda: has_finalized_proof(q.proof.proofid)) | |||||
def has_finalized_block(block_hash): | |||||
can_find_inv_in_poll(quorum, int(block_hash, 16)) | |||||
return node.isfinalblock(block_hash) | |||||
tip = node.getbestblockhash() | |||||
self.wait_until(lambda: has_finalized_block(tip)) | |||||
# Now we can focus on transactions | # Now we can focus on transactions | ||||
roqqit: this comment should follow the moved block of code | |||||
def has_accepted_tx(txid): | |||||
can_find_inv_in_poll( | |||||
quorum, | |||||
int(txid, 16), | |||||
response=AvalancheTxVoteError.ACCEPTED, | |||||
other_response=AvalancheTxVoteError.UNKNOWN, | |||||
) | |||||
return txid in node.getrawmempool() | |||||
def has_finalized_tx(txid): | def has_finalized_tx(txid): | ||||
can_find_inv_in_poll(quorum, int(txid, 16)) | return has_accepted_tx(txid) and node.isfinaltransaction(txid) | ||||
return node.isfinaltransaction(txid) | |||||
def has_invalidated_tx(txid): | def has_rejected_tx(txid): | ||||
can_find_inv_in_poll( | can_find_inv_in_poll( | ||||
quorum, int(txid, 16), response=AvalancheTxVoteError.INVALID | quorum, | ||||
int(txid, 16), | |||||
response=AvalancheTxVoteError.CONFLICTING, | |||||
other_response=AvalancheTxVoteError.UNKNOWN, | |||||
) | ) | ||||
return txid not in node.getrawmempool() | return txid not in node.getrawmempool() | ||||
def has_invalidated_tx(txid): | |||||
return ( | |||||
has_rejected_tx(txid) | |||||
and node.gettransactionstatus(txid)["pool"] == "none" | |||||
) | |||||
self.wait_until(lambda: has_finalized_tx(mempool_tx["txid"])) | self.wait_until(lambda: has_finalized_tx(mempool_tx["txid"])) | ||||
self.log.info("Check the node rejects txs that conflict with a finalized tx") | self.log.info("Check the node rejects txs that conflict with a finalized tx") | ||||
another_conflicting_tx = wallet.create_self_transfer(utxo_to_spend=utxo) | another_conflicting_tx = wallet.create_self_transfer(utxo_to_spend=utxo) | ||||
another_conflicting_txid = int(another_conflicting_tx["txid"], 16) | another_conflicting_txid = int(another_conflicting_tx["txid"], 16) | ||||
peer.send_txs_and_test( | peer.send_txs_and_test( | ||||
[from_wallet_tx(another_conflicting_tx)], | [from_wallet_tx(another_conflicting_tx)], | ||||
▲ Show 20 Lines • Show All 108 Lines • ▼ Show 20 Lines | def run_test(self): | ||||
conflicting_block.hashMerkleRoot = conflicting_block.calc_merkle_root() | conflicting_block.hashMerkleRoot = conflicting_block.calc_merkle_root() | ||||
conflicting_block.solve() | conflicting_block.solve() | ||||
peer.send_blocks_and_test( | peer.send_blocks_and_test( | ||||
[conflicting_block], | [conflicting_block], | ||||
node, | node, | ||||
) | ) | ||||
self.log.info("Check the node polls for conflicting txs") | |||||
tip = self.generate(node, 1)[0] | |||||
assert_equal(node.getrawmempool(), []) | |||||
self.wait_until(lambda: has_finalized_block(tip)) | |||||
utxo = wallet.get_utxo() | |||||
mempool_tx = wallet.create_self_transfer( | |||||
utxo_to_spend=utxo, fee_rate=Decimal("2000") | |||||
) | |||||
conflicting_tx = wallet.create_self_transfer( | |||||
utxo_to_spend=utxo, fee_rate=Decimal("3000") | |||||
) | |||||
mempool_tx_obj = from_wallet_tx(mempool_tx) | |||||
peer.send_txs_and_test( | |||||
[mempool_tx_obj], | |||||
node, | |||||
success=True, | |||||
) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
conflicting_tx_obj = from_wallet_tx(conflicting_tx) | |||||
with node.assert_debug_log([f"stored conflicting tx {conflicting_tx['txid']}"]): | |||||
peer.send_txs_and_test( | |||||
[conflicting_tx_obj], | |||||
node, | |||||
success=False, | |||||
reject_reason="txn-mempool-conflict", | |||||
) | |||||
self.wait_until( | |||||
lambda: can_find_inv_in_poll( | |||||
quorum, | |||||
int(conflicting_tx["txid"], 16), | |||||
response=AvalancheTxVoteError.CONFLICTING, | |||||
other_response=AvalancheTxVoteError.UNKNOWN, | |||||
) | |||||
) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
self.log.info("Check the node can pull back conflicting txs via avalanche") | |||||
self.wait_until(lambda: has_accepted_tx(conflicting_tx["txid"])) | |||||
assert mempool_tx["txid"] not in node.getrawmempool() | |||||
assert conflicting_tx["txid"] in node.getrawmempool() | |||||
self.log.info("Check the node can accept/reject conflicting txs via avalanche") | |||||
self.wait_until(lambda: has_rejected_tx(conflicting_tx["txid"])) | |||||
# The previously rejected transaction is pulled back to the mempool | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
# Rinse and repeat | |||||
self.wait_until(lambda: has_accepted_tx(conflicting_tx["txid"])) | |||||
assert mempool_tx["txid"] not in node.getrawmempool() | |||||
assert conflicting_tx["txid"] in node.getrawmempool() | |||||
self.wait_until(lambda: has_rejected_tx(conflicting_tx["txid"])) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
# Accept again and finalize | |||||
self.wait_until(lambda: has_finalized_tx(conflicting_tx["txid"])) | |||||
assert mempool_tx["txid"] not in node.getrawmempool() | |||||
assert conflicting_tx["txid"] in node.getrawmempool() | |||||
self.log.info("Check the node can invalidate conflicting txs via avalanche") | |||||
# This is very similar to the previous tests but will end with | |||||
# conflicting_tx being invalidated instead of finalized | |||||
utxo = wallet.get_utxo() | |||||
mempool_tx = wallet.create_self_transfer( | |||||
utxo_to_spend=utxo, fee_rate=Decimal("2000") | |||||
) | |||||
conflicting_tx = wallet.create_self_transfer( | |||||
utxo_to_spend=utxo, fee_rate=Decimal("3000") | |||||
) | |||||
mempool_tx_obj = from_wallet_tx(mempool_tx) | |||||
peer.send_txs_and_test( | |||||
[mempool_tx_obj], | |||||
node, | |||||
success=True, | |||||
) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
conflicting_tx_obj = from_wallet_tx(conflicting_tx) | |||||
with node.assert_debug_log([f"stored conflicting tx {conflicting_tx['txid']}"]): | |||||
peer.send_txs_and_test( | |||||
[conflicting_tx_obj], | |||||
node, | |||||
success=False, | |||||
reject_reason="txn-mempool-conflict", | |||||
) | |||||
self.wait_until( | |||||
lambda: can_find_inv_in_poll( | |||||
quorum, | |||||
int(conflicting_tx["txid"], 16), | |||||
response=AvalancheTxVoteError.CONFLICTING, | |||||
other_response=AvalancheTxVoteError.UNKNOWN, | |||||
) | |||||
) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
self.wait_until(lambda: has_rejected_tx(conflicting_tx["txid"])) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
self.wait_until(lambda: has_invalidated_tx(conflicting_tx["txid"])) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
# Now that the conflicting_tx has been invalidated, it will be ignored | |||||
# if submitted again because it's in the recent rejects | |||||
peer.send_txs_and_test( | |||||
[conflicting_tx_obj], node, success=False, expect_disconnect=False | |||||
) | |||||
# mempool_tx is not finalized so we accept another conflicting tx | |||||
another_conflicting_tx = wallet.create_self_transfer( | |||||
utxo_to_spend=utxo, fee_rate=Decimal("4000") | |||||
) | |||||
another_conflicting_tx_obj = from_wallet_tx(another_conflicting_tx) | |||||
with node.assert_debug_log( | |||||
[f"stored conflicting tx {another_conflicting_tx['txid']}"] | |||||
): | |||||
peer.send_txs_and_test( | |||||
[another_conflicting_tx_obj], | |||||
node, | |||||
success=False, | |||||
expect_disconnect=False, | |||||
reject_reason="txn-mempool-conflict", | |||||
) | |||||
self.log.info("Check all conflicting txs are erased upon finalization") | |||||
utxo = wallet.get_utxo() | |||||
mempool_tx = wallet.create_self_transfer( | |||||
utxo_to_spend=utxo, fee_rate=Decimal("2000") | |||||
) | |||||
conflicting_txs = [ | |||||
wallet.create_self_transfer(utxo_to_spend=utxo, fee_rate=Decimal("3000")) | |||||
for _ in range(5) | |||||
] | |||||
mempool_tx_obj = from_wallet_tx(mempool_tx) | |||||
peer.send_txs_and_test( | |||||
[mempool_tx_obj], | |||||
node, | |||||
success=True, | |||||
) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
for conflicting_tx in conflicting_txs: | |||||
conflicting_tx_obj = from_wallet_tx(conflicting_tx) | |||||
with node.assert_debug_log( | |||||
[f"stored conflicting tx {conflicting_tx['txid']}"] | |||||
): | |||||
peer.send_txs_and_test( | |||||
[conflicting_tx_obj], | |||||
node, | |||||
success=False, | |||||
reject_reason="txn-mempool-conflict", | |||||
) | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
assert_equal( | |||||
node.gettransactionstatus(conflicting_tx["txid"])["pool"], "conflicting" | |||||
) | |||||
self.wait_until(lambda: has_accepted_tx(mempool_tx["txid"])) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
for conflicting_tx in conflicting_txs: | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
assert_equal( | |||||
node.gettransactionstatus(conflicting_tx["txid"])["pool"], "conflicting" | |||||
) | |||||
self.wait_until(lambda: has_finalized_tx(mempool_tx["txid"])) | |||||
assert mempool_tx["txid"] in node.getrawmempool() | |||||
for conflicting_tx in conflicting_txs: | |||||
assert conflicting_tx["txid"] not in node.getrawmempool() | |||||
assert_equal( | |||||
node.gettransactionstatus(conflicting_tx["txid"])["pool"], "none" | |||||
) | |||||
# Sending them again will not add them back because they're all in | |||||
# the recent_reject | |||||
conflicting_tx_obj = from_wallet_tx(conflicting_tx) | |||||
peer.send_txs_and_test( | |||||
[conflicting_tx_obj], node, success=False, expect_disconnect=False | |||||
) | |||||
# A new conflict is rejected because it conflicts with the finalized tx | |||||
another_conflicting_tx = wallet.create_self_transfer( | |||||
utxo_to_spend=utxo, fee_rate=Decimal("4000") | |||||
) | |||||
another_conflicting_tx_obj = from_wallet_tx(another_conflicting_tx) | |||||
with node.assert_debug_log( | |||||
["finalized-tx-conflict"], | |||||
[f"stored conflicting tx {another_conflicting_tx['txid']}"], | |||||
): | |||||
peer.send_txs_and_test( | |||||
[another_conflicting_tx_obj], | |||||
node, | |||||
success=False, | |||||
expect_disconnect=False, | |||||
reject_reason="finalized-tx-conflict", | |||||
) | |||||
if __name__ == "__main__": | if __name__ == "__main__": | ||||
AvalancheTransactionVotingTest().main() | AvalancheTransactionVotingTest().main() |
this comment should follow the moved block of code