Skip to content

discv5/discv5_enr.cpp

Namespaces

Name
discv5

Attributes

Name
const std::array< uint8_t, 256 > kBase64UrlTable
Static decode table for the base64url alphabet (RFC-4648 §5). Index = ASCII code, value = 6-bit group (or kBase64Invalid). Built from the named constants in discv5_constants.hpp so that no bare literals appear in the initialiser.

Attributes Documentation

variable kBase64UrlTable

static const std::array< uint8_t, 256 > kBase64UrlTable                                         = []()
{
    std::array<uint8_t, 256> t{};
    t.fill(kBase64Invalid);


    for (uint8_t i = 0U; i < kBase64UpperCount; ++i)
    {
        t[static_cast<uint8_t>('A') + i] = i;
    }


    for (uint8_t i = 0U; i < kBase64LowerCount; ++i)
    {
        t[static_cast<uint8_t>('a') + i] = static_cast<uint8_t>(kBase64LowerStart + i);
    }


    for (uint8_t i = 0U; i < kBase64DigitCount; ++i)
    {
        t[static_cast<uint8_t>('0') + i] = static_cast<uint8_t>(kBase64DigitStart + i);
    }

    t[static_cast<uint8_t>('-')] = kBase64DashIndex;
    t[static_cast<uint8_t>('_')] = kBase64UnderIndex;
    return t;
}();

Static decode table for the base64url alphabet (RFC-4648 §5). Index = ASCII code, value = 6-bit group (or kBase64Invalid). Built from the named constants in discv5_constants.hpp so that no bare literals appear in the initialiser.

Source code

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

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

#include <rlp/rlp_decoder.hpp>
#include <rlp/rlp_encoder.hpp>
#include <rlp/common.hpp>

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

#include <secp256k1.h>

#include <algorithm>
#include <array>
#include <boost/asio/ip/address_v4.hpp>
#include <boost/asio/ip/address_v6.hpp>
#include <cstring>
#include <sstream>

namespace discv5
{

// ---------------------------------------------------------------------------
// Base64url decode lookup table
// ---------------------------------------------------------------------------

static const std::array<uint8_t, 256> kBase64UrlTable = []()
{
    std::array<uint8_t, 256> t{};
    t.fill(kBase64Invalid);

    // A–Z map to indices 0–25.
    for (uint8_t i = 0U; i < kBase64UpperCount; ++i)
    {
        t[static_cast<uint8_t>('A') + i] = i;
    }

    // a–z map to indices 26–51 (kBase64LowerStart).
    for (uint8_t i = 0U; i < kBase64LowerCount; ++i)
    {
        t[static_cast<uint8_t>('a') + i] = static_cast<uint8_t>(kBase64LowerStart + i);
    }

    // 0–9 map to indices 52–61 (kBase64DigitStart).
    for (uint8_t i = 0U; i < kBase64DigitCount; ++i)
    {
        t[static_cast<uint8_t>('0') + i] = static_cast<uint8_t>(kBase64DigitStart + i);
    }

    t[static_cast<uint8_t>('-')] = kBase64DashIndex;
    t[static_cast<uint8_t>('_')] = kBase64UnderIndex;
    return t;
}();

// ---------------------------------------------------------------------------
// Public: parse
// ---------------------------------------------------------------------------

Result<EnrRecord> EnrParser::parse(const std::string& enr_uri) noexcept
{
    BOOST_OUTCOME_TRY(auto raw, decode_uri(enr_uri));

    if (raw.size() > kEnrMaxBytes)
    {
        return discv5Error::kEnrTooLarge;
    }

    BOOST_OUTCOME_TRY(auto record, decode_rlp(raw));

    BOOST_OUTCOME_TRY(verify_signature(record));

    return record;
}

// ---------------------------------------------------------------------------
// Public: decode_uri
// ---------------------------------------------------------------------------

Result<std::vector<uint8_t>> EnrParser::decode_uri(const std::string& enr_uri) noexcept
{
    if (enr_uri.size() < kEnrPrefixLen ||
        enr_uri.compare(0, kEnrPrefixLen, kEnrPrefix) != 0)
    {
        return discv5Error::kEnrMissingPrefix;
    }

    const std::string body = enr_uri.substr(kEnrPrefixLen);
    return base64url_decode(body);
}

// ---------------------------------------------------------------------------
// Public: base64url_decode
// ---------------------------------------------------------------------------

Result<std::vector<uint8_t>> EnrParser::base64url_decode(const std::string& body) noexcept
{
    // Strip any trailing '=' padding that some implementations add.
    const size_t effective_len = [&]()
    {
        size_t n = body.size();
        while (n > 0U && body[n - 1U] == '=')
        {
            --n;
        }
        return n;
    }();

    // Output size = floor(effective_len * 6 / 8)
    const size_t out_size = (effective_len * 6U) / 8U;
    std::vector<uint8_t> out;
    out.reserve(out_size);

    uint32_t accumulator = 0U;
    size_t   bits        = 0U;

    for (size_t i = 0U; i < effective_len; ++i)
    {
        const uint8_t ch  = static_cast<uint8_t>(body[i]);
        const uint8_t val = kBase64UrlTable[ch];

        if (val == kBase64Invalid)
        {
            return discv5Error::kEnrBase64DecodeFailed;
        }

        accumulator = (accumulator << kBase64BitsPerChar) | val;
        bits += kBase64BitsPerChar;

        if (bits >= kBase64BitsPerByte)
        {
            bits -= kBase64BitsPerByte;
            out.push_back(static_cast<uint8_t>((accumulator >> bits) & 0xFFU));
        }
    }

    return out;
}

// ---------------------------------------------------------------------------
// Public: decode_rlp
// ---------------------------------------------------------------------------

Result<EnrRecord> EnrParser::decode_rlp(const std::vector<uint8_t>& raw) noexcept
{
    EnrRecord record;
    record.raw_rlp = raw;

    const rlp::ByteView view(raw.data(), raw.size());
    rlp::RlpDecoder     decoder(view);

    // Outer structure must be a list.
    {
        auto is_list_result = decoder.IsList();
        if (!is_list_result || !is_list_result.value())
        {
            return discv5Error::kEnrRlpDecodeFailed;
        }
    }

    // Read list header; returns the payload length in bytes.
    auto list_len_result = decoder.ReadListHeaderBytes();
    if (!list_len_result)
    {
        return discv5Error::kEnrRlpDecodeFailed;
    }
    const size_t list_payload_len = list_len_result.value();

    // Snapshot the view immediately after the outer list header.
    const rlp::ByteView after_list_header = decoder.Remaining();

    // ----- Element 0: signature (64 bytes, compact secp256k1) ---------------

    {
        rlp::Bytes sig_bytes;
        if (!decoder.read(sig_bytes))
        {
            return discv5Error::kEnrRlpDecodeFailed;
        }
        if (sig_bytes.size() != kEnrSigBytes)
        {
            return discv5Error::kEnrSignatureWrongSize;
        }
        // Store sig bytes for verify_signature.
        record.extra_fields["__sig__"] =
            std::vector<uint8_t>(sig_bytes.begin(), sig_bytes.end());
    }

    // ----- Compute content bytes for signature verification -----------------
    // content = everything in the outer list AFTER the signature field,
    // re-wrapped as a new RLP list: RLP([seq, k1, v1, ...])
    {
        const rlp::ByteView after_sig = decoder.Remaining();
        const size_t        sig_consumed =
            after_list_header.size() - after_sig.size();
        const size_t content_elements_len = list_payload_len - sig_consumed;

        rlp::RlpEncoder content_enc;
        if (!content_enc.BeginList() ||
            !content_enc.AddRaw(rlp::ByteView(after_sig.data(), content_elements_len)) ||
            !content_enc.EndList())
        {
            return discv5Error::kEnrRlpDecodeFailed;
        }
        auto content_bytes_result = content_enc.MoveBytes();
        if (!content_bytes_result)
        {
            return discv5Error::kEnrRlpDecodeFailed;
        }
        const rlp::Bytes& cb = content_bytes_result.value();
        record.extra_fields["__content__"] =
            std::vector<uint8_t>(cb.begin(), cb.end());
    }

    // ----- Element 1: sequence number (uint64) ------------------------------

    if (!decoder.read(record.seq))
    {
        return discv5Error::kEnrRlpDecodeFailed;
    }

    // ----- Elements 2..N: key–value pairs -----------------------------------

    while (!decoder.IsFinished())
    {
        // Key: encoded as RLP string (bytes → interpret as ASCII).
        rlp::Bytes key_bytes;
        if (!decoder.read(key_bytes))
        {
            break;
        }
        const std::string key(key_bytes.begin(), key_bytes.end());

        // Value: encoded as RLP string or embedded list.
        rlp::Bytes val_bytes;
        if (!decoder.read(val_bytes))
        {
            break;
        }
        const std::vector<uint8_t> val(val_bytes.begin(), val_bytes.end());

        if (key == "id")
        {
            record.identity_scheme = std::string(val.begin(), val.end());
        }
        else if (key == "secp256k1")
        {
            if (val.size() == kCompressedKeyBytes)
            {
                std::copy(val.begin(), val.end(), record.compressed_pubkey.begin());
            }
        }
        else if (key == "ip")
        {
            auto ip_result = decode_ipv4(val);
            if (ip_result)
            {
                record.ip = ip_result.value();
            }
        }
        else if (key == "ip6")
        {
            auto ip6_result = decode_ipv6(val);
            if (ip6_result)
            {
                record.ip6 = ip6_result.value();
            }
        }
        else if (key == "tcp")
        {
            auto port_result = decode_port(val);
            if (port_result)
            {
                record.tcp_port = port_result.value();
            }
        }
        else if (key == "udp")
        {
            auto port_result = decode_port(val);
            if (port_result)
            {
                record.udp_port = port_result.value();
            }
        }
        else if (key == "tcp6")
        {
            auto port_result = decode_port(val);
            if (port_result)
            {
                record.tcp6_port = port_result.value();
            }
        }
        else if (key == "udp6")
        {
            auto port_result = decode_port(val);
            if (port_result)
            {
                record.udp6_port = port_result.value();
            }
        }
        else if (key == "eth")
        {
            auto eth_result = decode_eth_entry(val);
            if (eth_result)
            {
                record.eth_fork_id = eth_result.value();
            }
        }
        else
        {
            record.extra_fields[key] = val;
        }
    }

    // Require at least the "secp256k1" key to be present.
    const bool has_pubkey =
        (record.compressed_pubkey != std::array<uint8_t, kCompressedKeyBytes>{});

    if (!has_pubkey)
    {
        return discv5Error::kEnrMissingSecp256k1Key;
    }

    return record;
}

// ---------------------------------------------------------------------------
// Public: verify_signature
// ---------------------------------------------------------------------------

VoidResult EnrParser::verify_signature(EnrRecord& record) noexcept
{
    // Retrieve stored signature and content bytes.
    const auto sig_it     = record.extra_fields.find("__sig__");
    const auto content_it = record.extra_fields.find("__content__");

    if (sig_it == record.extra_fields.end() || content_it == record.extra_fields.end())
    {
        return discv5Error::kEnrRlpDecodeFailed;
    }

    const std::vector<uint8_t>& sig_bytes     = sig_it->second;
    const std::vector<uint8_t>& content_bytes = content_it->second;

    // hash = keccak256(content)
    const auto hash_val =
        nil::crypto3::hash<nil::crypto3::hashes::keccak_1600<256>>(
            content_bytes.cbegin(), content_bytes.cend());
    const std::array<uint8_t, kKeccak256Bytes> hash_array = hash_val;

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

    // Parse the compressed public key from "secp256k1" field.
    secp256k1_pubkey pubkey;
    if (!secp256k1_ec_pubkey_parse(
            ctx, &pubkey,
            record.compressed_pubkey.data(),
            kCompressedKeyBytes))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kEnrInvalidSecp256k1Key;
    }

