mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 14:59:39 -04:00
p2p, test: Add tx reconciliation functional tests
We may still need to add more tests, specially around extensions (if we keep them) Co-authored-by: Gleb Naumenko <naumenko.gs@gmail.com>
This commit is contained in:
parent
463a661866
commit
236dd9b8bb
6 changed files with 722 additions and 1 deletions
224
test/functional/p2p_txrecon_initiator.py
Executable file
224
test/functional/p2p_txrecon_initiator.py
Executable file
|
@ -0,0 +1,224 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2021-2025 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test reconciliation-based transaction relay (node initiates)"""
|
||||
|
||||
import time
|
||||
|
||||
from test_framework.messages import msg_reconcildiff
|
||||
|
||||
from test_framework.wallet import MiniWallet
|
||||
from test_framework.util import assert_equal
|
||||
from test_framework.p2p import P2PDataStore
|
||||
from test_framework.p2p_txrecon import (
|
||||
create_sketch, get_short_id, ReconciliationTest,
|
||||
TxReconTestP2PConn, Q_PRECISION, RECON_Q,
|
||||
MAX_SKETCH_CAPACITY, BYTES_PER_SKETCH_CAPACITY
|
||||
)
|
||||
|
||||
# Taken from net_processing.cpp
|
||||
OUTBOUND_INVENTORY_BROADCAST_INTERVAL = 2
|
||||
|
||||
# Taken from txreconciliation.h
|
||||
OUTBOUND_FANOUT_THRESHOLD = 4
|
||||
RECON_REQUEST_INTERVAL = 8
|
||||
|
||||
|
||||
class ReconciliationInitiatorTest(ReconciliationTest):
|
||||
def set_test_params(self):
|
||||
super().set_test_params()
|
||||
|
||||
# Wait for the next REQTXRCNCL message to be received by the
|
||||
# given peer. Clear and return it.
|
||||
def wait_for_reqtxrcncl(self, peer):
|
||||
def received_reqtxrcncl():
|
||||
return (len(peer.last_reqtxrcncl) > 0)
|
||||
self.wait_until(received_reqtxrcncl)
|
||||
|
||||
return peer.last_reqtxrcncl.pop()
|
||||
|
||||
# Wait for the next RECONCILDIFF message to be received by the
|
||||
# given peer. Clear and return it.
|
||||
def wait_for_reconcildiff(self, peer):
|
||||
def received_reconcildiff():
|
||||
return (len(peer.last_reconcildiff) > 0)
|
||||
self.wait_until(received_reconcildiff)
|
||||
|
||||
return peer.last_reconcildiff.pop()
|
||||
|
||||
# Creates a Sketch using the provided transactions and capacity
|
||||
# and sends it from the given peer.
|
||||
# Returns a list of the short ids contained in the Sketch.
|
||||
def send_sketch_from(self, peer, unique_wtxids, shared_wtxids, capacity):
|
||||
unique_short_txids = [get_short_id(wtxid, peer.combined_salt)
|
||||
for wtxid in unique_wtxids]
|
||||
shared_short_txids = [get_short_id(wtxid, peer.combined_salt)
|
||||
for wtxid in shared_wtxids]
|
||||
|
||||
sketch = create_sketch(unique_short_txids + shared_short_txids, capacity)
|
||||
peer.send_sketch(sketch)
|
||||
|
||||
return unique_short_txids
|
||||
|
||||
def test_reconciliation_initiator_flow_empty_sketch(self):
|
||||
peer = self.test_node.add_outbound_p2p_connection(TxReconTestP2PConn(), p2p_idx=0)
|
||||
|
||||
# Generate transaction only on the node's end, so it has something to announce at the end
|
||||
_, node_unique_txs, _ = self.generate_txs(self.wallet, 0, 10, 0)
|
||||
|
||||
# Do the reconciliation dance announcing an empty sketch
|
||||
# Wait enough to make sure the node adds the transaction to our tracker
|
||||
# And sends us a reconciliation request
|
||||
self.log.info('Testing reconciliation flow sending an empty sketch')
|
||||
self.test_node.bumpmocktime(OUTBOUND_INVENTORY_BROADCAST_INTERVAL * 20)
|
||||
peer.sync_with_ping()
|
||||
self.test_node.bumpmocktime(RECON_REQUEST_INTERVAL)
|
||||
peer.sync_with_ping()
|
||||
self.wait_for_reqtxrcncl(peer)
|
||||
# Sketch is empty
|
||||
self.send_sketch_from(peer, [], [], 0)
|
||||
recon_diff = self.wait_for_reconcildiff(peer)
|
||||
# The node's reply is also empty, signaling early exit
|
||||
assert_equal(recon_diff.ask_shortids, [])
|
||||
|
||||
# The node simply defaults to announce all the transaction it had for us
|
||||
node_unique_wtxids = set([tx.calc_sha256(True) for tx in node_unique_txs])
|
||||
self.wait_for_inv(peer, node_unique_wtxids)
|
||||
self.request_transactions_from(peer, node_unique_wtxids)
|
||||
self.wait_for_txs(peer, node_unique_wtxids)
|
||||
|
||||
# Clear peer
|
||||
peer.peer_disconnect()
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
def test_reconciliation_initiator_protocol_violations(self):
|
||||
# Test disconnect on sending Erlay messages as a non-Erlay peer
|
||||
self.log.info('Testing protocol violation: erlay messages as non-erlay peer')
|
||||
peer = self.test_node.add_outbound_p2p_connection(P2PDataStore(), p2p_idx=0)
|
||||
peer.send_without_ping(msg_reconcildiff())
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
# Test disconnect on sending a REQTXRCNCL as a responder
|
||||
self.log.info('Testing protocol violation: sending REQTXRCNCL as a responder')
|
||||
peer = self.test_node.add_outbound_p2p_connection(TxReconTestP2PConn(), p2p_idx=0)
|
||||
peer.send_reqtxrcncl(0, int(RECON_Q * Q_PRECISION))
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
# Test disconnect on sending a SKETCH out of order
|
||||
self.log.info('Testing protocol violation: sending SKETCH out of order')
|
||||
peer = self.test_node.add_outbound_p2p_connection(TxReconTestP2PConn(), p2p_idx=0)
|
||||
peer.send_sketch([])
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
# Test disconnect on sending a RECONCILDIFF as a responder
|
||||
self.log.info('Testing protocol violation: sending RECONCILDIFF as a responder')
|
||||
peer = self.test_node.add_outbound_p2p_connection(TxReconTestP2PConn(), p2p_idx=0)
|
||||
peer.send_reconcildiff(True, [])
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
# Test disconnect on SKETCH that exceeds maximum capacity
|
||||
self.log.info('Testing protocol violation: sending SKETCH exceeding the maximum capacity')
|
||||
peer = self.test_node.add_outbound_p2p_connection(TxReconTestP2PConn(), p2p_idx=0)
|
||||
# Do the reconciliation dance until announcing the SKETCH
|
||||
self.test_node.bumpmocktime(RECON_REQUEST_INTERVAL)
|
||||
peer.sync_with_ping()
|
||||
self.wait_for_reqtxrcncl(peer)
|
||||
# Send an over-sized sketch (over the maximum allowed capacity)
|
||||
peer.send_sketch([0] * ((MAX_SKETCH_CAPACITY + 1) * BYTES_PER_SKETCH_CAPACITY))
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
def test_reconciliation_initiator_no_extension(self, n_node, n_mininode, n_shared):
|
||||
self.log.info('Testing reconciliation flow without extensions')
|
||||
peers = [self.test_node.add_outbound_p2p_connection(TxReconTestP2PConn(), p2p_idx=i) for i in range(8)]
|
||||
|
||||
# Generate and submit transactions.
|
||||
mininode_unique_txs, node_unique_txs, shared_txs = self.generate_txs(
|
||||
self.wallet, n_mininode, n_node, n_shared)
|
||||
|
||||
# For transactions to have been added to the reconciliation sets of the node's outbound peers, we need no make sure
|
||||
# that the Poisson timer for all of them has ticked. Each timer ticks every 2 seconds on average. However, given the
|
||||
# nature of the test we have no way of checking if the timer has ticked, but we can work around this by making sure we
|
||||
# have waited long enough. By bumping the time ~20 times the expected value, we have a 1/100000000 chances of any of the
|
||||
# timers not ticking (i.e. failing the test later on), which should be more than acceptable
|
||||
self.test_node.bumpmocktime(OUTBOUND_INVENTORY_BROADCAST_INTERVAL * 20)
|
||||
for peer in peers:
|
||||
peer.sync_with_ping()
|
||||
|
||||
# Tick for as many peers as test_node has, so all of them receive a reconciliation request
|
||||
for peer in peers:
|
||||
self.test_node.bumpmocktime(int(RECON_REQUEST_INTERVAL/len(peers)))
|
||||
peer.sync_with_ping()
|
||||
|
||||
empty_recon_requests = 0
|
||||
for peer in peers:
|
||||
# Check we have received a reconciliation request. The request contains either no
|
||||
# elements (the node has been picked for fanout) or as many elements as transactions
|
||||
# where created by the node (n_node + n_shared)
|
||||
node_set_size = self.wait_for_reqtxrcncl(peer).set_size
|
||||
if (node_set_size == 0):
|
||||
empty_recon_requests+=1
|
||||
peer.chosen_for_fanout = True
|
||||
else:
|
||||
assert_equal(node_set_size, n_node + n_shared)
|
||||
peer.chosen_for_fanout = False
|
||||
|
||||
# For outbound peers, if the transaction was created by the node, or receive via fanout,
|
||||
# it will be fanout to up to OUTBOUND_FANOUT_THRESHOLD. We will be reconciling with the rest
|
||||
assert_equal(empty_recon_requests, OUTBOUND_FANOUT_THRESHOLD + 1)
|
||||
|
||||
for peer in peers:
|
||||
# If we received an empty request we can simply respond with an empty sketch
|
||||
# the node will shortcircuit and send us all transactions via fanout
|
||||
capacity = 0 if peer.chosen_for_fanout else n_node + n_mininode
|
||||
unique_wtxids = [tx.calc_sha256(True) for tx in mininode_unique_txs]
|
||||
shared_wtxids = [tx.calc_sha256(True) for tx in shared_txs]
|
||||
peer.unique_short_txids = self.send_sketch_from(peer, unique_wtxids, shared_wtxids, capacity)
|
||||
|
||||
# Check that we received the expected sketch difference, based on the sketch we have sent
|
||||
for peer in peers:
|
||||
recon_diff = self.wait_for_reconcildiff(peer)
|
||||
expected_diff = msg_reconcildiff()
|
||||
if peer.chosen_for_fanout:
|
||||
# If we replied with an empty sketch, they will flag failure and reply with an
|
||||
# empty diff to signal an early exit and default to fanout
|
||||
assert_equal(recon_diff, expected_diff)
|
||||
else:
|
||||
# Otherwise, we expect the decoding to succeed and a request of all out transactions
|
||||
# (given there were no shared transaction)
|
||||
expected_diff.success = 1
|
||||
expected_diff.ask_shortids = peer.unique_short_txids
|
||||
assert_equal(recon_diff, expected_diff)
|
||||
|
||||
# If we were chosen for reconciliation, the node will announce only the transaction we are missing (node_unique)
|
||||
# Otherwise, it will announce all the ones it has (node_unique + shared)
|
||||
node_unique_wtxids = [tx.calc_sha256(True) for tx in node_unique_txs]
|
||||
shared_wtxids = [tx.calc_sha256(True) for tx in shared_txs]
|
||||
for peer in peers:
|
||||
expected_wtxids = set(node_unique_wtxids + shared_wtxids) if peer.chosen_for_fanout else set(node_unique_wtxids)
|
||||
self.wait_for_inv(peer, expected_wtxids)
|
||||
self.request_transactions_from(peer, expected_wtxids)
|
||||
self.wait_for_txs(peer, expected_wtxids)
|
||||
|
||||
if not peer.chosen_for_fanout:
|
||||
# If we received a populated diff, the node will be expecting
|
||||
# some transactions in return. The reconciliation flow has really
|
||||
# finished already, but we should be well behaved
|
||||
peer.send_txs_and_test(mininode_unique_txs, self.test_node)
|
||||
|
||||
def run_test(self):
|
||||
self.test_node = self.nodes[0]
|
||||
self.test_node.setmocktime(int(time.time()))
|
||||
self.wallet = MiniWallet(self.nodes[0])
|
||||
self.generate(self.wallet, 512)
|
||||
|
||||
self.test_reconciliation_initiator_flow_empty_sketch()
|
||||
self.test_reconciliation_initiator_protocol_violations()
|
||||
self.test_reconciliation_initiator_no_extension(20, 15, 0)
|
||||
|
||||
# TODO: Add more cases, potentially including also extensions
|
||||
# if we end up not dropping them from the PR
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ReconciliationInitiatorTest(__file__).main()
|
161
test/functional/p2p_txrecon_responder.py
Executable file
161
test/functional/p2p_txrecon_responder.py
Executable file
|
@ -0,0 +1,161 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2021-2025 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test reconciliation-based transaction relay (node responds)"""
|
||||
|
||||
import time
|
||||
|
||||
from test_framework.messages import msg_reqtxrcncl
|
||||
from test_framework.p2p import P2PDataStore
|
||||
from test_framework.p2p_txrecon import (
|
||||
create_sketch, get_short_id, estimate_capacity,
|
||||
Q_PRECISION, RECON_Q, ReconciliationTest, TxReconTestP2PConn
|
||||
)
|
||||
from test_framework.util import assert_equal
|
||||
from test_framework.wallet import MiniWallet
|
||||
|
||||
# Taken from net_processing.cpp
|
||||
INBOUND_INVENTORY_BROADCAST_INTERVAL = 5
|
||||
|
||||
|
||||
class ReconciliationResponderTest(ReconciliationTest):
|
||||
def set_test_params(self):
|
||||
super().set_test_params()
|
||||
|
||||
# Wait for the next SKETCH message to be received by the
|
||||
# given peer. Clear and return it.
|
||||
def wait_for_sketch(self, peer):
|
||||
def received_sketch():
|
||||
return (len(peer.last_sketch) > 0)
|
||||
self.wait_until(received_sketch, timeout=2)
|
||||
|
||||
return peer.last_sketch.pop()
|
||||
|
||||
# Check that the node announced the exact sketch we expected (of the expected capacity
|
||||
# and over the expected transactions)
|
||||
def check_sketch(self, peer, skdata, expected_wtxids, local_set_size):
|
||||
expected_short_ids = [get_short_id(wtxid, peer.combined_salt)
|
||||
for wtxid in expected_wtxids]
|
||||
|
||||
if len(expected_wtxids) == 0:
|
||||
expected_capacity = 0
|
||||
else:
|
||||
expected_capacity = estimate_capacity(len(expected_wtxids), local_set_size)
|
||||
expected_sketch = create_sketch(expected_short_ids, expected_capacity)
|
||||
|
||||
assert_equal(skdata, expected_sketch)
|
||||
|
||||
# Send a RECONCILDIFF message from the given peer, including a sketch of
|
||||
# the given transactions.
|
||||
def send_reconcildiff_from(self, peer, success, wtxids_to_request, sync_with_ping=False):
|
||||
ask_shortids = [get_short_id(wtxid, peer.combined_salt)
|
||||
for wtxid in wtxids_to_request]
|
||||
peer.send_reconcildiff(success, ask_shortids, sync_with_ping)
|
||||
|
||||
def test_reconciliation_responder_flow_empty_sketch(self):
|
||||
self.log.info('Testing reconciliation flow sending an empty REQRXRCNCL')
|
||||
peer = self.test_node.add_p2p_connection(TxReconTestP2PConn())
|
||||
# Send a reconciliation request without creating any transactions
|
||||
peer.send_reqtxrcncl(0, int(RECON_Q * Q_PRECISION))
|
||||
|
||||
# We need to make sure the node has trickled for inbounds. Waiting bumping for 20x the expected
|
||||
# time gives us a 1/1000000000 chances of failing
|
||||
self.test_node.bumpmocktime(INBOUND_INVENTORY_BROADCAST_INTERVAL * 20)
|
||||
peer.sync_with_ping()
|
||||
|
||||
# Node sends us an empty sketch
|
||||
received_sketch = self.wait_for_sketch(peer)
|
||||
assert_equal(received_sketch.skdata, [])
|
||||
|
||||
# It doesn't really matter what we send them here as our diff, given they have no
|
||||
# transaction for us, so nothing will match their local set and the node will simply terminate.
|
||||
self.send_reconcildiff_from(peer, True, [], sync_with_ping=True)
|
||||
|
||||
# We can check this is the case by sending another reconciliation request, and check
|
||||
# how they reply to it (the node won't reply if the previous reconciliation was still pending)
|
||||
peer.send_reqtxrcncl(0, int(RECON_Q * Q_PRECISION))
|
||||
self.test_node.bumpmocktime(INBOUND_INVENTORY_BROADCAST_INTERVAL * 20)
|
||||
peer.sync_with_ping()
|
||||
received_sketch = self.wait_for_sketch(peer)
|
||||
|
||||
# Clear peer
|
||||
peer.peer_disconnect()
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
def test_reconciliation_responder_protocol_violations(self):
|
||||
# Test disconnect on sending Erlay messages as a non-Erlay peer
|
||||
self.log.info('Testing protocol violation: erlay messages as non-erlay peer')
|
||||
peer = self.test_node.add_p2p_connection(P2PDataStore())
|
||||
peer.send_without_ping(msg_reqtxrcncl())
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
# Test disconnect on sending multiple REQTXRCNCL without receiving a response
|
||||
self.log.info('Testing protocol violation: sending multiple REQTXRCNCL without waiting for a response')
|
||||
peer = self.test_node.add_p2p_connection(TxReconTestP2PConn())
|
||||
peer.send_reqtxrcncl(0, int(RECON_Q * Q_PRECISION))
|
||||
peer.send_reqtxrcncl(0, int(RECON_Q * Q_PRECISION))
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
# Test disconnect on sending SKETCH as initiator
|
||||
self.log.info('Testing protocol violation: sending SKETCH as initiator')
|
||||
peer = self.test_node.add_p2p_connection(TxReconTestP2PConn())
|
||||
peer.send_sketch([])
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
# Test disconnect on sending a RECONCILDIFF out-of-order
|
||||
self.log.info('Testing protocol violation: sending RECONCILDIFF out of order')
|
||||
peer = self.test_node.add_p2p_connection(TxReconTestP2PConn())
|
||||
self.send_reconcildiff_from(peer, True, [])
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
def test_reconciliation_responder_flow_no_extension(self, n_mininode, n_node):
|
||||
self.log.info('Testing reconciliation flow without extensions')
|
||||
peer = self.test_node.add_p2p_connection(TxReconTestP2PConn())
|
||||
# Generate and submit transactions.
|
||||
mininode_unique_txs, node_unique_txs, _ = self.generate_txs(self.wallet, n_mininode, n_node, 0)
|
||||
node_unique_wtxids = [tx.calc_sha256(True) for tx in node_unique_txs]
|
||||
|
||||
# Send a reconciliation request. The request will be queued and replied on the next inbound trickle
|
||||
peer.send_reqtxrcncl(n_mininode, int(RECON_Q * Q_PRECISION))
|
||||
|
||||
# We need to make sure the node has trickled for inbounds. Waiting bumping for 20x the expected
|
||||
# time gives us a 1/1000000000 chances of failing
|
||||
self.test_node.bumpmocktime(INBOUND_INVENTORY_BROADCAST_INTERVAL * 20)
|
||||
|
||||
received_sketch = self.wait_for_sketch(peer)
|
||||
self.check_sketch(peer, received_sketch.skdata, node_unique_wtxids, n_mininode)
|
||||
|
||||
# Diff should be all the node has that they don't have (their unique txs)
|
||||
self.send_reconcildiff_from(peer, True, node_unique_wtxids)
|
||||
|
||||
self.wait_for_inv(peer, set(node_unique_wtxids))
|
||||
self.request_transactions_from(peer, node_unique_wtxids)
|
||||
self.wait_for_txs(peer, node_unique_wtxids)
|
||||
|
||||
# Send our bit
|
||||
peer.send_txs_and_test(mininode_unique_txs, self.test_node)
|
||||
|
||||
# Clear peer
|
||||
peer.peer_disconnect()
|
||||
peer.wait_for_disconnect()
|
||||
|
||||
def run_test(self):
|
||||
self.test_node = self.nodes[0]
|
||||
self.test_node.setmocktime(int(time.time()))
|
||||
self.wallet = MiniWallet(self.nodes[0])
|
||||
self.generate(self.wallet, 512)
|
||||
|
||||
# These node will consume some of the low-fanout announcements.
|
||||
self.outbound_peers = [self.test_node.add_p2p_connection(TxReconTestP2PConn()) for _ in range(4)]
|
||||
|
||||
self.test_reconciliation_responder_flow_empty_sketch()
|
||||
self.test_reconciliation_responder_protocol_violations()
|
||||
self.test_reconciliation_responder_flow_no_extension(20, 15)
|
||||
|
||||
# TODO: Add more cases, potentially including also extensions
|
||||
# if we end up not dropping them from the PR
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ReconciliationResponderTest(__file__).main()
|
|
@ -152,10 +152,14 @@ def ser_string(s):
|
|||
def deser_uint256(f):
|
||||
return int.from_bytes(f.read(32), 'little')
|
||||
|
||||
|
||||
def ser_uint256(u):
|
||||
return u.to_bytes(32, 'little')
|
||||
|
||||
def deser_uint32(f):
|
||||
return int.from_bytes(f.read(4), 'little')
|
||||
|
||||
def ser_uint32(u):
|
||||
return u.to_bytes(4, 'little')
|
||||
|
||||
def uint256_from_str(s):
|
||||
return int.from_bytes(s[:32], 'little')
|
||||
|
@ -210,6 +214,34 @@ def ser_uint256_vector(l):
|
|||
r += ser_uint256(i)
|
||||
return r
|
||||
|
||||
def deser_uint32_vector(f):
|
||||
nit = deser_compact_size(f)
|
||||
r = []
|
||||
for _ in range(nit):
|
||||
t = deser_uint32(f)
|
||||
r.append(t)
|
||||
return r
|
||||
|
||||
def ser_uint32_vector(l):
|
||||
r = ser_compact_size(len(l))
|
||||
for i in l:
|
||||
r += ser_uint32(i)
|
||||
return r
|
||||
|
||||
def deser_uint8_vector(f):
|
||||
nit = deser_compact_size(f)
|
||||
r = []
|
||||
for _ in range(nit):
|
||||
t = int.from_bytes(f.read(1), 'little')
|
||||
r.append(t)
|
||||
return r
|
||||
|
||||
def ser_uint8_vector(l):
|
||||
r = ser_compact_size(len(l))
|
||||
for i in l:
|
||||
r += i.to_bytes(1, 'little')
|
||||
return r
|
||||
|
||||
|
||||
def deser_string_vector(f):
|
||||
nit = deser_compact_size(f)
|
||||
|
@ -1940,6 +1972,65 @@ class msg_reqtxrcncl:
|
|||
return "msg_reqtxrcncl(set_size=%lu, q=%lu)" %\
|
||||
(self.set_size, self.q)
|
||||
|
||||
class msg_sketch:
|
||||
__slots__ = ("skdata")
|
||||
msgtype = b"sketch"
|
||||
|
||||
def __init__(self):
|
||||
self.skdata = []
|
||||
|
||||
def deserialize(self, f):
|
||||
self.skdata = deser_uint8_vector(f)
|
||||
|
||||
def serialize(self):
|
||||
r = b""
|
||||
r += ser_uint8_vector(self.skdata)
|
||||
return r
|
||||
|
||||
def __repr__(self):
|
||||
return "msg_sketch(sketch_size=%i)" % (len(self.skdata))
|
||||
|
||||
class msg_reqsketchext:
|
||||
__slots__ = ()
|
||||
msgtype = b"reqsketchext"
|
||||
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def deserialize(self, f):
|
||||
return
|
||||
|
||||
def serialize(self):
|
||||
r = b""
|
||||
return r
|
||||
|
||||
def __repr__(self):
|
||||
return "msg_reqsketchext"
|
||||
|
||||
class msg_reconcildiff:
|
||||
__slots__ = ("success", "ask_shortids")
|
||||
msgtype = b"reconcildiff"
|
||||
|
||||
def __init__(self):
|
||||
self.success = 0
|
||||
self.ask_shortids = []
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.success == other.success and set(self.ask_shortids) == set(other.ask_shortids)
|
||||
|
||||
def deserialize(self, f):
|
||||
self.success = int.from_bytes(f.read(1), "little")
|
||||
self.ask_shortids = deser_uint32_vector(f)
|
||||
|
||||
def serialize(self):
|
||||
r = b""
|
||||
r += self.success.to_bytes(1, "little")
|
||||
r += ser_uint32_vector(self.ask_shortids)
|
||||
return r
|
||||
|
||||
def __repr__(self):
|
||||
return "msg_reconcildiff(success=%i,ask_shortids=%i)" % (self.success, len(self.ask_shortids))
|
||||
|
||||
class TestFrameworkScript(unittest.TestCase):
|
||||
def test_addrv2_encode_decode(self):
|
||||
def check_addrv2(ip, net):
|
||||
|
|
|
@ -60,11 +60,14 @@ from test_framework.messages import (
|
|||
msg_notfound,
|
||||
msg_ping,
|
||||
msg_pong,
|
||||
msg_reconcildiff,
|
||||
msg_reqsketchext,
|
||||
msg_reqtxrcncl,
|
||||
msg_sendaddrv2,
|
||||
msg_sendcmpct,
|
||||
msg_sendheaders,
|
||||
msg_sendtxrcncl,
|
||||
msg_sketch,
|
||||
msg_tx,
|
||||
MSG_TX,
|
||||
MSG_TYPE_MASK,
|
||||
|
@ -139,11 +142,14 @@ MESSAGEMAP = {
|
|||
b"notfound": msg_notfound,
|
||||
b"ping": msg_ping,
|
||||
b"pong": msg_pong,
|
||||
b"reconcildiff": msg_reconcildiff,
|
||||
b"reqsketchext": msg_reqsketchext,
|
||||
b"reqtxrcncl": msg_reqtxrcncl,
|
||||
b"sendaddrv2": msg_sendaddrv2,
|
||||
b"sendcmpct": msg_sendcmpct,
|
||||
b"sendheaders": msg_sendheaders,
|
||||
b"sendtxrcncl": msg_sendtxrcncl,
|
||||
b"sketch": msg_sketch,
|
||||
b"tx": msg_tx,
|
||||
b"verack": msg_verack,
|
||||
b"version": msg_version,
|
||||
|
|
237
test/functional/test_framework/p2p_txrecon.py
Normal file
237
test/functional/test_framework/p2p_txrecon.py
Normal file
|
@ -0,0 +1,237 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2021-2021 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Helpers to test reconciliation-based transaction relay, both initiator and responder roles"""
|
||||
|
||||
import random
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
from test_framework.messages import (
|
||||
msg_wtxidrelay, msg_verack, msg_sendtxrcncl,
|
||||
msg_reqtxrcncl, msg_sketch, msg_reconcildiff,
|
||||
msg_reqsketchext,msg_inv, msg_getdata,
|
||||
MSG_WTX, MSG_BLOCK, CTransaction, CInv,
|
||||
)
|
||||
from test_framework.key import TaggedHash
|
||||
from test_framework.p2p import P2PDataStore
|
||||
from test_framework.util import assert_equal
|
||||
from test_framework.crypto.siphash import siphash256
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
|
||||
|
||||
# These parameters are specified in the BIP-0330.
|
||||
Q_PRECISION = (2 << 14) - 1
|
||||
FIELD_BITS = 32
|
||||
FIELD_MODULUS = (1 << FIELD_BITS) + 0b10001101
|
||||
BYTES_PER_SKETCH_CAPACITY = int(FIELD_BITS / 8)
|
||||
# These parameters are suggested by the Erlay paper based on analysis and
|
||||
# simulations.
|
||||
RECON_Q = 0.25
|
||||
|
||||
MAX_SKETCH_CAPACITY = 2 << 12
|
||||
|
||||
|
||||
def mul2(x):
|
||||
"""Compute 2*x in GF(2^FIELD_BITS)"""
|
||||
return (x << 1) ^ (FIELD_MODULUS if x.bit_length() >= FIELD_BITS else 0)
|
||||
|
||||
|
||||
def mul(x, y):
|
||||
"""Compute x*y in GF(2^FIELD_BITS)"""
|
||||
ret = 0
|
||||
for bit in [(x >> i) & 1 for i in range(x.bit_length())]:
|
||||
ret, y = ret ^ bit * y, mul2(y)
|
||||
return ret
|
||||
|
||||
|
||||
def create_sketch(shortids, capacity):
|
||||
"""Compute the bytes of a sketch for given shortids and given capacity."""
|
||||
odd_sums = [0 for _ in range(capacity)]
|
||||
for shortid in shortids:
|
||||
squared = mul(shortid, shortid)
|
||||
for i in range(capacity):
|
||||
odd_sums[i] ^= shortid
|
||||
shortid = mul(shortid, squared)
|
||||
sketch_bytes = []
|
||||
for odd_sum in odd_sums:
|
||||
for i in range(4):
|
||||
sketch_bytes.append((odd_sum >> (i * 8)) & 0xff)
|
||||
return sketch_bytes
|
||||
|
||||
|
||||
def get_short_id(wtxid, salt):
|
||||
(k0, k1) = salt
|
||||
s = siphash256(k0, k1, wtxid)
|
||||
return 1 + (s & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def estimate_capacity(theirs, ours):
|
||||
set_size_diff = abs(theirs - ours)
|
||||
min_size = min(ours, theirs)
|
||||
weighted_min_size = int(RECON_Q * min_size)
|
||||
estimated_diff = 1 + weighted_min_size + set_size_diff
|
||||
|
||||
# Poor man's minisketch_compute_capacity.
|
||||
return estimated_diff if estimated_diff <= 9 else estimated_diff - 1
|
||||
|
||||
def generate_transaction(node, from_txid):
|
||||
to_address = node.getnewaddress()
|
||||
inputs = [{"txid": from_txid, "vout": 0}]
|
||||
outputs = {to_address: 0.0001}
|
||||
rawtx = node.createrawtransaction(inputs, outputs)
|
||||
signresult = node.signrawtransactionwithwallet(rawtx)
|
||||
tx = CTransaction()
|
||||
tx.deserialize(BytesIO(bytes.fromhex(signresult['hex'])))
|
||||
tx.rehash()
|
||||
return tx
|
||||
|
||||
|
||||
class TxReconTestP2PConn(P2PDataStore):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.recon_version = 1
|
||||
self.mininode_salt = random.randrange(0xffffff)
|
||||
self.node_salt = 0
|
||||
self.combined_salt = None
|
||||
self.last_sendtxrcncl = []
|
||||
self.last_reqtxrcncl = []
|
||||
self.last_sketch = []
|
||||
self.last_reconcildiff = []
|
||||
self.last_reqsketchext = []
|
||||
self.last_inv = []
|
||||
self.last_tx = []
|
||||
|
||||
def on_version(self, message):
|
||||
if self.recon_version == 1:
|
||||
if not self.p2p_connected_to_node:
|
||||
self.send_version()
|
||||
assert message.nVersion >= 70016, "We expect the node to support WTXID relay"
|
||||
self.send_without_ping(msg_wtxidrelay())
|
||||
self.send_sendtxrcncl()
|
||||
self.send_without_ping(msg_verack())
|
||||
self.nServices = message.nServices
|
||||
else:
|
||||
super().on_version(message)
|
||||
|
||||
def on_sendtxrcncl(self, message):
|
||||
self.node_salt = message.salt
|
||||
self.combined_salt = self.compute_salt()
|
||||
|
||||
def on_reqtxrcncl(self, message):
|
||||
self.last_reqtxrcncl.append(message)
|
||||
|
||||
def on_sketch(self, message):
|
||||
self.last_sketch.append(message)
|
||||
|
||||
def on_reconcildiff(self, message):
|
||||
self.last_reconcildiff.append(message)
|
||||
|
||||
def on_reqsketchext(self, message):
|
||||
self.last_reqsketchext.append(message)
|
||||
|
||||
def on_inv(self, message):
|
||||
self.last_inv.append([inv.hash for inv in message.inv if inv.type != MSG_BLOCK]) # ignore block invs
|
||||
|
||||
def on_tx(self, message):
|
||||
self.last_tx.append(message.tx.calc_sha256(True))
|
||||
|
||||
def send_sendtxrcncl(self):
|
||||
msg = msg_sendtxrcncl()
|
||||
msg.salt = self.mininode_salt
|
||||
msg.version = self.recon_version
|
||||
self.send_without_ping(msg)
|
||||
|
||||
def send_reqtxrcncl(self, set_size, q):
|
||||
msg = msg_reqtxrcncl()
|
||||
msg.set_size = set_size
|
||||
msg.q = q
|
||||
self.send_without_ping(msg)
|
||||
|
||||
def send_sketch(self, skdata):
|
||||
msg = msg_sketch()
|
||||
msg.skdata = skdata
|
||||
self.send_without_ping(msg)
|
||||
|
||||
def send_reconcildiff(self, success, ask_shortids, sync_with_ping=False):
|
||||
msg = msg_reconcildiff()
|
||||
msg.success = success
|
||||
msg.ask_shortids = ask_shortids
|
||||
if sync_with_ping:
|
||||
self.send_and_ping(msg)
|
||||
else :
|
||||
self.send_without_ping(msg)
|
||||
|
||||
def send_reqsketchext(self):
|
||||
self.send_without_ping(msg_reqsketchext())
|
||||
|
||||
def send_inv(self, inv_wtxids):
|
||||
msg = msg_inv(inv=[CInv(MSG_WTX, h=wtxid) for wtxid in inv_wtxids])
|
||||
self.send_without_ping(msg)
|
||||
|
||||
def send_getdata(self, ask_wtxids):
|
||||
msg = msg_getdata(inv=[CInv(MSG_WTX, h=wtxid) for wtxid in ask_wtxids])
|
||||
self.send_without_ping(msg)
|
||||
|
||||
def compute_salt(self):
|
||||
RECON_STATIC_SALT = "Tx Relay Salting"
|
||||
salt1, salt2 = self.node_salt, self.mininode_salt
|
||||
salt = min(salt1, salt2).to_bytes(8, "little") + max(salt1, salt2).to_bytes(8, "little")
|
||||
h = TaggedHash(RECON_STATIC_SALT, salt)
|
||||
k0 = int.from_bytes(h[0:8], "little")
|
||||
k1 = int.from_bytes(h[8:16], "little")
|
||||
return k0, k1
|
||||
|
||||
|
||||
class ReconciliationTest(BitcoinTestFramework):
|
||||
def add_options(self, parser):
|
||||
self.add_wallet_options(parser)
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_wallet()
|
||||
|
||||
def set_test_params(self):
|
||||
self.setup_clean_chain = True
|
||||
self.num_nodes = 1
|
||||
self.extra_args = [['-txreconciliation']]
|
||||
|
||||
def generate_txs(self, wallet, n_mininode_unique, n_node_unique, n_shared):
|
||||
mininode_unique = [wallet.create_self_transfer()["tx"] for _ in range(n_mininode_unique)]
|
||||
node_unique = [wallet.create_self_transfer()["tx"] for _ in range(n_node_unique)]
|
||||
shared = [wallet.create_self_transfer()["tx"] for _ in range(n_shared)]
|
||||
|
||||
tx_submitter = self.nodes[0].add_p2p_connection(P2PDataStore())
|
||||
tx_submitter.send_txs_and_test(
|
||||
node_unique + shared, self.nodes[0], success=True)
|
||||
tx_submitter.peer_disconnect()
|
||||
|
||||
return mininode_unique, node_unique, shared
|
||||
|
||||
# Wait for the next INV message to be received by the given peer.
|
||||
# Clear and check it matches the expected transactions.
|
||||
def wait_for_inv(self, peer, expected_wtxids):
|
||||
def received_inv():
|
||||
return (len(peer.last_inv) > 0)
|
||||
self.wait_until(received_inv)
|
||||
|
||||
received_wtxids = set(peer.last_inv.pop())
|
||||
assert_equal(expected_wtxids, received_wtxids)
|
||||
|
||||
def request_transactions_from(self, peer, wtxids_to_request):
|
||||
# Make sure there were no unexpected transactions received before
|
||||
assert_equal(peer.last_tx, [])
|
||||
peer.send_getdata(wtxids_to_request)
|
||||
|
||||
# Wait for the next TX message to be received by the given peer.
|
||||
# Clear and check it matches the expected transactions.
|
||||
def wait_for_txs(self, peer, expected_wtxids):
|
||||
def received_txs():
|
||||
return (len(peer.last_tx) == len(expected_wtxids))
|
||||
self.wait_until(received_txs)
|
||||
|
||||
assert_equal(set(expected_wtxids), set(peer.last_tx))
|
||||
peer.last_tx.clear()
|
||||
|
||||
def run_test(self):
|
||||
pass
|
|
@ -175,6 +175,8 @@ BASE_SCRIPTS = [
|
|||
'wallet_labels.py --legacy-wallet',
|
||||
'wallet_labels.py --descriptors',
|
||||
'p2p_compactblocks.py',
|
||||
'p2p_txrecon_initiator.py',
|
||||
'p2p_txrecon_responder.py',
|
||||
'p2p_compactblocks_blocksonly.py',
|
||||
'wallet_hd.py --legacy-wallet',
|
||||
'wallet_hd.py --descriptors',
|
||||
|
|
Loading…
Add table
Reference in a new issue