Skip to content

Commit 4e1a38c

Browse files
committed
Merge #28196: BIP324 connection support
db9888f net: detect wrong-network V1 talking to V2Transport (Pieter Wuille) 91e1ef8 test: add unit tests for V2Transport (Pieter Wuille) 297c888 net: make V2Transport preallocate receive buffer space (Pieter Wuille) 3ffa5fb net: make V2Transport send uniformly random number garbage bytes (Pieter Wuille) 0be752d net: add short message encoding/decoding support to V2Transport (Pieter Wuille) 8da8642 net: make V2Transport auto-detect incoming V1 and fall back to it (Pieter Wuille) 13a7f01 net: add V2Transport class with subset of BIP324 functionality (Pieter Wuille) dc2d7eb crypto: Spanify EllSwiftPubKey constructor (Pieter Wuille) 5f4b2c6 net: remove unused Transport::SetReceiveVersion (Pieter Wuille) c3fad1f net: 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: ACK db9888f theStack: re-ACK db9888f mzumsande: Code Review ACK db9888f Tree-SHA512: 8906ac1e733a99e1f31c9111055611f706d80bbfc2edf6a07fa6e47b21bb65baacd1ff17993cbbf588063b2f5ad30b3af674a50c7bc8e8ebf4671483a21bbfeb
2 parents 238d29a + db9888f commit 4e1a38c

9 files changed

+1558
-59
lines changed

src/net.cpp

+689-16
Large diffs are not rendered by default.

src/net.h

+248-19
Large diffs are not rendered by default.

src/pubkey.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,12 @@ bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChi
336336
return true;
337337
}
338338

