diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99be137a24e26..1cfa2601dd2dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: run: | # Run tests on commits after the last merge commit and before the PR head commit # Use clang++, because it is a bit faster and uses less memory than g++ - git rebase --exec "echo Running test-one-commit on \$( git log -1 ) && CC=clang CXX=clang++ cmake -B build -DWITH_ZMQ=ON -DBUILD_GUI=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DWITH_BDB=ON -DWITH_NATPMP=ON -DWITH_MINIUPNPC=ON -DWITH_USDT=ON && cmake --build build -j $(nproc) && ctest --test-dir build -j $(nproc) && ./build/test/functional/test_runner.py -j $(( $(nproc) * 2 ))" ${{ env.TEST_BASE }} + git rebase --exec "echo Running test-one-commit on \$( git log -1 ) && CC=clang CXX=clang++ cmake -B build -DWITH_ZMQ=ON -DBUILD_GUI=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DWITH_BDB=ON -DWITH_NATPMP=ON -DWITH_MINIUPNPC=ON -DWITH_USDT=ON -DWITH_SV2=ON && cmake --build build -j $(nproc) && ctest --test-dir build -j $(nproc) && ./build/test/functional/test_runner.py -j $(( $(nproc) * 2 ))" ${{ env.TEST_BASE }} macos-native-arm64: name: 'macOS 14 native, arm64, no depends, sqlite only, gui' @@ -182,7 +182,7 @@ jobs: - name: Generate build system run: | - cmake -B build --preset vs2022-static -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT\scripts\buildsystems\vcpkg.cmake" -DBUILD_GUI=ON -DWITH_BDB=ON -DWITH_MINIUPNPC=ON -DWITH_ZMQ=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DWERROR=ON + cmake -B build --preset vs2022-static -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT\scripts\buildsystems\vcpkg.cmake" -DBUILD_GUI=ON -DWITH_BDB=ON -DWITH_MINIUPNPC=ON -DWITH_ZMQ=ON -DWITH_SV2=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DWERROR=ON - name: Save vcpkg binary cache uses: actions/cache/save@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ae988ac904d6..3d525193b0fb5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,6 +169,10 @@ if(WITH_MULTIPROCESS) ) endif() +option(WITH_SV2 "Enable Stratum v2 functionality." OFF) + +option(BUILD_SV2_LIB "Build experimental bitcoinsv2 library." OFF) + cmake_dependent_option(BUILD_GUI_TESTS "Build test_bitcoin-qt executable." ON "BUILD_GUI;BUILD_TESTS" OFF) if(BUILD_GUI) set(qt_components Core Gui Widgets LinguistTools) @@ -242,6 +246,8 @@ if(BUILD_FOR_FUZZING) set(WITH_NATPMP OFF) set(WITH_MINIUPNPC OFF) set(WITH_ZMQ OFF) + set(WITH_SV2 OFF) + set(BUILD_SV2_LIB OFF) set(BUILD_TESTS OFF) set(BUILD_GUI_TESTS OFF) set(BUILD_BENCH OFF) @@ -628,6 +634,8 @@ message(" ZeroMQ .............................. ${WITH_ZMQ}") message(" USDT tracing ........................ ${WITH_USDT}") message(" QR code (GUI) ....................... ${WITH_QRENCODE}") message(" DBus (GUI, Linux only) .............. ${WITH_DBUS}") +message(" Stratum v2 .......................... ${WITH_SV2}") +message(" libbitcoinsv2 (experimental) ........ ${BUILD_SV2_LIB}") message("Tests:") message(" test_bitcoin ........................ ${BUILD_TESTS}") message(" test_bitcoin-qt ..................... ${BUILD_GUI_TESTS}") diff --git a/ci/test/00_setup_env_mac_native.sh b/ci/test/00_setup_env_mac_native.sh index 1e197d8c71880..24f37a3f93688 100755 --- a/ci/test/00_setup_env_mac_native.sh +++ b/ci/test/00_setup_env_mac_native.sh @@ -11,7 +11,7 @@ export LC_ALL=C.UTF-8 export PIP_PACKAGES="--break-system-packages zmq" export GOAL="install" export CMAKE_GENERATOR="Ninja" -export BITCOIN_CONFIG="-DBUILD_GUI=ON -DWITH_ZMQ=ON -DWITH_MINIUPNPC=ON -DWITH_NATPMP=ON -DREDUCE_EXPORTS=ON" +export BITCOIN_CONFIG="-DBUILD_GUI=ON -DWITH_ZMQ=ON -DWITH_MINIUPNPC=ON -DWITH_NATPMP=ON -DWITH_SV2=ON -DREDUCE_EXPORTS=ON" export CI_OS_NAME="macos" export NO_DEPENDS=1 export OSX_SDK="" diff --git a/ci/test/03_test_script.sh b/ci/test/03_test_script.sh index 1c1b5fa545b0c..c07243a61c889 100755 --- a/ci/test/03_test_script.sh +++ b/ci/test/03_test_script.sh @@ -119,7 +119,7 @@ BASE_BUILD_DIR=${BASE_BUILD_DIR:-$BASE_SCRATCH_DIR/build-$HOST} mkdir -p "${BASE_BUILD_DIR}" cd "${BASE_BUILD_DIR}" -BITCOIN_CONFIG_ALL="$BITCOIN_CONFIG_ALL -DENABLE_EXTERNAL_SIGNER=ON -DCMAKE_INSTALL_PREFIX=$BASE_OUTDIR" +BITCOIN_CONFIG_ALL="$BITCOIN_CONFIG_ALL -DENABLE_EXTERNAL_SIGNER=ON -DWITH_SV2=ON -DCMAKE_INSTALL_PREFIX=$BASE_OUTDIR" if [[ "${RUN_TIDY}" == "true" ]]; then BITCOIN_CONFIG_ALL="$BITCOIN_CONFIG_ALL -DCMAKE_EXPORT_COMPILE_COMMANDS=ON" diff --git a/doc/design/libraries.md b/doc/design/libraries.md index 24185bf4776df..8448fb7011d69 100644 --- a/doc/design/libraries.md +++ b/doc/design/libraries.md @@ -14,6 +14,7 @@ | *libbitcoin_wallet* | Wallet functionality used by *bitcoind* and *bitcoin-wallet* executables. | | *libbitcoin_wallet_tool* | Lower-level wallet functionality used by *bitcoin-wallet* executable. | | *libbitcoin_zmq* | [ZeroMQ](../zmq.md) functionality used by *bitcoind* and *bitcoin-qt* executables. | +| *libbitcoin_sv2* | Stratum v2 functionality (usage TBD) | ## Conventions diff --git a/libbitcoinsv2.pc.in b/libbitcoinsv2.pc.in new file mode 100644 index 0000000000000..391325e3c64b0 --- /dev/null +++ b/libbitcoinsv2.pc.in @@ -0,0 +1,11 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: @PACKAGE_NAME@ sv2 library +Description: Experimental library for the Bitcoin Core Stratum v2 functionality. +Version: @PACKAGE_VERSION@ +Libs: -L${libdir} -lbitcoinsv2 +Libs.private: -L${libdir} @LIBS_PRIVATE@ +Cflags: -I${includedir} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 895c17541f601..ce376306b5a55 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,9 @@ add_subdirectory(util) if(WITH_MULTIPROCESS) add_subdirectory(ipc) endif() +if(WITH_SV2) + add_subdirectory(sv2) +endif() #============================= # secp256k1 subtree diff --git a/src/logging.cpp b/src/logging.cpp index d04db767e6e9b..1314629fd1eb8 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -197,6 +197,7 @@ static const std::map> LOG_CATEGORIES_ {"blockstorage", BCLog::BLOCKSTORAGE}, {"txreconciliation", BCLog::TXRECONCILIATION}, {"scan", BCLog::SCAN}, + {"sv2", BCLog::SV2}, {"txpackages", BCLog::TXPACKAGES}, }; diff --git a/src/logging.h b/src/logging.h index 8605c8cd64f75..08f8ac241bb10 100644 --- a/src/logging.h +++ b/src/logging.h @@ -71,6 +71,7 @@ namespace BCLog { TXRECONCILIATION = (CategoryMask{1} << 26), SCAN = (CategoryMask{1} << 27), TXPACKAGES = (CategoryMask{1} << 28), + SV2 = (CategoryMask{1} << 30), ALL = ~NONE, }; enum class Level { diff --git a/src/pubkey.h b/src/pubkey.h index b4666aad228e1..798687de1fe5a 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -319,7 +319,7 @@ struct EllSwiftPubKey /** Construct a new ellswift public key from a given serialization. */ EllSwiftPubKey(Span ellswift) noexcept; - /** Decode to normal compressed CPubKey (for debugging purposes). */ + /** Decode to normal compressed CPubKey. */ CPubKey Decode() const; // Read-only access for serialization. diff --git a/src/sv2/CMakeLists.txt b/src/sv2/CMakeLists.txt new file mode 100644 index 0000000000000..98fe441ce5b83 --- /dev/null +++ b/src/sv2/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (c) 2024-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit/. + +add_library(bitcoin_sv2 STATIC EXCLUDE_FROM_ALL + noise.cpp + transport.cpp +) + +target_link_libraries(bitcoin_sv2 + PRIVATE + core_interface + bitcoin_clientversion + bitcoin_crypto + $<$:ws2_32> +) + +if (BUILD_SV2_LIB) + install(FILES bitcoinsv2.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + + configure_file(${PROJECT_SOURCE_DIR}/libbitcoinsv2.pc.in ${PROJECT_BINARY_DIR}/libbitcoinsv2.pc @ONLY) + install(FILES ${PROJECT_BINARY_DIR}/libbitcoinsv2.pc DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") + +endif() diff --git a/src/sv2/bitcoinsv2.h b/src/sv2/bitcoinsv2.h new file mode 100644 index 0000000000000..d0c696453d80c --- /dev/null +++ b/src/sv2/bitcoinsv2.h @@ -0,0 +1,63 @@ +// Copyright (c) 2024-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_SV2_BITCOINSV2_H +#define BITCOIN_SV2_BITCOINSV2_H + +#ifndef __cplusplus +#include +#include +#include +#else +#include +#include +#endif // __cplusplus + + +#if !defined(BITCOINSV2_GNUC_PREREQ) +#if defined(__GNUC__) && defined(__GNUC_MINOR__) +#define BITCOINSV2_GNUC_PREREQ(_maj, _min) \ + ((__GNUC__ << 16) + __GNUC_MINOR__ >= ((_maj) << 16) + (_min)) +#else +#define BITCOINSV2_GNUC_PREREQ(_maj, _min) 0 +#endif +#endif + +/* Warning attributes */ +#if defined(__GNUC__) && BITCOINSV2_GNUC_PREREQ(3, 4) +#define BITCOINSV2_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) +#else +#define BITCOINSV2_WARN_UNUSED_RESULT +#endif +#if !defined(BITCOINSV2_BUILD) && defined(__GNUC__) && BITCOINSV2_GNUC_PREREQ(3, 4) +#define BITCOINSV2_ARG_NONNULL(_x) __attribute__((__nonnull__(_x))) +#else +#define BITCOINSV2_ARG_NONNULL(_x) +#endif + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * ------ Context ------ + * + * The library provides Stratum v2 functionality. + * + * ------ Error handling ------ + * + * TODO + * + * ------ Pointer and argument conventions ------ + * + * TODO + */ + +// TODO: some actual methods + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // BITCOIN_SV2_BITCOINSV2_H diff --git a/src/sv2/messages.h b/src/sv2/messages.h new file mode 100644 index 0000000000000..fbbe68d63d6cf --- /dev/null +++ b/src/sv2/messages.h @@ -0,0 +1,209 @@ +// 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_SV2_MESSAGES_H +#define BITCOIN_SV2_MESSAGES_H + +#include // for CSerializedNetMsg and CNetMessage +#include +#include +#include +#include + +namespace node { +/** + * A type used as the message length field in stratum v2 messages. + */ +using u24_t = uint8_t[3]; + +/** + * All the stratum v2 message types handled by the template provider. + */ +enum class Sv2MsgType : uint8_t { + COINBASE_OUTPUT_DATA_SIZE = 0x70, +}; + +/** + * Set the coinbase outputs data len for the outputs that the client wants to add to the coinbase. + * The template provider MUST NOT provide NewWork messages which would represent consensus-invalid blocks once this + * additional size — along with a maximally-sized (100 byte) coinbase field — is added. + */ +struct Sv2CoinbaseOutputDataSizeMsg +{ + /** + * The default message type value for this Stratum V2 message. + */ + static constexpr auto m_msg_type = Sv2MsgType::COINBASE_OUTPUT_DATA_SIZE; + + /** + * The maximum additional serialized bytes which the pool will add in coinbase transaction outputs. + */ + uint32_t m_coinbase_output_max_additional_size; + + template + void Serialize(Stream& s) const + { + s << m_coinbase_output_max_additional_size; + }; + + + template + void Unserialize(Stream& s) + { + s >> m_coinbase_output_max_additional_size; + } +}; + +/** + * Header for all stratum v2 messages. Each header must contain the message type, + * the length of the serialized message and a 2 byte extension field currently + * not utilised by the template provider. + */ +class Sv2NetHeader +{ +public: + /** + * Unique identifier of the message. + */ + Sv2MsgType m_msg_type; + + /** + * Serialized length of the message. + */ + uint32_t m_msg_len; + + Sv2NetHeader() = default; + explicit Sv2NetHeader(Sv2MsgType msg_type, uint32_t msg_len) : m_msg_type{msg_type}, m_msg_len{msg_len} {}; + + template + void Serialize(Stream& s) const + { + // The template provider currently does not use the extension_type field, + // but the field is still required for all headers. + uint16_t extension_type = 0; + + u24_t msg_len; + msg_len[2] = (m_msg_len >> 16) & 0xff; + msg_len[1] = (m_msg_len >> 8) & 0xff; + msg_len[0] = m_msg_len & 0xff; + + s << extension_type + << static_cast(m_msg_type) + << msg_len; + }; + + template + void Unserialize(Stream& s) + { + // Ignore the first 2 bytes (extension type) as the template provider currently doesn't + // interpret this field. + s.ignore(2); + + uint8_t msg_type; + s >> msg_type; + m_msg_type = static_cast(msg_type); + + u24_t msg_len_bytes; + for (unsigned int i = 0; i < sizeof(u24_t); ++i) { + s >> msg_len_bytes[i]; + } + + m_msg_len = msg_len_bytes[2]; + m_msg_len = m_msg_len << 8 | msg_len_bytes[1]; + m_msg_len = m_msg_len << 8 | msg_len_bytes[0]; + } +}; + +/** + * The networked form for all stratum v2 messages, contains a header and a serialized + * payload from a referenced stratum v2 message. + */ +class Sv2NetMsg +{ +public: + Sv2MsgType m_msg_type; + std::vector m_msg; + + explicit Sv2NetMsg(const Sv2MsgType msg_type, const std::vector&& msg) : m_msg_type{msg_type}, m_msg{msg} {}; + + // Unwrap CSerializedNetMsg + Sv2NetMsg(CSerializedNetMsg&& net_msg) + { + Assume(net_msg.m_type == ""); + DataStream ss(MakeByteSpan(net_msg.data)); + Unserialize(ss); + }; + + // Unwrap CNetMsg + Sv2NetMsg(CNetMessage net_msg) + { + Unserialize(net_msg.m_recv); + }; + + operator CSerializedNetMsg() + { + CSerializedNetMsg net_msg; + net_msg.m_type = ""; + DataStream ser; + Serialize(ser); + net_msg.data.resize(ser.size()); + std::transform(ser.begin(), ser.end(), net_msg.data.begin(), + [](std::byte b) { return static_cast(b); }); + return net_msg; + } + + operator CNetMessage() + { + DataStream msg; + Serialize(msg); + CNetMessage ret{std::move(msg)}; + return ret; + } + + /** + * Serializes the message M and sets an Sv2 network header. + * @throws std::ios_base or std::out_of_range errors. + */ + template + explicit Sv2NetMsg(const M& msg) + { + m_msg_type = msg.m_msg_type; + + // Serialize the sv2 message. + VectorWriter{m_msg, 0, msg}; + } + + unsigned char* data() { return m_msg.data(); } + size_t size() { return m_msg.size(); } + + operator Sv2NetHeader() + { + Sv2NetHeader hdr; + hdr.m_msg_type = m_msg_type; + hdr.m_msg_len = static_cast(m_msg.size()); + return hdr; + } + + template + void Unserialize(Stream& s) + { + uint8_t msg_type; + s >> msg_type; + m_msg_type = static_cast(msg_type); + m_msg.resize(s.size()); + s.read(MakeWritableByteSpan(m_msg)); + } + + template + void Serialize(Stream& s) const + { + s << static_cast(m_msg_type); + s.write(MakeByteSpan(m_msg)); + } + +}; + +} + +#endif // BITCOIN_SV2_MESSAGES_H diff --git a/src/sv2/noise.cpp b/src/sv2/noise.cpp new file mode 100644 index 0000000000000..62336e3715ac7 --- /dev/null +++ b/src/sv2/noise.cpp @@ -0,0 +1,510 @@ +// 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 + +#include +#include +#include +#include +#include +#include + +Sv2SignatureNoiseMessage::Sv2SignatureNoiseMessage(uint16_t version, uint32_t valid_from, uint32_t valid_to, const XOnlyPubKey& static_key, const CKey& authority_key) : m_version{version}, m_valid_from{valid_from}, m_valid_to{valid_to}, m_static_key{static_key} +{ + SignSchnorr(authority_key, m_sig); +} + +uint256 Sv2SignatureNoiseMessage::GetHash() +{ + DataStream ss{}; + ss << m_version + << m_valid_from + << m_valid_to + << m_static_key; + + LogTrace(BCLog::SV2, "Certificate hashed data: %s\n", HexStr(ss)); + + CSHA256 hasher; + hasher.Write(reinterpret_cast(&(*ss.begin())), ss.end() - ss.begin()); + + uint256 hash_output; + hasher.Finalize(hash_output.begin()); + return hash_output; +} + +bool Sv2SignatureNoiseMessage::Validate(XOnlyPubKey authority_key) +{ + if (m_version > 0) { + LogTrace(BCLog::SV2, "Invalid certificate version: %d\n", m_version); + return false; + } + auto now{GetTime()}; + if (std::chrono::seconds{m_valid_from} > now) { + LogTrace(BCLog::SV2, "Certificate valid from is in the future: %d\n", m_valid_from); + return false; + } + if (std::chrono::seconds{m_valid_to} < now) { + LogTrace(BCLog::SV2, "Certificate expired: %d\n", m_valid_to); + return false; + } + + if (!authority_key.VerifySchnorr(this->GetHash(), m_sig)) { + LogTrace(BCLog::SV2, "Certificate signature is invalid\n"); + return false; + } + return true; +} + +void Sv2SignatureNoiseMessage::SignSchnorr(const CKey& authority_key, Span sig) +{ + authority_key.SignSchnorr(this->GetHash(), sig, nullptr, {}); +} + +Sv2CipherState::Sv2CipherState(NoiseHash&& key) : m_key(std::move(key)) {}; + +bool Sv2CipherState::DecryptWithAd(Span associated_data, Span ciphertext, Span plain) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + if (m_nonce == UINT64_MAX) { + // This nonce value is reserved, see chapter 5.1 of the Noise paper. + LogTrace(BCLog::SV2, "Nonce exceeds maximum value\n"); + return false; + } + AEADChaCha20Poly1305::Nonce96 nonce = {0, m_nonce}; + auto key = MakeByteSpan(m_key); + AEADChaCha20Poly1305 aead{key}; + if (!aead.Decrypt(ciphertext, associated_data, nonce, plain)) { + LogTrace(BCLog::SV2, "Message decryption failed\n"); + return false; + } + // Only increase nonce if decryption succeeded + m_nonce++; + return true; +} + +bool Sv2CipherState::EncryptWithAd(Span associated_data, Span plain, Span ciphertext) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + if (m_nonce == UINT64_MAX) { + // This nonce value is reserved, see chapter 5.1 of the Noise paper. + LogTrace(BCLog::SV2, "Nonce exceeds maximum value\n"); + return false; + } + AEADChaCha20Poly1305::Nonce96 nonce = {0, m_nonce++}; + auto key = MakeByteSpan(m_key); + AEADChaCha20Poly1305 aead{key}; + aead.Encrypt(plain, associated_data, nonce, ciphertext); + return true; +} + +bool Sv2CipherState::EncryptMessage(Span plain, Span ciphertext) +{ + Assume(ciphertext.size() == Sv2Cipher::EncryptedMessageSize(plain.size())); + + std::vector ad; // No associated data + + constexpr size_t max_chunk_size = NOISE_MAX_CHUNK_SIZE - Poly1305::TAGLEN; + size_t num_chunks = (plain.size() + max_chunk_size - 1) / max_chunk_size; + if (num_chunks > 1) { + LogTrace(BCLog::SV2, + "Split into %d chunks (max %d bytes)\n", + num_chunks, max_chunk_size); + } + + // Copy input bytes into output buffer + const std::vector padding(Poly1305::TAGLEN, std::byte(0)); + for (size_t i = 0; i < num_chunks; ++i) { + size_t chunk_start = i * max_chunk_size; + size_t chunk_end = std::min(chunk_start + max_chunk_size, plain.size()); + size_t chunk_size = chunk_end - chunk_start; + const auto encrypted_chunk_start = ciphertext.begin() + i * NOISE_MAX_CHUNK_SIZE; + std::copy(plain.begin() + chunk_start, plain.begin() + chunk_start + chunk_size, encrypted_chunk_start); + std::copy(padding.begin(), padding.end(), encrypted_chunk_start + chunk_size); + } + + // Encrypt each chunk + size_t bytes_written = 0; + for (size_t i = 0; i < num_chunks; ++i) { + size_t chunk_size = std::min(ciphertext.size() - bytes_written, NOISE_MAX_CHUNK_SIZE); + Span chunk = ciphertext.subspan(bytes_written, chunk_size); + Span chunk_plain = ciphertext.subspan(bytes_written, chunk_size - Poly1305::TAGLEN); + if (!EncryptWithAd(ad, chunk_plain, chunk)) { + return false; + } + bytes_written += chunk.size(); + } + + Assume(bytes_written == ciphertext.size()); + return true; +} + +bool Sv2CipherState::DecryptMessage(Span ciphertext, Span plain) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + size_t processed = 0; + size_t plain_position = 0; + std::vector ad; // No associated data + + while (processed < ciphertext.size()) { + size_t chunk_size = std::min(ciphertext.size() - processed, NOISE_MAX_CHUNK_SIZE); + Span chunk_cipher = ciphertext.subspan(processed, chunk_size); + Span chunk_plain = plain.subspan(plain_position, chunk_size - Poly1305::TAGLEN); + if (!DecryptWithAd(ad, chunk_cipher, chunk_plain)) return false; + processed += chunk_size; + plain_position += chunk_size - Poly1305::TAGLEN; + } + + return true; +} + +void Sv2SymmetricState::MixHash(const Span input) +{ + m_hash_output = (HashWriter{} << m_hash_output << input).GetSHA256(); +} + +void Sv2SymmetricState::MixKey(const Span input_key_material) +{ + NoiseHash out0; + NoiseHash out1; + HKDF2(input_key_material, out0, out1); + m_chaining_key = std::move(out0); + m_cipher_state = Sv2CipherState{std::move(out1)}; +} + +std::string Sv2SymmetricState::GetChainingKey() +{ + return HexStr(m_chaining_key); +} + +void Sv2SymmetricState::LogChainingKey() +{ + LogTrace(BCLog::SV2, "Chaining key: %s\n", GetChainingKey()); +} + +void Sv2SymmetricState::HKDF2(const Span input_key_material, NoiseHash& out0, NoiseHash& out1) +{ + NoiseHash tmp_key; + CHMAC_SHA256 tmp_mac(m_chaining_key.data(), m_chaining_key.size()); + tmp_mac.Write(UCharCast(input_key_material.data()), input_key_material.size()); + tmp_mac.Finalize(tmp_key.data()); + + CHMAC_SHA256 out0_mac(tmp_key.data(), tmp_key.size()); + uint8_t one[1]{0x1}; + out0_mac.Write(one, 1); + out0_mac.Finalize(out0.data()); + + std::vector in1; + in1.reserve(HASHLEN + 1); + std::copy(out0.begin(), out0.end(), std::back_inserter(in1)); + in1.push_back(0x02); + + CHMAC_SHA256 out1_mac(tmp_key.data(), tmp_key.size()); + out1_mac.Write(&in1[0], in1.size()); + out1_mac.Finalize(out1.data()); +} + +bool Sv2SymmetricState::EncryptAndHash(Span plain, Span ciphertext) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + if (!m_cipher_state.EncryptWithAd(MakeByteSpan(m_hash_output), plain, ciphertext)) { + return false; + } + MixHash(ciphertext); + return true; +} + +bool Sv2SymmetricState::DecryptAndHash(Span ciphertext, Span plain) +{ + Assume(Sv2Cipher::EncryptedMessageSize(plain.size()) == ciphertext.size()); + + // The handshake requires mix hashing the cipher text NOT the decrypted + // plaintext. + std::vector ciphertext_copy; + ciphertext_copy.assign(ciphertext.begin(), ciphertext.end()); + + bool res = m_cipher_state.DecryptWithAd(MakeByteSpan(m_hash_output), ciphertext, plain); + if (!res) return false; + MixHash(ciphertext_copy); + return true; +} + +std::array Sv2SymmetricState::Split() +{ + NoiseHash send_key; + NoiseHash recv_key; + HKDF2({}, send_key, recv_key); + return {Sv2CipherState{std::move(send_key)}, Sv2CipherState{std::move(recv_key)}}; +} + +uint256 Sv2SymmetricState::GetHashOutput() +{ + return m_hash_output; +} + +void Sv2HandshakeState::SetEphemeralKey(CKey&& key) +{ + m_ephemeral_key = key; + m_ephemeral_ellswift_pk = m_ephemeral_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); +}; + +void Sv2HandshakeState::GenerateEphemeralKey() noexcept +{ + Assume(!m_ephemeral_key.size()); + LogTrace(BCLog::SV2, "Generate ephemeral key\n"); + SetEphemeralKey(GenerateRandomKey()); +}; + +void Sv2HandshakeState::WriteMsgEphemeralPK(Span msg) +{ + if (msg.size() < ELLSWIFT_PUB_KEY_SIZE) { + throw std::runtime_error(strprintf("Invalid message size: %d bytes < %d", msg.size(), ELLSWIFT_PUB_KEY_SIZE)); + } + + if (!m_ephemeral_key.IsValid()) { + GenerateEphemeralKey(); + } + + LogTrace(BCLog::SV2, "Write our ephemeral key\n"); + std::copy(m_ephemeral_ellswift_pk.begin(), m_ephemeral_ellswift_pk.end(), msg.begin()); + + m_symmetric_state.MixHash(msg.subspan(0, ELLSWIFT_PUB_KEY_SIZE)); + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + std::vector empty; + m_symmetric_state.MixHash(empty); +} + +void Sv2HandshakeState::ReadMsgEphemeralPK(Span msg) +{ + LogTrace(BCLog::SV2, "Read their ephemeral key\n"); + Assume(msg.size() == ELLSWIFT_PUB_KEY_SIZE); + m_remote_ephemeral_ellswift_pk = EllSwiftPubKey(msg); + + m_symmetric_state.MixHash(msg.subspan(0, ELLSWIFT_PUB_KEY_SIZE)); + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + std::vector empty; + m_symmetric_state.MixHash(empty); +} + +void Sv2HandshakeState::WriteMsgES(Span msg) +{ + if (msg.size() < HANDSHAKE_STEP2_SIZE) { + throw std::runtime_error(strprintf("Invalid message size: %d bytes < %d", msg.size(), HANDSHAKE_STEP2_SIZE)); + } + + ssize_t bytes_written = 0; + + if (!m_ephemeral_key.IsValid()) { + GenerateEphemeralKey(); + } + + // Send our ephemeral pk. + LogTrace(BCLog::SV2, "Write our ephemeral key\n"); + std::copy(m_ephemeral_ellswift_pk.begin(), m_ephemeral_ellswift_pk.end(), msg.begin()); + + m_symmetric_state.MixHash(msg.subspan(0, ELLSWIFT_PUB_KEY_SIZE)); + bytes_written += ELLSWIFT_PUB_KEY_SIZE; + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + LogTrace(BCLog::SV2, "Perform ECDH with the remote ephemeral key\n"); + ECDHSecret ecdh_secret{m_ephemeral_key.ComputeBIP324ECDHSecret(m_remote_ephemeral_ellswift_pk, + m_ephemeral_ellswift_pk, + /*initiating=*/false)}; + + LogTrace(BCLog::SV2, "Mix key with ECDH result: ephemeral ours -- remote ephemeral\n"); + m_symmetric_state.MixKey(ecdh_secret); + m_symmetric_state.LogChainingKey(); + + // Send our static pk. + LogTrace(BCLog::SV2, "Encrypt and write our static key\n"); + + if (!m_symmetric_state.EncryptAndHash(m_static_ellswift_pk, msg.subspan(ELLSWIFT_PUB_KEY_SIZE, ELLSWIFT_PUB_KEY_SIZE + Poly1305::TAGLEN))) { + // This should never happen + Assume(false); + throw std::runtime_error("Failed to encrypt our ephemeral key\n"); + } + + bytes_written += ELLSWIFT_PUB_KEY_SIZE + Poly1305::TAGLEN; + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + LogTrace(BCLog::SV2, "Perform ECDH between our static and remote ephemeral key\n"); + ECDHSecret ecdh_static_secret{m_static_key.ComputeBIP324ECDHSecret(m_remote_ephemeral_ellswift_pk, + m_static_ellswift_pk, + /*initiating=*/false)}; + LogTrace(BCLog::SV2, "ECDH result: %s\n", HexStr(ecdh_static_secret)); + + LogTrace(BCLog::SV2, "Mix key with ECDH result: static ours -- remote ephemeral\n"); + m_symmetric_state.MixKey(ecdh_static_secret); + m_symmetric_state.LogChainingKey(); + + // Serialize our digital signature noise message and encrypt. + DataStream ss{}; + Assume(m_certificate); + ss << m_certificate.value(); + Assume(ss.size() == Sv2SignatureNoiseMessage::SIZE); + + LogTrace(BCLog::SV2, "Encrypt certificate: %s\n", HexStr(ss)); + if (!m_symmetric_state.EncryptAndHash(ss, msg.subspan(bytes_written, Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN))) { + // This should never happen + Assume(false); + throw std::runtime_error("Failed to encrypt our certificate\n"); + } + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + bytes_written += Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN; + Assume(bytes_written == HANDSHAKE_STEP2_SIZE); +} + +bool Sv2HandshakeState::ReadMsgES(Span msg) +{ + Assume(msg.size() == HANDSHAKE_STEP2_SIZE); + ssize_t bytes_read = 0; + + // Read the remote ephmeral key from the msg and decrypt. + LogTrace(BCLog::SV2, "Read remote ephemeral key\n"); + m_remote_ephemeral_ellswift_pk = EllSwiftPubKey(msg.subspan(0, ELLSWIFT_PUB_KEY_SIZE)); + bytes_read += ELLSWIFT_PUB_KEY_SIZE; + + m_symmetric_state.MixHash(m_remote_ephemeral_ellswift_pk); + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + LogTrace(BCLog::SV2, "Perform ECDH with the remote ephemeral key\n"); + ECDHSecret ecdh_secret{m_ephemeral_key.ComputeBIP324ECDHSecret(m_remote_ephemeral_ellswift_pk, + m_ephemeral_ellswift_pk, + /*initiating=*/true)}; + + LogTrace(BCLog::SV2, "Mix key with ECDH result: ephemeral ours -- remote ephemeral\n"); + m_symmetric_state.MixKey(ecdh_secret); + m_symmetric_state.LogChainingKey(); + + LogTrace(BCLog::SV2, "Decrypt remote static key\n"); + std::array remote_static_key_bytes; + bool res = m_symmetric_state.DecryptAndHash(msg.subspan(ELLSWIFT_PUB_KEY_SIZE, ELLSWIFT_PUB_KEY_SIZE + Poly1305::TAGLEN), remote_static_key_bytes); + if (!res) return false; + bytes_read += ELLSWIFT_PUB_KEY_SIZE + Poly1305::TAGLEN; + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + // Load remote static key from the decryted msg + m_remote_static_ellswift_pk = EllSwiftPubKey(remote_static_key_bytes); + + LogTrace(BCLog::SV2, "Perform ECDH on the remote static key\n"); + ECDHSecret ecdh_static_secret{m_ephemeral_key.ComputeBIP324ECDHSecret(m_remote_static_ellswift_pk, + m_ephemeral_ellswift_pk, + /*initiating=*/true)}; + LogTrace(BCLog::SV2, "ECDH result: %s\n", HexStr(ecdh_static_secret)); + + LogTrace(BCLog::SV2, "Mix key with ECDH result: ephemeral ours -- remote static\n"); + m_symmetric_state.MixKey(ecdh_static_secret); + m_symmetric_state.LogChainingKey(); + + LogTrace(BCLog::SV2, "Decrypt remote certificate\n"); + std::array remote_cert_bytes; + res = m_symmetric_state.DecryptAndHash(msg.subspan(bytes_read, Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN), remote_cert_bytes); + if (!res) return false; + bytes_read += (Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN); + + LogTrace(BCLog::SV2, "Validate remote certificate\n"); + DataStream ss_cert(remote_cert_bytes); + Sv2SignatureNoiseMessage cert; + ss_cert >> cert; + cert.m_static_key = XOnlyPubKey(m_remote_static_ellswift_pk.Decode()); + Assume(m_authority_pubkey); + if (!cert.Validate(m_authority_pubkey.value())) { + // We initiated the connection, so it's safe to unconditionally log this: + LogWarning("Invalid certificate: %s\n", HexStr(remote_cert_bytes)); + return false; + } + + LogTrace(BCLog::SV2, "Mix hash: %s\n", HexStr(m_symmetric_state.GetHashOutput())); + + Assume(bytes_read == HANDSHAKE_STEP2_SIZE); + return true; +} + +std::array Sv2HandshakeState::SplitSymmetricState() +{ + return m_symmetric_state.Split(); +} + +uint256 Sv2HandshakeState::GetHashOutput() +{ + return m_symmetric_state.GetHashOutput(); +} + +Sv2Cipher::Sv2Cipher(CKey&& static_key, XOnlyPubKey authority_pubkey) +{ + m_handshake_state = std::make_unique(std::move(static_key), authority_pubkey); + m_initiator = true; +} + +Sv2Cipher::Sv2Cipher(CKey&& static_key, Sv2SignatureNoiseMessage&& certificate) +{ + m_handshake_state = std::make_unique(std::move(static_key), std::move(certificate)); + m_initiator = false; +} + +Sv2HandshakeState& Sv2Cipher::GetHandshakeState() +{ + Assume(m_handshake_state); + return *m_handshake_state; +} + +void Sv2Cipher::FinishHandshake() +{ + Assume(m_handshake_state); + + auto cipher_state{m_handshake_state->SplitSymmetricState()}; + + m_hash = m_handshake_state->GetHashOutput(); + + m_cs1 = std::move(cipher_state[0]); + m_cs2 = std::move(cipher_state[1]); + + m_handshake_state.reset(); +} + +size_t Sv2Cipher::EncryptedMessageSize(const size_t msg_len) +{ + constexpr size_t chunk_size = NOISE_MAX_CHUNK_SIZE - Poly1305::TAGLEN; + const size_t num_chunks = (msg_len + chunk_size - 1) / chunk_size; + return msg_len + (num_chunks * Poly1305::TAGLEN); +} + +bool Sv2Cipher::DecryptMessage(Span ciphertext, Span plain) +{ + Assume(EncryptedMessageSize(plain.size()) == ciphertext.size()); + + if (m_initiator) { + return m_cs2.DecryptMessage(ciphertext, plain); + } else { + return m_cs1.DecryptMessage(ciphertext, plain); + } +} + +bool Sv2Cipher::EncryptMessage(Span input, Span output) +{ + Assume(output.size() == Sv2Cipher::EncryptedMessageSize(input.size())); + + if (m_initiator) { + if (!m_cs1.EncryptMessage(input, output)) return false; + } else { + if (!m_cs2.EncryptMessage(input, output)) return false; + } + return true; +} + +uint256 Sv2Cipher::GetHash() const +{ + return m_hash; +} diff --git a/src/sv2/noise.h b/src/sv2/noise.h new file mode 100644 index 0000000000000..f72cf3df44e6d --- /dev/null +++ b/src/sv2/noise.h @@ -0,0 +1,299 @@ +// 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_SV2_NOISE_H +#define BITCOIN_SV2_NOISE_H + +#include +#include +#include +#include +#include +#include +#include + +/** The Noise Protocol Framework + * https://noiseprotocol.org/noise.html + * Revision 38, 2018-07-11 + * + * Stratum v2 handshake and cipher specification: + * https://github.com/stratum-mining/sv2-spec/blob/main/04-Protocol-Security.md + */ + +/** Section 3: All Noise messages are less than or equal to 65535 bytes in length. */ +static constexpr size_t NOISE_MAX_CHUNK_SIZE = 65535; + +static constexpr size_t HASHLEN{32}; +using NoiseHash = std::array; + +/** Simple certificate for the static key signed by the authority key. + * See 4.5.2 and 4.5.3 of the Stratum v2 spec. + */ +class Sv2SignatureNoiseMessage +{ +public: + /** Size of a Schnorr signature. */ + static constexpr size_t SCHNORR_SIGNATURE_SIZE = 64; + /** Size of serialized message, which does not include the static key. */ + static constexpr size_t SIZE = 2 + 4 + 4 + SCHNORR_SIGNATURE_SIZE; + +private: + uint16_t m_version = 0; + uint32_t m_valid_from = 0; + uint32_t m_valid_to = 0; + std::array m_sig; + + /** Hash of version, valid from/to and the static key. */ + uint256 GetHash(); + void SignSchnorr(const CKey& authority_key, Span sig); + +public: + Sv2SignatureNoiseMessage() = default; + Sv2SignatureNoiseMessage(uint16_t version, uint32_t valid_from, uint32_t valid_to, const XOnlyPubKey& static_key, const CKey& authority_key); + + /* The certificate serializes pubkeys in x-only format, not EllSwift. */ + XOnlyPubKey m_static_key = {}; + + [[nodiscard]] bool Validate(XOnlyPubKey authority_key); + + template + // The static_key is signed for, but not serialized. + void Serialize(Stream& s) const + { + s << m_version + << m_valid_from + << m_valid_to + << m_sig; + } + template + void Unserialize(Stream& s) + { + s >> m_version + >> m_valid_from + >> m_valid_to + >> m_sig; + } +}; + +/* + * The CipherState uses m_key (k) and m_nonce (n) to encrypt and decrypt ciphertexts. + * During the handshake phase each party has a single CipherState, but during + * the transport phase each party has two CipherState objects: one for sending, + * and one for receiving. + * + * See chapter "5. Processing rules" of the Noise paper. + */ +class Sv2CipherState +{ +public: + Sv2CipherState() = default; + explicit Sv2CipherState(NoiseHash&& key); + + /** Decrypt message + * @param[in] associated_data associated data + * @param[in] ciphertext message with encrypted and authenticated chunks. + * @param[out] plain message (defragmented) + * @returns whether decryption succeeded + */ + [[nodiscard]] bool DecryptWithAd(Span associated_data, Span ciphertext, Span plain); + + /** Encrypt message + * @param[in] associated_data associated data + * @param[in] plain message + * @param[out] ciphertext message with encrypted and authenticated chunks. + * @returns whether encryption succeeded + */ + [[nodiscard]] bool EncryptWithAd(Span associated_data, Span plain, Span ciphertext); + + /** The message will be chunked in NOISE_MAX_CHUNK_SIZE parts and expanded + * by 16 bytes per chunk for its MAC. + * + * @param[in] plain message. Can't point to the same memory location as ciphertext, + * because each encrypted message chunk would override the + * start of the next plain text chunk. + * @param[out] ciphertext message with encrypted and authenticated chunks + * @return whether encryption succeeded. Only fails if nonce is uint64_max. + */ + [[nodiscard]] bool EncryptMessage(Span plain, Span ciphertext); + + /** Decrypt message. + * + * @param[in] ciphertext encrypted message + * @param[out] plain decrypted message. May point to the same memory location + * as ciphertext. The result is defragmented. + */ + [[nodiscard]] bool DecryptMessage(Span ciphertext, Span plain); + +private: + NoiseHash m_key{0}; + uint64_t m_nonce = 0; +}; + +/* + * A SymmetricState object contains a CipherState plus m_chaining_key (ck) and + * m_hash_output (h) variables. It is so-named because it encapsulates all the + * "symmetric crypto" used by Noise. During the handshake phase each party has + * a single SymmetricState, which can be deleted once the handshake is finished. + * + * See chapter "5. Processing rules" of the Noise paper. + */ +class Sv2SymmetricState +{ +public: + // Sha256 hash of the ascii encoding - "Noise_NX_Secp256k1+EllSwift_ChaChaPoly_SHA256". + // This is the first step required when setting up the chaining key. + static constexpr NoiseHash PROTOCOL_NAME_HASH = { + 46, 180, 120, 129, 32, 142, 158, 238, 31, 102, 159, 103, 198, 110, 231, 14, + 169, 234, 136, 9, 13, 80, 63, 232, 48, 220, 75, 200, 62, 41, 191, 16}; + + // The double hash of protocol name "Noise_NX_EllSwiftXonly_ChaChaPoly_SHA256". + static constexpr NoiseHash PROTOCOL_NAME_DOUBLE_HASH = { + 146, 47, 163, 46, 79, 72, 124, 13, 89, 202, 163, 190, 215, 137, 156, 227, + 217, 141, 183, 225, 61, 189, 59, 124, 242, 210, 61, 212, 51, 220, 97, 4}; + + Sv2SymmetricState() : m_chaining_key{PROTOCOL_NAME_HASH} {} + + void MixHash(const Span input); + void MixKey(const Span input_key_material); + [[nodiscard]] bool EncryptAndHash(Span plain, Span ciphertext); + [[nodiscard]] bool DecryptAndHash(Span ciphertext, Span plain); + std::array Split(); + + uint256 GetHashOutput(); + + /* For testing */ + void LogChainingKey(); + std::string GetChainingKey(); + +private: + NoiseHash m_chaining_key; + uint256 m_hash_output{uint256(PROTOCOL_NAME_DOUBLE_HASH)}; + Sv2CipherState m_cipher_state; + + void HKDF2(const Span input_key_material, + NoiseHash& out0, + NoiseHash& out1); +}; + +/* + * A HandshakeState object contains a SymmetricState plus DH variables (s, e, rs, re) + * and a variable representing the handshake pattern. During the handshake phase + * each party has a single HandshakeState, which can be deleted once the handshake + * is finished. + * + * See chapter "5. Processing rules" of the Noise paper. + */ + +class Sv2HandshakeState +{ +public: + static constexpr size_t ELLSWIFT_PUB_KEY_SIZE{64}; + static constexpr size_t ECDH_OUTPUT_SIZE{32}; + + static constexpr size_t HANDSHAKE_STEP2_SIZE = ELLSWIFT_PUB_KEY_SIZE + ELLSWIFT_PUB_KEY_SIZE + + Poly1305::TAGLEN + Sv2SignatureNoiseMessage::SIZE + Poly1305::TAGLEN; + + /* + * If we are the initiator m_authority_pubkey must be set in order to verify + * the received certificate. + */ + Sv2HandshakeState(CKey&& static_key, + XOnlyPubKey authority_pubkey) : m_static_key{static_key}, + m_authority_pubkey{authority_pubkey} + { + m_static_ellswift_pk = static_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); + }; + + /* + * If we are the responder, the certificate must be set + */ + Sv2HandshakeState(CKey&& static_key, + Sv2SignatureNoiseMessage&& certificate) : m_static_key{static_key}, + m_certificate{certificate} + { + m_static_ellswift_pk = static_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); + }; + + /** Handshake step 1 for initiator: -> e */ + void WriteMsgEphemeralPK(Span msg); + /** Handshake step 1 for responder: -> e */ + void ReadMsgEphemeralPK(Span msg); + /** During handshake step 2, put our ephmeral key, static key + * and certificate in the buffer: <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE + */ + void WriteMsgES(Span msg); + /** During handshake step 2, read the remote ephmeral key, static key + * and certificate. Verify their certificate. + * <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE + */ + [[nodiscard]] bool ReadMsgES(Span msg); + + std::array SplitSymmetricState(); + uint256 GetHashOutput(); + + void SetEphemeralKey(CKey&& key); + +private: + /** Our static key (s) */ + CKey m_static_key; + /** EllSwift encoded static key, for optimized ECDH */ + EllSwiftPubKey m_static_ellswift_pk; + /** Our ephemeral key (e) */ + CKey m_ephemeral_key; + /** EllSwift encoded ephemeral key, for optimized ECDH */ + EllSwiftPubKey m_ephemeral_ellswift_pk; + /** Remote static key (rs) */ + EllSwiftPubKey m_remote_static_ellswift_pk; + /** Remote ephemeral key (re) */ + EllSwiftPubKey m_remote_ephemeral_ellswift_pk; + Sv2SymmetricState m_symmetric_state; + /** Certificate signed by m_authority_pubkey. */ + std::optional m_certificate; + /** Authority public key. */ + std::optional m_authority_pubkey; + + /** Generate ephemeral key, sets set m_ephemeral_key and m_ephemeral_ellswift_pk */ + void GenerateEphemeralKey() noexcept; +}; + +/** + * Interface somewhat similar to BIP324Cipher for use by a Transport class. + * The initiator and responder roles have their own constructor. + * FinishHandshake() must be called after all handshake bytes have been processed. + */ +class Sv2Cipher +{ +public: + Sv2Cipher(CKey&& static_key, XOnlyPubKey authority_pubkey); + Sv2Cipher(CKey&& static_key, Sv2SignatureNoiseMessage&& certificate); + + Sv2Cipher(bool initiator, std::unique_ptr handshake_state) : m_initiator{initiator}, m_handshake_state{std::move(handshake_state)} {}; + + Sv2HandshakeState& GetHandshakeState(); + /** + * Populates m_hash, m_cs1 and m_cs2 from m_handshake_state and deletes the latter. + */ + void FinishHandshake(); + + /** Decrypts a message. May only be called after FinishHandshake() */ + bool DecryptMessage(Span ciphertext, Span plain); + /** Encrypts a message. May only be called after FinishHandshake() */ + [[nodiscard]] bool EncryptMessage(Span input, Span output); + + /* Expected size after chunking and with MAC */ + static size_t EncryptedMessageSize(const size_t msg_len); + + /* Test only */ + uint256 GetHash() const; + +private: + bool m_initiator; + std::unique_ptr m_handshake_state; + + uint256 m_hash; + Sv2CipherState m_cs1; + Sv2CipherState m_cs2; +}; + +#endif // BITCOIN_SV2_NOISE_H diff --git a/src/sv2/transport.cpp b/src/sv2/transport.cpp new file mode 100644 index 0000000000000..37a6e36ba19e0 --- /dev/null +++ b/src/sv2/transport.cpp @@ -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 + +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 header_encrypted{buffer_span.subspan(0, SV2_HEADER_ENCRYPTED_SIZE)}; + if (!m_cipher.EncryptMessage(ss_header_plain, header_encrypted)) { + return false; + } + + // Payload + Span 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 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& 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 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 payload; + payload.resize(m_header.m_msg_len); + + Span 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); +} diff --git a/src/sv2/transport.h b/src/sv2/transport.h new file mode 100644 index 0000000000000..26e538fac747f --- /dev/null +++ b/src/sv2/transport.h @@ -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_SV2_TRANSPORT_H +#define BITCOIN_SV2_TRANSPORT_H + +#include +#include +#include // For Transport, CNetMessage and CSerializedNetMsg +#include + +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 m_recv_buffer GUARDED_BY(m_recv_mutex); + /** AAD expected in next received packet (currently used only for garbage). */ + std::vector 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 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 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 GetMessageType(Span& 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& 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_SV2_TRANSPORT_H diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 2c9957117c0d2..0d06110ac3025 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -181,6 +181,15 @@ if(WITH_MULTIPROCESS) target_link_libraries(test_bitcoin bitcoin_ipc_test bitcoin_ipc) endif() +if(WITH_SV2) + target_sources(test_bitcoin + PRIVATE + sv2_noise_tests.cpp + sv2_transport_tests.cpp + ) + target_link_libraries(test_bitcoin bitcoin_sv2 bitcoin_common) +endif() + function(add_boost_test source_file) if(NOT EXISTS ${source_file}) return() diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt index 1c7b0d5c258c7..663f6526b186b 100644 --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -145,3 +145,13 @@ target_link_libraries(fuzz if(ENABLE_WALLET) add_subdirectory(${PROJECT_SOURCE_DIR}/src/wallet/test/fuzz wallet) endif() + +if(WITH_SV2) + target_sources(fuzz + PRIVATE + sv2_noise.cpp + ) + target_link_libraries(fuzz + bitcoin_sv2 + ) +endif() diff --git a/src/test/fuzz/sv2_noise.cpp b/src/test/fuzz/sv2_noise.cpp new file mode 100644 index 0000000000000..465e4171d99a9 --- /dev/null +++ b/src/test/fuzz/sv2_noise.cpp @@ -0,0 +1,168 @@ +// Copyright (c) 2024 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + + +void Initialize() +{ + // Add test context for debugging. Usage: + // --debug=sv2 --loglevel=sv2:trace --printtoconsole=1 + static const auto testing_setup = std::make_unique(); +} +} // namespace + +bool MaybeDamage(FuzzedDataProvider& provider, std::vector& transport) +{ + if (transport.size() == 0) return false; + + // Optionally damage 1 bit in the ciphertext. + const bool damage = provider.ConsumeBool(); + if (damage) { + unsigned damage_bit = provider.ConsumeIntegralInRange(0, + transport.size() * 8U - 1U); + unsigned damage_pos = damage_bit >> 3; + LogTrace(BCLog::SV2, "Damage byte %d of %d\n", damage_pos, transport.size()); + std::byte damage_val{(uint8_t)(1U << (damage_bit & 7))}; + transport.at(damage_pos) ^= damage_val; + } + return damage; +} + +FUZZ_TARGET(sv2_noise_cipher_roundtrip, .init = Initialize) +{ + // Test that Sv2Noise's encryption and decryption agree. + + // To conserve fuzzer entropy, deterministically generate Alice and Bob keys. + FuzzedDataProvider provider(buffer.data(), buffer.size()); + auto seed_ent = provider.ConsumeBytes(32); + seed_ent.resize(32); + CExtKey seed; + seed.SetSeed(seed_ent); + + CExtKey tmp; + if (!seed.Derive(tmp, 0)) return; + CKey alice_authority_key{tmp.key}; + + if (!seed.Derive(tmp, 1)) return; + CKey alice_static_key{tmp.key}; + + if (!seed.Derive(tmp, 2)) return; + CKey alice_ephemeral_key{tmp.key}; + + if (!seed.Derive(tmp, 10)) return; + CKey bob_authority_key{tmp.key}; + + if (!seed.Derive(tmp, 11)) return; + CKey bob_static_key{tmp.key}; + + if (!seed.Derive(tmp, 12)) return; + CKey bob_ephemeral_key{tmp.key}; + + // Create certificate + // Pick random times in the past or future + uint32_t now = provider.ConsumeIntegralInRange(10000U, UINT32_MAX); + SetMockTime(now); + uint16_t version = provider.ConsumeBool() ? 0 : provider.ConsumeIntegral(); + uint32_t past = provider.ConsumeIntegralInRange(0, now); + uint32_t future = provider.ConsumeIntegralInRange(now, UINT32_MAX); + uint32_t valid_from = int32_t(provider.ConsumeBool() ? past : future); + uint32_t valid_to = int32_t(provider.ConsumeBool() ? future : past); + + auto bob_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(bob_static_key.GetPubKey()), bob_authority_key); + + bool valid_certificate = version == 0 && + (valid_from <= now) && + (valid_to >= now); + + LogTrace(BCLog::SV2, "valid_certificate: %d - version %u, past: %u, now %u, future: %u\n", valid_certificate, version, past, now, future); + + // Alice's static is not used in the test + // Alice needs to verify Bob's certificate, so we pass his authority key + auto alice_handshake = std::make_unique(std::move(alice_static_key), XOnlyPubKey(bob_authority_key.GetPubKey())); + alice_handshake->SetEphemeralKey(std::move(alice_ephemeral_key)); + // Bob is the responder and does not receive (or verify) Alice's certificate, + // so we don't pass her authority key. + auto bob_handshake = std::make_unique(std::move(bob_static_key), std::move(bob_certificate)); + bob_handshake->SetEphemeralKey(std::move(bob_ephemeral_key)); + + // Handshake Act 1: e -> + + std::vector transport; + transport.resize(Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE); + // Alice generates her ephemeral public key and write it into the buffer: + alice_handshake->WriteMsgEphemeralPK(transport); + + bool damage_e = MaybeDamage(provider, transport); + + // Bob reads the ephemeral key () + // With EllSwift encoding this step can't fail + bob_handshake->ReadMsgEphemeralPK(transport); + ClearShrink(transport); + + // Handshake Act 2: <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE + transport.resize(Sv2HandshakeState::HANDSHAKE_STEP2_SIZE); + bob_handshake->WriteMsgES(transport); + + bool damage_es = MaybeDamage(provider, transport); + + // This ignores the remote possibility that the fuzzer finds two equivalent + // EllSwift encodings by flipping a single ephemeral key bit. + assert(alice_handshake->ReadMsgES(transport) == (valid_certificate && !damage_e && !damage_es)); + + if (!valid_certificate || damage_e || damage_es) return; + + // Construct Sv2Cipher from the Sv2HandshakeState and test transport + auto alice{Sv2Cipher(/*initiator=*/true, std::move(alice_handshake))}; + auto bob{Sv2Cipher(/*initiator=*/false, std::move(bob_handshake))}; + alice.FinishHandshake(); + bob.FinishHandshake(); + + // Use deterministic RNG to generate content rather than creating it from + // the fuzzer input. + InsecureRandomContext rng(provider.ConsumeIntegral()); + + LIMITED_WHILE(provider.remaining_bytes(), 1000) + { + ClearShrink(transport); + + // Alice or Bob sends a message + bool from_alice = provider.ConsumeBool(); + + // Set content length (slightly above NOISE_MAX_CHUNK_SIZE) + unsigned length = provider.ConsumeIntegralInRange(0, NOISE_MAX_CHUNK_SIZE + 100); + std::vector plain(length); + for (auto& val : plain) + val = std::byte{(uint8_t)rng()}; + + const size_t encrypted_size = Sv2Cipher::EncryptedMessageSize(plain.size()); + transport.resize(encrypted_size); + + assert((from_alice ? alice : bob).EncryptMessage(plain, transport)); + + const bool damage = MaybeDamage(provider, transport); + + std::vector plain_read; + plain_read.resize(plain.size()); + + bool ok = (from_alice ? bob : alice).DecryptMessage(transport, plain_read); + assert(!ok == damage); + if (!ok) break; + + assert(plain == plain_read); + } +} diff --git a/src/test/sv2_noise_tests.cpp b/src/test/sv2_noise_tests.cpp new file mode 100644 index 0000000000000..07da06e4e361d --- /dev/null +++ b/src/test/sv2_noise_tests.cpp @@ -0,0 +1,159 @@ +#include +#include +#include +#include +#include + +#include + +BOOST_FIXTURE_TEST_SUITE(sv2_noise_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(MixKey_test) +{ + Sv2SymmetricState i_ss; + Sv2SymmetricState r_ss; + BOOST_CHECK_EQUAL(r_ss.GetChainingKey(), i_ss.GetChainingKey()); + + CKey initiator_key{GenerateRandomKey()}; + auto initiator_pk = initiator_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); + + CKey responder_key{GenerateRandomKey()}; + auto responder_pk = responder_key.EllSwiftCreate(MakeByteSpan(GetRandHash())); + + auto ecdh_output_1 = initiator_key.ComputeBIP324ECDHSecret(responder_pk, initiator_pk, true); + auto ecdh_output_2 = responder_key.ComputeBIP324ECDHSecret(initiator_pk, responder_pk, false); + + BOOST_CHECK(std::memcmp(&ecdh_output_1[0], &ecdh_output_2[0], 32) == 0); + + i_ss.MixKey(ecdh_output_1); + r_ss.MixKey(ecdh_output_2); + + BOOST_CHECK_EQUAL(r_ss.GetChainingKey(), i_ss.GetChainingKey()); +} + +BOOST_AUTO_TEST_CASE(certificate_test) +{ + auto alice_static_key{GenerateRandomKey()}; + auto alice_authority_key{GenerateRandomKey()}; + + // Create certificate + auto epoch_now = std::chrono::system_clock::now().time_since_epoch(); + uint32_t now = static_cast(std::chrono::duration_cast(epoch_now).count()); + uint16_t version = 0; + uint32_t valid_from = now; + uint32_t valid_to = std::numeric_limits::max(); + + auto alice_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(alice_static_key.GetPubKey()), alice_authority_key); + + BOOST_REQUIRE(alice_certificate.Validate(XOnlyPubKey(alice_authority_key.GetPubKey()))); + + auto malory_authority_key{GenerateRandomKey()}; + BOOST_REQUIRE(!alice_certificate.Validate(XOnlyPubKey(malory_authority_key.GetPubKey()))); + + // Check that certificate is not from the future + valid_from = now + 10000; + alice_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(alice_static_key.GetPubKey()), alice_authority_key); + BOOST_REQUIRE(!alice_certificate.Validate(XOnlyPubKey(alice_authority_key.GetPubKey()))); + + valid_from = now; + + // Check certificate expiration + valid_to = now - 10000; + alice_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(alice_static_key.GetPubKey()), alice_authority_key); + BOOST_REQUIRE(!alice_certificate.Validate(XOnlyPubKey(alice_authority_key.GetPubKey()))); + + valid_to = now; + + // Only version 0 is supported + version = 1; + alice_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(alice_static_key.GetPubKey()), alice_authority_key); + BOOST_REQUIRE(!alice_certificate.Validate(XOnlyPubKey(alice_authority_key.GetPubKey()))); +} + +BOOST_AUTO_TEST_CASE(handshake_and_transport_test) +{ + // Alice initiates a handshake with Bob + + auto alice_static_key{GenerateRandomKey()}; + auto bob_static_key{GenerateRandomKey()}; + auto bob_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(std::chrono::duration_cast(epoch_now).count()); + uint32_t valid_to = std::numeric_limits::max(); + + auto bob_certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, + XOnlyPubKey(bob_static_key.GetPubKey()), + bob_authority_key); + + // Alice's static is not used in the test + // Alice needs to verify Bob's certificate, so we pass his authority key + auto alice_handshake = std::make_unique(std::move(alice_static_key), + XOnlyPubKey(bob_authority_key.GetPubKey())); + // Bob is the responder and does not receive (or verify) Alice's certificate, + // so we don't pass her authority key. + auto bob_handshake = std::make_unique(std::move(bob_static_key), + std::move(bob_certificate)); + + // Handshake Act 1: e -> + + std::vector transport; + transport.resize(Sv2HandshakeState::ELLSWIFT_PUB_KEY_SIZE); + // Alice generates her ephemeral public key and write it into the buffer: + alice_handshake->WriteMsgEphemeralPK(transport); + EllSwiftPubKey alice_pubkey(transport); + + // Bob reads the ephemeral key + bob_handshake->ReadMsgEphemeralPK(transport); + + ClearShrink(transport); + + // Handshake Act 2: <- e, ee, s, es, SIGNATURE_NOISE_MESSAGE + transport.resize(Sv2HandshakeState::HANDSHAKE_STEP2_SIZE); + bob_handshake->WriteMsgES(transport); + BOOST_REQUIRE(alice_handshake->ReadMsgES(transport)); + + // Construct Sv2Cipher from the Sv2HandshakeState and test transport + auto alice{Sv2Cipher(/*initiator=*/true, std::move(alice_handshake))}; + auto bob{Sv2Cipher(/*initiator=*/false, std::move(bob_handshake))}; + alice.FinishHandshake(); + bob.FinishHandshake(); + + ClearShrink(transport); + + constexpr std::array TEST{ + // hello world + 0x68, + 0x65, + 0x6C, + 0x6C, + 0x6F, + 0x20, + 0x77, + 0x6F, + 0x72, + 0x6C, + 0x64, + }; + + const size_t encrypted_size = Sv2Cipher::EncryptedMessageSize(TEST.size()); + BOOST_CHECK_EQUAL(encrypted_size, TEST.size() + Poly1305::TAGLEN); + + transport.resize(encrypted_size); + + auto plain_send{MakeByteSpan(TEST)}; + BOOST_TEST_CHECKPOINT("Alice encrypts message"); + BOOST_REQUIRE(alice.EncryptMessage(plain_send, transport)); + + std::vector plain_receive(TEST.size(), std::byte(0)); + BOOST_TEST_CHECKPOINT("Bob decrypts message"); + BOOST_REQUIRE(bob.DecryptMessage(transport, plain_receive)); + BOOST_CHECK_EQUAL(HexStr(plain_receive), HexStr(TEST)); +} +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/sv2_transport_tests.cpp b/src/test/sv2_transport_tests.cpp new file mode 100644 index 0000000000000..9a353d231ad3e --- /dev/null +++ b/src/test/sv2_transport_tests.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +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 m_transport; //!< Sv2Transport being tested + std::unique_ptr 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 m_to_send; //!< Bytes we have queued up to send to m_transport-> + std::vector m_received; //!< Bytes we have received from m_transport-> + std::deque 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(std::chrono::duration_cast(epoch_now).count()); + uint32_t valid_to = std::numeric_limits::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(initiator_static_key, XOnlyPubKey(responder_authority_key.GetPubKey())); + m_peer_cipher = std::make_unique(std::move(responder_static_key), std::move(responder_certificate)); + } else { + m_transport = std::make_unique(responder_static_key, responder_certificate); + m_peer_cipher = std::make_unique(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>>; + + 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> 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 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 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 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 ciphertext; + const size_t encrypted_payload_size = Sv2Cipher::EncryptedMessageSize(msg.size()); + ciphertext.resize(SV2_HEADER_ENCRYPTED_SIZE + encrypted_payload_size); + Span 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 header_encrypted{buffer_span.subspan(0, SV2_HEADER_ENCRYPTED_SIZE)}; + BOOST_REQUIRE(m_peer_cipher->EncryptMessage(ss_header_plain, header_encrypted)); + + // Payload + Span 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 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 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 encrypted_payload{MakeWritableByteSpan(m_received).subspan(SV2_HEADER_ENCRYPTED_SIZE, expanded_size)}; + Span payload = encrypted_payload.subspan(0, header.m_msg_len); + + BOOST_REQUIRE(m_peer_cipher->DecryptMessage(encrypted_payload, payload)); + + std::vector decode_buffer; + decode_buffer.resize(header.m_msg_len); + + std::transform(payload.begin(), payload.end(), decode_buffer.begin(), + [](std::byte b) { return static_cast(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()