Skip to content

Commit

Permalink
Introduce Sv2Transport
Browse files Browse the repository at this point in the history
Implemented starting from a copy of V2Transport and the V2TransportTester,
modifying it to fit Stratum v2 and Noise Protocol requirements.

Co-Authored-By: Christopher Coverdale <chris.coverdale24@gmail.com>
Co-Authored-By: Fi3
Sjors and ccdle12 committed Aug 29, 2024
1 parent b6a9982 commit 50b6eb8
Showing 5 changed files with 1,079 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -119,6 +119,7 @@ add_library(bitcoin_common STATIC EXCLUDE_FROM_ALL
common/settings.cpp
common/signmessage.cpp
common/sv2_noise.cpp
common/sv2_transport.cpp
common/system.cpp
common/url.cpp
compressor.cpp
494 changes: 494 additions & 0 deletions src/common/sv2_transport.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,494 @@
// Copyright (c) 2023-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <common/sv2_transport.h>

#include <logging.h>
#include <memusage.h>
#include <common/sv2_messages.h>
#include <common/sv2_noise.h>
#include <random.h>
#include <util/check.h>
#include <util/strencodings.h>
#include <util/vector.h>

Sv2Transport::Sv2Transport(CKey static_key, Sv2SignatureNoiseMessage certificate) noexcept
: m_cipher{Sv2Cipher(std::move(static_key), std::move(certificate))}, m_initiating{false},
m_recv_state{RecvState::HANDSHAKE_STEP_1},
m_send_state{SendState::HANDSHAKE_STEP_2},
m_message{Sv2NetMsg(Sv2NetHeader{})}
{
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Noise session receive state -> %s\n",
RecvStateAsString(m_recv_state));
}

Sv2Transport::Sv2Transport(CKey static_key, XOnlyPubKey responder_authority_key) noexcept
: m_cipher{Sv2Cipher(std::move(static_key), responder_authority_key)}, m_initiating{true},
m_recv_state{RecvState::HANDSHAKE_STEP_2},
m_send_state{SendState::HANDSHAKE_STEP_1},
m_message{Sv2NetMsg(Sv2NetHeader{})}
{
/** Start sending immediately since we're the initiator of the connection.
This only happens in test code.
*/
LOCK(m_send_mutex);
StartSendingHandshake();

}

void Sv2Transport::SetReceiveState(RecvState recv_state) noexcept
{
AssertLockHeld(m_recv_mutex);
// Enforce allowed state transitions.
switch (m_recv_state) {
case RecvState::HANDSHAKE_STEP_1:
Assume(recv_state == RecvState::HANDSHAKE_STEP_2);
break;
case RecvState::HANDSHAKE_STEP_2:
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;
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Noise session receive state -> %s\n",
RecvStateAsString(m_recv_state));

}

void Sv2Transport::SetSendState(SendState send_state) noexcept
{
AssertLockHeld(m_send_mutex);
// Enforce allowed state transitions.
switch (m_send_state) {
case SendState::HANDSHAKE_STEP_1:
Assume(send_state == SendState::HANDSHAKE_STEP_2);
break;
case SendState::HANDSHAKE_STEP_2:
Assume(send_state == SendState::READY);
break;
case SendState::READY:
Assume(false); // Final state
break;
}
// Change state.
m_send_state = send_state;
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Noise session send state -> %s\n",
SendStateAsString(m_send_state));
}

void Sv2Transport::StartSendingHandshake() noexcept
{
AssertLockHeld(m_send_mutex);
AssertLockNotHeld(m_recv_mutex);
Assume(m_send_state == SendState::HANDSHAKE_STEP_1);
Assume(m_send_buffer.empty());

m_send_buffer.resize(Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE);
m_cipher.GetHandshakeState().WriteMsgEphemeralPK(MakeWritableByteSpan(m_send_buffer));

m_send_state = SendState::HANDSHAKE_STEP_2;
}

void Sv2Transport::SendHandshakeReply() noexcept
{
AssertLockHeld(m_send_mutex);
AssertLockHeld(m_recv_mutex);
Assume(m_send_state == SendState::HANDSHAKE_STEP_2);

Assume(m_send_buffer.empty());
m_send_buffer.resize(Sv2HandshakeState::HANDSHAKE_STEP2_SIZE);
m_cipher.GetHandshakeState().WriteMsgES(MakeWritableByteSpan(m_send_buffer));

m_cipher.FinishHandshake();

// We can send and receive stuff now, unless the other side hangs up
SetSendState(SendState::READY);
Assume(m_recv_state == RecvState::HANDSHAKE_STEP_2);
SetReceiveState(RecvState::APP);
}

Transport::BytesToSend Sv2Transport::GetBytesToSend(bool have_next_message) const noexcept
{
AssertLockNotHeld(m_send_mutex);
LOCK(m_send_mutex);

const std::string dummy_m_type; // m_type is set to "" when wrapping Sv2NetMsg

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,
dummy_m_type
};
}

