Skip to content

discv5/discv5_client.cpp

Namespaces

Name
discv5

Source code

// Copyright 2025 GeniusVentures
// SPDX-License-Identifier: Apache-2.0

#include "discv5/discv5_client.hpp"
#include "discv5/discv5_constants.hpp"
#include "discv5/discv5_enr.hpp"

#include <rlp/rlp_decoder.hpp>
#include <rlp/rlp_encoder.hpp>
#include <rlpx/crypto/ecdh.hpp>

#include <boost/asio/spawn.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/steady_timer.hpp>

#include <nil/crypto3/hash/algorithm/hash.hpp>
#include <nil/crypto3/hash/keccak.hpp>

#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <secp256k1.h>
#include <secp256k1_recovery.h>

#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <string_view>
#include <vector>

namespace discv5
{

namespace
{

using NodeAddress = std::array<uint8_t, kKeccak256Bytes>;

static constexpr size_t  kMessageAuthDataBytes = kKeccak256Bytes;

struct PacketView
{
    uint8_t               flag{};
    std::array<uint8_t, kGcmNonceBytes> nonce{};
    uint16_t              auth_size{};
    std::vector<uint8_t>  header_data{};  
    std::vector<uint8_t>  auth_data{};    
    std::vector<uint8_t>  msg_data{};     
};

struct HandshakeAuthView
{
    NodeAddress           src_id{};
    std::vector<uint8_t>  signature{};
    std::vector<uint8_t>  pubkey{};
    std::vector<uint8_t>  record{};
};

NodeAddress derive_node_address(const NodeId& public_key) noexcept
{
    const auto hash_val =
        nil::crypto3::hash<nil::crypto3::hashes::keccak_1600<256>>(
            public_key.cbegin(), public_key.cend());
    return static_cast<NodeAddress>(hash_val);
}

std::string endpoint_key(const udp::endpoint& endpoint)
{
    return endpoint.address().to_string() + ":" + std::to_string(endpoint.port());
}

std::string endpoint_key(const std::string& ip, uint16_t port)
{
    return ip + ":" + std::to_string(port);
}

uint16_t read_u16_be(const uint8_t* data) noexcept
{
    return static_cast<uint16_t>(
        (static_cast<uint16_t>(data[0U]) << 8U) |
        static_cast<uint16_t>(data[1U]));
}

uint64_t read_u64_be(const uint8_t* data) noexcept
{
    uint64_t value = 0U;
    for (size_t i = 0U; i < sizeof(uint64_t); ++i)
    {
        value = static_cast<uint64_t>((value << 8U) | data[i]);
    }
    return value;
}

void append_u16_be(std::vector<uint8_t>& out, uint16_t value)
{
    out.push_back(static_cast<uint8_t>((value >> 8U) & 0xFFU));
    out.push_back(static_cast<uint8_t>(value & 0xFFU));
}

void append_u64_be(std::vector<uint8_t>& out, uint64_t value)
{
    for (int shift = 56; shift >= 0; shift -= 8)
    {
        out.push_back(static_cast<uint8_t>((value >> shift) & 0xFFU));
    }
}

bool random_bytes(uint8_t* out, size_t size) noexcept
{
    return RAND_bytes(out, static_cast<int>(size)) == 1;
}

std::array<uint8_t, SHA256_DIGEST_LENGTH> sha256_bytes(const std::vector<uint8_t>& data) noexcept
{
    std::array<uint8_t, SHA256_DIGEST_LENGTH> digest{};
    SHA256(data.data(), data.size(), digest.data());
    return digest;
}

std::array<uint8_t, SHA256_DIGEST_LENGTH> hmac_sha256(
    const uint8_t* key,
    size_t key_size,
    const uint8_t* data,
    size_t data_size) noexcept
{
    std::array<uint8_t, SHA256_DIGEST_LENGTH> digest{};
    unsigned int digest_len = 0U;
    HMAC(
        EVP_sha256(),
        key,
        static_cast<int>(key_size),
        data,
        data_size,
        digest.data(),
        &digest_len);
    return digest;
}

Result<std::array<uint8_t, kPrivateKeyBytes>> hkdf_expand_32(
    const std::vector<uint8_t>& salt,
    const std::vector<uint8_t>& ikm,
    const std::vector<uint8_t>& info) noexcept
{
    const auto prk = hmac_sha256(salt.data(), salt.size(), ikm.data(), ikm.size());

    std::vector<uint8_t> t1_input;
    t1_input.reserve(info.size() + kMessageTypePrefixBytes);
    t1_input.insert(t1_input.end(), info.begin(), info.end());
    t1_input.push_back(kHkdfFirstBlockCounter);

    return hmac_sha256(prk.data(), prk.size(), t1_input.data(), t1_input.size());
}

Result<std::array<uint8_t, kCompressedKeyBytes>> compress_public_key(const NodeId& public_key) noexcept
{
    secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
    if (ctx == nullptr)
    {
        return discv5Error::kContextCreationFailed;
    }

    std::array<uint8_t, kUncompressedKeyBytes> raw{};
    raw[0U] = kUncompressedPubKeyPrefix;
    std::copy(public_key.begin(), public_key.end(), raw.begin() + kUncompressedPubKeyDataOffset);

    secp256k1_pubkey pubkey;
    if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, raw.data(), raw.size()))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kEnrInvalidSecp256k1Key;
    }

    std::array<uint8_t, kCompressedKeyBytes> compressed{};
    size_t compressed_len = compressed.size();
    if (!secp256k1_ec_pubkey_serialize(
            ctx,
            compressed.data(),
            &compressed_len,
            &pubkey,
            SECP256K1_EC_COMPRESSED))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kEnrInvalidSecp256k1Key;
    }

    secp256k1_context_destroy(ctx);
    return compressed;
}

Result<std::array<uint8_t, kCompressedKeyBytes>> shared_secret_from_uncompressed_pubkey(
    const NodeId& remote_node_id,
    const std::array<uint8_t, kPrivateKeyBytes>& private_key) noexcept
{
    secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
    if (ctx == nullptr)
    {
        return discv5Error::kContextCreationFailed;
    }

    std::array<uint8_t, kUncompressedKeyBytes> raw{};
    raw[0U] = kUncompressedPubKeyPrefix;
    std::copy(remote_node_id.begin(), remote_node_id.end(), raw.begin() + kUncompressedPubKeyDataOffset);

    secp256k1_pubkey pubkey;
    if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, raw.data(), raw.size()))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kEnrInvalidSecp256k1Key;
    }

    if (!secp256k1_ec_pubkey_tweak_mul(ctx, &pubkey, private_key.data()))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kContextCreationFailed;
    }

    std::array<uint8_t, kCompressedKeyBytes> shared{};
    size_t shared_len = shared.size();
    if (!secp256k1_ec_pubkey_serialize(
            ctx,
            shared.data(),
            &shared_len,
            &pubkey,
            SECP256K1_EC_COMPRESSED))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kContextCreationFailed;
    }

    secp256k1_context_destroy(ctx);
    return shared;
}

Result<std::array<uint8_t, kCompressedKeyBytes>> shared_secret_from_compressed_pubkey(
    const std::vector<uint8_t>& remote_pubkey,
    const std::array<uint8_t, kPrivateKeyBytes>& private_key) noexcept
{
    if (remote_pubkey.size() != kCompressedKeyBytes)
    {
        return discv5Error::kEnrInvalidSecp256k1Key;
    }

    secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
    if (ctx == nullptr)
    {
        return discv5Error::kContextCreationFailed;
    }

    secp256k1_pubkey pubkey;
    if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, remote_pubkey.data(), remote_pubkey.size()))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kEnrInvalidSecp256k1Key;
    }

    if (!secp256k1_ec_pubkey_tweak_mul(ctx, &pubkey, private_key.data()))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kContextCreationFailed;
    }

    std::array<uint8_t, kCompressedKeyBytes> shared{};
    size_t shared_len = shared.size();
    if (!secp256k1_ec_pubkey_serialize(
            ctx,
            shared.data(),
            &shared_len,
            &pubkey,
            SECP256K1_EC_COMPRESSED))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kContextCreationFailed;
    }

    secp256k1_context_destroy(ctx);
    return shared;
}

Result<std::vector<uint8_t>> make_id_signature(
    const std::array<uint8_t, kPrivateKeyBytes>& private_key,
    const std::vector<uint8_t>& challenge_data,
    const std::vector<uint8_t>& eph_pubkey,
    const NodeAddress& destination_node_addr) noexcept
{
    std::vector<uint8_t> input;
    static constexpr std::string_view kPrefix = "discovery v5 identity proof";
    input.reserve(kPrefix.size() + challenge_data.size() + eph_pubkey.size() + destination_node_addr.size());
    input.insert(input.end(), kPrefix.begin(), kPrefix.end());
    input.insert(input.end(), challenge_data.begin(), challenge_data.end());
    input.insert(input.end(), eph_pubkey.begin(), eph_pubkey.end());
    input.insert(input.end(), destination_node_addr.begin(), destination_node_addr.end());

    const auto digest = sha256_bytes(input);

    secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
    if (ctx == nullptr)
    {
        return discv5Error::kContextCreationFailed;
    }

    secp256k1_ecdsa_recoverable_signature sig;
    if (!secp256k1_ecdsa_sign_recoverable(ctx, &sig, digest.data(), private_key.data(), nullptr, nullptr))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kContextCreationFailed;
    }

    std::vector<uint8_t> compact(kEnrSigBytes);
    int recid = 0;
    secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, compact.data(), &recid, &sig);
    secp256k1_context_destroy(ctx);
    return compact;
}

bool verify_id_signature(
    const NodeId& node_id,
    const std::vector<uint8_t>& signature,
    const std::vector<uint8_t>& challenge_data,
    const std::vector<uint8_t>& eph_pubkey,
    const NodeAddress& destination_node_addr) noexcept
{
    if (signature.size() != kEnrSigBytes)
    {
        return false;
    }

    std::vector<uint8_t> input;
    static constexpr std::string_view kPrefix = "discovery v5 identity proof";
    input.reserve(kPrefix.size() + challenge_data.size() + eph_pubkey.size() + destination_node_addr.size());
    input.insert(input.end(), kPrefix.begin(), kPrefix.end());
    input.insert(input.end(), challenge_data.begin(), challenge_data.end());
    input.insert(input.end(), eph_pubkey.begin(), eph_pubkey.end());
    input.insert(input.end(), destination_node_addr.begin(), destination_node_addr.end());

    const auto digest = sha256_bytes(input);

    secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
    if (ctx == nullptr)
    {
        return false;
    }

    std::array<uint8_t, kUncompressedKeyBytes> raw{};
    raw[0U] = kUncompressedPubKeyPrefix;
    std::copy(node_id.begin(), node_id.end(), raw.begin() + kUncompressedPubKeyDataOffset);

    secp256k1_pubkey pubkey;
    if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, raw.data(), raw.size()))
    {
        secp256k1_context_destroy(ctx);
        return false;
    }

    secp256k1_ecdsa_signature sig;
    if (!secp256k1_ecdsa_signature_parse_compact(ctx, &sig, signature.data()))
    {
        secp256k1_context_destroy(ctx);
        return false;
    }

    const bool verified = secp256k1_ecdsa_verify(ctx, &sig, digest.data(), &pubkey) == 1;
    secp256k1_context_destroy(ctx);
    return verified;
}

Result<std::pair<std::array<uint8_t, kAes128KeyBytes>, std::array<uint8_t, kAes128KeyBytes>>> derive_session_keys(
    const std::array<uint8_t, kCompressedKeyBytes>& shared_secret,
    const std::vector<uint8_t>& challenge_data,
    const NodeAddress& first_id,
    const NodeAddress& second_id) noexcept
{
    static constexpr std::string_view kInfoPrefix = "discovery v5 key agreement";

    std::vector<uint8_t> info;
    info.reserve(kInfoPrefix.size() + first_id.size() + second_id.size());
    info.insert(info.end(), kInfoPrefix.begin(), kInfoPrefix.end());
    info.insert(info.end(), first_id.begin(), first_id.end());
    info.insert(info.end(), second_id.begin(), second_id.end());

    std::vector<uint8_t> ikm(shared_secret.begin(), shared_secret.end());
    auto okm_result = hkdf_expand_32(challenge_data, ikm, info);
    if (!okm_result)
    {
        return okm_result.error();
    }

    std::array<uint8_t, kAes128KeyBytes> write_key{};
    std::array<uint8_t, kAes128KeyBytes> read_key{};
    const auto& okm = okm_result.value();
    std::copy_n(okm.begin(), write_key.size(), write_key.begin());
    std::copy_n(okm.begin() + write_key.size(), read_key.size(), read_key.begin());
    return std::make_pair(write_key, read_key);
}

bool apply_aes128_ctr(
    const std::array<uint8_t, kAes128KeyBytes>& key,
    const std::array<uint8_t, kMaskingIvBytes>& iv,
    uint8_t* out,
    const uint8_t* in,
    size_t size) noexcept
{
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    if (ctx == nullptr)
    {
        return false;
    }

    const int init_ok = EVP_EncryptInit_ex(ctx, EVP_aes_128_ctr(), nullptr, key.data(), iv.data());
    if (init_ok != 1)
    {
        EVP_CIPHER_CTX_free(ctx);
        return false;
    }

    int out_len = 0;
    const int update_ok = EVP_EncryptUpdate(ctx, out, &out_len, in, static_cast<int>(size));
    EVP_CIPHER_CTX_free(ctx);
    return update_ok == 1 && out_len == static_cast<int>(size);
}

Result<std::vector<uint8_t>> encrypt_gcm(
    const std::array<uint8_t, kAes128KeyBytes>& key,
    const std::array<uint8_t, kGcmNonceBytes>& nonce,
    const std::vector<uint8_t>& plaintext,
    const std::vector<uint8_t>& auth_data) noexcept
{
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    if (ctx == nullptr)
    {
        return discv5Error::kContextCreationFailed;
    }

    std::vector<uint8_t> ciphertext(plaintext.size() + kGcmTagBytes);
    int out_len = 0;
    int total_len = 0;

    if (EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1 ||
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, kGcmNonceBytes, nullptr) != 1 ||
        EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), nonce.data()) != 1)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kContextCreationFailed;
    }

    if (!auth_data.empty())
    {
        if (EVP_EncryptUpdate(ctx, nullptr, &out_len, auth_data.data(), static_cast<int>(auth_data.size())) != 1)
        {
            EVP_CIPHER_CTX_free(ctx);
            return discv5Error::kContextCreationFailed;
        }
    }

    if (!plaintext.empty())
    {
        if (EVP_EncryptUpdate(
                ctx,
                ciphertext.data(),
                &out_len,
                plaintext.data(),
                static_cast<int>(plaintext.size())) != 1)
        {
            EVP_CIPHER_CTX_free(ctx);
            return discv5Error::kContextCreationFailed;
        }
        total_len += out_len;
    }

    if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + total_len, &out_len) != 1)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kContextCreationFailed;
    }
    total_len += out_len;

    if (EVP_CIPHER_CTX_ctrl(
            ctx,
            EVP_CTRL_GCM_GET_TAG,
            kGcmTagBytes,
            ciphertext.data() + total_len) != 1)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kContextCreationFailed;
    }

    EVP_CIPHER_CTX_free(ctx);
    ciphertext.resize(static_cast<size_t>(total_len) + kGcmTagBytes);
    return ciphertext;
}

Result<std::vector<uint8_t>> decrypt_gcm(
    const std::array<uint8_t, kAes128KeyBytes>& key,
    const std::array<uint8_t, kGcmNonceBytes>& nonce,
    const std::vector<uint8_t>& ciphertext,
    const std::vector<uint8_t>& auth_data) noexcept
{
    if (ciphertext.size() < kGcmTagBytes)
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    if (ctx == nullptr)
    {
        return discv5Error::kContextCreationFailed;
    }

    const size_t text_size = ciphertext.size() - kGcmTagBytes;
    std::vector<uint8_t> plaintext(text_size);
    int out_len = 0;
    int total_len = 0;

    if (EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1 ||
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, kGcmNonceBytes, nullptr) != 1 ||
        EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), nonce.data()) != 1)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kContextCreationFailed;
    }

    if (!auth_data.empty())
    {
        if (EVP_DecryptUpdate(ctx, nullptr, &out_len, auth_data.data(), static_cast<int>(auth_data.size())) != 1)
        {
            EVP_CIPHER_CTX_free(ctx);
            return discv5Error::kContextCreationFailed;
        }
    }

    if (text_size > 0U)
    {
        if (EVP_DecryptUpdate(
                ctx,
                plaintext.data(),
                &out_len,
                ciphertext.data(),
                static_cast<int>(text_size)) != 1)
        {
            EVP_CIPHER_CTX_free(ctx);
            return discv5Error::kNetworkReceiveFailed;
        }
        total_len += out_len;
    }

    if (EVP_CIPHER_CTX_ctrl(
            ctx,
            EVP_CTRL_GCM_SET_TAG,
            kGcmTagBytes,
            const_cast<uint8_t*>(ciphertext.data() + text_size)) != 1)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kNetworkReceiveFailed;
    }

    const int final_ok = EVP_DecryptFinal_ex(ctx, plaintext.data() + total_len, &out_len);
    EVP_CIPHER_CTX_free(ctx);
    if (final_ok != 1)
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    total_len += out_len;
    plaintext.resize(static_cast<size_t>(total_len));
    return plaintext;
}

Result<PacketView> decode_packet(
    const uint8_t* data,
    size_t length,
    const NodeAddress& destination_node_addr) noexcept
{
    if (length < kStaticPacketBytes)
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    PacketView packet;
    packet.header_data.resize(kStaticPacketBytes);
    std::copy(data, data + kMaskingIvBytes, packet.header_data.begin());

    std::array<uint8_t, kMaskingIvBytes> key{};
    std::copy_n(destination_node_addr.begin(), key.size(), key.begin());
    std::array<uint8_t, kMaskingIvBytes> iv{};
    std::copy_n(data, iv.size(), iv.begin());

    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    if (ctx == nullptr)
    {
        return discv5Error::kContextCreationFailed;
    }

    if (EVP_EncryptInit_ex(ctx, EVP_aes_128_ctr(), nullptr, key.data(), iv.data()) != 1)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kContextCreationFailed;
    }

    int out_len = 0;
    if (EVP_EncryptUpdate(
            ctx,
            packet.header_data.data() + kMaskingIvBytes,
            &out_len,
            data + kMaskingIvBytes,
            static_cast<int>(kStaticHeaderBytes)) != 1)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kNetworkReceiveFailed;
    }

    const uint8_t* static_header = packet.header_data.data() + kMaskingIvBytes;
    if (std::memcmp(static_header, kProtocolId, kProtocolIdBytes) != 0)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kNetworkReceiveFailed;
    }

    const uint16_t version = read_u16_be(static_header + kProtocolIdBytes);
    if (version < kProtocolVersion)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kNetworkReceiveFailed;
    }

    packet.flag = static_header[kStaticHeaderFlagOffset];
    std::copy_n(
        static_header + kStaticHeaderNonceOffset,
        packet.nonce.size(),
        packet.nonce.begin());
    packet.auth_size = read_u16_be(
        static_header + kStaticHeaderAuthSizeOffset);

    const size_t auth_end = kStaticPacketBytes + packet.auth_size;
    if (auth_end > length)
    {
        EVP_CIPHER_CTX_free(ctx);
        return discv5Error::kNetworkReceiveFailed;
    }

    packet.auth_data.resize(packet.auth_size);
    packet.header_data.resize(auth_end);
    if (packet.auth_size > 0U)
    {
        if (EVP_EncryptUpdate(
                ctx,
                packet.auth_data.data(),
                &out_len,
                data + kStaticPacketBytes,
                static_cast<int>(packet.auth_size)) != 1)
        {
            EVP_CIPHER_CTX_free(ctx);
            return discv5Error::kNetworkReceiveFailed;
        }
        std::copy(packet.auth_data.begin(), packet.auth_data.end(), packet.header_data.begin() + kStaticPacketBytes);
    }

    EVP_CIPHER_CTX_free(ctx);
    packet.msg_data.assign(data + auth_end, data + length);
    return packet;
}

Result<std::vector<uint8_t>> encode_packet(
    uint8_t flag,
    const std::array<uint8_t, kGcmNonceBytes>& nonce,
    const std::vector<uint8_t>& auth_data,
    const std::vector<uint8_t>& msg_data,
    const NodeAddress& destination_node_addr,
    std::vector<uint8_t>* unmasked_header_out = nullptr) noexcept
{
    std::vector<uint8_t> packet;
    packet.reserve(kStaticPacketBytes + auth_data.size() + msg_data.size());

    std::array<uint8_t, kMaskingIvBytes> iv{};
    if (!random_bytes(iv.data(), iv.size()))
    {
        return discv5Error::kNetworkSendFailed;
    }

    packet.insert(packet.end(), iv.begin(), iv.end());
    packet.insert(packet.end(), kProtocolId, kProtocolId + kProtocolIdBytes);
    append_u16_be(packet, kProtocolVersion);
    packet.push_back(flag);
    packet.insert(packet.end(), nonce.begin(), nonce.end());
    append_u16_be(packet, static_cast<uint16_t>(auth_data.size()));
    packet.insert(packet.end(), auth_data.begin(), auth_data.end());

    if (unmasked_header_out != nullptr)
    {
        *unmasked_header_out = packet;
    }

    std::array<uint8_t, kAes128KeyBytes> key{};
    std::copy_n(destination_node_addr.begin(), key.size(), key.begin());
    if (!apply_aes128_ctr(
            key,
            iv,
            packet.data() + kMaskingIvBytes,
            packet.data() + kMaskingIvBytes,
            packet.size() - kMaskingIvBytes))
    {
        return discv5Error::kNetworkSendFailed;
    }

    packet.insert(packet.end(), msg_data.begin(), msg_data.end());
    return packet;
}

std::vector<uint8_t> make_message_auth_data(const NodeAddress& local_node_addr)
{
    return std::vector<uint8_t>(local_node_addr.begin(), local_node_addr.end());
}

Result<std::vector<uint8_t>> make_local_enr_record(const discv5Config& config, uint16_t udp_port) noexcept
{
    const auto compressed_result = compress_public_key(config.public_key);
    if (!compressed_result)
    {
        return compressed_result.error();
    }

    boost::system::error_code ec;
    const asio::ip::address bind_addr = asio::ip::make_address(config.bind_ip, ec);
    if (ec)
    {
        return discv5Error::kEnrInvalidIp;
    }

    std::vector<uint8_t> ip_bytes;
    if (bind_addr.is_v4())
    {
        const auto bytes = bind_addr.to_v4().to_bytes();
        ip_bytes.assign(bytes.begin(), bytes.end());
    }
    else
    {
        ip_bytes = std::vector<uint8_t>(kIPv4Bytes, 0U);
    }

    const uint16_t tcp_port = (config.tcp_port != 0U) ? config.tcp_port : udp_port;

    rlp::RlpEncoder content_enc;
    if (!content_enc.BeginList() ||
        !content_enc.add(static_cast<uint64_t>(kInitialEnrSeq)) ||
        !content_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeyId), kEnrKeyIdBytes)) ||
        !content_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kIdentitySchemeV4), kIdentitySchemeV4Bytes)) ||
        !content_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeyIp), kEnrKeyIpBytes)) ||
        !content_enc.add(rlp::ByteView(ip_bytes.data(), ip_bytes.size())) ||
        !content_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeySecp256k1), kEnrKeySecp256k1Bytes)) ||
        !content_enc.add(rlp::ByteView(compressed_result.value().data(), compressed_result.value().size())) ||
        !content_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeyTcp), kEnrKeyTcpBytes)) ||
        !content_enc.add(tcp_port) ||
        !content_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeyUdp), kEnrKeyUdpBytes)) ||
        !content_enc.add(udp_port) ||
        !content_enc.EndList())
    {
        return discv5Error::kEnrRlpDecodeFailed;
    }

    auto content_bytes_result = content_enc.MoveBytes();
    if (!content_bytes_result)
    {
        return discv5Error::kEnrRlpDecodeFailed;
    }

    const rlp::Bytes& content_bytes = content_bytes_result.value();
    const auto content_hash = nil::crypto3::hash<nil::crypto3::hashes::keccak_1600<256>>(
        content_bytes.cbegin(), content_bytes.cend());
    const std::array<uint8_t, kKeccak256Bytes> content_hash_bytes = content_hash;

    secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
    if (ctx == nullptr)
    {
        return discv5Error::kContextCreationFailed;
    }

    secp256k1_ecdsa_recoverable_signature sig;
    if (!secp256k1_ecdsa_sign_recoverable(
            ctx,
            &sig,
            content_hash_bytes.data(),
            config.private_key.data(),
            nullptr,
            nullptr))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kContextCreationFailed;
    }

    std::array<uint8_t, kEnrSigBytes> compact_sig{};
    int recid = 0;
    secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, compact_sig.data(), &recid, &sig);
    secp256k1_context_destroy(ctx);

    rlp::RlpEncoder full_enc;
    if (!full_enc.BeginList() ||
        !full_enc.add(rlp::ByteView(compact_sig.data(), compact_sig.size())) ||
        !full_enc.add(static_cast<uint64_t>(kInitialEnrSeq)) ||
        !full_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeyId), kEnrKeyIdBytes)) ||
        !full_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kIdentitySchemeV4), kIdentitySchemeV4Bytes)) ||
        !full_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeyIp), kEnrKeyIpBytes)) ||
        !full_enc.add(rlp::ByteView(ip_bytes.data(), ip_bytes.size())) ||
        !full_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeySecp256k1), kEnrKeySecp256k1Bytes)) ||
        !full_enc.add(rlp::ByteView(compressed_result.value().data(), compressed_result.value().size())) ||
        !full_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeyTcp), kEnrKeyTcpBytes)) ||
        !full_enc.add(tcp_port) ||
        !full_enc.add(rlp::ByteView(reinterpret_cast<const uint8_t*>(kEnrKeyUdp), kEnrKeyUdpBytes)) ||
        !full_enc.add(udp_port) ||
        !full_enc.EndList())
    {
        return discv5Error::kEnrRlpDecodeFailed;
    }

    auto full_bytes_result = full_enc.MoveBytes();
    if (!full_bytes_result)
    {
        return discv5Error::kEnrRlpDecodeFailed;
    }

    const rlp::Bytes& full_bytes = full_bytes_result.value();
    return std::vector<uint8_t>(full_bytes.begin(), full_bytes.end());
}

Result<std::vector<uint8_t>> make_findnode_plaintext(const std::vector<uint8_t>& req_id) noexcept
{
    rlp::RlpEncoder enc;
    if (!enc.BeginList())
    {
        return discv5Error::kNetworkSendFailed;
    }

    if (!enc.add(rlp::ByteView(req_id.data(), req_id.size())))
    {
        return discv5Error::kNetworkSendFailed;
    }

    if (!enc.BeginList() ||
        !enc.add(static_cast<uint32_t>(kFindNodeDistanceAll)) ||
        !enc.EndList() ||
        !enc.EndList())
    {
        return discv5Error::kNetworkSendFailed;
    }

    auto bytes_result = enc.MoveBytes();
    if (!bytes_result)
    {
        return discv5Error::kNetworkSendFailed;
    }

    std::vector<uint8_t> plaintext;
    plaintext.reserve(kMessageTypePrefixBytes + bytes_result.value().size());
    plaintext.push_back(kMsgFindNode);
    plaintext.insert(plaintext.end(), bytes_result.value().begin(), bytes_result.value().end());
    return plaintext;
}

Result<std::vector<uint8_t>> make_nodes_plaintext(
    const std::vector<uint8_t>& req_id,
    const std::vector<uint8_t>& enr_record) noexcept
{
    rlp::RlpEncoder enc;
    if (!enc.BeginList() ||
        !enc.add(rlp::ByteView(req_id.data(), req_id.size())) ||!enc.add(kNodesResponseCountSingle) ||
        !enc.BeginList() ||
        !enc.AddRaw(rlp::ByteView(enr_record.data(), enr_record.size())) ||
        !enc.EndList() ||
        !enc.EndList())
    {
        return discv5Error::kNetworkSendFailed;
    }

    auto bytes_result = enc.MoveBytes();
    if (!bytes_result)
    {
        return discv5Error::kNetworkSendFailed;
    }

    std::vector<uint8_t> plaintext;
    plaintext.reserve(kMessageTypePrefixBytes + bytes_result.value().size());
    plaintext.push_back(kMsgNodes);
    plaintext.insert(plaintext.end(), bytes_result.value().begin(), bytes_result.value().end());
    return plaintext;
}

Result<std::vector<uint8_t>> parse_findnode_req_id(const std::vector<uint8_t>& body) noexcept
{
    rlp::RlpDecoder decoder(rlp::ByteView(body.data(), body.size()));
    auto outer_len = decoder.ReadListHeaderBytes();
    if (!outer_len)
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    rlp::Bytes req_id;
    if (!decoder.read(req_id))
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    return std::vector<uint8_t>(req_id.begin(), req_id.end());
}

Result<std::pair<std::vector<uint8_t>, std::vector<ValidatedPeer>>> parse_nodes_message(const std::vector<uint8_t>& body) noexcept
{
    rlp::RlpDecoder decoder(rlp::ByteView(body.data(), body.size()));
    auto outer_len = decoder.ReadListHeaderBytes();
    if (!outer_len)
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    rlp::Bytes req_id;
    if (!decoder.read(req_id))
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    uint8_t resp_count = 0U;
    if (!decoder.read(resp_count))
    {
        return discv5Error::kNetworkReceiveFailed;
    }
    (void)resp_count;

    auto nodes_len_result = decoder.ReadListHeaderBytes();
    if (!nodes_len_result)
    {
        return discv5Error::kNetworkReceiveFailed;
    }
    const size_t nodes_len = nodes_len_result.value();
    const rlp::ByteView nodes_start = decoder.Remaining();

    std::vector<ValidatedPeer> peers;
    while (!decoder.IsFinished())
    {
        const size_t consumed = nodes_start.size() - decoder.Remaining().size();
        if (consumed >= nodes_len)
        {
            break;
        }

        auto header_result = decoder.PeekHeader();
        if (!header_result)
        {
            break;
        }

        const auto& header = header_result.value();
        const size_t raw_len = header.header_size_bytes + header.payload_size_bytes;
        const rlp::ByteView raw_item = decoder.Remaining().substr(0U, raw_len);
        std::vector<uint8_t> raw_record(raw_item.begin(), raw_item.end());

        auto skip_result = decoder.SkipItem();
        if (!skip_result)
        {
            break;
        }

        auto record_result = EnrParser::decode_rlp(raw_record);
        if (!record_result)
        {
            continue;
        }

        auto verify_result = EnrParser::verify_signature(record_result.value());
        if (!verify_result)
        {
            continue;
        }

        auto peer_result = EnrParser::to_validated_peer(record_result.value());
        if (!peer_result)
        {
            continue;
        }

        peers.push_back(peer_result.value());
    }

    return std::make_pair(std::vector<uint8_t>(req_id.begin(), req_id.end()), peers);
}

Result<HandshakeAuthView> parse_handshake_auth(const std::vector<uint8_t>& auth_data) noexcept
{
    if (auth_data.size() < kHandshakeAuthFixedBytes)
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    HandshakeAuthView view;
    std::copy_n(auth_data.begin(), view.src_id.size(), view.src_id.begin());
    const uint8_t sig_size = auth_data[kHandshakeAuthSigSizeOffset];
    const uint8_t pubkey_size = auth_data[kHandshakeAuthPubkeySizeOffset];

    const size_t key_offset = kHandshakeAuthFixedBytes;
    const size_t pubkey_offset = key_offset + sig_size;
    const size_t record_offset = pubkey_offset + pubkey_size;
    if (record_offset > auth_data.size())
    {
        return discv5Error::kNetworkReceiveFailed;
    }

    view.signature.assign(auth_data.begin() + key_offset, auth_data.begin() + pubkey_offset);
    view.pubkey.assign(auth_data.begin() + pubkey_offset, auth_data.begin() + record_offset);
    view.record.assign(auth_data.begin() + record_offset, auth_data.end());
    return view;
}

} // anonymous namespace

 // ---------------------------------------------------------------------------
 // Constructor / Destructor
 // ---------------------------------------------------------------------------

 discv5_client::discv5_client(asio::io_context& io_context, const discv5Config& config)
     : io_context_(io_context)
     , config_(config)
     , socket_(io_context, udp::endpoint(udp::v4(), config.bind_port))
     , crawler_(config)
 {
 }

 discv5_client::~discv5_client()
 {
     stop();
 }

 // ---------------------------------------------------------------------------
 // add_bootnode
 // ---------------------------------------------------------------------------

 void discv5_client::add_bootnode(const std::string& enr_uri) noexcept
 {
     config_.bootstrap_enrs.push_back(enr_uri);
 }

 // ---------------------------------------------------------------------------
 // set_peer_discovered_callback / set_error_callback
 // ---------------------------------------------------------------------------

 void discv5_client::set_peer_discovered_callback(PeerDiscoveredCallback callback) noexcept
 {
     crawler_.set_peer_discovered_callback(std::move(callback));
 }

 void discv5_client::set_error_callback(ErrorCallback callback) noexcept
 {
     crawler_.set_error_callback(std::move(callback));
 }

 // ---------------------------------------------------------------------------
 // start
 // ---------------------------------------------------------------------------

 VoidResult discv5_client::start() noexcept
 {
     if (running_.exchange(true))
     {
         return rlp::outcome::success();  // Idempotent: already running
     }

     // Start the receive loop on the io_context.
     asio::spawn(io_context_, [this](asio::yield_context yield)
     {
         receive_loop(yield);
     });

     // Start the crawler loop.
     asio::spawn(io_context_, [this](asio::yield_context yield)
     {
         crawler_loop(yield);
     });

     // Seed the crawler with configured bootstrap entries.
     auto crawler_start = crawler_.start();
     if (!crawler_start)
     {
         logger_->warn("discv5_client: crawler start returned: {}",
                       to_string(crawler_start.error()));
     }

     logger_->info("discv5_client started on port {}", bound_port());
     return rlp::outcome::success();
 }

 // ---------------------------------------------------------------------------
 // stop
 // ---------------------------------------------------------------------------

 void discv5_client::stop() noexcept
 {
     if (!running_.exchange(false))
     {
         return;
     }

     boost::system::error_code ec;
     socket_.close(ec);
     if (ec)
     {
         logger_->warn("discv5_client: socket close error: {}", ec.message());
     }

     auto stop_result = crawler_.stop();
     (void)stop_result;
 }

 // ---------------------------------------------------------------------------
 // stats / local_node_id
 // ---------------------------------------------------------------------------

 CrawlerStats discv5_client::stats() const noexcept
 {
     return crawler_.stats();
 }

 const NodeId& discv5_client::local_node_id() const noexcept
 {
     return config_.public_key;
 }

 bool discv5_client::is_running() const noexcept
 {
     return running_.load();
 }

 uint16_t discv5_client::bound_port() const noexcept
 {
     boost::system::error_code ec;
     const auto endpoint = socket_.local_endpoint(ec);
     if (ec)
     {
         return 0U;
     }

     return endpoint.port();
 }

 size_t discv5_client::received_packet_count() const noexcept
 {
     return received_packets_.load();
 }

 size_t discv5_client::dropped_undersized_packet_count() const noexcept
 {
     return dropped_undersized_packets_.load();
 }

 size_t discv5_client::send_findnode_failure_count() const noexcept
 {
     return send_findnode_failures_.load();
 }

 size_t discv5_client::whoareyou_packet_count() const noexcept
 {
     return whoareyou_packets_.load();
 }

size_t discv5_client::handshake_packet_count() const noexcept
{
    return handshake_packets_.load();
}

size_t discv5_client::outbound_handshake_attempt_count() const noexcept
{
    return outbound_handshake_attempts_.load();
}

size_t discv5_client::outbound_handshake_failure_count() const noexcept
{
    return outbound_handshake_failures_.load();
}

size_t discv5_client::inbound_handshake_reject_auth_count() const noexcept
{
    return inbound_handshake_reject_auth_.load();
}

size_t discv5_client::inbound_handshake_reject_challenge_count() const noexcept
{
    return inbound_handshake_reject_challenge_.load();
}

size_t discv5_client::inbound_handshake_reject_record_count() const noexcept
{
    return inbound_handshake_reject_record_.load();
}

size_t discv5_client::inbound_handshake_reject_crypto_count() const noexcept
{
    return inbound_handshake_reject_crypto_.load();
}

size_t discv5_client::inbound_handshake_reject_decrypt_count() const noexcept
{
    return inbound_handshake_reject_decrypt_.load();
}

