aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPieter Wuille <pieter@wuille.net>2023-07-27 15:10:34 -0400
committerPieter Wuille <pieter@wuille.net>2023-09-07 09:00:58 -0400
commit13a7f01557272db652b3f333af3f06af6897253f (patch)
tree6c3cbead248669af9da2fbfd246b183ba3e66d22
parentdc2d7eb810ef95b06620f334c198687579916435 (diff)
downloadbitcoin-13a7f01557272db652b3f333af3f06af6897253f.tar.xz
net: add V2Transport class with subset of BIP324 functionality
This introduces a V2Transport with a basic subset of BIP324 functionality: * no ability to send garbage (but receiving is supported) * no ability to send decoy packets (but receiving them is supported) * no support for short message id encoding (neither encoding or decoding) * no waiting until 12 non-V1 bytes have been received * (and thus) no detection of V1 connections on the responder side (on the sender side, detecting V1 is not supported either, but that needs to be dealt with at a higher layer, by reconnecting)
-rw-r--r--src/net.cpp427
-rw-r--r--src/net.h181
-rw-r--r--src/test/fuzz/p2p_transport_serialization.cpp25
3 files changed, 629 insertions, 4 deletions
diff --git a/src/net.cpp b/src/net.cpp
index eaa99e6601..f5425bf50e 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -913,6 +913,427 @@ size_t V1Transport::GetSendMemoryUsage() const noexcept
return m_message_to_send.GetMemoryUsage();
}
+V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in) noexcept :
+ m_cipher{}, m_initiating{initiating}, m_nodeid{nodeid},
+ m_recv_type{type_in}, m_recv_version{version_in},
+ m_recv_state{RecvState::KEY},
+ m_send_state{SendState::AWAITING_KEY}
+{
+ // Initialize the send buffer with ellswift pubkey.
+ m_send_buffer.resize(EllSwiftPubKey::size());
+ std::copy(std::begin(m_cipher.GetOurPubKey()), std::end(m_cipher.GetOurPubKey()), MakeWritableByteSpan(m_send_buffer).begin());
+}
+
+V2Transport::V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span<const std::byte> ent32) noexcept :
+ m_cipher{key, ent32}, m_initiating{initiating}, m_nodeid{nodeid},
+ m_recv_type{type_in}, m_recv_version{version_in},
+ m_recv_state{RecvState::KEY},
+ m_send_state{SendState::AWAITING_KEY}
+{
+ // Initialize the send buffer with ellswift pubkey.
+ m_send_buffer.resize(EllSwiftPubKey::size());
+ std::copy(std::begin(m_cipher.GetOurPubKey()), std::end(m_cipher.GetOurPubKey()), MakeWritableByteSpan(m_send_buffer).begin());
+}
+
+void V2Transport::SetReceiveState(RecvState recv_state) noexcept
+{
+ AssertLockHeld(m_recv_mutex);
+ // Enforce allowed state transitions.
+ switch (m_recv_state) {
+ case RecvState::KEY:
+ Assume(recv_state == RecvState::GARB_GARBTERM);
+ break;
+ case RecvState::GARB_GARBTERM:
+ Assume(recv_state == RecvState::GARBAUTH);
+ break;
+ case RecvState::GARBAUTH:
+ Assume(recv_state == RecvState::VERSION);
+ break;
+ case RecvState::VERSION:
+ Assume(recv_state == RecvState::APP);
+ break;
+ case RecvState::APP:
+ Assume(recv_state == RecvState::APP_READY);
+ break;
+ case RecvState::APP_READY:
+ Assume(recv_state == RecvState::APP);
+ break;
+ }
+ // Change state.
+ m_recv_state = recv_state;
+}
+
+void V2Transport::SetSendState(SendState send_state) noexcept
+{
+ AssertLockHeld(m_send_mutex);
+ // Enforce allowed state transitions.
+ switch (m_send_state) {
+ case SendState::AWAITING_KEY:
+ Assume(send_state == SendState::READY);
+ break;
+ case SendState::READY:
+ Assume(false); // Final state
+ break;
+ }
+ // Change state.
+ m_send_state = send_state;
+}
+
+bool V2Transport::ReceivedMessageComplete() const noexcept
+{
+ AssertLockNotHeld(m_recv_mutex);
+ LOCK(m_recv_mutex);
+ return m_recv_state == RecvState::APP_READY;
+}
+
+void V2Transport::ProcessReceivedKeyBytes() noexcept
+{
+ AssertLockHeld(m_recv_mutex);
+ AssertLockNotHeld(m_send_mutex);
+ Assume(m_recv_state == RecvState::KEY);
+ Assume(m_recv_buffer.size() <= EllSwiftPubKey::size());
+ if (m_recv_buffer.size() == EllSwiftPubKey::size()) {
+ // Other side's key has been fully received, and can now be Diffie-Hellman combined with
+ // our key to initialize the encryption ciphers.
+
+ // Initialize the ciphers.
+ EllSwiftPubKey ellswift(MakeByteSpan(m_recv_buffer));
+ LOCK(m_send_mutex);
+ m_cipher.Initialize(ellswift, m_initiating);
+
+ // Switch receiver state to GARB_GARBTERM.
+ SetReceiveState(RecvState::GARB_GARBTERM);
+ m_recv_buffer.clear();
+
+ // Switch sender state to READY.
+ SetSendState(SendState::READY);
+
+ // Append the garbage terminator to the send buffer.
+ m_send_buffer.resize(m_send_buffer.size() + BIP324Cipher::GARBAGE_TERMINATOR_LEN);
+ std::copy(m_cipher.GetSendGarbageTerminator().begin(),
+ m_cipher.GetSendGarbageTerminator().end(),
+ MakeWritableByteSpan(m_send_buffer).last(BIP324Cipher::GARBAGE_TERMINATOR_LEN).begin());
+
+ // Construct garbage authentication packet in the send buffer.
+ m_send_buffer.resize(m_send_buffer.size() + BIP324Cipher::EXPANSION);
+ m_cipher.Encrypt(
+ /*contents=*/{},
+ /*aad=*/{}, /* empty garbage for now */
+ /*ignore=*/false,
+ /*output=*/MakeWritableByteSpan(m_send_buffer).last(BIP324Cipher::EXPANSION));
+
+ // Construct version packet in the send buffer.
+ m_send_buffer.resize(m_send_buffer.size() + BIP324Cipher::EXPANSION + VERSION_CONTENTS.size());
+ m_cipher.Encrypt(
+ /*contents=*/VERSION_CONTENTS,
+ /*aad=*/{},
+ /*ignore=*/false,
+ /*output=*/MakeWritableByteSpan(m_send_buffer).last(BIP324Cipher::EXPANSION + VERSION_CONTENTS.size()));
+ } else {
+ // We still have to receive more key bytes.
+ }
+}
+
+bool V2Transport::ProcessReceivedGarbageBytes() noexcept
+{
+ AssertLockHeld(m_recv_mutex);
+ Assume(m_recv_state == RecvState::GARB_GARBTERM);
+ Assume(m_recv_buffer.size() <= MAX_GARBAGE_LEN + BIP324Cipher::GARBAGE_TERMINATOR_LEN);
+ if (m_recv_buffer.size() >= BIP324Cipher::GARBAGE_TERMINATOR_LEN) {
+ if (MakeByteSpan(m_recv_buffer).last(BIP324Cipher::GARBAGE_TERMINATOR_LEN) == m_cipher.GetReceiveGarbageTerminator()) {
+ // Garbage terminator received. Switch to receiving garbage authentication packet.
+ m_recv_garbage = std::move(m_recv_buffer);
+ m_recv_garbage.resize(m_recv_garbage.size() - BIP324Cipher::GARBAGE_TERMINATOR_LEN);
+ m_recv_buffer.clear();
+ SetReceiveState(RecvState::GARBAUTH);
+ } else if (m_recv_buffer.size() == MAX_GARBAGE_LEN + BIP324Cipher::GARBAGE_TERMINATOR_LEN) {
+ // We've reached the maximum length for garbage + garbage terminator, and the
+ // terminator still does not match. Abort.
+ LogPrint(BCLog::NET, "V2 transport error: missing garbage terminator, peer=%d\n", m_nodeid);
+ return false;
+ } else {
+ // We still need to receive more garbage and/or garbage terminator bytes.
+ }
+ } else {
+ // We have less than GARBAGE_TERMINATOR_LEN (16) bytes, so we certainly need to receive
+ // more first.
+ }
+ return true;
+}
+
+bool V2Transport::ProcessReceivedPacketBytes() noexcept
+{
+ AssertLockHeld(m_recv_mutex);
+ Assume(m_recv_state == RecvState::GARBAUTH || m_recv_state == RecvState::VERSION ||
+ m_recv_state == RecvState::APP);
+
+ // The maximum permitted contents length for a packet, consisting of:
+ // - 0x00 byte: indicating long message type encoding
+ // - 12 bytes of message type
+ // - payload
+ static constexpr size_t MAX_CONTENTS_LEN =
+ 1 + CMessageHeader::COMMAND_SIZE +
+ std::min<size_t>(MAX_SIZE, MAX_PROTOCOL_MESSAGE_LENGTH);
+
+ if (m_recv_buffer.size() == BIP324Cipher::LENGTH_LEN) {
+ // Length descriptor received.
+ m_recv_len = m_cipher.DecryptLength(MakeByteSpan(m_recv_buffer));
+ if (m_recv_len > MAX_CONTENTS_LEN) {
+ LogPrint(BCLog::NET, "V2 transport error: packet too large (%u bytes), peer=%d\n", m_recv_len, m_nodeid);
+ return false;
+ }
+ } else if (m_recv_buffer.size() > BIP324Cipher::LENGTH_LEN && m_recv_buffer.size() == m_recv_len + BIP324Cipher::EXPANSION) {
+ // Ciphertext received, decrypt it into m_recv_decode_buffer.
+ // Note that it is impossible to reach this branch without hitting the branch above first,
+ // as GetMaxBytesToProcess only allows up to LENGTH_LEN into the buffer before that point.
+ m_recv_decode_buffer.resize(m_recv_len);
+ bool ignore{false};
+ Span<const std::byte> aad;
+ if (m_recv_state == RecvState::GARBAUTH) aad = MakeByteSpan(m_recv_garbage);
+ bool ret = m_cipher.Decrypt(
+ /*input=*/MakeByteSpan(m_recv_buffer).subspan(BIP324Cipher::LENGTH_LEN),
+ /*aad=*/aad,
+ /*ignore=*/ignore,
+ /*contents=*/MakeWritableByteSpan(m_recv_decode_buffer));
+ if (!ret) {
+ LogPrint(BCLog::NET, "V2 transport error: packet decryption failure (%u bytes), peer=%d\n", m_recv_len, m_nodeid);
+ return false;
+ }
+ // Feed the last 4 bytes of the Poly1305 authentication tag (and its timing) into our RNG.
+ RandAddEvent(ReadLE32(m_recv_buffer.data() + m_recv_buffer.size() - 4));
+
+ // At this point we have a valid packet decrypted into m_recv_decode_buffer. Depending on
+ // the current state, decide what to do with it.
+ switch (m_recv_state) {
+ case RecvState::GARBAUTH:
+ // Ignore flag does not matter for garbage authentication. Any valid packet functions
+ // as authentication. Receive and process the version packet next.
+ SetReceiveState(RecvState::VERSION);
+ m_recv_garbage = {};
+ break;
+ case RecvState::VERSION:
+ if (!ignore) {
+ // Version message received; transition to application phase. The contents is
+ // ignored, but can be used for future extensions.
+ SetReceiveState(RecvState::APP);
+ }
+ break;
+ case RecvState::APP:
+ if (!ignore) {
+ // Application message decrypted correctly. It can be extracted using GetMessage().
+ SetReceiveState(RecvState::APP_READY);
+ }
+ break;
+ default:
+ // Any other state is invalid (this function should not have been called).
+ Assume(false);
+ }
+ // Wipe the receive buffer where the next packet will be received into.
+ m_recv_buffer = {};
+ // In all but APP_READY state, we can wipe the decoded contents.
+ if (m_recv_state != RecvState::APP_READY) m_recv_decode_buffer = {};
+ } else {
+ // We either have less than 3 bytes, so we don't know the packet's length yet, or more
+ // than 3 bytes but less than the packet's full ciphertext. Wait until those arrive.
+ }
+ return true;
+}
+
+size_t V2Transport::GetMaxBytesToProcess() noexcept
+{
+ AssertLockHeld(m_recv_mutex);
+ switch (m_recv_state) {
+ case RecvState::KEY:
+ // During the KEY state, we only allow the 64-byte key into the receive buffer.
+ Assume(m_recv_buffer.size() <= EllSwiftPubKey::size());
+ // As long as we have not received the other side's public key, don't receive more than
+ // that (64 bytes), as garbage follows, and locating the garbage terminator requires the
+ // key exchange first.
+ return EllSwiftPubKey::size() - m_recv_buffer.size();
+ case RecvState::GARB_GARBTERM:
+ // Process garbage bytes one by one (because terminator may appear anywhere).
+ return 1;
+ case RecvState::GARBAUTH:
+ case RecvState::VERSION:
+ case RecvState::APP:
+ // These three states all involve decoding a packet. Process the length descriptor first,
+ // so that we know where the current packet ends (and we don't process bytes from the next
+ // packet or decoy yet). Then, process the ciphertext bytes of the current packet.
+ if (m_recv_buffer.size() < BIP324Cipher::LENGTH_LEN) {
+ return BIP324Cipher::LENGTH_LEN - m_recv_buffer.size();
+ } else {
+ // Note that BIP324Cipher::EXPANSION is the total difference between contents size
+ // and encoded packet size, which includes the 3 bytes due to the packet length.
+ // When transitioning from receiving the packet length to receiving its ciphertext,
+ // the encrypted packet length is left in the receive buffer.
+ return BIP324Cipher::EXPANSION + m_recv_len - m_recv_buffer.size();
+ }
+ case RecvState::APP_READY:
+ // No bytes can be processed until GetMessage() is called.
+ return 0;
+ }
+ Assume(false); // unreachable
+ return 0;
+}
+
+bool V2Transport::ReceivedBytes(Span<const uint8_t>& msg_bytes) noexcept
+{
+ AssertLockNotHeld(m_recv_mutex);
+ LOCK(m_recv_mutex);
+ // Process the provided bytes in msg_bytes in a loop. In each iteration a nonzero number of
+ // bytes (decided by GetMaxBytesToProcess) are taken from the beginning om msg_bytes, and
+ // appended to m_recv_buffer. Then, depending on the receiver state, one of the
+ // ProcessReceived*Bytes functions is called to process the bytes in that buffer.
+ while (!msg_bytes.empty()) {
+ // Decide how many bytes to copy from msg_bytes to m_recv_buffer.
+ size_t max_read = GetMaxBytesToProcess();
+ // Can't read more than provided input.
+ max_read = std::min(msg_bytes.size(), max_read);
+ // Copy data to buffer.
+ m_recv_buffer.insert(m_recv_buffer.end(), UCharCast(msg_bytes.data()), UCharCast(msg_bytes.data() + max_read));
+ msg_bytes = msg_bytes.subspan(max_read);
+
+ // Process data in the buffer.
+ switch (m_recv_state) {
+ case RecvState::KEY:
+ ProcessReceivedKeyBytes();
+ break;
+
+ case RecvState::GARB_GARBTERM:
+ if (!ProcessReceivedGarbageBytes()) return false;
+ break;
+
+ case RecvState::GARBAUTH:
+ case RecvState::VERSION:
+ case RecvState::APP:
+ if (!ProcessReceivedPacketBytes()) return false;
+ break;
+
+ case RecvState::APP_READY:
+ return true;
+ }
+ // Make sure we have made progress before continuing.
+ Assume(max_read > 0);
+ }
+
+ return true;
+}
+
+std::optional<std::string> V2Transport::GetMessageType(Span<const uint8_t>& contents) noexcept
+{
+ if (contents.size() == 0) return std::nullopt; // Empty contents
+ uint8_t first_byte = contents[0];
+ contents = contents.subspan(1); // Strip first byte.
+
+ if (first_byte != 0) return std::nullopt; // TODO: implement short encoding
+
+ if (contents.size() < CMessageHeader::COMMAND_SIZE) {
+ return std::nullopt; // Long encoding needs 12 message type bytes.
+ }
+
+ size_t msg_type_len{0};
+ while (msg_type_len < CMessageHeader::COMMAND_SIZE && contents[msg_type_len] != 0) {
+ // Verify that message type bytes before the first 0x00 are in range.
+ if (contents[msg_type_len] < ' ' || contents[msg_type_len] > 0x7F) {
+ return {};
+ }
+ ++msg_type_len;
+ }
+ std::string ret{reinterpret_cast<const char*>(contents.data()), msg_type_len};
+ while (msg_type_len < CMessageHeader::COMMAND_SIZE) {
+ // Verify that message type bytes after the first 0x00 are also 0x00.
+ if (contents[msg_type_len] != 0) return {};
+ ++msg_type_len;
+ }
+ // Strip message type bytes of contents.
+ contents = contents.subspan(CMessageHeader::COMMAND_SIZE);
+ return {std::move(ret)};
+}
+
+CNetMessage V2Transport::GetReceivedMessage(std::chrono::microseconds time, bool& reject_message) noexcept
+{
+ AssertLockNotHeld(m_recv_mutex);
+ LOCK(m_recv_mutex);
+ Assume(m_recv_state == RecvState::APP_READY);
+ Span<const uint8_t> contents{m_recv_decode_buffer};
+ auto msg_type = GetMessageType(contents);
+ CDataStream ret(m_recv_type, m_recv_version);
+ CNetMessage msg{std::move(ret)};
+ // Note that BIP324Cipher::EXPANSION also includes the length descriptor size.
+ msg.m_raw_message_size = m_recv_decode_buffer.size() + BIP324Cipher::EXPANSION;
+ if (msg_type) {
+ reject_message = false;
+ msg.m_type = std::move(*msg_type);
+ msg.m_time = time;
+ msg.m_message_size = contents.size();
+ msg.m_recv.resize(contents.size());
+ std::copy(contents.begin(), contents.end(), UCharCast(msg.m_recv.data()));
+ } else {
+ LogPrint(BCLog::NET, "V2 transport error: invalid message type (%u bytes contents), peer=%d\n", m_recv_decode_buffer.size(), m_nodeid);
+ reject_message = true;
+ }
+ m_recv_decode_buffer = {};
+ SetReceiveState(RecvState::APP);
+
+ return msg;
+}
+
+bool V2Transport::SetMessageToSend(CSerializedNetMsg& msg) noexcept
+{
+ AssertLockNotHeld(m_send_mutex);
+ LOCK(m_send_mutex);
+ // We only allow adding a new message to be sent when in the READY state (so the packet cipher
+ // is available) and the send buffer is empty. This limits the number of messages in the send
+ // buffer to just one, and leaves the responsibility for queueing them up to the caller.
+ if (!(m_send_state == SendState::READY && m_send_buffer.empty())) return false;
+ // Construct contents (encoding message type + payload).
+ // Initialize with zeroes, and then write the message type string starting at offset 1.
+ // This means contents[0] and the unused positions in contents[1..13] remain 0x00.
+ std::vector<uint8_t> contents(1 + CMessageHeader::COMMAND_SIZE + msg.data.size(), 0);
+ std::copy(msg.m_type.begin(), msg.m_type.end(), contents.data() + 1);
+ std::copy(msg.data.begin(), msg.data.end(), contents.begin() + 1 + CMessageHeader::COMMAND_SIZE);
+ // Construct ciphertext in send buffer.
+ m_send_buffer.resize(contents.size() + BIP324Cipher::EXPANSION);
+ m_cipher.Encrypt(MakeByteSpan(contents), {}, false, MakeWritableByteSpan(m_send_buffer));
+ m_send_type = msg.m_type;
+ // Release memory
+ msg.data = {};
+ return true;
+}
+
+Transport::BytesToSend V2Transport::GetBytesToSend(bool have_next_message) const noexcept
+{
+ AssertLockNotHeld(m_send_mutex);
+ LOCK(m_send_mutex);
+ Assume(m_send_pos <= m_send_buffer.size());
+ return {
+ Span{m_send_buffer}.subspan(m_send_pos),
+ // We only have more to send after the current m_send_buffer if there is a (next)
+ // message to be sent, and we're capable of sending packets. */
+ have_next_message && m_send_state == SendState::READY,
+ m_send_type
+ };
+}
+
+void V2Transport::MarkBytesSent(size_t bytes_sent) noexcept
+{
+ AssertLockNotHeld(m_send_mutex);
+ LOCK(m_send_mutex);
+ m_send_pos += bytes_sent;
+ Assume(m_send_pos <= m_send_buffer.size());
+ if (m_send_pos == m_send_buffer.size()) {
+ m_send_pos = 0;
+ m_send_buffer = {};
+ }
+}
+
+size_t V2Transport::GetSendMemoryUsage() const noexcept
+{
+ AssertLockNotHeld(m_send_mutex);
+ LOCK(m_send_mutex);
+ return sizeof(m_send_buffer) + memusage::DynamicUsage(m_send_buffer);
+}
+
std::pair<size_t, bool> CConnman::SocketSendData(CNode& node) const
{
auto it = node.vSendMsg.begin();
@@ -923,7 +1344,8 @@ std::pair<size_t, bool> CConnman::SocketSendData(CNode& node) const
while (true) {
if (it != node.vSendMsg.end()) {
// If possible, move one message from the send queue to the transport. This fails when
- // there is an existing message still being sent.
+ // there is an existing message still being sent, or (for v2 transports) when the
+ // handshake has not yet completed.
size_t memusage = it->GetMemoryUsage();
if (node.m_transport->SetMessageToSend(*it)) {
// Update memory usage of send buffer (as *it will be deleted).
@@ -3031,7 +3453,8 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg)
// because the poll/select loop may pause for SELECT_TIMEOUT_MILLISECONDS before actually
// doing a send, try sending from the calling thread if the queue was empty before.
// With a V1Transport, more will always be true here, because adding a message always
- // results in sendable bytes there.
+ // results in sendable bytes there, but with V2Transport this is not the case (it may
+ // still be in the handshake).
if (queue_was_empty && more) {
std::tie(nBytesSent, std::ignore) = SocketSendData(*pnode);
}
diff --git a/src/net.h b/src/net.h
index 6f989aa175..27d141bc6e 100644
--- a/src/net.h
+++ b/src/net.h
@@ -6,6 +6,7 @@
#ifndef BITCOIN_NET_H
#define BITCOIN_NET_H
+#include <bip324.h>
#include <chainparams.h>
#include <common/bloom.h>
#include <compat/compat.h>
@@ -298,7 +299,8 @@ public:
* - Span<const uint8_t> to_send: span of bytes to be sent over the wire (possibly empty).
* - bool more: whether there will be more bytes to be sent after the ones in to_send are
* all sent (as signaled by MarkBytesSent()).
- * - const std::string& m_type: message type on behalf of which this is being sent.
+ * - const std::string& m_type: message type on behalf of which this is being sent
+ * ("" for bytes that are not on behalf of any message).
*/
using BytesToSend = std::tuple<
Span<const uint8_t> /*to_send*/,
@@ -327,7 +329,9 @@ public:
* happens when sending the payload of a message.
* - Blocked: the transport itself has no more bytes to send, and is also incapable
* of sending anything more at all now, if it were handed another
- * message to send.
+ * message to send. This occurs in V2Transport before the handshake is
+ * complete, as the encryption ciphers are not set up for sending
+ * messages before that point.
*
* The boolean 'more' is true for Yes, false for Blocked, and have_next_message
* controls what is returned for No.
@@ -432,6 +436,179 @@ public:
size_t GetSendMemoryUsage() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex);
};
+class V2Transport final : public Transport
+{
+private:
+ /** Contents of the version packet to send. BIP324 stipulates that senders should leave this
+ * empty, and receivers should ignore it. Future extensions can change what is sent as long as
+ * an empty version packet contents is interpreted as no extensions supported. */
+ static constexpr std::array<std::byte, 0> VERSION_CONTENTS = {};
+
+ // The sender side and receiver side of V2Transport are state machines that are transitioned
+ // through, based on what has been received. The receive state corresponds to the contents of,
+ // and bytes received to, the receive buffer. The send state controls what can be appended to
+ // the send buffer.
+
+ /** State type that defines the current contents of the receive buffer and/or how the next
+ * received bytes added to it will be interpreted.
+ *
+ * Diagram:
+ *
+ * start /---------\
+ * | | |
+ * v v |
+ * KEY -> GARB_GARBTERM -> GARBAUTH -> VERSION -> APP -> APP_READY
+ */
+ enum class RecvState : uint8_t {
+ /** Public key.
+ *
+ * This is the initial state, during which the other side's public key is
+ * received. When that information arrives, the ciphers get initialized and the state
+ * becomes GARB_GARBTERM. */
+ KEY,
+
+ /** Garbage and garbage terminator.
+ *
+ * Whenever a byte is received, the last 16 bytes are compared with the expected garbage
+ * terminator. When that happens, the state becomes GARBAUTH. If no matching terminator is
+ * received in 4111 bytes (4095 for the maximum garbage length, and 16 bytes for the
+ * terminator), the connection aborts. */
+ GARB_GARBTERM,
+
+ /** Garbage authentication packet.
+ *
+ * A packet is received, and decrypted/verified with AAD set to the garbage received during
+ * the GARB_GARBTERM state. If that succeeds, the state becomes VERSION. If it fails the
+ * connection aborts. */
+ GARBAUTH,
+
+ /** Version packet.
+ *
+ * A packet is received, and decrypted/verified. If that succeeds, the state becomes APP,
+ * and the decrypted contents is interpreted as version negotiation (currently, that means
+ * ignoring it, but it can be used for negotiating future extensions). If it fails, the
+ * connection aborts. */
+ VERSION,
+
+ /** Application packet.
+ *
+ * A packet is received, and decrypted/verified. If that succeeds, the state becomes
+ * APP_READY and the decrypted contents is kept in m_recv_decode_buffer until it is
+ * retrieved as a message by GetMessage(). */
+ APP,
+
+ /** Nothing (an application packet is available for GetMessage()).
+ *
+ * Nothing can be received in this state. When the message is retrieved by GetMessage,
+ * the state becomes APP again. */
+ APP_READY,
+ };
+
+ /** State type that controls the sender side.
+ *
+ * Diagram:
+ *
+ * start
+ * |
+ * v
+ * AWAITING_KEY -> READY
+ */
+ enum class SendState : uint8_t {
+ /** Waiting for the other side's public key.
+ *
+ * This is the initial state. The public key is sent out. When the receiver receives the
+ * other side's public key and transitions to GARB_GARBTERM, the sender state becomes
+ * READY. */
+ AWAITING_KEY,
+
+ /** Normal sending state.
+ *
+ * In this state, the ciphers are initialized, so packets can be sent. When this state is
+ * entered, the garbage terminator, garbage authentication packet, and version packet are
+ * appended to the send buffer (in addition to the key which may still be there). In this
+ * state a message can be provided if the send buffer is empty. */
+ READY,
+ };
+
+ /** Cipher state. */
+ BIP324Cipher m_cipher;
+ /** Whether we are the initiator side. */
+ const bool m_initiating;
+ /** NodeId (for debug logging). */
+ const NodeId m_nodeid;
+
+ /** Lock for receiver-side fields. */
+ mutable Mutex m_recv_mutex ACQUIRED_BEFORE(m_send_mutex);
+ /** In {GARBAUTH, VERSION, APP}, the decrypted packet length, if m_recv_buffer.size() >=
+ * BIP324Cipher::LENGTH_LEN. Unspecified otherwise. */
+ uint32_t m_recv_len GUARDED_BY(m_recv_mutex) {0};
+ /** Receive buffer; meaning is determined by m_recv_state. */
+ std::vector<uint8_t> m_recv_buffer GUARDED_BY(m_recv_mutex);
+ /** During GARBAUTH, the garbage received during GARB_GARBTERM. */
+ std::vector<uint8_t> m_recv_garbage GUARDED_BY(m_recv_mutex);
+ /** Buffer to put decrypted contents in, for converting to CNetMessage. */
+ std::vector<uint8_t> m_recv_decode_buffer GUARDED_BY(m_recv_mutex);
+ /** Deserialization type. */
+ const int m_recv_type;
+ /** Deserialization version number. */
+ const int m_recv_version;
+ /** Current receiver state. */
+ RecvState m_recv_state GUARDED_BY(m_recv_mutex);
+
+ /** Lock for sending-side fields. If both sending and receiving fields are accessed,
+ * m_recv_mutex must be acquired before m_send_mutex. */
+ mutable Mutex m_send_mutex ACQUIRED_AFTER(m_recv_mutex);
+ /** The send buffer; meaning is determined by m_send_state. */
+ std::vector<uint8_t> m_send_buffer GUARDED_BY(m_send_mutex);
+ /** How many bytes from the send buffer have been sent so far. */
+ uint32_t m_send_pos GUARDED_BY(m_send_mutex) {0};
+ /** Type of the message being sent. */
+ std::string m_send_type GUARDED_BY(m_send_mutex);
+ /** Current sender state. */
+ SendState m_send_state GUARDED_BY(m_send_mutex);
+
+ /** Change the receive state. */
+ void SetReceiveState(RecvState recv_state) noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex);
+ /** Change the send state. */
+ void SetSendState(SendState send_state) noexcept EXCLUSIVE_LOCKS_REQUIRED(m_send_mutex);
+ /** Given a packet's contents, find the message type (if valid), and strip it from contents. */
+ static std::optional<std::string> GetMessageType(Span<const uint8_t>& contents) noexcept;
+ /** Determine how many received bytes can be processed in one go (not allowed in V1 state). */
+ size_t GetMaxBytesToProcess() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex);
+ /** Process bytes in m_recv_buffer, while in KEY state. */
+ void ProcessReceivedKeyBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex);
+ /** Process bytes in m_recv_buffer, while in GARB_GARBTERM state. */
+ bool ProcessReceivedGarbageBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex);
+ /** Process bytes in m_recv_buffer, while in GARBAUTH/VERSION/APP state. */
+ bool ProcessReceivedPacketBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex);
+
+public:
+ static constexpr uint32_t MAX_GARBAGE_LEN = 4095;
+
+ /** Construct a V2 transport with securely generated random keys.
+ *
+ * @param[in] nodeid the node's NodeId (only for debug log output).
+ * @param[in] initiating whether we are the initiator side.
+ * @param[in] type_in the serialization type of returned CNetMessages.
+ * @param[in] version_in the serialization version of returned CNetMessages.
+ */
+ V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in) noexcept;
+
+ /** Construct a V2 transport with specified keys (test use only). */
+ V2Transport(NodeId nodeid, bool initiating, int type_in, int version_in, const CKey& key, Span<const std::byte> ent32) noexcept;
+
+ // Receive side functions.
+ bool ReceivedMessageComplete() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex);
+ bool ReceivedBytes(Span<const uint8_t>& msg_bytes) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex, !m_send_mutex);
+ CNetMessage GetReceivedMessage(std::chrono::microseconds time, bool& reject_message) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex);
+
+ // Send side functions.
+ bool SetMessageToSend(CSerializedNetMsg& msg) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex);
+ BytesToSend GetBytesToSend(bool have_next_message) const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex);
+ void MarkBytesSent(size_t bytes_sent) noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex);
+ size_t GetSendMemoryUsage() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex);
+};
+
struct CNodeOptions
{
NetPermissionFlags permission_flags = NetPermissionFlags::None;
diff --git a/src/test/fuzz/p2p_transport_serialization.cpp b/src/test/fuzz/p2p_transport_serialization.cpp
index 468bb789ed..f9454eab69 100644
--- a/src/test/fuzz/p2p_transport_serialization.cpp
+++ b/src/test/fuzz/p2p_transport_serialization.cpp
@@ -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());
@@ -334,6 +335,19 @@ 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 {};
+ // Retrieve entropy
+ auto ent = provider.ConsumeBytes<std::byte>(32);
+ ent.resize(32);
+
+ return std::make_unique<V2Transport>(nodeid, initiator, SER_NETWORK, INIT_PROTO_VERSION, key, ent);
+}
+
} // namespace
FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serialization)
@@ -346,3 +360,14 @@ 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);
+}