void Sv2Transport::MarkBytesSent(size_t bytes_sent) noexcept
{
AssertLockNotHeld(m_send_mutex);
LOCK(m_send_mutex);

// if (m_send_state == SendState::AWAITING_KEY && m_send_pos == 0 && bytes_sent > 0) {
// LogPrint(BCLog::NET, "start sending v2 handshake to peer=%d\n", m_nodeid);
// }

m_send_pos += bytes_sent;
Assume(m_send_pos <= m_send_buffer.size());
// Wipe the buffer when everything is sent.
if (m_send_pos == m_send_buffer.size()) {
m_send_pos = 0;
ClearShrink(m_send_buffer);
}
}

bool Sv2Transport::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) {
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "SendState is not READY\n");
return false;
}

if (!m_send_buffer.empty()) {
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Send buffer is not empty\n");
return false;
}

// The Sv2NetMsg is wrapped inside a dummy CSerializedNetMsg, extract it:
Sv2NetMsg sv2_msg(std::move(msg));
// Reconstruct the header:
Sv2NetHeader hdr(sv2_msg.m_msg_type, sv2_msg.size());

// Construct ciphertext in send buffer.
const size_t encrypted_msg_size = Sv2Cipher::EncryptedMessageSize(sv2_msg.size());
m_send_buffer.resize(SV2_HEADER_ENCRYPTED_SIZE + encrypted_msg_size);
Span<std::byte> buffer_span{MakeWritableByteSpan(m_send_buffer)};

// Header
DataStream ss_header_plain{};
ss_header_plain << hdr;
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Header: %s\n", HexStr(ss_header_plain));
Span<std::byte> header_encrypted{buffer_span.subspan(0, SV2_HEADER_ENCRYPTED_SIZE)};
if (!m_cipher.EncryptMessage(ss_header_plain, header_encrypted)) {
return false;
}

// Payload
Span<const std::byte> payload_plain = MakeByteSpan(sv2_msg);
// TODO: truncate very long messages, about 100 bytes at the start and end
// is probably enough for most debugging.
// LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Payload: %s\n", HexStr(payload_plain));
Span<std::byte> payload_encrypted{buffer_span.subspan(SV2_HEADER_ENCRYPTED_SIZE, encrypted_msg_size)};
if (!m_cipher.EncryptMessage(payload_plain, payload_encrypted)) {
return false;
}

// Release memory (not needed with std::move above)
// ClearShrink(msg.data);

return true;
}

size_t Sv2Transport::GetSendMemoryUsage() const noexcept
{
AssertLockNotHeld(m_send_mutex);
LOCK(m_send_mutex);

return sizeof(m_send_buffer) + memusage::DynamicUsage(m_send_buffer);
}

bool Sv2Transport::ReceivedBytes(Span<const uint8_t>& msg_bytes) noexcept
{
AssertLockNotHeld(m_send_mutex);
AssertLockNotHeld(m_recv_mutex);
/** How many bytes to allocate in the receive buffer at most above what is received so far. */
static constexpr size_t MAX_RESERVE_AHEAD = 256 * 1024; // TODO: reduce to NOISE_MAX_CHUNK_SIZE?

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();

// Reserve space in the buffer if there is not enough.
if (m_recv_buffer.size() + std::min(msg_bytes.size(), max_read) > m_recv_buffer.capacity()) {
switch (m_recv_state) {
case RecvState::HANDSHAKE_STEP_1:
m_recv_buffer.reserve(Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE);
break;
case RecvState::HANDSHAKE_STEP_2:
m_recv_buffer.reserve(Sv2HandshakeState::HANDSHAKE_STEP2_SIZE);
break;
case RecvState::APP: {
// During states where a packet is being received, as much as is expected but never
// more than MAX_RESERVE_AHEAD bytes in addition to what is received so far.
// This means attackers that want to cause us to waste allocated memory are limited
// to MAX_RESERVE_AHEAD above the largest allowed message contents size, and to
// MAX_RESERVE_AHEAD more than they've actually sent us.
size_t alloc_add = std::min(max_read, msg_bytes.size() + MAX_RESERVE_AHEAD);
m_recv_buffer.reserve(m_recv_buffer.size() + alloc_add);
break;
}
case RecvState::APP_READY:
// The buffer is empty in this state.
Assume(m_recv_buffer.empty());
break;
}
}

// 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::HANDSHAKE_STEP_1:
if (!ProcessReceivedEphemeralKeyBytes()) return false;
break;

case RecvState::HANDSHAKE_STEP_2:
if (!ProcessReceivedHandshakeReplyBytes()) return false;
break;

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;
}