size_t discv5_client::inbound_handshake_seen_count() const noexcept
{
    return inbound_handshake_seen_.load();
}

size_t discv5_client::inbound_message_seen_count() const noexcept
{
    return inbound_message_seen_.load();
}

size_t discv5_client::inbound_message_decrypt_fail_count() const noexcept
{
    return inbound_message_decrypt_fail_.load();
}

size_t discv5_client::nodes_packet_count() const noexcept
{
    return nodes_packets_.load();
}

 // ---------------------------------------------------------------------------
 // receive_loop
 // ---------------------------------------------------------------------------

 void discv5_client::receive_loop(asio::yield_context yield)
 {
     // Receive buffer sized to the maximum valid discv5 packet.
     std::vector<uint8_t> buf(kMaxPacketBytes);

     while (running_.load())
     {
         udp::endpoint sender;
         boost::system::error_code ec;

         const size_t received = socket_.async_receive_from(
             asio::buffer(buf),
             sender,
             asio::redirect_error(yield, ec));

         if (ec)
         {
             if (!running_.load())
             {
                 break;  // Normal shutdown
             }
             logger_->warn("discv5 recv error: {}", ec.message());
             continue;
         }

         if (received < kMinPacketBytes)
         {
             ++dropped_undersized_packets_;
             logger_->debug("discv5: dropping undersized packet ({} bytes) from {}",
                            received, sender.address().to_string());
             continue;
         }

         ++received_packets_;
         handle_packet(buf.data(), received, sender);
     }
 }

 // ---------------------------------------------------------------------------
 // crawler_loop
 // ---------------------------------------------------------------------------

 void discv5_client::crawler_loop(asio::yield_context yield)
 {
     const auto interval = std::chrono::seconds(config_.query_interval_sec);
     asio::steady_timer timer(io_context_);

     while (running_.load())
     {
         // Drain the queued peer set: issue concurrent FINDNODE requests.
         size_t queries_issued = 0U;

         while (queries_issued < config_.max_concurrent_queries)
         {
             auto next = crawler_.dequeue_next();
             if (!next.has_value())
             {
                 break;
             }

             const ValidatedPeer peer = next.value();

             asio::spawn(io_context_,
                 [this, peer](asio::yield_context inner_yield)
                 {
                     auto result = send_findnode(peer, inner_yield);
                     if (!result)
                     {
                         ++send_findnode_failures_;
                         crawler_.mark_failed(peer.node_id);
                         logger_->debug("discv5 FINDNODE failed for {}:{}",
                                        peer.ip, peer.udp_port);
                     }
                     else
                     {
                         crawler_.mark_measured(peer.node_id);
                     }
                 });

             ++queries_issued;
         }

         if (queries_issued > 0U)
         {
             logger_->debug("discv5 crawler: {} FINDNODE queries issued", queries_issued);
         }

         // Sleep until next round.
         boost::system::error_code ec;
         timer.expires_after(interval);
         timer.async_wait(asio::redirect_error(yield, ec));

         if (ec && running_.load())
         {
             logger_->warn("discv5 crawler timer error: {}", ec.message());
         }
     }
 }

 // ---------------------------------------------------------------------------
 // handle_packet
 // ---------------------------------------------------------------------------

 void discv5_client::handle_packet(
     const uint8_t*       data,
     size_t               length,
     const udp::endpoint& sender) noexcept
 {
     logger_->debug("discv5: packet ({} bytes) from {}:{}",
                    length,
                    sender.address().to_string(),
                    sender.port());

     const NodeAddress local_node_addr = derive_node_address(config_.public_key);

     auto packet_result = decode_packet(data, length, local_node_addr);
     if (!packet_result)
     {
         logger_->debug("discv5: failed to decode packet from {}:{}",
                        sender.address().to_string(),
                        sender.port());
         return;
     }

     const PacketView& packet = packet_result.value();
     const std::string key = endpoint_key(sender);

     if (packet.flag == kFlagWhoareyou)
     {
         if (packet.auth_size != kWhoareyouAuthDataBytes)
         {
             return;
         }

         auto pending_it = pending_requests_.find(key);
         if (pending_it == pending_requests_.end() || pending_it->second.request_nonce != packet.nonce)
         {
             return;
         }

         std::copy_n(packet.auth_data.begin(), pending_it->second.id_nonce.size(), pending_it->second.id_nonce.begin());
         pending_it->second.record_seq = read_u64_be(packet.auth_data.data() + kWhoareyouIdNonceBytes);
         pending_it->second.challenge_data = packet.header_data;
         pending_it->second.have_challenge = true;

         ++whoareyou_packets_;
         asio::spawn(io_context_,
             [this, peer = pending_it->second.peer](asio::yield_context yield)
             {
                 auto result = send_findnode(peer, yield);
                 if (!result)
                 {
                     ++send_findnode_failures_;
                     crawler_.mark_failed(peer.node_id);
                 }
             });
         return;
     }

     if (packet.flag == kFlagMessage)
     {
         ++inbound_message_seen_;

         if (packet.auth_size != kMessageAuthDataBytes)
         {
             return;
         }

         NodeAddress remote_node_addr{};
         std::copy_n(packet.auth_data.begin(), remote_node_addr.size(), remote_node_addr.begin());

         auto session_it = sessions_.find(key);
         if (session_it == sessions_.end() || session_it->second.remote_node_addr != remote_node_addr)
         {
             asio::spawn(io_context_,
                 [this, sender, remote_node_addr, nonce = packet.nonce](asio::yield_context yield)
                 {
                     (void)send_whoareyou(sender, remote_node_addr, nonce, yield);
                 });
             return;
         }

         auto plaintext_result = decrypt_gcm(
             session_it->second.read_key,
             packet.nonce,
             packet.msg_data,
             packet.header_data);
         if (!plaintext_result)
         {
             ++inbound_message_decrypt_fail_;

             asio::spawn(io_context_,
                 [this, sender, remote_node_addr, nonce = packet.nonce](asio::yield_context yield)
                 {
                     (void)send_whoareyou(sender, remote_node_addr, nonce, yield);
                 });
             return;
         }

         const std::vector<uint8_t>& plaintext = plaintext_result.value();
         if (plaintext.empty())
         {
             return;
         }

         const uint8_t msg_type = plaintext.front();
         const std::vector<uint8_t> body(plaintext.begin() + kMessageTypePrefixBytes, plaintext.end());

         if (msg_type == kMsgNodes)
         {
             auto nodes_result = parse_nodes_message(body);
             if (!nodes_result)
             {
                 return;
             }

             if (!session_it->second.last_req_id.empty() &&
                 nodes_result.value().first != session_it->second.last_req_id)
             {
                 return;
             }

             ++nodes_packets_;
             crawler_.mark_measured(session_it->second.remote_node_id);
             crawler_.ingest_discovered_peers(nodes_result.value().second);
             return;
         }

         if (msg_type == kMsgFindNode)
         {
             auto req_id_result = parse_findnode_req_id(body);
             if (!req_id_result)
             {
                 return;
             }

             asio::spawn(io_context_,
                 [this, sender, req_id = req_id_result.value()](asio::yield_context yield)
                 {
                     (void)handle_findnode_request(req_id, sender, yield);
                 });
         }

         return;
     }

     if (packet.flag == kFlagHandshake)
     {
          ++inbound_handshake_seen_;

         auto auth_result = parse_handshake_auth(packet.auth_data);
         if (!auth_result)
         {
              ++inbound_handshake_reject_auth_;
             return;
         }

         auto challenge_it = sent_challenges_.find(key);
         if (challenge_it == sent_challenges_.end() || auth_result.value().src_id != challenge_it->second.remote_node_addr)
         {
              ++inbound_handshake_reject_challenge_;
             return;
         }

         if (auth_result.value().record.empty())
         {
              ++inbound_handshake_reject_record_;
             return;
         }

         auto record_result = EnrParser::decode_rlp(auth_result.value().record);
         if (!record_result)
         {
              ++inbound_handshake_reject_record_;
             return;
         }

         auto verify_result = EnrParser::verify_signature(record_result.value());
         if (!verify_result)
         {
              ++inbound_handshake_reject_record_;
             return;
         }

         const NodeAddress record_node_addr = derive_node_address(record_result.value().node_id);
         if (record_node_addr != auth_result.value().src_id)
         {
              ++inbound_handshake_reject_record_;
             return;
         }

         const NodeAddress local_id = derive_node_address(config_.public_key);
         if (!verify_id_signature(
                 record_result.value().node_id,
                 auth_result.value().signature,
                 challenge_it->second.challenge_data,
                 auth_result.value().pubkey,
                 local_id))
         {
              ++inbound_handshake_reject_record_;
             return;
         }

         auto shared_result = shared_secret_from_compressed_pubkey(auth_result.value().pubkey, config_.private_key);
         if (!shared_result)
         {
              ++inbound_handshake_reject_crypto_;
             return;
         }

         auto keys_result = derive_session_keys(
             shared_result.value(),
             challenge_it->second.challenge_data,
             auth_result.value().src_id,
             local_id);
         if (!keys_result)
         {
              ++inbound_handshake_reject_crypto_;
             return;
         }

         SessionState session;
         session.write_key = keys_result.value().second;
         session.read_key = keys_result.value().first;
         session.remote_node_addr = auth_result.value().src_id;
         session.remote_node_id = record_result.value().node_id;
         sessions_[key] = session;

         auto plaintext_result = decrypt_gcm(
             sessions_[key].read_key,
             packet.nonce,
             packet.msg_data,
             packet.header_data);
         if (!plaintext_result)
         {
             sessions_.erase(key);
              ++inbound_handshake_reject_decrypt_;
             return;
         }

         ++handshake_packets_;
         sent_challenges_.erase(key);

         const std::vector<uint8_t>& plaintext = plaintext_result.value();
         if (plaintext.empty())
         {
             return;
         }

         if (plaintext.front() == kMsgFindNode)
         {
             auto req_id_result = parse_findnode_req_id(
                 std::vector<uint8_t>(plaintext.begin() + kMessageTypePrefixBytes, plaintext.end()));
             if (!req_id_result)
             {
                 return;
             }

             sessions_[key].last_req_id = req_id_result.value();
             asio::spawn(io_context_,
                 [this, sender, req_id = req_id_result.value()](asio::yield_context yield)
                 {
                     (void)handle_findnode_request(req_id, sender, yield);
                 });
         }
     }
 }

 VoidResult discv5_client::send_findnode(const ValidatedPeer& peer, asio::yield_context yield)
 {
     const std::string key = endpoint_key(peer.ip, peer.udp_port);
     const NodeAddress local_node_addr = derive_node_address(config_.public_key);
     const NodeAddress remote_node_addr = derive_node_address(peer.node_id);

     std::vector<uint8_t> req_id =
     {
         static_cast<uint8_t>((peer.udp_port >> 8U) & 0xFFU),
         static_cast<uint8_t>(peer.udp_port & 0xFFU),
         static_cast<uint8_t>((peer.tcp_port >> 8U) & 0xFFU),
         static_cast<uint8_t>(peer.tcp_port & 0xFFU),
     };

     auto session_it = sessions_.find(key);
     if (session_it != sessions_.end())
     {
         auto plaintext_result = make_findnode_plaintext(req_id);
         if (!plaintext_result)
         {
             return plaintext_result.error();
         }

         std::array<uint8_t, kGcmNonceBytes> nonce{};
         if (!random_bytes(nonce.data(), nonce.size()))
         {
             return discv5Error::kNetworkSendFailed;
         }

         const std::vector<uint8_t> auth_data = make_message_auth_data(local_node_addr);
         std::vector<uint8_t> header_data;
         auto packet_result = encode_packet(
             kFlagMessage,
             nonce,
             auth_data,
             {},
             remote_node_addr,
             &header_data);
         if (!packet_result)
         {
             return packet_result.error();
         }

         auto encrypted_msg_result = encrypt_gcm(
             session_it->second.write_key,
             nonce,
             plaintext_result.value(),
             header_data);
         if (!encrypted_msg_result)
         {
             return encrypted_msg_result.error();
         }

          std::vector<uint8_t> packet = std::move(packet_result.value());
          packet.insert(
              packet.end(),
              encrypted_msg_result.value().begin(),
              encrypted_msg_result.value().end());

         session_it->second.last_req_id = req_id;
          return send_packet(packet, peer, yield);
     }

     auto pending_it = pending_requests_.find(key);
     if (pending_it != pending_requests_.end() && !pending_it->second.have_challenge)
     {
         pending_requests_.erase(pending_it);
         pending_it = pending_requests_.end();
     }

     if (pending_it == pending_requests_.end())
     {
         PendingRequest pending;
         pending.peer = peer;
         pending.req_id = req_id;
         if (!random_bytes(pending.request_nonce.data(), pending.request_nonce.size()))
         {
             return discv5Error::kNetworkSendFailed;
         }
         pending_requests_[key] = pending;

         const std::vector<uint8_t> auth_data = make_message_auth_data(local_node_addr);
         std::vector<uint8_t> random_msg(kRandomMessageCiphertextBytes);
         if (!random_bytes(random_msg.data(), random_msg.size()))
         {
             return discv5Error::kNetworkSendFailed;
         }

         auto packet_result = encode_packet(
             kFlagMessage,
             pending.request_nonce,
             auth_data,
             random_msg,
             remote_node_addr);
         if (!packet_result)
         {
             pending_requests_.erase(key);
             return packet_result.error();
         }

         return send_packet(packet_result.value(), peer, yield);
     }


      ++outbound_handshake_attempts_;

     auto eph_result = rlpx::crypto::Ecdh::generate_ephemeral_keypair();
     if (!eph_result)
     {
          ++outbound_handshake_failures_;
         return discv5Error::kContextCreationFailed;
     }

     auto eph_compressed_result = compress_public_key(eph_result.value().public_key);
     if (!eph_compressed_result)
     {
          ++outbound_handshake_failures_;
         return eph_compressed_result.error();
     }

     auto signature_result = make_id_signature(
         config_.private_key,
         pending_it->second.challenge_data,
         std::vector<uint8_t>(eph_compressed_result.value().begin(), eph_compressed_result.value().end()),
         remote_node_addr);
     if (!signature_result)
     {
          ++outbound_handshake_failures_;
         return signature_result.error();
     }

      std::vector<uint8_t> local_enr_record;
      if (pending_it->second.record_seq < static_cast<uint64_t>(kInitialEnrSeq))
      {
          auto enr_result = build_local_enr();
          if (!enr_result)
          {
              ++outbound_handshake_failures_;
              return enr_result.error();
          }
          local_enr_record = std::move(enr_result.value());
      }

     auto shared_result = shared_secret_from_uncompressed_pubkey(
         pending_it->second.peer.node_id,
         eph_result.value().private_key);
     if (!shared_result)
     {
          ++outbound_handshake_failures_;
         return shared_result.error();
     }

     auto keys_result = derive_session_keys(
         shared_result.value(),
         pending_it->second.challenge_data,
         local_node_addr,
         remote_node_addr);
     if (!keys_result)
     {
          ++outbound_handshake_failures_;
         return keys_result.error();
     }

     std::vector<uint8_t> auth_data;
      auth_data.reserve(kHandshakeAuthFixedBytes + signature_result.value().size() + eph_compressed_result.value().size() + local_enr_record.size());
     auth_data.insert(auth_data.end(), local_node_addr.begin(), local_node_addr.end());
     auth_data.push_back(static_cast<uint8_t>(signature_result.value().size()));
     auth_data.push_back(static_cast<uint8_t>(eph_compressed_result.value().size()));
     auth_data.insert(auth_data.end(), signature_result.value().begin(), signature_result.value().end());
     auth_data.insert(auth_data.end(), eph_compressed_result.value().begin(), eph_compressed_result.value().end());
      auth_data.insert(auth_data.end(), local_enr_record.begin(), local_enr_record.end());

     auto plaintext_result = make_findnode_plaintext(pending_it->second.req_id);
     if (!plaintext_result)
     {
          ++outbound_handshake_failures_;
         return plaintext_result.error();
     }

     std::array<uint8_t, kGcmNonceBytes> nonce{};
     if (!random_bytes(nonce.data(), nonce.size()))
     {
          ++outbound_handshake_failures_;
         return discv5Error::kNetworkSendFailed;
     }

     std::vector<uint8_t> header_data;
     auto packet_placeholder_result = encode_packet(
         kFlagHandshake,
         nonce,
         auth_data,
         {},
         remote_node_addr,
         &header_data);
     if (!packet_placeholder_result)
     {
          ++outbound_handshake_failures_;
         return packet_placeholder_result.error();
     }

     auto encrypted_result = encrypt_gcm(
         keys_result.value().first,
         nonce,
         plaintext_result.value(),
         header_data);
     if (!encrypted_result)
     {
          ++outbound_handshake_failures_;
         return encrypted_result.error();
     }

      std::vector<uint8_t> handshake_packet = std::move(packet_placeholder_result.value());
      handshake_packet.insert(
          handshake_packet.end(),
          encrypted_result.value().begin(),
          encrypted_result.value().end());

     SessionState session;
     session.write_key = keys_result.value().first;
     session.read_key = keys_result.value().second;
     session.remote_node_addr = remote_node_addr;
     session.remote_node_id = pending_it->second.peer.node_id;
     session.last_req_id = pending_it->second.req_id;
     sessions_[key] = session;
     pending_requests_.erase(key);

        auto send_result = send_packet(handshake_packet, peer, yield);
      if (!send_result)
      {
          ++outbound_handshake_failures_;
          return send_result.error();
      }

      return send_result;
 }

 VoidResult discv5_client::send_packet(
     const std::vector<uint8_t>& packet,
     const ValidatedPeer& peer,
     asio::yield_context yield)
 {
     boost::system::error_code ec;
     const auto address = asio::ip::make_address(peer.ip, ec);
     if (ec)
     {
         logger_->warn("discv5 send address parse failed for {}:{}: {}",
                       peer.ip, peer.udp_port, ec.message());
         return discv5Error::kNetworkSendFailed;
     }

     const udp::endpoint destination(address, peer.udp_port);
     socket_.async_send_to(
         asio::buffer(packet),
         destination,
         asio::redirect_error(yield, ec));

     if (ec)
     {
         logger_->warn("discv5 UDP send to {}:{} failed: {}",
                       peer.ip, peer.udp_port, ec.message());
         return discv5Error::kNetworkSendFailed;
     }

     return rlp::outcome::success();
 }

 VoidResult discv5_client::send_whoareyou(
     const udp::endpoint& sender,
     const std::array<uint8_t, kKeccak256Bytes>& remote_node_addr,
     const std::array<uint8_t, kGcmNonceBytes>& request_nonce,
     asio::yield_context yield)
 {
     ChallengeState challenge;
     challenge.remote_node_addr = remote_node_addr;
     challenge.request_nonce = request_nonce;
     challenge.record_seq = 0U;
     if (!random_bytes(challenge.id_nonce.data(), challenge.id_nonce.size()))
     {
         return discv5Error::kNetworkSendFailed;
     }

     std::vector<uint8_t> auth_data;
     auth_data.reserve(kWhoareyouAuthDataBytes);
     auth_data.insert(auth_data.end(), challenge.id_nonce.begin(), challenge.id_nonce.end());
     append_u64_be(auth_data, challenge.record_seq);

     auto packet_result = encode_packet(
         kFlagWhoareyou,
         request_nonce,
         auth_data,
         {},
         remote_node_addr,
         &challenge.challenge_data);
     if (!packet_result)
     {
         return packet_result.error();
     }

     sent_challenges_[endpoint_key(sender)] = challenge;

     boost::system::error_code ec;
     socket_.async_send_to(
         asio::buffer(packet_result.value()),
         sender,
         asio::redirect_error(yield, ec));
     if (ec)
     {
         return discv5Error::kNetworkSendFailed;
     }

     return rlp::outcome::success();
 }

 VoidResult discv5_client::handle_findnode_request(
     const std::vector<uint8_t>& req_id,
     const udp::endpoint& sender,
     asio::yield_context yield)
 {
     const std::string key = endpoint_key(sender);
     auto session_it = sessions_.find(key);
     if (session_it == sessions_.end())
     {
         return discv5Error::kNetworkSendFailed;
     }

     auto enr_result = build_local_enr();
     if (!enr_result)
     {
         return enr_result.error();
     }

     auto plaintext_result = make_nodes_plaintext(req_id, enr_result.value());
     if (!plaintext_result)
     {
         return plaintext_result.error();
     }

     std::array<uint8_t, kGcmNonceBytes> nonce{};
     if (!random_bytes(nonce.data(), nonce.size()))
     {
         return discv5Error::kNetworkSendFailed;
     }

     const NodeAddress local_node_addr = derive_node_address(config_.public_key);
     const std::vector<uint8_t> auth_data = make_message_auth_data(local_node_addr);

     std::vector<uint8_t> header_data;
     auto header_result = encode_packet(
         kFlagMessage,
         nonce,
         auth_data,
         {},
         session_it->second.remote_node_addr,
         &header_data);
     if (!header_result)
     {
         return header_result.error();
     }

     auto encrypted_result = encrypt_gcm(
         session_it->second.write_key,
         nonce,
         plaintext_result.value(),
         header_data);
     if (!encrypted_result)
     {
         return encrypted_result.error();
     }

      std::vector<uint8_t> packet = std::move(header_result.value());
      packet.insert(
          packet.end(),
          encrypted_result.value().begin(),
          encrypted_result.value().end());

     ValidatedPeer peer;
     peer.node_id = session_it->second.remote_node_id;
     peer.ip = sender.address().to_string();
     peer.udp_port = sender.port();
     peer.tcp_port = sender.port();
      return send_packet(packet, peer, yield);
 }

 Result<std::vector<uint8_t>> discv5_client::build_local_enr() noexcept
 {
     boost::system::error_code ec;
     const auto endpoint = socket_.local_endpoint(ec);
     if (ec)
     {
         return discv5Error::kNetworkSendFailed;
     }

     return make_local_enr_record(config_, endpoint.port());
 }

 } // namespace discv5

Updated on 2026-04-13 at 23:22:46 -0700