Merge #13003: qa: Add test for orphan handling
fa02c5b
qa: Clarify documentation for send_txs_and_test (MarcoFalke)fadfbd3
qa: Add test for orphan handling (MarcoFalke) Pull request description: Tree-SHA512: e0932d6bd03c73e3113f5457a3ffa3bbfc7b6075dfca8de95224d9df875e60ca6eb15cd8baa226f13de965483006559556191630a83c3bb431e79c53a85ef73f
This commit is contained in:
commit
8d045a0f66
3 changed files with 116 additions and 26 deletions
|
@ -82,10 +82,7 @@ class FullBlockTest(BitcoinTestFramework):
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
node = self.nodes[0] # convenience reference to the node
|
node = self.nodes[0] # convenience reference to the node
|
||||||
|
|
||||||
# reconnect_p2p() expects the network thread to be running
|
self.bootstrap_p2p() # Add one p2p connection to the node
|
||||||
network_thread_start()
|
|
||||||
|
|
||||||
self.reconnect_p2p()
|
|
||||||
|
|
||||||
self.block_heights = {}
|
self.block_heights = {}
|
||||||
self.coinbase_key = CECKey()
|
self.coinbase_key = CECKey()
|
||||||
|
@ -1296,14 +1293,10 @@ class FullBlockTest(BitcoinTestFramework):
|
||||||
self.blocks[block_number] = block
|
self.blocks[block_number] = block
|
||||||
return block
|
return block
|
||||||
|
|
||||||
def reconnect_p2p(self):
|
def bootstrap_p2p(self):
|
||||||
"""Add a P2P connection to the node.
|
"""Add a P2P connection to the node.
|
||||||
|
|
||||||
The node gets disconnected several times in this test. This helper
|
Helper to connect and wait for version handshake."""
|
||||||
method reconnects the p2p and restarts the network thread."""
|
|
||||||
|
|
||||||
network_thread_join()
|
|
||||||
self.nodes[0].disconnect_p2ps()
|
|
||||||
self.nodes[0].add_p2p_connection(P2PDataStore())
|
self.nodes[0].add_p2p_connection(P2PDataStore())
|
||||||
network_thread_start()
|
network_thread_start()
|
||||||
# We need to wait for the initial getheaders from the peer before we
|
# We need to wait for the initial getheaders from the peer before we
|
||||||
|
@ -1314,6 +1307,15 @@ class FullBlockTest(BitcoinTestFramework):
|
||||||
# unexpectedly disconnected if the DoS score for that error is 50.
|
# unexpectedly disconnected if the DoS score for that error is 50.
|
||||||
self.nodes[0].p2p.wait_for_getheaders(timeout=5)
|
self.nodes[0].p2p.wait_for_getheaders(timeout=5)
|
||||||
|
|
||||||
|
def reconnect_p2p(self):
|
||||||
|
"""Tear down and bootstrap the P2P connection to the node.
|
||||||
|
|
||||||
|
The node gets disconnected several times in this test. This helper
|
||||||
|
method reconnects the p2p and restarts the network thread."""
|
||||||
|
self.nodes[0].disconnect_p2ps()
|
||||||
|
network_thread_join()
|
||||||
|
self.bootstrap_p2p()
|
||||||
|
|
||||||
def sync_blocks(self, blocks, success=True, reject_code=None, reject_reason=None, request_block=True, reconnect=False, timeout=60):
|
def sync_blocks(self, blocks, success=True, reject_code=None, reject_reason=None, request_block=True, reconnect=False, timeout=60):
|
||||||
"""Sends blocks to test node. Syncs and verifies that tip has advanced to most recent block.
|
"""Sends blocks to test node. Syncs and verifies that tip has advanced to most recent block.
|
||||||
|
|
||||||
|
|
|
@ -6,24 +6,48 @@
|
||||||
|
|
||||||
In this test we connect to one node over p2p, and test tx requests."""
|
In this test we connect to one node over p2p, and test tx requests."""
|
||||||
from test_framework.blocktools import create_block, create_coinbase, create_transaction
|
from test_framework.blocktools import create_block, create_coinbase, create_transaction
|
||||||
from test_framework.messages import COIN
|
from test_framework.messages import (
|
||||||
from test_framework.mininode import network_thread_start, P2PDataStore
|
COIN,
|
||||||
|
COutPoint,
|
||||||
|
CTransaction,
|
||||||
|
CTxIn,
|
||||||
|
CTxOut,
|
||||||
|
)
|
||||||
|
from test_framework.mininode import network_thread_start, P2PDataStore, network_thread_join
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
|
from test_framework.util import (
|
||||||
|
assert_equal,
|
||||||
|
wait_until,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvalidTxRequestTest(BitcoinTestFramework):
|
class InvalidTxRequestTest(BitcoinTestFramework):
|
||||||
|
|
||||||
def set_test_params(self):
|
def set_test_params(self):
|
||||||
self.num_nodes = 1
|
self.num_nodes = 1
|
||||||
self.setup_clean_chain = True
|
self.setup_clean_chain = True
|
||||||
self.extra_args = [["-whitelist=127.0.0.1"]]
|
|
||||||
|
def bootstrap_p2p(self, *, num_connections=1):
|
||||||
|
"""Add a P2P connection to the node.
|
||||||
|
|
||||||
|
Helper to connect and wait for version handshake."""
|
||||||
|
for _ in range(num_connections):
|
||||||
|
self.nodes[0].add_p2p_connection(P2PDataStore())
|
||||||
|
network_thread_start()
|
||||||
|
self.nodes[0].p2p.wait_for_verack()
|
||||||
|
|
||||||
|
def reconnect_p2p(self, **kwargs):
|
||||||
|
"""Tear down and bootstrap the P2P connection to the node.
|
||||||
|
|
||||||
|
The node gets disconnected several times in this test. This helper
|
||||||
|
method reconnects the p2p and restarts the network thread."""
|
||||||
|
self.nodes[0].disconnect_p2ps()
|
||||||
|
network_thread_join()
|
||||||
|
self.bootstrap_p2p(**kwargs)
|
||||||
|
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
# Add p2p connection to node0
|
|
||||||
node = self.nodes[0] # convenience reference to the node
|
node = self.nodes[0] # convenience reference to the node
|
||||||
node.add_p2p_connection(P2PDataStore())
|
|
||||||
|
|
||||||
network_thread_start()
|
self.bootstrap_p2p() # Add one p2p connection to the node
|
||||||
node.p2p.wait_for_verack()
|
|
||||||
|
|
||||||
best_block = self.nodes[0].getbestblockhash()
|
best_block = self.nodes[0].getbestblockhash()
|
||||||
tip = int(best_block, 16)
|
tip = int(best_block, 16)
|
||||||
|
@ -44,12 +68,73 @@ class InvalidTxRequestTest(BitcoinTestFramework):
|
||||||
|
|
||||||
# b'\x64' is OP_NOTIF
|
# b'\x64' is OP_NOTIF
|
||||||
# Transaction will be rejected with code 16 (REJECT_INVALID)
|
# Transaction will be rejected with code 16 (REJECT_INVALID)
|
||||||
|
# and we get disconnected immediately
|
||||||
|
self.log.info('Test a transaction that is rejected')
|
||||||
tx1 = create_transaction(block1.vtx[0], 0, b'\x64', 50 * COIN - 12000)
|
tx1 = create_transaction(block1.vtx[0], 0, b'\x64', 50 * COIN - 12000)
|
||||||
node.p2p.send_txs_and_test([tx1], node, success=False, reject_code=16, reject_reason=b'mandatory-script-verify-flag-failed (Invalid OP_IF construction)')
|
node.p2p.send_txs_and_test([tx1], node, success=False, expect_disconnect=True)
|
||||||
|
|
||||||
# Verify valid transaction
|
# Make two p2p connections to provide the node with orphans
|
||||||
tx1 = create_transaction(block1.vtx[0], 0, b'', 50 * COIN - 12000)
|
# * p2ps[0] will send valid orphan txs (one with low fee)
|
||||||
node.p2p.send_txs_and_test([tx1], node, success=True)
|
# * p2ps[1] will send an invalid orphan tx (and is later disconnected for that)
|
||||||
|
self.reconnect_p2p(num_connections=2)
|
||||||
|
|
||||||
|
self.log.info('Test orphan transaction handling ... ')
|
||||||
|
# Create a root transaction that we withold until all dependend transactions
|
||||||
|
# are sent out and in the orphan cache
|
||||||
|
tx_withhold = CTransaction()
|
||||||
|
tx_withhold.vin.append(CTxIn(outpoint=COutPoint(block1.vtx[0].sha256, 0)))
|
||||||
|
tx_withhold.vout.append(CTxOut(nValue=50 * COIN - 12000, scriptPubKey=b'\x51'))
|
||||||
|
tx_withhold.calc_sha256()
|
||||||
|
|
||||||
|
# Our first orphan tx with some outputs to create further orphan txs
|
||||||
|
tx_orphan_1 = CTransaction()
|
||||||
|
tx_orphan_1.vin.append(CTxIn(outpoint=COutPoint(tx_withhold.sha256, 0)))
|
||||||
|
tx_orphan_1.vout = [CTxOut(nValue=10 * COIN, scriptPubKey=b'\x51')] * 3
|
||||||
|
tx_orphan_1.calc_sha256()
|
||||||
|
|
||||||
|
# A valid transaction with low fee
|
||||||
|
tx_orphan_2_no_fee = CTransaction()
|
||||||
|
tx_orphan_2_no_fee.vin.append(CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 0)))
|
||||||
|
tx_orphan_2_no_fee.vout.append(CTxOut(nValue=10 * COIN, scriptPubKey=b'\x51'))
|
||||||
|
|
||||||
|
# A valid transaction with sufficient fee
|
||||||
|
tx_orphan_2_valid = CTransaction()
|
||||||
|
tx_orphan_2_valid.vin.append(CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 1)))
|
||||||
|
tx_orphan_2_valid.vout.append(CTxOut(nValue=10 * COIN - 12000, scriptPubKey=b'\x51'))
|
||||||
|
tx_orphan_2_valid.calc_sha256()
|
||||||
|
|
||||||
|
# An invalid transaction with negative fee
|
||||||
|
tx_orphan_2_invalid = CTransaction()
|
||||||
|
tx_orphan_2_invalid.vin.append(CTxIn(outpoint=COutPoint(tx_orphan_1.sha256, 2)))
|
||||||
|
tx_orphan_2_invalid.vout.append(CTxOut(nValue=11 * COIN, scriptPubKey=b'\x51'))
|
||||||
|
|
||||||
|
self.log.info('Send the orphans ... ')
|
||||||
|
# Send valid orphan txs from p2ps[0]
|
||||||
|
node.p2p.send_txs_and_test([tx_orphan_1, tx_orphan_2_no_fee, tx_orphan_2_valid], node, success=False)
|
||||||
|
# Send invalid tx from p2ps[1]
|
||||||
|
node.p2ps[1].send_txs_and_test([tx_orphan_2_invalid], node, success=False)
|
||||||
|
|
||||||
|
assert_equal(0, node.getmempoolinfo()['size']) # Mempool should be empty
|
||||||
|
assert_equal(2, len(node.getpeerinfo())) # p2ps[1] is still connected
|
||||||
|
|
||||||
|
self.log.info('Send the withhold tx ... ')
|
||||||
|
node.p2p.send_txs_and_test([tx_withhold], node, success=True)
|
||||||
|
|
||||||
|
# Transactions that should end up in the mempool
|
||||||
|
expected_mempool = {
|
||||||
|
t.hash
|
||||||
|
for t in [
|
||||||
|
tx_withhold, # The transaction that is the root for all orphans
|
||||||
|
tx_orphan_1, # The orphan transaction that splits the coins
|
||||||
|
tx_orphan_2_valid, # The valid transaction (with sufficient fee)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# Transactions that do not end up in the mempool
|
||||||
|
# tx_orphan_no_fee, because it has too low fee (p2ps[0] is not disconnected for relaying that tx)
|
||||||
|
# tx_orphan_invaid, because it has negative fee (p2ps[1] is disconnected for relaying that tx)
|
||||||
|
|
||||||
|
wait_until(lambda: 1 == len(node.getpeerinfo()), timeout=12) # p2ps[1] is no longer connected
|
||||||
|
assert_equal(expected_mempool, set(node.getrawmempool()))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -554,13 +554,13 @@ class P2PDataStore(P2PInterface):
|
||||||
if reject_reason is not None:
|
if reject_reason is not None:
|
||||||
wait_until(lambda: self.reject_reason_received == reject_reason, lock=mininode_lock)
|
wait_until(lambda: self.reject_reason_received == reject_reason, lock=mininode_lock)
|
||||||
|
|
||||||
def send_txs_and_test(self, txs, rpc, success=True, reject_code=None, reject_reason=None):
|
def send_txs_and_test(self, txs, rpc, success=True, expect_disconnect=False, reject_code=None, reject_reason=None):
|
||||||
"""Send txs to test node and test whether they're accepted to the mempool.
|
"""Send txs to test node and test whether they're accepted to the mempool.
|
||||||
|
|
||||||
- add all txs to our tx_store
|
- add all txs to our tx_store
|
||||||
- send tx messages for all txs
|
- send tx messages for all txs
|
||||||
- if success is True: assert that the tx is accepted to the mempool
|
- if success is True/False: assert that the txs are/are not accepted to the mempool
|
||||||
- if success is False: assert that the tx is not accepted to the mempool
|
- if expect_disconnect is True: Skip the sync with ping
|
||||||
- if reject_code and reject_reason are set: assert that the correct reject message is received."""
|
- if reject_code and reject_reason are set: assert that the correct reject message is received."""
|
||||||
|
|
||||||
with mininode_lock:
|
with mininode_lock:
|
||||||
|
@ -573,7 +573,10 @@ class P2PDataStore(P2PInterface):
|
||||||
for tx in txs:
|
for tx in txs:
|
||||||
self.send_message(msg_tx(tx))
|
self.send_message(msg_tx(tx))
|
||||||
|
|
||||||
self.sync_with_ping()
|
if expect_disconnect:
|
||||||
|
self.wait_for_disconnect()
|
||||||
|
else:
|
||||||
|
self.sync_with_ping()
|
||||||
|
|
||||||
raw_mempool = rpc.getrawmempool()
|
raw_mempool = rpc.getrawmempool()
|
||||||
if success:
|
if success:
|
||||||
|
|
Loading…
Reference in a new issue