discovery/test_enr_survey.cpp¶
Functions¶
| Name | |
|---|---|
| std::string | format_hash4(const std::array< uint8_t, 4U > & h) Format a 4-byte array as "aa bb cc dd". |
| int | main(int argc, char ** argv) |
Functions Documentation¶
function format_hash4¶
Format a 4-byte array as "aa bb cc dd".
function main¶
Source code¶
// Copyright 2026 Genius Ventures, Inc.
// SPDX-License-Identifier: MIT
//
// examples/discovery/test_enr_survey.cpp
//
// Diagnostic live test: run discv4 discovery with **no pre-dial filter**, collect
// every DiscoveredPeer produced by the ENR-enrichment path, and at the end print
// a frequency table of the eth fork-hashes actually seen in live ENR responses.
//
// This intentionally does zero dialing / RLPx — its only purpose is to determine:
// 1. Whether request_enr() is successfully completing for live Sepolia peers.
// 2. Which fork-hash bytes actually appear in live ENR `eth` entries.
// 3. Whether the Sepolia fork-hash used by make_fork_id_filter() is correct.
//
// Usage:
// ./test_enr_survey [--log-level debug] [--timeout 60]
#include <array>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#include <boost/asio/io_context.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <discv4/chain_peers.hpp>
#include <discv4/discv4_client.hpp>
#include <rlpx/crypto/ecdh.hpp>
#include <base/rlp-logger.hpp>
#include <spdlog/spdlog.h>
#include "../chain_config.hpp"
// ── Helpers ───────────────────────────────────────────────────────────────────
static std::string format_hash4( const std::array<uint8_t, 4U>& h ) noexcept
{
std::ostringstream oss;
oss << std::hex << std::setfill( '0' );
for ( size_t i = 0; i < h.size(); ++i )
{
if ( i != 0 ) { oss << ' '; }
oss << std::setw( 2 ) << static_cast<unsigned>( h[i] );
}
return oss.str();
}
// ── main ──────────────────────────────────────────────────────────────────────
int main( int argc, char** argv )
{
int timeout_secs = 60;
for ( int i = 1; i < argc; ++i )
{
std::string_view arg( argv[i] );
if ( arg == "--log-level" && i + 1 < argc )
{
std::string_view lvl( argv[++i] );
if ( lvl == "debug" ) { spdlog::set_level( spdlog::level::debug ); }
else if ( lvl == "info" ) { spdlog::set_level( spdlog::level::info ); }
else if ( lvl == "warn" ) { spdlog::set_level( spdlog::level::warn ); }
else if ( lvl == "off" ) { spdlog::set_level( spdlog::level::off ); }
}
else if ( arg == "--timeout" && i + 1 < argc )
{
timeout_secs = std::atoi( argv[++i] );
}
}
boost::asio::io_context io;
// ── discv4 setup (identical to test_discovery) ────────────────────────────
auto keypair_result = rlpx::crypto::Ecdh::generate_ephemeral_keypair();
if ( !keypair_result )
{
std::cout << "Failed to generate keypair\n";
return 1;
}
const auto& keypair = keypair_result.value();
discv4::discv4Config dv4_cfg;
dv4_cfg.bind_port = 0;
std::copy( keypair.private_key.begin(), keypair.private_key.end(), dv4_cfg.private_key.begin() );
std::copy( keypair.public_key.begin(), keypair.public_key.end(), dv4_cfg.public_key.begin() );
auto dv4 = std::make_shared<discv4::discv4_client>( io, dv4_cfg );
// ── Counters (written only from the single io_context thread) ─────────────
std::atomic<int> peers_total{ 0 };
std::atomic<int> peers_with_fork_id{ 0 };
std::atomic<int> peers_without_fork_id{ 0 };
// fork_hash → count (only safe to read after io.run() returns)
std::map<std::array<uint8_t, 4U>, int> fork_hash_counts;
// ── Peer callback: record without filtering ────────────────────────────────
dv4->set_peer_discovered_callback(
[&peers_total, &peers_with_fork_id, &peers_without_fork_id, &fork_hash_counts]
( const discv4::DiscoveredPeer& peer )
{
++peers_total;
if ( peer.eth_fork_id.has_value() )
{
++peers_with_fork_id;
fork_hash_counts[peer.eth_fork_id.value().hash]++;
}
else
{
++peers_without_fork_id;
}
} );
dv4->set_error_callback( []( const std::string& ) {} );
// ── Timeout ───────────────────────────────────────────────────────────────
boost::asio::steady_timer deadline( io, std::chrono::seconds( timeout_secs ) );
deadline.async_wait( [&]( boost::system::error_code )
{
dv4->stop();
io.stop();
} );
// ── Signal handler ────────────────────────────────────────────────────────
boost::asio::signal_set signals( io, SIGINT, SIGTERM );
signals.async_wait( [&]( boost::system::error_code, int )
{
deadline.cancel();
dv4->stop();
io.stop();
} );
const auto start_result = dv4->start();
if ( !start_result )
{
std::cout << "Failed to start discv4\n";
return 1;
}
const auto chain_config = load_chain_peer_config(
"ethereum-sepolia",
argv[0],
"",
"https://enodes.gnus.ai/chain_enodes.json.gz",
true );
if ( !chain_config.has_value() )
{
std::cout << "Failed to load ethereum-sepolia chain metadata\n";
return 1;
}
for ( const auto& bootnode_peer : chain_config->bootnodes )
{
std::string host_copy = bootnode_peer.peer.ip;
uint16_t port_copy = bootnode_peer.peer.udp_port;
discv4::NodeId bn_id = bootnode_peer.peer.node_id;
boost::asio::spawn( io,
[dv4, host_copy, port_copy, bn_id]( boost::asio::yield_context yc )
{
(void)dv4->find_node( host_copy, port_copy, bn_id, yc );
} );
}
std::cout << "\n[ ENR SURVEY ] Running for " << timeout_secs << "s ...\n\n";
io.run();
// ── Report ────────────────────────────────────────────────────────────────
// Load expected Sepolia hash from chain_enodes.json(.gz); fall back to compiled-in value.
static const std::array<uint8_t, 4U> kSepoliaHashFallback{ 0x26, 0x89, 0x56, 0xb6 };
const std::array<uint8_t, 4U> kSepoliaHash =
load_fork_hash( "ethereum-sepolia", argv[0] ).value_or( kSepoliaHashFallback );
const int total = peers_total.load();
const int with_id = peers_with_fork_id.load();
const int without = peers_without_fork_id.load();
std::cout << "========== ENR Survey Results ==========\n\n";
std::cout << " Peers discovered (total): " << total << "\n";
std::cout << " Peers WITH eth_fork_id: " << with_id << "\n";
std::cout << " Peers WITHOUT eth_fork_id: " << without << "\n\n";
if ( with_id == 0 )
{
std::cout << " *** No eth_fork_id was populated for ANY peer. ***\n";
std::cout << " This means request_enr() is failing or returning no eth entry\n";
std::cout << " for all live Sepolia peers. Check --log-level debug output.\n\n";
}
else
{
std::cout << " Fork hash breakdown (" << fork_hash_counts.size() << " distinct hash(es)):\n\n";
for ( const auto& [hash, count] : fork_hash_counts )
{
const bool is_sepolia = ( hash == kSepoliaHash );
std::cout << " " << format_hash4( hash )
<< " : " << std::setw( 6 ) << count << " peer(s)";
if ( is_sepolia )
{
std::cout << " <-- Sepolia expected hash MATCH";
}
std::cout << "\n";
}
std::cout << "\n Expected Sepolia hash: " << format_hash4( kSepoliaHash ) << "\n";
bool found_sepolia = ( fork_hash_counts.count( kSepoliaHash ) > 0 );
if ( found_sepolia )
{
std::cout << " Result: Sepolia hash IS present in live ENR data.\n";
std::cout << " The filter logic should work — investigate filter hookup.\n";
}
else
{
std::cout << " Result: Sepolia hash NOT found in live ENR data.\n";
std::cout << " Either the expected hash is wrong, or these peers are\n";
std::cout << " not on the Prague fork. Check the hashes above.\n";
}
}
std::cout << "\n==========================================\n\n";
std::cout.flush();
std::exit( 0 );
}
Updated on 2026-06-05 at 17:22:18 -0700