bool Sv2Transport::ProcessReceivedEphemeralKeyBytes() noexcept
{
AssertLockHeld(m_recv_mutex);
AssertLockNotHeld(m_send_mutex);
Assume(m_recv_state == RecvState::HANDSHAKE_STEP_1);
Assume(m_recv_buffer.size() <= Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE);

if (m_recv_buffer.size() == Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE) {
// Other side's key has been fully received, and can now be Diffie-Hellman
// combined with our key. This is act 1 of the Noise Protocol handshake.
// TODO handle failure
// TODO: MakeByteSpan instead of MakeWritableByteSpan
m_cipher.GetHandshakeState().ReadMsgEphemeralPK(MakeWritableByteSpan(m_recv_buffer));
m_recv_buffer.clear();
SetReceiveState(RecvState::HANDSHAKE_STEP_2);

LOCK(m_send_mutex);
Assume(m_send_buffer.size() == 0);

// Send our act 2 handshake
SendHandshakeReply();
} else {
// We still have to receive more key bytes.
}
return true;
}

bool Sv2Transport::ProcessReceivedHandshakeReplyBytes() noexcept
{
AssertLockHeld(m_recv_mutex);
AssertLockNotHeld(m_send_mutex);
Assume(m_recv_state == RecvState::HANDSHAKE_STEP_2);
Assume(m_recv_buffer.size() <= Sv2HandshakeState::HANDSHAKE_STEP2_SIZE);

if (m_recv_buffer.size() == Sv2HandshakeState::HANDSHAKE_STEP2_SIZE) {
// TODO handle failure
// TODO: MakeByteSpan instead of MakeWritableByteSpan
bool res = m_cipher.GetHandshakeState().ReadMsgES(MakeWritableByteSpan(m_recv_buffer));
if (!res) return false;
m_recv_buffer.clear();
m_cipher.FinishHandshake();
SetReceiveState(RecvState::APP);

LOCK(m_send_mutex);
Assume(m_send_buffer.size() == 0);

SetSendState(SendState::READY);
} else {
// We still have to receive more key bytes.
}
return true;
}

size_t Sv2Transport::GetMaxBytesToProcess() noexcept
{
AssertLockHeld(m_recv_mutex);
switch (m_recv_state) {
case RecvState::HANDSHAKE_STEP_1:
// In this state, we only allow the 64-byte key into the receive buffer.
Assume(m_recv_buffer.size() <= Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE);
return Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE - m_recv_buffer.size();
case RecvState::HANDSHAKE_STEP_2:
// In this state, we only allow the handshake reply into the receive buffer.
Assume(m_recv_buffer.size() <= Sv2HandshakeState::HANDSHAKE_STEP2_SIZE);
return Sv2HandshakeState::HANDSHAKE_STEP2_SIZE - m_recv_buffer.size();
case RecvState::APP:
// Decode a packet. Process the header first,
// so that we know where the current packet ends (and we don't process bytes from the next
// packet yet). Then, process the ciphertext bytes of the current packet.
if (m_recv_buffer.size() < SV2_HEADER_ENCRYPTED_SIZE) {
return SV2_HEADER_ENCRYPTED_SIZE - m_recv_buffer.size();
} else {
// When transitioning from receiving the packet length to receiving its ciphertext,
// the encrypted header is left in the receive buffer.
size_t expanded_size_with_header = SV2_HEADER_ENCRYPTED_SIZE + Sv2Cipher::EncryptedMessageSize(m_header.m_msg_len);
return expanded_size_with_header - 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 Sv2Transport::ProcessReceivedPacketBytes() noexcept
{
AssertLockHeld(m_recv_mutex);
Assume(m_recv_state == RecvState::APP);

// The maximum permitted decrypted payload size for a packet
static constexpr size_t MAX_CONTENTS_LEN = 16777215; // 24 bit unsigned;

Assume(m_recv_buffer.size() <= SV2_HEADER_ENCRYPTED_SIZE || m_header.m_msg_len > 0);

if (m_recv_buffer.size() == SV2_HEADER_ENCRYPTED_SIZE) {
// Header received, decrypt it.
std::array<std::byte, SV2_HEADER_PLAIN_SIZE> header_plain;
if (!m_cipher.DecryptMessage(MakeWritableByteSpan(m_recv_buffer), header_plain)) {
LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Failed to decrypt header\n");
return false;
}

LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Header: %s\n", HexStr(header_plain));

// Decode header
DataStream ss_header{header_plain};
node::Sv2NetHeader header;
ss_header >> header;
m_header = std::move(header);

// TODO: 16 MB is pretty large, maybe set lower limits for most or all message types?
if (m_header.m_msg_len > MAX_CONTENTS_LEN) {
LogTrace(BCLog::SV2, "Packet too large (%u bytes)\n", m_header.m_msg_len);
return false;
}

// Disconnect for empty messages (TODO: check the spec)
if (m_header.m_msg_len == 0) {
LogTrace(BCLog::SV2, "Empty message\n");
return false;
}
LogTrace(BCLog::SV2, "Expecting %d bytes payload (plain)\n", m_header.m_msg_len);
} else if (m_recv_buffer.size() > SV2_HEADER_ENCRYPTED_SIZE &&
m_recv_buffer.size() == SV2_HEADER_ENCRYPTED_SIZE + Sv2Cipher::EncryptedMessageSize(m_header.m_msg_len)) {
/** Ciphertext received: decrypt into decode_buffer and deserialize into m_message.
*
* Note that it is impossible to reach this branch without hitting the
* branch above first, as GetMaxBytesToProcess only allows up to
* SV2_HEADER_ENCRYPTED_SIZE into the buffer before that point. */
std::vector<std::uint8_t> payload;
payload.resize(m_header.m_msg_len);

Span<std::byte> recv_span{MakeWritableByteSpan(m_recv_buffer).subspan(SV2_HEADER_ENCRYPTED_SIZE)};
if (!m_cipher.DecryptMessage(recv_span, MakeWritableByteSpan(payload))) {
LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Failed to decrypt message payload\n");
return false;
}
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Payload: %s\n", HexStr(payload));

// Wipe the receive buffer where the next packet will be received into.
ClearShrink(m_recv_buffer);

Sv2NetMsg message{m_header.m_msg_type, std::move(payload)};
m_message = std::move(message);

// At this point we have a valid message decrypted into m_message.
SetReceiveState(RecvState::APP_READY);
} else {
// We either have less than 22 bytes, so we don't know the packet's length yet, or more
// than 22 bytes but less than the packet's full ciphertext. Wait until those arrive.
LogTrace(BCLog::SV2, "Waiting for more bytes\n");
}
return true;
}

bool Sv2Transport::ReceivedMessageComplete() const noexcept
{
AssertLockNotHeld(m_recv_mutex);
LOCK(m_recv_mutex);

return m_recv_state == RecvState::APP_READY;
}

CNetMessage Sv2Transport::GetReceivedMessage(std::chrono::microseconds time, bool& reject_message) noexcept
{
AssertLockNotHeld(m_recv_mutex);
LOCK(m_recv_mutex);
Assume(m_recv_state == RecvState::APP_READY);

SetReceiveState(RecvState::APP);
return m_message; // Sv2NetMsg is wrapped in a CNetMessage
}

Transport::Info Sv2Transport::GetInfo() const noexcept
{
return {.transport_type = TransportProtocolType::V1, .session_id = {}};
}

std::string RecvStateAsString(Sv2Transport::RecvState state)
{
switch (state) {
case Sv2Transport::RecvState::HANDSHAKE_STEP_1:
return "HANDSHAKE_STEP_1";
case Sv2Transport::RecvState::HANDSHAKE_STEP_2:
return "HANDSHAKE_STEP_2";
case Sv2Transport::RecvState::APP:
return "APP";
case Sv2Transport::RecvState::APP_READY:
return "APP_READY";
} // no default case, so the compiler can warn about missing cases

assert(false);
}

std::string SendStateAsString(Sv2Transport::SendState state)
{
switch (state) {
case Sv2Transport::SendState::HANDSHAKE_STEP_1:
return "HANDSHAKE_STEP_1";
case Sv2Transport::SendState::HANDSHAKE_STEP_2:
return "HANDSHAKE_STEP_2";
case Sv2Transport::SendState::READY:
return "READY";
} // no default case, so the compiler can warn about missing cases

assert(false);
}
194 changes: 194 additions & 0 deletions src/common/sv2_transport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright (c) 2023-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_COMMON_SV2_TRANSPORT_H
#define BITCOIN_COMMON_SV2_TRANSPORT_H

#include <common/sv2_messages.h>
#include <common/sv2_noise.h>
#include <net.h> // For Transport, CNetMessage and CSerializedNetMsg
#include <sync.h>

static constexpr size_t SV2_HEADER_PLAIN_SIZE{6};
static constexpr size_t SV2_HEADER_ENCRYPTED_SIZE{SV2_HEADER_PLAIN_SIZE + Poly1305::TAGLEN};

using node::Sv2NetHeader;
using node::Sv2NetMsg;

class Sv2Transport final : public Transport
{
public:

// The sender side and receiver side of Sv2Transport 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 and what can be sent from it.

/** 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(responder)
* | start(initiator)
* | | /---------\
* | | | |
* v v v |
* HANDSHAKE_STEP_1 -> HANDSHAKE_STEP_2 -> APP -> APP_READY
*/
enum class RecvState : uint8_t {
/** Handshake Act 1: -> E */
HANDSHAKE_STEP_1,

/** Handshake Act 2: <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE */
HANDSHAKE_STEP_2,

/** Application packet.
*
* A packet is received, and decrypted/verified. If that succeeds, the
* state becomes APP_READY and the decrypted message is kept in m_message
* until it is retrieved 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(initiator)
* | start(responder)
* | |
* | |
* v v
* HANDSHAKE_STEP_1 -> HANDSHAKE_STEP_2 -> READY
*/
enum class SendState : uint8_t {
/** Handshake Act 1: -> E */
HANDSHAKE_STEP_1,

/** Handshake Act 2: <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE */
HANDSHAKE_STEP_2,

/** Normal sending state.
*
* In this state, the ciphers are initialized, so packets can be sent.
* In this state a message can be provided if the send buffer is empty. */
READY,
};

private:

/** Cipher state. */
Sv2Cipher m_cipher;

/** Whether we are the initiator side. */
const bool m_initiating;

/** Lock for receiver-side fields. */
mutable Mutex m_recv_mutex ACQUIRED_BEFORE(m_send_mutex);
/** Receive buffer; meaning is determined by m_recv_state. */
std::vector<uint8_t> m_recv_buffer GUARDED_BY(m_recv_mutex);
/** AAD expected in next received packet (currently used only for garbage). */
std::vector<uint8_t> m_recv_aad GUARDED_BY(m_recv_mutex);
/** 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};
/** The garbage sent, or to be sent (MAYBE_V1 and AWAITING_KEY state only). */
std::vector<uint8_t> m_send_garbage GUARDED_BY(m_send_mutex);
/** 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);
/** Put our ephemeral public key in the send buffer. */
void StartSendingHandshake() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_send_mutex, !m_recv_mutex);
/** Put second part of the handshake in the send buffer. */
void SendHandshakeReply() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_send_mutex, m_recv_mutex);
/** Process bytes in m_recv_buffer, while in HANDSHAKE_STEP_1 state. */
bool ProcessReceivedEphemeralKeyBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex);
/** Process bytes in m_recv_buffer, while in HANDSHAKE_STEP_2 state. */
bool ProcessReceivedHandshakeReplyBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex, !m_send_mutex);

/** Process bytes in m_recv_buffer, while in VERSION/APP state. */
bool ProcessReceivedPacketBytes() noexcept EXCLUSIVE_LOCKS_REQUIRED(m_recv_mutex);

/** In APP, the decrypted header, if m_recv_buffer.size() >=
* SV2_HEADER_ENCRYPTED_SIZE. Unspecified otherwise. */
Sv2NetHeader m_header GUARDED_BY(m_recv_mutex);
/* In APP_READY the last retrieved message. Unspecified otherwise */
Sv2NetMsg m_message GUARDED_BY(m_recv_mutex);

public:
/** Construct a Stratum v2 transport as the initiator
*
* @param[in] static_key a securely generated key
*/
Sv2Transport(CKey static_key, XOnlyPubKey responder_authority_key) noexcept;

/** Construct a Stratum v2 transport as the responder
*
* @param[in] static_key a securely generated key
*/
Sv2Transport(CKey static_key, Sv2SignatureNoiseMessage certificate) 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);

