This PR furthers the P2P message serialization/deserialization abstraction introduced in #16202 and #16562, in preparation for introducing the BIP324 v2 transport (making this part of #27634). However, nothing in this PR is BIP324-specific, and it contains a number of independently useful improvements.
The overall idea is to have a single object in every CNode
(called m_transport
) that is responsible for converting sent messages to wire bytes, and for converting received wire bytes back to messages, while having as little as possible knowledge about this conversion process in higher-level net code. To accomplish that, there is an abstract Transport
class with (currently) a single V1Transport
implementation.
Structurally, the above is accomplished by:
- Merging the
TransportDeserializer
andTransportSerializer
classes into a singleTransport
class, which encompasses both the sending and receiving side. ForV1Transport
these two sides are entirely separate, but this assumption doesn’t hold for the BIP324 transport where e.g. the sending encryption key depends on the DH key negotiation data received from the other side. Merging the two means a futureV2Transport
can handle all this interaction without callers needing to be aware. - Removing the assumption that each message is sent using a computed header followed by (unmodified) data bytes. To achieve that, the sending side of
Transport
mirrors what the receiver side does: callers can set a message to be sent, then ask what bytes must be sent out, and then allowing them to transition to the next message. - Adding internal locks to protect the sending and receiving state of the
V1Transport
implementation. I believe these aren’t strictly needed (opinions welcome) as there is no real way to useTransport
objects in a multi-threaded fashion without some form of external synchronization (e.g. “get next bytes to send” isn’t meaningful to call from multiple threads at the same time without mechanism to control the order they’ll actually get sent). Still, I feel it’s cleaner to make the object responsible for its own consistency (as we definitely do not want the entire object to be under a single external GUARDED_BY, as that’d prevent simultaneous sending and receiving). - Moving the conversion of messages to bytes on the sending side from
PushMessage
toSocketSendData
, which is needed to deal with the fact that a transport may not immediately be able to send messages.
This PR is not a refactor, though some commits are. Among the semantic changes are:
- Changing the send buffer pushback mechanism to trigger based on the memory usage of the buffer rather than the amount of bytes to be sent. This is both closer to the desired behavior, and makes the buffering independent from transport details (which is why it’s included here).
- When optimistic send is not applicable, the V1 message checksum calculation now runs in the net thread rather than the message handling thread. I believe that’s generally an improvement, as the message handling thread is far more computationally bottlenecked already.
- The checksum calculation now runs under the
CNode::cs_vSend
lock, which does mean no two checksum calculations for messages sent to the same node can run in parallel, even if running in separate threads. Despite that limitation, having the checksum for non-optimistic sends moved in the net thread is still an improvement, I believe. - Statistics for per-message-type sent bytes are now updated when the bytes are actually handed to the OS rather than in
PushMessage
. This is because the actual serialized sizes aren’t known until they’ve gone through the transport object.
A fuzz test of the entire V1Transport
is included. More elaborate rationale for each of the changes can be found in the commit messages.