mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-20 15:19:07 +01:00
Merge bitcoin/bitcoin#28196: BIP324 connection support
db9888feecnet: detect wrong-network V1 talking to V2Transport (Pieter Wuille)91e1ef8684test: add unit tests for V2Transport (Pieter Wuille)297c888997net: make V2Transport preallocate receive buffer space (Pieter Wuille)3ffa5fb49enet: make V2Transport send uniformly random number garbage bytes (Pieter Wuille)0be752d9f8net: add short message encoding/decoding support to V2Transport (Pieter Wuille)8da8642062net: make V2Transport auto-detect incoming V1 and fall back to it (Pieter Wuille)13a7f01557net: add V2Transport class with subset of BIP324 functionality (Pieter Wuille)dc2d7eb810crypto: Spanify EllSwiftPubKey constructor (Pieter Wuille)5f4b2c6d79net: remove unused Transport::SetReceiveVersion (Pieter Wuille)c3fad1f29dnet: add have_next_message argument to Transport::GetBytesToSend() (Pieter Wuille) Pull request description: This is part of #27634. This implements the BIP324 v2 transport (which implements all of what the BIP calls transport layer *and* application layer), though in a non-exposed way. It is tested through an extensive fuzz test, which verifies that v2 transports can talk to v2 transports, and v1 transports can talk to v2 transports, and a unit test that exercises a number of unusual scenarios. The transport is functionally complete, including: * Autodetection of incoming V1 connections. * Garbage, both sending and receiving. * Short message type IDs, both sending and receiving. * Ignore packets (receiving only, but tested in a unit test). * Session IDs are visible in `getpeerinfo` output (for manual comparison). Things that are not included, left for future PRs, are: * Actually using the v2 transport for connections. * Support for the `NODE_P2P_V2` service flag. * Retrying downgrade to V1 when attempted outbound V2 connections immediately fail. * P2P functional and unit tests ACKs for top commit: naumenkogs: ACKdb9888feectheStack: re-ACKdb9888feecmzumsande: Code Review ACKdb9888feecTree-SHA512: 8906ac1e733a99e1f31c9111055611f706d80bbfc2edf6a07fa6e47b21bb65baacd1ff17993cbbf588063b2f5ad30b3af674a50c7bc8e8ebf4671483a21bbfeb
This commit is contained in:
@@ -38,14 +38,8 @@ void TestBIP324PacketVector(
|
||||
{
|
||||
// Convert input from hex to char/byte vectors/arrays.
|
||||
const auto in_priv_ours = ParseHex(in_priv_ours_hex);
|
||||
const auto in_ellswift_ours_vec = ParseHex<std::byte>(in_ellswift_ours_hex);
|
||||
assert(in_ellswift_ours_vec.size() == 64);
|
||||
std::array<std::byte, 64> in_ellswift_ours;
|
||||
std::copy(in_ellswift_ours_vec.begin(), in_ellswift_ours_vec.end(), in_ellswift_ours.begin());
|
||||
const auto in_ellswift_theirs_vec = ParseHex<std::byte>(in_ellswift_theirs_hex);
|
||||
assert(in_ellswift_theirs_vec.size() == 64);
|
||||
std::array<std::byte, 64> in_ellswift_theirs;
|
||||
std::copy(in_ellswift_theirs_vec.begin(), in_ellswift_theirs_vec.end(), in_ellswift_theirs.begin());
|
||||
const auto in_ellswift_ours = ParseHex<std::byte>(in_ellswift_ours_hex);
|
||||
const auto in_ellswift_theirs = ParseHex<std::byte>(in_ellswift_theirs_hex);
|
||||
const auto in_contents = ParseHex<std::byte>(in_contents_hex);
|
||||
const auto in_aad = ParseHex<std::byte>(in_aad_hex);
|
||||
const auto mid_send_garbage = ParseHex<std::byte>(mid_send_garbage_hex);
|
||||
|
||||
@@ -86,7 +86,7 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction)
|
||||
|
||||
{
|
||||
LOCK(dummyNode1.cs_vSend);
|
||||
const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend();
|
||||
const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend(false);
|
||||
BOOST_CHECK(!to_send.empty());
|
||||
}
|
||||
connman.FlushSendBuffer(dummyNode1);
|
||||
@@ -97,7 +97,7 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction)
|
||||
BOOST_CHECK(peerman.SendMessages(&dummyNode1)); // should result in getheaders
|
||||
{
|
||||
LOCK(dummyNode1.cs_vSend);
|
||||
const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend();
|
||||
const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend(false);
|
||||
BOOST_CHECK(!to_send.empty());
|
||||
}
|
||||
// Wait 3 more minutes
|
||||
|
||||
@@ -25,6 +25,7 @@ std::vector<std::string> g_all_messages;
|
||||
|
||||
void initialize_p2p_transport_serialization()
|
||||
{
|
||||
ECC_Start();
|
||||
SelectParams(ChainType::REGTEST);
|
||||
g_all_messages = getAllNetMessageTypes();
|
||||
std::sort(g_all_messages.begin(), g_all_messages.end());
|
||||
@@ -92,7 +93,7 @@ FUZZ_TARGET(p2p_transport_serialization, .init = initialize_p2p_transport_serial
|
||||
assert(queued);
|
||||
std::optional<bool> known_more;
|
||||
while (true) {
|
||||
const auto& [to_send, more, _msg_type] = send_transport.GetBytesToSend();
|
||||
const auto& [to_send, more, _msg_type] = send_transport.GetBytesToSend(false);
|
||||
if (known_more) assert(!to_send.empty() == *known_more);
|
||||
if (to_send.empty()) break;
|
||||
send_transport.MarkBytesSent(to_send.size());
|
||||
@@ -124,11 +125,13 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
|
||||
// Vectors with bytes last returned by GetBytesToSend() on transport[i].
|
||||
std::array<std::vector<uint8_t>, 2> to_send;
|
||||
|
||||
// Last returned 'more' values (if still relevant) by transport[i]->GetBytesToSend().
|
||||
std::array<std::optional<bool>, 2> last_more;
|
||||
// Last returned 'more' values (if still relevant) by transport[i]->GetBytesToSend(), for
|
||||
// both have_next_message false and true.
|
||||
std::array<std::optional<bool>, 2> last_more, last_more_next;
|
||||
|
||||
// Whether more bytes to be sent are expected on transport[i].
|
||||
std::array<std::optional<bool>, 2> expect_more;
|
||||
// Whether more bytes to be sent are expected on transport[i], before and after
|
||||
// SetMessageToSend().
|
||||
std::array<std::optional<bool>, 2> expect_more, expect_more_next;
|
||||
|
||||
// Function to consume a message type.
|
||||
auto msg_type_fn = [&]() {
|
||||
@@ -177,18 +180,27 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
|
||||
|
||||
// Wrapper around transport[i]->GetBytesToSend() that performs sanity checks.
|
||||
auto bytes_to_send_fn = [&](int side) -> Transport::BytesToSend {
|
||||
const auto& [bytes, more, msg_type] = transports[side]->GetBytesToSend();
|
||||
// Invoke GetBytesToSend twice (for have_next_message = {false, true}). This function does
|
||||
// not modify state (it's const), and only the "more" return value should differ between
|
||||
// the calls.
|
||||
const auto& [bytes, more_nonext, msg_type] = transports[side]->GetBytesToSend(false);
|
||||
const auto& [bytes_next, more_next, msg_type_next] = transports[side]->GetBytesToSend(true);
|
||||
// Compare with expected more.
|
||||
if (expect_more[side].has_value()) assert(!bytes.empty() == *expect_more[side]);
|
||||
// Verify consistency between the two results.
|
||||
assert(bytes == bytes_next);
|
||||
assert(msg_type == msg_type_next);
|
||||
if (more_nonext) assert(more_next);
|
||||
// Compare with previously reported output.
|
||||
assert(to_send[side].size() <= bytes.size());
|
||||
assert(to_send[side] == Span{bytes}.first(to_send[side].size()));
|
||||
to_send[side].resize(bytes.size());
|
||||
std::copy(bytes.begin(), bytes.end(), to_send[side].begin());
|
||||
// Remember 'more' result.
|
||||
last_more[side] = {more};
|
||||
// Remember 'more' results.
|
||||
last_more[side] = {more_nonext};
|
||||
last_more_next[side] = {more_next};
|
||||
// Return.
|
||||
return {bytes, more, msg_type};
|
||||
return {bytes, more_nonext, msg_type};
|
||||
};
|
||||
|
||||
// Function to make side send a new message.
|
||||
@@ -199,7 +211,8 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
|
||||
CSerializedNetMsg msg = next_msg[side].Copy();
|
||||
bool queued = transports[side]->SetMessageToSend(msg);
|
||||
// Update expected more data.
|
||||
expect_more[side] = std::nullopt;
|
||||
expect_more[side] = expect_more_next[side];
|
||||
expect_more_next[side] = std::nullopt;
|
||||
// Verify consistency of GetBytesToSend after SetMessageToSend
|
||||
bytes_to_send_fn(/*side=*/side);
|
||||
if (queued) {
|
||||
@@ -223,6 +236,7 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
|
||||
// If all to-be-sent bytes were sent, move last_more data to expect_more data.
|
||||
if (send_now == bytes.size()) {
|
||||
expect_more[side] = last_more[side];
|
||||
expect_more_next[side] = last_more_next[side];
|
||||
}
|
||||
// Remove the bytes from the last reported to-be-sent vector.
|
||||
assert(to_send[side].size() >= send_now);
|
||||
@@ -251,6 +265,7 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
|
||||
// Clear cached expected 'more' information: if certainly no more data was to be sent
|
||||
// before, receiving bytes makes this uncertain.
|
||||
if (expect_more[!side] == false) expect_more[!side] = std::nullopt;
|
||||
if (expect_more_next[!side] == false) expect_more_next[!side] = std::nullopt;
|
||||
// Verify consistency of GetBytesToSend after ReceivedBytes
|
||||
bytes_to_send_fn(/*side=*/!side);
|
||||
bool progress = to_recv.size() < old_len;
|
||||
@@ -320,6 +335,40 @@ std::unique_ptr<Transport> MakeV1Transport(NodeId nodeid) noexcept
|
||||
return std::make_unique<V1Transport>(nodeid, SER_NETWORK, INIT_PROTO_VERSION);
|
||||
}
|
||||
|
||||
template<typename RNG>
|
||||
std::unique_ptr<Transport> MakeV2Transport(NodeId nodeid, bool initiator, RNG& rng, FuzzedDataProvider& provider)
|
||||
{
|
||||
// Retrieve key
|
||||
auto key = ConsumePrivateKey(provider);
|
||||
if (!key.IsValid()) return {};
|
||||
// Construct garbage
|
||||
size_t garb_len = provider.ConsumeIntegralInRange<size_t>(0, V2Transport::MAX_GARBAGE_LEN);
|
||||
std::vector<uint8_t> garb;
|
||||
if (garb_len <= 64) {
|
||||
// When the garbage length is up to 64 bytes, read it directly from the fuzzer input.
|
||||
garb = provider.ConsumeBytes<uint8_t>(garb_len);
|
||||
garb.resize(garb_len);
|
||||
} else {
|
||||
// If it's longer, generate it from the RNG. This avoids having large amounts of
|
||||
// (hopefully) irrelevant data needing to be stored in the fuzzer data.
|
||||
for (auto& v : garb) v = uint8_t(rng());
|
||||
}
|
||||
// Retrieve entropy
|
||||
auto ent = provider.ConsumeBytes<std::byte>(32);
|
||||
ent.resize(32);
|
||||
// Use as entropy SHA256(ent || garbage). This prevents a situation where the fuzzer manages to
|
||||
// include the garbage terminator (which is a function of both ellswift keys) in the garbage.
|
||||
// This is extremely unlikely (~2^-116) with random keys/garbage, but the fuzzer can choose
|
||||
// both non-randomly and dependently. Since the entropy is hashed anyway inside the ellswift
|
||||
// computation, no coverage should be lost by using a hash as entropy, and it removes the
|
||||
// possibility of garbage that happens to contain what is effectively a hash of the keys.
|
||||
CSHA256().Write(UCharCast(ent.data()), ent.size())
|
||||
.Write(garb.data(), garb.size())
|
||||
.Finalize(UCharCast(ent.data()));
|
||||
|
||||
return std::make_unique<V2Transport>(nodeid, initiator, SER_NETWORK, INIT_PROTO_VERSION, key, ent, garb);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serialization)
|
||||
@@ -332,3 +381,25 @@ FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serial
|
||||
if (!t1 || !t2) return;
|
||||
SimulationTest(*t1, *t2, rng, provider);
|
||||
}
|
||||
|
||||
FUZZ_TARGET(p2p_transport_bidirectional_v2, .init = initialize_p2p_transport_serialization)
|
||||
{
|
||||
// Test with two V2 transports talking to each other.
|
||||
FuzzedDataProvider provider{buffer.data(), buffer.size()};
|
||||
XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>());
|
||||
auto t1 = MakeV2Transport(NodeId{0}, true, rng, provider);
|
||||
auto t2 = MakeV2Transport(NodeId{1}, false, rng, provider);
|
||||
if (!t1 || !t2) return;
|
||||
SimulationTest(*t1, *t2, rng, provider);
|
||||
}
|
||||
|
||||
FUZZ_TARGET(p2p_transport_bidirectional_v1v2, .init = initialize_p2p_transport_serialization)
|
||||
{
|
||||
// Test with a V1 initiator talking to a V2 responder.
|
||||
FuzzedDataProvider provider{buffer.data(), buffer.size()};
|
||||
XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>());
|
||||
auto t1 = MakeV1Transport(NodeId{0});
|
||||
auto t2 = MakeV2Transport(NodeId{1}, false, rng, provider);
|
||||
if (!t1 || !t2) return;
|
||||
SimulationTest(*t1, *t2, rng, provider);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <serialize.h>
|
||||
#include <span.h>
|
||||
#include <streams.h>
|
||||
#include <test/util/random.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <test/util/validation.h>
|
||||
#include <timedata.h>
|
||||
@@ -1005,4 +1006,530 @@ BOOST_AUTO_TEST_CASE(advertise_local_address)
|
||||
RemoveLocal(addr_cjdns);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** A class for scenario-based tests of V2Transport
|
||||
*
|
||||
* Each V2TransportTester encapsulates a V2Transport (the one being tested), and can be told to
|
||||
* interact with it. To do so, it also encapsulates a BIP324Cipher to act as the other side. A
|
||||
* second V2Transport is not used, as doing so would not permit scenarios that involve sending
|
||||
* invalid data, or ones scenarios using BIP324 features that are not implemented on the sending
|
||||
* side (like decoy packets).
|
||||
*/
|
||||
class V2TransportTester
|
||||
{
|
||||
V2Transport m_transport; //!< V2Transport being tested
|
||||
BIP324Cipher m_cipher; //!< Cipher to help with the other side
|
||||
bool m_test_initiator; //!< Whether m_transport is the initiator (true) or responder (false)
|
||||
|
||||
std::vector<uint8_t> m_sent_garbage; //!< The garbage we've sent to m_transport.
|
||||
std::vector<uint8_t> m_to_send; //!< Bytes we have queued up to send to m_transport.
|
||||
std::vector<uint8_t> m_received; //!< Bytes we have received from m_transport.
|
||||
std::deque<CSerializedNetMsg> m_msg_to_send; //!< Messages to be sent *by* m_transport to us.
|
||||
|
||||
public:
|
||||
/** Construct a tester object. test_initiator: whether the tested transport is initiator. */
|
||||
V2TransportTester(bool test_initiator) :
|
||||
m_transport(0, test_initiator, SER_NETWORK, INIT_PROTO_VERSION),
|
||||
m_test_initiator(test_initiator) {}
|
||||
|
||||
/** Data type returned by Interact:
|
||||
*
|
||||
* - std::nullopt: transport error occurred
|
||||
* - otherwise: a vector of
|
||||
* - std::nullopt: invalid message received
|
||||
* - otherwise: a CNetMessage retrieved
|
||||
*/
|
||||
using InteractResult = std::optional<std::vector<std::optional<CNetMessage>>>;
|
||||
|
||||
/** Send/receive scheduled/available bytes and messages.
|
||||
*
|
||||
* This is the only function that interacts with the transport being tested; everything else is
|
||||
* scheduling things done by Interact(), or processing things learned by it.
|
||||
*/
|
||||
InteractResult Interact()
|
||||
{
|
||||
std::vector<std::optional<CNetMessage>> ret;
|
||||
while (true) {
|
||||
bool progress{false};
|
||||
// Send bytes from m_to_send to the transport.
|
||||
if (!m_to_send.empty()) {
|
||||
Span<const uint8_t> to_send = Span{m_to_send}.first(1 + InsecureRandRange(m_to_send.size()));
|
||||
size_t old_len = to_send.size();
|
||||
if (!m_transport.ReceivedBytes(to_send)) {
|
||||
return std::nullopt; // transport error occurred
|
||||
}
|
||||
if (old_len != to_send.size()) {
|
||||
progress = true;
|
||||
m_to_send.erase(m_to_send.begin(), m_to_send.begin() + (old_len - to_send.size()));
|
||||
}
|
||||
}
|
||||
// Retrieve messages received by the transport.
|
||||
if (m_transport.ReceivedMessageComplete() && (!progress || InsecureRandBool())) {
|
||||
bool reject{false};
|
||||
auto msg = m_transport.GetReceivedMessage({}, reject);
|
||||
if (reject) {
|
||||
ret.push_back(std::nullopt);
|
||||
} else {
|
||||
ret.push_back(std::move(msg));
|
||||
}
|
||||
progress = true;
|
||||
}
|
||||
// Enqueue a message to be sent by the transport to us.
|
||||
if (!m_msg_to_send.empty() && (!progress || InsecureRandBool())) {
|
||||
if (m_transport.SetMessageToSend(m_msg_to_send.front())) {
|
||||
m_msg_to_send.pop_front();
|
||||
progress = true;
|
||||
}
|
||||
}
|
||||
// Receive bytes from the transport.
|
||||
const auto& [recv_bytes, _more, _msg_type] = m_transport.GetBytesToSend(!m_msg_to_send.empty());
|
||||
if (!recv_bytes.empty() && (!progress || InsecureRandBool())) {
|
||||
size_t to_receive = 1 + InsecureRandRange(recv_bytes.size());
|
||||
m_received.insert(m_received.end(), recv_bytes.begin(), recv_bytes.begin() + to_receive);
|
||||
progress = true;
|
||||
m_transport.MarkBytesSent(to_receive);
|
||||
}
|
||||
if (!progress) break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** Expose the cipher. */
|
||||
BIP324Cipher& GetCipher() { return m_cipher; }
|
||||
|
||||
/** Schedule bytes to be sent to the transport. */
|
||||
void Send(Span<const uint8_t> data)
|
||||
{
|
||||
m_to_send.insert(m_to_send.end(), data.begin(), data.end());
|
||||
}
|
||||
|
||||
/** Send V1 version message header to the transport. */
|
||||
void SendV1Version(const CMessageHeader::MessageStartChars& magic)
|
||||
{
|
||||
CMessageHeader hdr(magic, "version", 126 + InsecureRandRange(11));
|
||||
CDataStream ser(SER_NETWORK, CLIENT_VERSION);
|
||||
ser << hdr;
|
||||
m_to_send.insert(m_to_send.end(), UCharCast(ser.data()), UCharCast(ser.data() + ser.size()));
|
||||
}
|
||||
|
||||
/** Schedule bytes to be sent to the transport. */
|
||||
void Send(Span<const std::byte> data) { Send(MakeUCharSpan(data)); }
|
||||
|
||||
/** Schedule our ellswift key to be sent to the transport. */
|
||||
void SendKey() { Send(m_cipher.GetOurPubKey()); }
|
||||
|
||||
/** Schedule specified garbage to be sent to the transport. */
|
||||
void SendGarbage(Span<const uint8_t> garbage)
|
||||
{
|
||||
// Remember the specified garbage (so we can use it for constructing the garbage
|
||||
// authentication packet).
|
||||
m_sent_garbage.assign(garbage.begin(), garbage.end());
|
||||
// Schedule it for sending.
|
||||
Send(m_sent_garbage);
|
||||
}
|
||||
|
||||
/** Schedule garbage (of specified length) to be sent to the transport. */
|
||||
void SendGarbage(size_t garbage_len)
|
||||
{
|
||||
// Generate random garbage and send it.
|
||||
SendGarbage(g_insecure_rand_ctx.randbytes<uint8_t>(garbage_len));
|
||||
}
|
||||
|
||||
/** Schedule garbage (with valid random length) to be sent to the transport. */
|
||||
void SendGarbage()
|
||||
{
|
||||
SendGarbage(InsecureRandRange(V2Transport::MAX_GARBAGE_LEN + 1));
|
||||
}
|
||||
|
||||
/** Schedule a message to be sent to us by the transport. */
|
||||
void AddMessage(std::string m_type, std::vector<uint8_t> payload)
|
||||
{
|
||||
CSerializedNetMsg msg;
|
||||
msg.m_type = std::move(m_type);
|
||||
msg.data = std::move(payload);
|
||||
m_msg_to_send.push_back(std::move(msg));
|
||||
}
|
||||
|
||||
/** Expect ellswift key to have been received from transport and process it.
|
||||
*
|
||||
* Many other V2TransportTester functions cannot be called until after ReceiveKey() has been
|
||||
* called, as no encryption keys are set up before that point.
|
||||
*/
|
||||
void ReceiveKey()
|
||||
{
|
||||
// When processing a key, enough bytes need to have been received already.
|
||||
BOOST_REQUIRE(m_received.size() >= EllSwiftPubKey::size());
|
||||
// Initialize the cipher using it (acting as the opposite side of the tested transport).
|
||||
m_cipher.Initialize(MakeByteSpan(m_received).first(EllSwiftPubKey::size()), !m_test_initiator);
|
||||
// Strip the processed bytes off the front of the receive buffer.
|
||||
m_received.erase(m_received.begin(), m_received.begin() + EllSwiftPubKey::size());
|
||||
}
|
||||
|
||||
/** Schedule an encrypted packet with specified content/aad/ignore to be sent to transport
|
||||
* (only after ReceiveKey). */
|
||||
void SendPacket(Span<const uint8_t> content, Span<const uint8_t> aad = {}, bool ignore = false)
|
||||
{
|
||||
// Use cipher to construct ciphertext.
|
||||
std::vector<std::byte> ciphertext;
|
||||
ciphertext.resize(content.size() + BIP324Cipher::EXPANSION);
|
||||
m_cipher.Encrypt(
|
||||
/*contents=*/MakeByteSpan(content),
|
||||
/*aad=*/MakeByteSpan(aad),
|
||||
/*ignore=*/ignore,
|
||||
/*output=*/ciphertext);
|
||||
// Schedule it for sending.
|
||||
Send(ciphertext);
|
||||
}
|
||||
|
||||
/** Schedule garbage terminator and authentication packet to be sent to the transport (only
|
||||
* after ReceiveKey). */
|
||||
void SendGarbageTermAuth(size_t garb_auth_data_len = 0, bool garb_auth_ignore = false)
|
||||
{
|
||||
// Generate random data to include in the garbage authentication packet (ignored by peer).
|
||||
auto garb_auth_data = g_insecure_rand_ctx.randbytes<uint8_t>(garb_auth_data_len);
|
||||
// Schedule the garbage terminator to be sent.
|
||||
Send(m_cipher.GetSendGarbageTerminator());
|
||||
// Schedule the garbage authentication packet to be sent.
|
||||
SendPacket(/*content=*/garb_auth_data, /*aad=*/m_sent_garbage, /*ignore=*/garb_auth_ignore);
|
||||
}
|
||||
|
||||
/** Schedule version packet to be sent to the transport (only after ReceiveKey). */
|
||||
void SendVersion(Span<const uint8_t> version_data = {}, bool vers_ignore = false)
|
||||
{
|
||||
SendPacket(/*content=*/version_data, /*aad=*/{}, /*ignore=*/vers_ignore);
|
||||
}
|
||||
|
||||
/** Expect a packet to have been received from transport, process it, and return its contents
|
||||
* (only after ReceiveKey). By default, decoys are skipped. */
|
||||
std::vector<uint8_t> ReceivePacket(Span<const std::byte> aad = {}, bool skip_decoy = true)
|
||||
{
|
||||
std::vector<uint8_t> contents;
|
||||
// Loop as long as there are ignored packets that are to be skipped.
|
||||
while (true) {
|
||||
// When processing a packet, at least enough bytes for its length descriptor must be received.
|
||||
BOOST_REQUIRE(m_received.size() >= BIP324Cipher::LENGTH_LEN);
|
||||
// Decrypt the content length.
|
||||
size_t size = m_cipher.DecryptLength(MakeByteSpan(Span{m_received}.first(BIP324Cipher::LENGTH_LEN)));
|
||||
// Check that the full packet is in the receive buffer.
|
||||
BOOST_REQUIRE(m_received.size() >= size + BIP324Cipher::EXPANSION);
|
||||
// Decrypt the packet contents.
|
||||
contents.resize(size);
|
||||
bool ignore{false};
|
||||
bool ret = m_cipher.Decrypt(
|
||||
/*input=*/MakeByteSpan(
|
||||
Span{m_received}.first(size + BIP324Cipher::EXPANSION).subspan(BIP324Cipher::LENGTH_LEN)),
|
||||
/*aad=*/aad,
|
||||
/*ignore=*/ignore,
|
||||
/*contents=*/MakeWritableByteSpan(contents));
|
||||
BOOST_CHECK(ret);
|
||||
// Strip the processed packet's bytes off the front of the receive buffer.
|
||||
m_received.erase(m_received.begin(), m_received.begin() + size + BIP324Cipher::EXPANSION);
|
||||
// Stop if the ignore bit is not set on this packet, or if we choose to not honor it.
|
||||
if (!ignore || !skip_decoy) break;
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
/** Expect garbage, garbage terminator, and garbage auth packet to have been received, and
|
||||
* process them (only after ReceiveKey). */
|
||||
void ReceiveGarbage()
|
||||
{
|
||||
// Figure out the garbage length.
|
||||
size_t garblen;
|
||||
for (garblen = 0; garblen <= V2Transport::MAX_GARBAGE_LEN; ++garblen) {
|
||||
BOOST_REQUIRE(m_received.size() >= garblen + BIP324Cipher::GARBAGE_TERMINATOR_LEN);
|
||||
auto term_span = MakeByteSpan(Span{m_received}.subspan(garblen, BIP324Cipher::GARBAGE_TERMINATOR_LEN));
|
||||
if (term_span == m_cipher.GetReceiveGarbageTerminator()) break;
|
||||
}
|
||||
// Copy the garbage to a buffer.
|
||||
std::vector<uint8_t> garbage(m_received.begin(), m_received.begin() + garblen);
|
||||
// Strip garbage + garbage terminator off the front of the receive buffer.
|
||||
m_received.erase(m_received.begin(), m_received.begin() + garblen + BIP324Cipher::GARBAGE_TERMINATOR_LEN);
|
||||
// Process the expected garbage authentication packet. Such a packet still functions as one
|
||||
// even when its ignore bit is set to true, so we do not skip decoy packets here.
|
||||
ReceivePacket(/*aad=*/MakeByteSpan(garbage), /*skip_decoy=*/false);
|
||||
}
|
||||
|
||||
/** Expect version packet to have been received, and process it (only after ReceiveKey). */
|
||||
void ReceiveVersion()
|
||||
{
|
||||
auto contents = ReceivePacket();
|
||||
// Version packets from real BIP324 peers are expected to be empty, despite the fact that
|
||||
// this class supports *sending* non-empty version packets (to test that BIP324 peers
|
||||
// correctly ignore version packet contents).
|
||||
BOOST_CHECK(contents.empty());
|
||||
}
|
||||
|
||||
/** Expect application packet to have been received, with specified short id and payload.
|
||||
* (only after ReceiveKey). */
|
||||
void ReceiveMessage(uint8_t short_id, Span<const uint8_t> payload)
|
||||
{
|
||||
auto ret = ReceivePacket();
|
||||
BOOST_CHECK(ret.size() == payload.size() + 1);
|
||||
BOOST_CHECK(ret[0] == short_id);
|
||||
BOOST_CHECK(Span{ret}.subspan(1) == payload);
|
||||
}
|
||||
|
||||
/** Expect application packet to have been received, with specified 12-char message type and
|
||||
* payload (only after ReceiveKey). */
|
||||
void ReceiveMessage(const std::string& m_type, Span<const uint8_t> payload)
|
||||
{
|
||||
auto ret = ReceivePacket();
|
||||
BOOST_REQUIRE(ret.size() == payload.size() + 1 + CMessageHeader::COMMAND_SIZE);
|
||||
BOOST_CHECK(ret[0] == 0);
|
||||
for (unsigned i = 0; i < 12; ++i) {
|
||||
if (i < m_type.size()) {
|
||||
BOOST_CHECK(ret[1 + i] == m_type[i]);
|
||||
} else {
|
||||
BOOST_CHECK(ret[1 + i] == 0);
|
||||
}
|
||||
}
|
||||
BOOST_CHECK(Span{ret}.subspan(1 + CMessageHeader::COMMAND_SIZE) == payload);
|
||||
}
|
||||
|
||||
/** Schedule an encrypted packet with specified message type and payload to be sent to
|
||||
* transport (only after ReceiveKey). */
|
||||
void SendMessage(std::string mtype, Span<const uint8_t> payload)
|
||||
{
|
||||
// Construct contents consisting of 0x00 + 12-byte message type + payload.
|
||||
std::vector<uint8_t> contents(1 + CMessageHeader::COMMAND_SIZE + payload.size());
|
||||
std::copy(mtype.begin(), mtype.end(), reinterpret_cast<char*>(contents.data() + 1));
|
||||
std::copy(payload.begin(), payload.end(), contents.begin() + 1 + CMessageHeader::COMMAND_SIZE);
|
||||
// Send a packet with that as contents.
|
||||
SendPacket(contents);
|
||||
}
|
||||
|
||||
/** Schedule an encrypted packet with specified short message id and payload to be sent to
|
||||
* transport (only after ReceiveKey). */
|
||||
void SendMessage(uint8_t short_id, Span<const uint8_t> payload)
|
||||
{
|
||||
// Construct contents consisting of short_id + payload.
|
||||
std::vector<uint8_t> contents(1 + payload.size());
|
||||
contents[0] = short_id;
|
||||
std::copy(payload.begin(), payload.end(), contents.begin() + 1);
|
||||
// Send a packet with that as contents.
|
||||
SendPacket(contents);
|
||||
}
|
||||
|
||||
/** Introduce a bit error in the data scheduled to be sent. */
|
||||
void Damage()
|
||||
{
|
||||
m_to_send[InsecureRandRange(m_to_send.size())] ^= (uint8_t{1} << InsecureRandRange(8));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
BOOST_AUTO_TEST_CASE(v2transport_test)
|
||||
{
|
||||
// A mostly normal scenario, testing a transport in initiator mode.
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
V2TransportTester tester(true);
|
||||
auto ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.SendKey();
|
||||
tester.SendGarbage();
|
||||
tester.ReceiveKey();
|
||||
tester.SendGarbageTermAuth();
|
||||
tester.SendVersion();
|
||||
ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.ReceiveGarbage();
|
||||
tester.ReceiveVersion();
|
||||
auto msg_data_1 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(100000));
|
||||
auto msg_data_2 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000));
|
||||
tester.SendMessage(uint8_t(4), msg_data_1); // cmpctblock short id
|
||||
tester.SendMessage(0, {}); // Invalidly encoded message
|
||||
tester.SendMessage("tx", msg_data_2); // 12-character encoded message type
|
||||
ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->size() == 3);
|
||||
BOOST_CHECK((*ret)[0] && (*ret)[0]->m_type == "cmpctblock" && Span{(*ret)[0]->m_recv} == MakeByteSpan(msg_data_1));
|
||||
BOOST_CHECK(!(*ret)[1]);
|
||||
BOOST_CHECK((*ret)[2] && (*ret)[2]->m_type == "tx" && Span{(*ret)[2]->m_recv} == MakeByteSpan(msg_data_2));
|
||||
|
||||
// Then send a message with a bit error, expecting failure.
|
||||
tester.SendMessage("bad", msg_data_1);
|
||||
tester.Damage();
|
||||
ret = tester.Interact();
|
||||
BOOST_CHECK(!ret);
|
||||
}
|
||||
|
||||
// Normal scenario, with a transport in responder node.
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
V2TransportTester tester(false);
|
||||
tester.SendKey();
|
||||
tester.SendGarbage();
|
||||
auto ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.ReceiveKey();
|
||||
tester.SendGarbageTermAuth();
|
||||
tester.SendVersion();
|
||||
ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.ReceiveGarbage();
|
||||
tester.ReceiveVersion();
|
||||
auto msg_data_1 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(100000));
|
||||
auto msg_data_2 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000));
|
||||
tester.SendMessage(uint8_t(14), msg_data_1); // inv short id
|
||||
tester.SendMessage(uint8_t(19), msg_data_2); // pong short id
|
||||
ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->size() == 2);
|
||||
BOOST_CHECK((*ret)[0] && (*ret)[0]->m_type == "inv" && Span{(*ret)[0]->m_recv} == MakeByteSpan(msg_data_1));
|
||||
BOOST_CHECK((*ret)[1] && (*ret)[1]->m_type == "pong" && Span{(*ret)[1]->m_recv} == MakeByteSpan(msg_data_2));
|
||||
|
||||
// Then send a too-large message.
|
||||
auto msg_data_3 = g_insecure_rand_ctx.randbytes<uint8_t>(4005000);
|
||||
tester.SendMessage(uint8_t(11), msg_data_3); // getdata short id
|
||||
ret = tester.Interact();
|
||||
BOOST_CHECK(!ret);
|
||||
}
|
||||
|
||||
// Various valid but unusual scenarios.
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
/** Whether an initiator or responder is being tested. */
|
||||
bool initiator = InsecureRandBool();
|
||||
/** Use either 0 bytes or the maximum possible (4095 bytes) garbage length. */
|
||||
size_t garb_len = InsecureRandBool() ? 0 : V2Transport::MAX_GARBAGE_LEN;
|
||||
/** Sometimes, use non-empty contents in the garbage authentication packet (which is to be ignored). */
|
||||
size_t garb_auth_data_len = InsecureRandBool() ? 0 : InsecureRandRange(100000);
|
||||
/** Whether to set the ignore bit on the garbage authentication packet (it still functions as garbage authentication). */
|
||||
bool garb_ignore = InsecureRandBool();
|
||||
/** How many decoy packets to send before the version packet. */
|
||||
unsigned num_ignore_version = InsecureRandRange(10);
|
||||
/** What data to send in the version packet (ignored by BIP324 peers, but reserved for future extensions). */
|
||||
auto ver_data = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandBool() ? 0 : InsecureRandRange(1000));
|
||||
/** Whether to immediately send key and garbage out (required for responders, optional otherwise). */
|
||||
bool send_immediately = !initiator || InsecureRandBool();
|
||||
/** How many decoy packets to send before the first and second real message. */
|
||||
unsigned num_decoys_1 = InsecureRandRange(1000), num_decoys_2 = InsecureRandRange(1000);
|
||||
V2TransportTester tester(initiator);
|
||||
if (send_immediately) {
|
||||
tester.SendKey();
|
||||
tester.SendGarbage(garb_len);
|
||||
}
|
||||
auto ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
if (!send_immediately) {
|
||||
tester.SendKey();
|
||||
tester.SendGarbage(garb_len);
|
||||
}
|
||||
tester.ReceiveKey();
|
||||
tester.SendGarbageTermAuth(garb_auth_data_len, garb_ignore);
|
||||
for (unsigned v = 0; v < num_ignore_version; ++v) {
|
||||
size_t ver_ign_data_len = InsecureRandBool() ? 0 : InsecureRandRange(1000);
|
||||
auto ver_ign_data = g_insecure_rand_ctx.randbytes<uint8_t>(ver_ign_data_len);
|
||||
tester.SendVersion(ver_ign_data, true);
|
||||
}
|
||||
tester.SendVersion(ver_data, false);
|
||||
ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.ReceiveGarbage();
|
||||
tester.ReceiveVersion();
|
||||
for (unsigned d = 0; d < num_decoys_1; ++d) {
|
||||
auto decoy_data = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000));
|
||||
tester.SendPacket(/*content=*/decoy_data, /*aad=*/{}, /*ignore=*/true);
|
||||
}
|
||||
auto msg_data_1 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(4000000));
|
||||
tester.SendMessage(uint8_t(28), msg_data_1);
|
||||
for (unsigned d = 0; d < num_decoys_2; ++d) {
|
||||
auto decoy_data = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000));
|
||||
tester.SendPacket(/*content=*/decoy_data, /*aad=*/{}, /*ignore=*/true);
|
||||
}
|
||||
auto msg_data_2 = g_insecure_rand_ctx.randbytes<uint8_t>(InsecureRandRange(1000));
|
||||
tester.SendMessage(uint8_t(13), msg_data_2); // headers short id
|
||||
// Send invalidly-encoded message
|
||||
tester.SendMessage(std::string("blocktxn\x00\x00\x00a", CMessageHeader::COMMAND_SIZE), {});
|
||||
tester.SendMessage("foobar", {}); // test receiving unknown message type
|
||||
tester.AddMessage("barfoo", {}); // test sending unknown message type
|
||||
ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->size() == 4);
|
||||
BOOST_CHECK((*ret)[0] && (*ret)[0]->m_type == "addrv2" && Span{(*ret)[0]->m_recv} == MakeByteSpan(msg_data_1));
|
||||
BOOST_CHECK((*ret)[1] && (*ret)[1]->m_type == "headers" && Span{(*ret)[1]->m_recv} == MakeByteSpan(msg_data_2));
|
||||
BOOST_CHECK(!(*ret)[2]);
|
||||
BOOST_CHECK((*ret)[3] && (*ret)[3]->m_type == "foobar" && (*ret)[3]->m_recv.empty());
|
||||
tester.ReceiveMessage("barfoo", {});
|
||||
}
|
||||
|
||||
// Too long garbage (initiator).
|
||||
{
|
||||
V2TransportTester tester(true);
|
||||
auto ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.SendKey();
|
||||
tester.SendGarbage(V2Transport::MAX_GARBAGE_LEN + 1);
|
||||
tester.ReceiveKey();
|
||||
tester.SendGarbageTermAuth();
|
||||
ret = tester.Interact();
|
||||
BOOST_CHECK(!ret);
|
||||
}
|
||||
|
||||
// Too long garbage (responder).
|
||||
{
|
||||
V2TransportTester tester(false);
|
||||
tester.SendKey();
|
||||
tester.SendGarbage(V2Transport::MAX_GARBAGE_LEN + 1);
|
||||
auto ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.ReceiveKey();
|
||||
tester.SendGarbageTermAuth();
|
||||
ret = tester.Interact();
|
||||
BOOST_CHECK(!ret);
|
||||
}
|
||||
|
||||
// Send garbage that includes the first 15 garbage terminator bytes somewhere.
|
||||
{
|
||||
V2TransportTester tester(true);
|
||||
auto ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.SendKey();
|
||||
tester.ReceiveKey();
|
||||
/** The number of random garbage bytes before the included first 15 bytes of terminator. */
|
||||
size_t len_before = InsecureRandRange(V2Transport::MAX_GARBAGE_LEN - 16 + 1);
|
||||
/** The number of random garbage bytes after it. */
|
||||
size_t len_after = InsecureRandRange(V2Transport::MAX_GARBAGE_LEN - 16 - len_before + 1);
|
||||
// Construct len_before + 16 + len_after random bytes.
|
||||
auto garbage = g_insecure_rand_ctx.randbytes<uint8_t>(len_before + 16 + len_after);
|
||||
// Replace the designed 16 bytes in the middle with the to-be-sent garbage terminator.
|
||||
auto garb_term = MakeUCharSpan(tester.GetCipher().GetSendGarbageTerminator());
|
||||
std::copy(garb_term.begin(), garb_term.begin() + 16, garbage.begin() + len_before);
|
||||
// Introduce a bit error in the last byte of that copied garbage terminator, making only
|
||||
// the first 15 of them match.
|
||||
garbage[len_before + 15] ^= (uint8_t(1) << InsecureRandRange(8));
|
||||
tester.SendGarbage(garbage);
|
||||
tester.SendGarbageTermAuth();
|
||||
tester.SendVersion();
|
||||
ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->empty());
|
||||
tester.ReceiveGarbage();
|
||||
tester.ReceiveVersion();
|
||||
auto msg_data_1 = g_insecure_rand_ctx.randbytes<uint8_t>(4000000); // test that receiving 4M payload works
|
||||
auto msg_data_2 = g_insecure_rand_ctx.randbytes<uint8_t>(4000000); // test that sending 4M payload works
|
||||
tester.SendMessage(uint8_t(InsecureRandRange(223) + 33), {}); // unknown short id
|
||||
tester.SendMessage(uint8_t(2), msg_data_1); // "block" short id
|
||||
tester.AddMessage("blocktxn", msg_data_2); // schedule blocktxn to be sent to us
|
||||
ret = tester.Interact();
|
||||
BOOST_REQUIRE(ret && ret->size() == 2);
|
||||
BOOST_CHECK(!(*ret)[0]);
|
||||
BOOST_CHECK((*ret)[1] && (*ret)[1]->m_type == "block" && Span{(*ret)[1]->m_recv} == MakeByteSpan(msg_data_1));
|
||||
tester.ReceiveMessage(uint8_t(3), msg_data_2); // "blocktxn" short id
|
||||
}
|
||||
|
||||
// Send correct network's V1 header
|
||||
{
|
||||
V2TransportTester tester(false);
|
||||
tester.SendV1Version(Params().MessageStart());
|
||||
auto ret = tester.Interact();
|
||||
BOOST_CHECK(ret);
|
||||
}
|
||||
|
||||
// Send wrong network's V1 header
|
||||
{
|
||||
V2TransportTester tester(false);
|
||||
tester.SendV1Version(CChainParams::Main()->MessageStart());
|
||||
auto ret = tester.Interact();
|
||||
BOOST_CHECK(!ret);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -78,7 +78,7 @@ void ConnmanTestMsg::FlushSendBuffer(CNode& node) const
|
||||
node.vSendMsg.clear();
|
||||
node.m_send_memusage = 0;
|
||||
while (true) {
|
||||
const auto& [to_send, _more, _msg_type] = node.m_transport->GetBytesToSend();
|
||||
const auto& [to_send, _more, _msg_type] = node.m_transport->GetBytesToSend(false);
|
||||
if (to_send.empty()) break;
|
||||
node.m_transport->MarkBytesSent(to_send.size());
|
||||
}
|
||||
@@ -90,7 +90,7 @@ bool ConnmanTestMsg::ReceiveMsgFrom(CNode& node, CSerializedNetMsg&& ser_msg) co
|
||||
assert(queued);
|
||||
bool complete{false};
|
||||
while (true) {
|
||||
const auto& [to_send, _more, _msg_type] = node.m_transport->GetBytesToSend();
|
||||
const auto& [to_send, _more, _msg_type] = node.m_transport->GetBytesToSend(false);
|
||||
if (to_send.empty()) break;
|
||||
NodeReceiveMsgBytes(node, to_send, complete);
|
||||
node.m_transport->MarkBytesSent(to_send.size());
|
||||
|
||||
Reference in New Issue
Block a user