// Miscellaneous functions.
bool ShouldReconnectV1() const noexcept override { return false; };
Info GetInfo() const noexcept override EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex);

// Test only
uint256 NoiseHash() const { return m_cipher.GetHash(); };
RecvState GetRecvState() EXCLUSIVE_LOCKS_REQUIRED(!m_recv_mutex) {
AssertLockNotHeld(m_recv_mutex);
LOCK(m_recv_mutex);
return m_recv_state;
};
SendState GetSendState() EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex) {
AssertLockNotHeld(m_send_mutex);
LOCK(m_send_mutex);
return m_send_state;
};
};

/** Convert TransportProtocolType enum to a string value */
std::string RecvStateAsString(Sv2Transport::RecvState state);
std::string SendStateAsString(Sv2Transport::SendState state);

#endif // BITCOIN_COMMON_SV2_TRANSPORT_H
1 change: 1 addition & 0 deletions src/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -120,6 +120,7 @@ add_executable(test_bitcoin
span_tests.cpp
streams_tests.cpp
sv2_noise_tests.cpp
sv2_transport_tests.cpp
sync_tests.cpp
system_tests.cpp
timeoffsets_tests.cpp
389 changes: 389 additions & 0 deletions src/test/sv2_transport_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,389 @@
// Copyright (c) 2023-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <common/sv2_noise.h>
#include <common/sv2_transport.h>
#include <logging.h>
#include <serialize.h>
#include <span.h>
#include <streams.h>
#include <test/util/random.h>
#include <test/util/setup_common.h>
#include <node/timeoffsets.h>
#include <util/bitdeque.h>
#include <util/strencodings.h>
#include <util/string.h>

#include <boost/test/unit_test.hpp>

#include <algorithm>
#include <ios>
#include <memory>
#include <optional>
#include <string>

using namespace std::literals;
using node::Sv2NetMsg;
using node::Sv2CoinbaseOutputDataSizeMsg;
using node::Sv2MsgType;

BOOST_FIXTURE_TEST_SUITE(sv2_transport_tests, RegTestingSetup)

namespace {

/** A class for scenario-based tests of Sv2Transport
*
* Each Sv2TransportTester encapsulates a Sv2Transport (the one being tested),
* and can be told to interact with it. To do so, it also encapsulates a Sv2Cipher
* to act as the other side. A second Sv2Transport is not used, as doing so would
* not permit scenarios that involve sending invalid data.
*/
class Sv2TransportTester
{
FastRandomContext& m_rng;
std::unique_ptr<Sv2Transport> m_transport; //!< Sv2Transport being tested
std::unique_ptr<Sv2Cipher> m_peer_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_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<Sv2NetMsg> 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. */

explicit Sv2TransportTester(FastRandomContext& rng, bool test_initiator) : m_rng{rng}, m_test_initiator(test_initiator)
{
auto initiator_static_key{GenerateRandomKey()};
auto responder_static_key{GenerateRandomKey()};
auto responder_authority_key{GenerateRandomKey()};

// Create certificates
auto epoch_now = std::chrono::system_clock::now().time_since_epoch();
uint16_t version = 0;
uint32_t valid_from = static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::seconds>(epoch_now).count());
uint32_t valid_to = std::numeric_limits<unsigned int>::max();

auto responder_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to,
XOnlyPubKey(responder_static_key.GetPubKey()), responder_authority_key);

if (test_initiator) {
m_transport = std::make_unique<Sv2Transport>(initiator_static_key, XOnlyPubKey(responder_authority_key.GetPubKey()));
m_peer_cipher = std::make_unique<Sv2Cipher>(std::move(responder_static_key), std::move(responder_certificate));
} else {
m_transport = std::make_unique<Sv2Transport>(responder_static_key, responder_certificate);
m_peer_cipher = std::make_unique<Sv2Cipher>(std::move(initiator_static_key), XOnlyPubKey(responder_authority_key.GetPubKey()));
}
}

/** Data type returned by Interact:
*
* - std::nullopt: transport error occurred
* - otherwise: a vector of
* - std::nullopt: invalid message received
* - otherwise: a Sv2NetMsg retrieved
*/
using InteractResult = std::optional<std::vector<std::optional<Sv2NetMsg>>>;

void LogProgress(bool should_progress, bool progress, bool pretend_no_progress) {
if (!should_progress) {
BOOST_TEST_MESSAGE("[Interact] !should_progress");
} else if (!progress) {
BOOST_TEST_MESSAGE("[Interact] should_progress && !progress");
} else if (pretend_no_progress) {
BOOST_TEST_MESSAGE("[Interact] pretend !progress");
}
}

/** 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<Sv2NetMsg>> ret;
while (true) {
bool progress{false};
// Send bytes from m_to_send to the transport.
if (!m_to_send.empty()) {
size_t n_bytes_to_send = 1 + m_rng.randrange(m_to_send.size());
BOOST_TEST_MESSAGE(strprintf("[Interact] send %d of %d bytes", n_bytes_to_send, m_to_send.size()));
Span<const uint8_t> to_send = Span{m_to_send}.first(n_bytes_to_send);
size_t old_len = to_send.size();
if (!m_transport->ReceivedBytes(to_send)) {
BOOST_TEST_MESSAGE("[Interact] transport error");
return std::nullopt;
}
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.
bool should_progress = m_transport->ReceivedMessageComplete();
bool pretend_no_progress = m_rng.randbool();
LogProgress(should_progress, progress, pretend_no_progress);
if (should_progress && (!progress || pretend_no_progress)) {
bool dummy_reject_message = false;
CNetMessage net_msg = m_transport->GetReceivedMessage(std::chrono::microseconds(0), dummy_reject_message);
Sv2NetMsg msg(std::move(net_msg));
ret.emplace_back(std::move(msg));
progress = true;
}
// Enqueue a message to be sent by the transport to us.
should_progress = !m_msg_to_send.empty();
pretend_no_progress = m_rng.randbool();
LogProgress(should_progress, progress, pretend_no_progress);
if (should_progress && (!progress || pretend_no_progress)) {
BOOST_TEST_MESSAGE("Shoehorn into CSerializedNetMsg");
CSerializedNetMsg msg{m_msg_to_send.front()};
BOOST_TEST_MESSAGE("Call SetMessageToSend");
if (m_transport->SetMessageToSend(msg)) {
BOOST_TEST_MESSAGE("Finished SetMessageToSend");
m_msg_to_send.pop_front();
progress = true;
}
}
// Receive bytes from the transport.
const auto& [recv_bytes, _more, _m_type] = m_transport->GetBytesToSend(!m_msg_to_send.empty());
should_progress = !recv_bytes.empty();
pretend_no_progress = m_rng.randbool();
LogProgress(should_progress, progress, pretend_no_progress);
if (should_progress && (!progress || pretend_no_progress)) {
size_t to_receive = 1 + m_rng.randrange(recv_bytes.size());
BOOST_TEST_MESSAGE(strprintf("[Interact] receive %d of %d bytes", to_receive, 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;
}

/** Schedule bytes to be sent to the transport. */
void Send(Span<const uint8_t> data)
{
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Send: %s\n", HexStr(data));
m_to_send.insert(m_to_send.end(), data.begin(), data.end());
}

/** Schedule bytes to be sent to the transport. */
void Send(Span<const std::byte> data) { Send(MakeUCharSpan(data)); }

/** Schedule a message to be sent to us by the transport. */
void AddMessage(Sv2NetMsg msg)
{
m_msg_to_send.push_back(std::move(msg));
}

/**
* If we are the initiator, the send buffer should contain our ephemeral public
* key. Pass this to the peer cipher and clear the buffer.
*
* If we are the responder, put the peer ephemeral public key on our receive buffer.
*/
void ProcessHandshake1() {
if (m_test_initiator) {
BOOST_REQUIRE(m_received.size() == Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE);
m_peer_cipher->GetHandshakeState().ReadMsgEphemeralPK(MakeWritableByteSpan(m_received));
m_received.clear();
} else {
BOOST_REQUIRE(m_to_send.empty());
m_to_send.resize(Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE);
m_peer_cipher->GetHandshakeState().WriteMsgEphemeralPK(MakeWritableByteSpan(m_to_send));
}

}

/** Expect key to have been received from transport and process it.
*
* Many other Sv2TransportTester functions cannot be called until after
* ProcessHandshake2() has been called, as no encryption keys are set up before that point.
*/
void ProcessHandshake2()
{
if (m_test_initiator) {
BOOST_REQUIRE(m_to_send.empty());

// Have the peer cypher write the second part of the handshake into our receive buffer
m_to_send.resize(Sv2HandshakeState::HANDSHAKE_STEP2_SIZE);
m_peer_cipher->GetHandshakeState().WriteMsgES(MakeWritableByteSpan(m_to_send));

// At this point the peer is done with the handshake:
m_peer_cipher->FinishHandshake();
} else {
BOOST_REQUIRE(m_received.size() == Sv2HandshakeState::HANDSHAKE_STEP2_SIZE);
BOOST_REQUIRE(m_peer_cipher->GetHandshakeState().ReadMsgES(MakeWritableByteSpan(m_received)));
m_received.clear();

m_peer_cipher->FinishHandshake();
}
}

/** Schedule an encrypted packet with specified content to be sent to transport
* (only after ReceiveKey). */
void SendPacket(Sv2NetMsg msg)
{
// TODO: randomly break stuff

std::vector<std::byte> ciphertext;
const size_t encrypted_payload_size = Sv2Cipher::EncryptedMessageSize(msg.size());
ciphertext.resize(SV2_HEADER_ENCRYPTED_SIZE + encrypted_payload_size);
Span<std::byte> buffer_span{MakeWritableByteSpan(ciphertext)};

// Header
DataStream ss_header_plain{};
ss_header_plain << Sv2NetHeader(msg);
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Header: %s\n", HexStr(ss_header_plain));
Span<std::byte> header_encrypted{buffer_span.subspan(0, SV2_HEADER_ENCRYPTED_SIZE)};
BOOST_REQUIRE(m_peer_cipher->EncryptMessage(ss_header_plain, header_encrypted));

// Payload
Span<const std::byte> payload_plain = MakeByteSpan(msg);
// TODO: truncate very long messages, about 100 bytes at the start and end
// is probably enough for most debugging.
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Payload: %s\n", HexStr(payload_plain));
Span<std::byte> payload_encrypted{buffer_span.subspan(SV2_HEADER_ENCRYPTED_SIZE, encrypted_payload_size)};
BOOST_REQUIRE(m_peer_cipher->EncryptMessage(payload_plain, payload_encrypted));

// Schedule it for sending.
Send(ciphertext);
}

/** Expect application packet to have been received, with specified message type and payload.
* (only after ReceiveKey). */
void ReceiveMessage(Sv2NetMsg expected_msg)
{
// When processing a packet, at least enough bytes for its length descriptor must be received.
BOOST_REQUIRE(m_received.size() >= SV2_HEADER_ENCRYPTED_SIZE);

auto header_encrypted{MakeWritableByteSpan(m_received).subspan(0, SV2_HEADER_ENCRYPTED_SIZE)};
std::array<std::byte, SV2_HEADER_PLAIN_SIZE> header_plain;
BOOST_REQUIRE(m_peer_cipher->DecryptMessage(header_encrypted, header_plain));

// Decode header
DataStream ss_header{header_plain};
node::Sv2NetHeader header;
ss_header >> header;

BOOST_CHECK(header.m_msg_type == expected_msg.m_msg_type);

size_t expanded_size = Sv2Cipher::EncryptedMessageSize(header.m_msg_len);
BOOST_REQUIRE(m_received.size() >= SV2_HEADER_ENCRYPTED_SIZE + expanded_size);

Span<std::byte> encrypted_payload{MakeWritableByteSpan(m_received).subspan(SV2_HEADER_ENCRYPTED_SIZE, expanded_size)};
Span<std::byte> payload = encrypted_payload.subspan(0, header.m_msg_len);

BOOST_REQUIRE(m_peer_cipher->DecryptMessage(encrypted_payload, payload));

std::vector<uint8_t> decode_buffer;
decode_buffer.resize(header.m_msg_len);

std::transform(payload.begin(), payload.end(), decode_buffer.begin(),
[](std::byte b) { return static_cast<uint8_t>(b); });

// TODO: clear the m_received we used

Sv2NetMsg message{header.m_msg_type, std::move(decode_buffer)};

// TODO: compare payload
}

/** Test whether the transport's m_hash matches the other side. */
void CompareHash() const
{
BOOST_REQUIRE(m_transport);
BOOST_CHECK(m_transport->NoiseHash() == m_peer_cipher->GetHash());
}

void CheckRecvState(Sv2Transport::RecvState state) {
BOOST_REQUIRE(m_transport);
BOOST_CHECK_EQUAL(RecvStateAsString(m_transport->GetRecvState()), RecvStateAsString(state));
}

void CheckSendState(Sv2Transport::SendState state) {
BOOST_REQUIRE(m_transport);
BOOST_CHECK_EQUAL(SendStateAsString(m_transport->GetSendState()), SendStateAsString(state));
}

/** Introduce a bit error in the data scheduled to be sent. */
// void Damage()
// {
// BOOST_TEST_MESSAGE("[Interact] introduce a bit error");
// m_to_send[m_rng.randrange(m_to_send.size())] ^= (uint8_t{1} << m_rng.randrange(8));
// }
};

} // namespace

