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 | |||||||||
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, the it will be | ||||||||||
PiRKUnsubmitted Not Done Inline Actions
PiRK: | ||||||||||
# 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() |