339+
EllSwiftPubKey::EllSwiftPubKey(Span<const std::byte> ellswift) noexcept
340+
{
341+
assert(ellswift.size() == SIZE);
342+
std::copy(ellswift.begin(), ellswift.end(), m_pubkey.begin());
343+
}
344+
339345
CPubKey EllSwiftPubKey::Decode() const
340346
{
341347
secp256k1_pubkey pubkey;

src/pubkey.h

+1-2
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,7 @@ struct EllSwiftPubKey
303303
EllSwiftPubKey() noexcept = default;
304304

305305
/** Construct a new ellswift public key from a given serialization. */
306-
EllSwiftPubKey(const std::array<std::byte, SIZE>& ellswift) :
307-
m_pubkey(ellswift) {}
306+
EllSwiftPubKey(Span<const std::byte> ellswift) noexcept;
308307

309308
/** Decode to normal compressed CPubKey (for debugging purposes). */
310309
CPubKey Decode() const;

src/test/bip324_tests.cpp

+2-8
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,8 @@ void TestBIP324PacketVector(
3838
{
3939
// Convert input from hex to char/byte vectors/arrays.
4040
const auto in_priv_ours = ParseHex(in_priv_ours_hex);
41-
const auto in_ellswift_ours_vec = ParseHex<std::byte>(in_ellswift_ours_hex);
42-
assert(in_ellswift_ours_vec.size() == 64);
43-
std::array<std::byte, 64> in_ellswift_ours;
44-
std::copy(in_ellswift_ours_vec.begin(), in_ellswift_ours_vec.end(), in_ellswift_ours.begin());
45-
const auto in_ellswift_theirs_vec = ParseHex<std::byte>(in_ellswift_theirs_hex);
46-
assert(in_ellswift_theirs_vec.size() == 64);
47-
std::array<std::byte, 64> in_ellswift_theirs;
48-
std::copy(in_ellswift_theirs_vec.begin(), in_ellswift_theirs_vec.end(), in_ellswift_theirs.begin());
41+
const auto in_ellswift_ours = ParseHex<std::byte>(in_ellswift_ours_hex);
42+
const auto in_ellswift_theirs = ParseHex<std::byte>(in_ellswift_theirs_hex);
4943
const auto in_contents = ParseHex<std::byte>(in_contents_hex);
5044
const auto in_aad = ParseHex<std::byte>(in_aad_hex);
5145
const auto mid_send_garbage = ParseHex<std::byte>(mid_send_garbage_hex);

src/test/denialofservice_tests.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction)
8686

8787
{
8888
LOCK(dummyNode1.cs_vSend);
89-
const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend();
89+
const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend(false);
9090
BOOST_CHECK(!to_send.empty());
9191
}
9292
connman.FlushSendBuffer(dummyNode1);
@@ -97,7 +97,7 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction)
9797
BOOST_CHECK(peerman.SendMessages(&dummyNode1)); // should result in getheaders
9898
{
9999
LOCK(dummyNode1.cs_vSend);
100-
const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend();
100+
const auto& [to_send, _more, _msg_type] = dummyNode1.m_transport->GetBytesToSend(false);
101101
BOOST_CHECK(!to_send.empty());
102102
}
103103
// Wait 3 more minutes

src/test/fuzz/p2p_transport_serialization.cpp

+81-10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ std::vector<std::string> g_all_messages;
2525

2626
void initialize_p2p_transport_serialization()
2727
{
28+
ECC_Start();
2829
SelectParams(ChainType::REGTEST);
2930
g_all_messages = getAllNetMessageTypes();
3031
std::sort(g_all_messages.begin(), g_all_messages.end());
@@ -92,7 +93,7 @@ FUZZ_TARGET(p2p_transport_serialization, .init = initialize_p2p_transport_serial
9293
assert(queued);
9394
std::optional<bool> known_more;
9495
while (true) {
95-
const auto& [to_send, more, _msg_type] = send_transport.GetBytesToSend();
96+
const auto& [to_send, more, _msg_type] = send_transport.GetBytesToSend(false);
9697
if (known_more) assert(!to_send.empty() == *known_more);
9798
if (to_send.empty()) break;
9899
send_transport.MarkBytesSent(to_send.size());
@@ -124,11 +125,13 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
124125
// Vectors with bytes last returned by GetBytesToSend() on transport[i].
125126
std::array<std::vector<uint8_t>, 2> to_send;
126127

127-
// Last returned 'more' values (if still relevant) by transport[i]->GetBytesToSend().
128-
std::array<std::optional<bool>, 2> last_more;
128+
// Last returned 'more' values (if still relevant) by transport[i]->GetBytesToSend(), for
129+
// both have_next_message false and true.
130+
std::array<std::optional<bool>, 2> last_more, last_more_next;
129131

130-
// Whether more bytes to be sent are expected on transport[i].
131-
std::array<std::optional<bool>, 2> expect_more;
132+
// Whether more bytes to be sent are expected on transport[i], before and after
133+
// SetMessageToSend().
134+
std::array<std::optional<bool>, 2> expect_more, expect_more_next;
132135

133136
// Function to consume a message type.
134137
auto msg_type_fn = [&]() {
@@ -177,18 +180,27 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
177180

178181
// Wrapper around transport[i]->GetBytesToSend() that performs sanity checks.
179182
auto bytes_to_send_fn = [&](int side) -> Transport::BytesToSend {
180-
const auto& [bytes, more, msg_type] = transports[side]->GetBytesToSend();
183+
// Invoke GetBytesToSend twice (for have_next_message = {false, true}). This function does
184+
// not modify state (it's const), and only the "more" return value should differ between
185+
// the calls.
186+
const auto& [bytes, more_nonext, msg_type] = transports[side]->GetBytesToSend(false);
187+
const auto& [bytes_next, more_next, msg_type_next] = transports[side]->GetBytesToSend(true);
181188
// Compare with expected more.
182189
if (expect_more[side].has_value()) assert(!bytes.empty() == *expect_more[side]);
190+
// Verify consistency between the two results.
191+
assert(bytes == bytes_next);
192+
assert(msg_type == msg_type_next);
193+
if (more_nonext) assert(more_next);
183194
// Compare with previously reported output.
184195
assert(to_send[side].size() <= bytes.size());
185196
assert(to_send[side] == Span{bytes}.first(to_send[side].size()));
186197
to_send[side].resize(bytes.size());
187198
std::copy(bytes.begin(), bytes.end(), to_send[side].begin());
188-
// Remember 'more' result.
189-
last_more[side] = {more};
199+
// Remember 'more' results.
200+
last_more[side] = {more_nonext};
201+
last_more_next[side] = {more_next};
190202
// Return.
191-
return {bytes, more, msg_type};
203+
return {bytes, more_nonext, msg_type};
192204
};
193205

194206
// Function to make side send a new message.
@@ -199,7 +211,8 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
199211
CSerializedNetMsg msg = next_msg[side].Copy();
200212
bool queued = transports[side]->SetMessageToSend(msg);
201213
// Update expected more data.
202-
expect_more[side] = std::nullopt;
214+
expect_more[side] = expect_more_next[side];
215+
expect_more_next[side] = std::nullopt;
203216
// Verify consistency of GetBytesToSend after SetMessageToSend
204217
bytes_to_send_fn(/*side=*/side);
205218
if (queued) {
@@ -223,6 +236,7 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
223236
// If all to-be-sent bytes were sent, move last_more data to expect_more data.
224237
if (send_now == bytes.size()) {
225238
expect_more[side] = last_more[side];
239+
expect_more_next[side] = last_more_next[side];
226240
}
227241
// Remove the bytes from the last reported to-be-sent vector.
228242
assert(to_send[side].size() >= send_now);
@@ -251,6 +265,7 @@ void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDa
251265
// Clear cached expected 'more' information: if certainly no more data was to be sent
252266
// before, receiving bytes makes this uncertain.
253267
if (expect_more[!side] == false) expect_more[!side] = std::nullopt;
268+
if (expect_more_next[!side] == false) expect_more_next[!side] = std::nullopt;
254269
// Verify consistency of GetBytesToSend after ReceivedBytes
255270
bytes_to_send_fn(/*side=*/!side);
256271
bool progress = to_recv.size() < old_len;
@@ -320,6 +335,40 @@ std::unique_ptr<Transport> MakeV1Transport(NodeId nodeid) noexcept
320335
return std::make_unique<V1Transport>(nodeid, SER_NETWORK, INIT_PROTO_VERSION);
321336
}
322337

338+
template<typename RNG>
339+
std::unique_ptr<Transport> MakeV2Transport(NodeId nodeid, bool initiator, RNG& rng, FuzzedDataProvider& provider)
340+
{
341+
// Retrieve key
342+
auto key = ConsumePrivateKey(provider);
343+
if (!key.IsValid()) return {};
344+
// Construct garbage
345+
size_t garb_len = provider.ConsumeIntegralInRange<size_t>(0, V2Transport::MAX_GARBAGE_LEN);
346+
std::vector<uint8_t> garb;
347+
if (garb_len <= 64) {
348+
// When the garbage length is up to 64 bytes, read it directly from the fuzzer input.
349+
garb = provider.ConsumeBytes<uint8_t>(garb_len);
350+
garb.resize(garb_len);
351+
} else {
352+
// If it's longer, generate it from the RNG. This avoids having large amounts of
353+
// (hopefully) irrelevant data needing to be stored in the fuzzer data.
354+
for (auto& v : garb) v = uint8_t(rng());
355+
}
356+
// Retrieve entropy
357+
auto ent = provider.ConsumeBytes<std::byte>(32);
358+
ent.resize(32);
359+
// Use as entropy SHA256(ent || garbage). This prevents a situation where the fuzzer manages to
360+
// include the garbage terminator (which is a function of both ellswift keys) in the garbage.
361+
// This is extremely unlikely (~2^-116) with random keys/garbage, but the fuzzer can choose
362+
// both non-randomly and dependently. Since the entropy is hashed anyway inside the ellswift
363+
// computation, no coverage should be lost by using a hash as entropy, and it removes the
364+
// possibility of garbage that happens to contain what is effectively a hash of the keys.
365+
CSHA256().Write(UCharCast(ent.data()), ent.size())
366+
.Write(garb.data(), garb.size())
367+
.Finalize(UCharCast(ent.data()));
368+
369+
return std::make_unique<V2Transport>(nodeid, initiator, SER_NETWORK, INIT_PROTO_VERSION, key, ent, garb);
370+
}
371+
323372
} // namespace
324373

325374
FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serialization)
@@ -332,3 +381,25 @@ FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serial
332381
if (!t1 || !t2) return;
333382
SimulationTest(*t1, *t2, rng, provider);
334383
}
384+
385+
FUZZ_TARGET(p2p_transport_bidirectional_v2, .init = initialize_p2p_transport_serialization)
386+
{
387+
// Test with two V2 transports talking to each other.
388+
FuzzedDataProvider provider{buffer.data(), buffer.size()};
389+
XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>());
390+
auto t1 = MakeV2Transport(NodeId{0}, true, rng, provider);
391+
auto t2 = MakeV2Transport(NodeId{1}, false, rng, provider);
392+
if (!t1 || !t2) return;
393+
SimulationTest(*t1, *t2, rng, provider);
394+
}
395+
396+
FUZZ_TARGET(p2p_transport_bidirectional_v1v2, .init = initialize_p2p_transport_serialization)
397+
{
398+
// Test with a V1 initiator talking to a V2 responder.
399+
FuzzedDataProvider provider{buffer.data(), buffer.size()};
400+
XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>());
401+
auto t1 = MakeV1Transport(NodeId{0});
402+
auto t2 = MakeV2Transport(NodeId{1}, false, rng, provider);
403+
if (!t1 || !t2) return;
404+
SimulationTest(*t1, *t2, rng, provider);
405+
}

0 commit comments

Comments
 (0)