BOOST_AUTO_TEST_CASE(sv2_transport_initiator_test)
{
// A mostly normal scenario, testing a transport in initiator mode.
// Interact() introduces randomness, so run multiple times
for (int i = 0; i < 10; ++i) {
BOOST_TEST_MESSAGE(strprintf("\nIteration %d (initiator)", i));
Sv2TransportTester tester(m_rng, true);
// As the initiator, our ephemeral public key is immedidately put
// onto the buffer.
tester.CheckSendState(Sv2Transport::SendState::HANDSHAKE_STEP_2);
tester.CheckRecvState(Sv2Transport::RecvState::HANDSHAKE_STEP_2);
auto ret = tester.Interact();
BOOST_REQUIRE(ret && ret->empty());
tester.ProcessHandshake1();
ret = tester.Interact();
BOOST_REQUIRE(ret && ret->empty());
tester.ProcessHandshake2();
ret = tester.Interact();
BOOST_REQUIRE(ret && ret->empty());
tester.CheckSendState(Sv2Transport::SendState::READY);
tester.CheckRecvState(Sv2Transport::RecvState::APP);
tester.CompareHash();
}
}

BOOST_AUTO_TEST_CASE(sv2_transport_responder_test)
{
// Normal scenario, with a transport in responder node.
for (int i = 0; i < 10; ++i) {
BOOST_TEST_MESSAGE(strprintf("\nIteration %d (responder)", i));
Sv2TransportTester tester(m_rng, false);
tester.CheckSendState(Sv2Transport::SendState::HANDSHAKE_STEP_2);
tester.CheckRecvState(Sv2Transport::RecvState::HANDSHAKE_STEP_1);
tester.ProcessHandshake1();
auto ret = tester.Interact();
BOOST_REQUIRE(ret && ret->empty());
tester.CheckSendState(Sv2Transport::SendState::READY);
tester.CheckRecvState(Sv2Transport::RecvState::APP);

// Have the test cypher process our handshake reply
tester.ProcessHandshake2();
tester.CompareHash();

// Handshake complete, have the initiator send us a message:
Sv2CoinbaseOutputDataSizeMsg body{4000};
Sv2NetMsg msg{body};
BOOST_REQUIRE(msg.m_msg_type == Sv2MsgType::COINBASE_OUTPUT_DATA_SIZE);

tester.SendPacket(msg);
ret = tester.Interact();
BOOST_REQUIRE(ret && ret->size() == 1);
BOOST_CHECK((*ret)[0] &&
(*ret)[0]->m_msg_type == Sv2MsgType::COINBASE_OUTPUT_DATA_SIZE);

tester.CompareHash();

// Send a message back to the initiator
tester.AddMessage(msg);
ret = tester.Interact();
BOOST_REQUIRE(ret && ret->size() == 0);
tester.ReceiveMessage(msg);

// TODO: send / receive message larger than the chunk size
}
}


BOOST_AUTO_TEST_SUITE_END()

0 comments on commit 50b6eb8

Please sign in to comment.