    // Parse the compact (64-byte) ECDSA signature.
    secp256k1_ecdsa_signature sig;
    if (!secp256k1_ecdsa_signature_parse_compact(
            ctx, &sig, sig_bytes.data()))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kEnrSignatureInvalid;
    }

    // Verify.
    if (!secp256k1_ecdsa_verify(ctx, &sig, hash_array.data(), &pubkey))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kEnrSignatureInvalid;
    }

    secp256k1_context_destroy(ctx);

    // Derive the 64-byte uncompressed node_id.
    auto node_id_result = decompress_pubkey(record.compressed_pubkey);
    if (!node_id_result)
    {
        return node_id_result.error();
    }
    record.node_id = node_id_result.value();

    // Clean up internal bookkeeping fields.
    record.extra_fields.erase("__sig__");
    record.extra_fields.erase("__content__");

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

// ---------------------------------------------------------------------------
// Public: to_validated_peer
// ---------------------------------------------------------------------------

Result<ValidatedPeer> EnrParser::to_validated_peer(const EnrRecord& record) noexcept
{
    if (record.ip.empty() && record.ip6.empty())
    {
        return discv5Error::kEnrMissingAddress;
    }

    ValidatedPeer peer;
    peer.node_id    = record.node_id;
    peer.eth_fork_id = record.eth_fork_id;
    peer.last_seen  = std::chrono::steady_clock::now();

    // Prefer IPv4 when available.
    if (!record.ip.empty())
    {
        peer.ip       = record.ip;
        peer.udp_port = (record.udp_port != 0U) ? record.udp_port : kDefaultUdpPort;
        peer.tcp_port = (record.tcp_port != 0U) ? record.tcp_port : kDefaultTcpPort;
    }
    else
    {
        peer.ip       = record.ip6;
        peer.udp_port = (record.udp6_port != 0U) ? record.udp6_port : kDefaultUdpPort;
        peer.tcp_port = (record.tcp6_port != 0U) ? record.tcp6_port : kDefaultTcpPort;
    }

    return peer;
}

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

Result<std::string> EnrParser::decode_ipv4(const std::vector<uint8_t>& bytes) noexcept
{
    if (bytes.size() != kIPv4Bytes)
    {
        return discv5Error::kEnrInvalidIp;
    }

    // Use IPv4Wire struct to name each field — no magic byte-array indexes.
    IPv4Wire ip{};
    std::memcpy(&ip, bytes.data(), sizeof(IPv4Wire));

    const boost::asio::ip::address_v4::bytes_type address_bytes{
        ip.msb,
        ip.b1,
        ip.b2,
        ip.lsb
    };

    return boost::asio::ip::address_v4(address_bytes).to_string();
}

Result<std::string> EnrParser::decode_ipv6(const std::vector<uint8_t>& bytes) noexcept
{
    if (bytes.size() != kIPv6Bytes)
    {
        return discv5Error::kEnrInvalidIp6;
    }

    // Use IPv6Wire struct and convert it via Boost.Asio.
    IPv6Wire ip6{};
    std::memcpy(ip6.bytes, bytes.data(), sizeof(IPv6Wire));

    boost::asio::ip::address_v6::bytes_type address_bytes{};
    std::copy(std::begin(ip6.bytes), std::end(ip6.bytes), address_bytes.begin());

    return boost::asio::ip::address_v6(address_bytes).to_string();
}

Result<uint16_t> EnrParser::decode_port(const std::vector<uint8_t>& bytes) noexcept
{
    if (bytes.empty() || bytes.size() > kMaxPortBytes)
    {
        return discv5Error::kEnrInvalidUdpPort;
    }

    uint16_t port = 0U;
    for (const uint8_t b : bytes)
    {
        port = static_cast<uint16_t>((port << 8U) | b);
    }

    if (port == 0U)
    {
        return discv5Error::kEnrInvalidUdpPort;
    }

    return port;
}

Result<ForkId> EnrParser::decode_eth_entry(const std::vector<uint8_t>& bytes) noexcept
{
    // "eth" value is RLP([[fork_hash(4 bytes), fork_next(uint64)]]).
    // The outer value bytes are the RLP-encoded list payload.
    if (bytes.empty())
    {
        return discv5Error::kEnrInvalidEthEntry;
    }

    const rlp::ByteView view(bytes.data(), bytes.size());
    rlp::RlpDecoder     outer_dec(view);

    // Outer list (the fork-id list).
    {
        auto is_list = outer_dec.IsList();
        if (!is_list || !is_list.value())
        {
            return discv5Error::kEnrInvalidEthEntry;
        }
    }
    if (!outer_dec.ReadListHeaderBytes())
    {
        return discv5Error::kEnrInvalidEthEntry;
    }

    // Inner fork-id record: [hash(4 bytes), next(uint64)].
    {
        auto is_list = outer_dec.IsList();
        if (!is_list || !is_list.value())
        {
            return discv5Error::kEnrInvalidEthEntry;
        }
    }
    if (!outer_dec.ReadListHeaderBytes())
    {
        return discv5Error::kEnrInvalidEthEntry;
    }

    // fork_hash — exactly kForkHashBytes bytes.
    rlp::Bytes hash_bytes;
    if (!outer_dec.read(hash_bytes) || hash_bytes.size() != kForkHashBytes)
    {
        return discv5Error::kEnrInvalidEthEntry;
    }

    ForkId fork_id;
    std::copy(hash_bytes.begin(), hash_bytes.end(), fork_id.hash.begin());

    // fork_next — uint64.
    if (!outer_dec.read(fork_id.next))
    {
        return discv5Error::kEnrInvalidEthEntry;
    }

    return fork_id;
}

Result<NodeId> EnrParser::decompress_pubkey(
    const std::array<uint8_t, kCompressedKeyBytes>& compressed) noexcept
{
    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, compressed.data(), kCompressedKeyBytes))
    {
        secp256k1_context_destroy(ctx);
        return discv5Error::kEnrInvalidSecp256k1Key;
    }

    // Serialise as uncompressed into a named wire struct — no bare 65 literal.
    UncompressedPubKeyWire raw_key{};
    size_t len = sizeof(UncompressedPubKeyWire);
    secp256k1_ec_pubkey_serialize(
        ctx, reinterpret_cast<uint8_t*>(&raw_key), &len,
        &pubkey, SECP256K1_EC_UNCOMPRESSED);

    secp256k1_context_destroy(ctx);

    // Copy the 64-byte X||Y payload (skip the 0x04 prefix stored in raw_key.prefix).
    NodeId node_id{};
    std::copy(raw_key.xy, raw_key.xy + kNodeIdBytes, node_id.begin());

    return node_id;
}

} // namespace discv5

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