-
Notifications
You must be signed in to change notification settings - Fork 38k
test/BIP324: functional tests for v2 P2P encryption #24748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
4487b80
[rpc/net] Allow v2 p2p support in addconnection
stratospher 595ad4b
[test/crypto] Add ECDH
stratospher 8d6c848
[test] Move MAGIC_BYTES to messages.py
stratospher b89fa59
[test] Construct class to handle v2 P2P protocol functions
stratospher a049d1b
[test] Introduce EncryptedP2PState object in P2PConnection
stratospher 05bddb2
[test] Perform initial v2 handshake
stratospher 5b91fb1
[test] Read v2 P2P messages
stratospher bb7bffe
[test] Use lock for sending P2P messages in test framework
stratospher a94e350
[test] Build v2 P2P messages
stratospher 382894c
[test] Reconnect using v1 P2P when v2 P2P terminates due to magic by…
stratospher 8c054aa
[test] Allow inbound and outbound connections supporting v2 P2P protocol
stratospher 4115cf9
[test] Ignore BIP324 decoy messages
stratospher ba73735
[test] Add functional tests to test v2 P2P behaviour
stratospher ffe6a56
[test] Check whether v2 TestNode performs downgrading
stratospher bc9283c
[test] Add functional test to test early key response behaviour in BI…
stratospher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright (c) 2022 The Bitcoin Core developers | ||
# Distributed under the MIT software license, see the accompanying | ||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
||
import random | ||
|
||
from test_framework.test_framework import BitcoinTestFramework | ||
from test_framework.crypto.ellswift import ellswift_create | ||
from test_framework.p2p import P2PInterface | ||
from test_framework.v2_p2p import EncryptedP2PState | ||
|
||
|
||
class TestEncryptedP2PState(EncryptedP2PState): | ||
stratospher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" Modify v2 P2P protocol functions for testing that "The responder waits until one byte is received which does | ||
not match the 16 bytes consisting of the network magic followed by "version\x00\x00\x00\x00\x00"." (see BIP 324) | ||
|
||
- if `send_net_magic` is True, send first 4 bytes of ellswift (match network magic) else send remaining 60 bytes | ||
- `can_data_be_received` is a variable used to assert if data is received on recvbuf. | ||
- v2 TestNode shouldn't respond back if we send V1_PREFIX and data shouldn't be received on recvbuf. | ||
This state is represented using `can_data_be_received` = False. | ||
- v2 TestNode responds back when mismatch from V1_PREFIX happens and data can be received on recvbuf. | ||
This state is represented using `can_data_be_received` = True. | ||
""" | ||
|
||
def __init__(self): | ||
super().__init__(initiating=True, net='regtest') | ||
self.send_net_magic = True | ||
self.can_data_be_received = False | ||
|
||
def initiate_v2_handshake(self, garbage_len=random.randrange(4096)): | ||
"""Initiator begins the v2 handshake by sending its ellswift bytes and garbage. | ||
Here, the 64 bytes ellswift is assumed to have it's 4 bytes match network magic bytes. It is sent in 2 phases: | ||
1. when `send_network_magic` = True, send first 4 bytes of ellswift (matches network magic bytes) | ||
2. when `send_network_magic` = False, send remaining 60 bytes of ellswift | ||
""" | ||
if self.send_net_magic: | ||
self.privkey_ours, self.ellswift_ours = ellswift_create() | ||
self.sent_garbage = random.randbytes(garbage_len) | ||
self.send_net_magic = False | ||
return b"\xfa\xbf\xb5\xda" | ||
else: | ||
self.can_data_be_received = True | ||
return self.ellswift_ours[4:] + self.sent_garbage | ||
|
||
|
||
class PeerEarlyKey(P2PInterface): | ||
"""Custom implementation of P2PInterface which uses modified v2 P2P protocol functions for testing purposes.""" | ||
def __init__(self): | ||
super().__init__() | ||
self.v2_state = None | ||
|
||
def connection_made(self, transport): | ||
"""64 bytes ellswift is sent in 2 parts during `initial_v2_handshake()`""" | ||
self.v2_state = TestEncryptedP2PState() | ||
super().connection_made(transport) | ||
|
||
def data_received(self, t): | ||
# check that data can be received on recvbuf only when mismatch from V1_PREFIX happens (send_net_magic = False) | ||
assert self.v2_state.can_data_be_received and not self.v2_state.send_net_magic | ||
|
||
|
||
class P2PEarlyKey(BitcoinTestFramework): | ||
def set_test_params(self): | ||
self.num_nodes = 1 | ||
self.extra_args = [["-v2transport=1", "-peertimeout=3"]] | ||
|
||
def run_test(self): | ||
self.log.info('Sending ellswift bytes in parts to ensure that response from responder is received only when') | ||
self.log.info('ellswift bytes have a mismatch from the 16 bytes(network magic followed by "version\\x00\\x00\\x00\\x00\\x00")') | ||
node0 = self.nodes[0] | ||
self.log.info('Sending first 4 bytes of ellswift which match network magic') | ||
self.log.info('If a response is received, assertion failure would happen in our custom data_received() function') | ||
# send happens in `initiate_v2_handshake()` in `connection_made()` | ||
peer1 = node0.add_p2p_connection(PeerEarlyKey(), wait_for_verack=False, send_version=False, supports_v2_p2p=True) | ||
self.log.info('Sending remaining ellswift and garbage which are different from V1_PREFIX. Since a response is') | ||
self.log.info('expected now, our custom data_received() function wouldn\'t result in assertion failure') | ||
ellswift_and_garbage_data = peer1.v2_state.initiate_v2_handshake() | ||
peer1.send_raw_message(ellswift_and_garbage_data) | ||
peer1.wait_for_disconnect(timeout=5) | ||
self.log.info('successful disconnection when MITM happens in the key exchange phase') | ||
stratospher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
if __name__ == '__main__': | ||
P2PEarlyKey().main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright (c) 2022 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 encrypted v2 p2p proposed in BIP 324 | ||
""" | ||
from test_framework.blocktools import ( | ||
create_block, | ||
create_coinbase, | ||
) | ||
from test_framework.p2p import ( | ||
P2PDataStore, | ||
P2PInterface, | ||
) | ||
from test_framework.test_framework import BitcoinTestFramework | ||
from test_framework.util import ( | ||
assert_equal, | ||
assert_greater_than, | ||
check_node_connections, | ||
) | ||
from test_framework.crypto.chacha20 import REKEY_INTERVAL | ||
|
||
|
||
class P2PEncrypted(BitcoinTestFramework): | ||
def set_test_params(self): | ||
self.num_nodes = 2 | ||
self.extra_args = [["-v2transport=1"], ["-v2transport=1"]] | ||
|
||
def setup_network(self): | ||
self.setup_nodes() | ||
|
||
def generate_blocks(self, node, number): | ||
test_blocks = [] | ||
last_block = node.getbestblockhash() | ||
tip = int(last_block, 16) | ||
tipheight = node.getblockcount() | ||
last_block_time = node.getblock(last_block)['time'] | ||
for _ in range(number): | ||
# Create some blocks | ||
block = create_block(tip, create_coinbase(tipheight + 1), last_block_time + 1) | ||
block.solve() | ||
test_blocks.append(block) | ||
tip = block.sha256 | ||
tipheight += 1 | ||
last_block_time += 1 | ||
return test_blocks | ||
|
||
def create_test_block(self, txs): | ||
block = create_block(self.tip, create_coinbase(self.tipheight + 1), self.last_block_time + 600, txlist=txs) | ||
block.solve() | ||
return block | ||
|
||
def run_test(self): | ||
node0, node1 = self.nodes[0], self.nodes[1] | ||
self.log.info("Check inbound connection to v2 TestNode from v2 P2PConnection is v2") | ||
peer1 = node0.add_p2p_connection(P2PInterface(), wait_for_verack=True, supports_v2_p2p=True) | ||
assert peer1.supports_v2_p2p | ||
assert_equal(node0.getpeerinfo()[-1]["transport_protocol_type"], "v2") | ||
|
||
self.log.info("Check inbound connection to v2 TestNode from v1 P2PConnection is v1") | ||
peer2 = node0.add_p2p_connection(P2PInterface(), wait_for_verack=True, supports_v2_p2p=False) | ||
assert not peer2.supports_v2_p2p | ||
assert_equal(node0.getpeerinfo()[-1]["transport_protocol_type"], "v1") | ||
|
||
self.log.info("Check outbound connection from v2 TestNode to v1 P2PConnection advertised as v1 is v1") | ||
peer3 = node0.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, supports_v2_p2p=False, advertise_v2_p2p=False) | ||
assert not peer3.supports_v2_p2p | ||
assert_equal(node0.getpeerinfo()[-1]["transport_protocol_type"], "v1") | ||
|
||
# v2 TestNode performs downgrading here | ||
self.log.info("Check outbound connection from v2 TestNode to v1 P2PConnection advertised as v2 is v1") | ||
peer4 = node0.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, supports_v2_p2p=False, advertise_v2_p2p=True) | ||
assert not peer4.supports_v2_p2p | ||
assert_equal(node0.getpeerinfo()[-1]["transport_protocol_type"], "v1") | ||
|
||
self.log.info("Check outbound connection from v2 TestNode to v2 P2PConnection advertised as v2 is v2") | ||
peer5 = node0.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, supports_v2_p2p=True, advertise_v2_p2p=True) | ||
assert peer5.supports_v2_p2p | ||
assert_equal(node0.getpeerinfo()[-1]["transport_protocol_type"], "v2") | ||
|
||
self.log.info("Check if version is sent and verack is received in inbound/outbound connections") | ||
assert_equal(len(node0.getpeerinfo()), 5) # check if above 5 connections are present in node0's getpeerinfo() | ||
for peer in node0.getpeerinfo(): | ||
assert_greater_than(peer['bytessent_per_msg']['version'], 0) | ||
assert_greater_than(peer['bytesrecv_per_msg']['verack'], 0) | ||
|
||
self.log.info("Testing whether blocks propagate - check if tips sync when number of blocks >= REKEY_INTERVAL") | ||
# tests whether rekeying (which happens every REKEY_INTERVAL packets) works correctly | ||
test_blocks = self.generate_blocks(node0, REKEY_INTERVAL+1) | ||
|
||
for i in range(2): | ||
peer6 = node0.add_p2p_connection(P2PDataStore(), supports_v2_p2p=True) | ||
assert peer6.supports_v2_p2p | ||
assert_equal(node0.getpeerinfo()[-1]["transport_protocol_type"], "v2") | ||
|
||
# Consider: node0 <-- peer6. node0 and node1 aren't connected here. | ||
# Construct the following topology: node1 <--> node0 <-- peer6 | ||
# and test that blocks produced by peer6 will be received by node1 if sent normally | ||
# and won't be received by node1 if sent as decoy messages | ||
|
||
# First, check whether blocks produced be peer6 are received by node0 if sent normally | ||
# and not received by node0 if sent as decoy messages. | ||
if i: | ||
# check that node0 receives blocks produced by peer6 | ||
self.log.info("Check if blocks produced by node0's p2p connection is received by node0") | ||
peer6.send_blocks_and_test(test_blocks, node0, success=True) # node0's tip advances | ||
else: | ||
# check that node0 doesn't receive blocks produced by peer6 since they are sent as decoy messages | ||
self.log.info("Check if blocks produced by node0's p2p connection sent as decoys aren't received by node0") | ||
peer6.send_blocks_and_test(test_blocks, node0, success=False, is_decoy=True) # node0's tip doesn't advance | ||
|
||
# Then, connect node0 and node1 using v2 and check whether the blocks are received by node1 | ||
self.connect_nodes(0, 1, peer_advertises_v2=True) | ||
self.log.info("Wait for node1 to receive all the blocks from node0") | ||
self.sync_all() | ||
self.log.info("Make sure node0 and node1 have same block tips") | ||
assert_equal(node0.getbestblockhash(), node1.getbestblockhash()) | ||
|
||
self.disconnect_nodes(0, 1) | ||
|
||
self.log.info("Check the connections opened as expected") | ||
check_node_connections(node=node0, num_in=4, num_out=3) | ||
|
||
self.log.info("Check inbound connection to v1 TestNode from v2 P2PConnection is v1") | ||
self.restart_node(0, ["-v2transport=0"]) | ||
peer1 = node0.add_p2p_connection(P2PInterface(), wait_for_verack=True, supports_v2_p2p=True) | ||
assert not peer1.supports_v2_p2p | ||
assert_equal(node0.getpeerinfo()[-1]["transport_protocol_type"], "v1") | ||
check_node_connections(node=node0, num_in=1, num_out=0) | ||
|
||
|
||
if __name__ == '__main__': | ||
P2PEncrypted().main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.