forked from bitcoin/bitcoin
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
5 changed files
with
1,079 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |