discv5_crawl/discv5_crawl.cpp¶
Functions¶
| Name | |
|---|---|
| int | main(int argc, char ** argv) |
Functions Documentation¶
function main¶
Source code¶
// Copyright 2025 GeniusVentures
// SPDX-License-Identifier: Apache-2.0
//
// discv5_crawl — live discv5 peer discovery example.
//
// Usage:
// discv5_crawl [options]
//
// Options:
// --chain <name> Chain to discover on. Supported names:
// ethereum (default), sepolia, holesky,
// polygon, amoy, bsc, bsc-testnet,
// base, base-sepolia
// --bootnode-enr <uri> Add an extra bootstrap ENR ("enr:…") or
// enode ("enode://…") URI. May be repeated.
// --port <udp-port> Local UDP bind port. Default: 9000.
// --timeout <secs> Stop after this many seconds. Default: 60.
// --log-level <level> spdlog level (trace/debug/info/warn/error).
// Default: info.
//
// The binary starts a discv5_client, seeds it from the selected chain's
// bootnode registry plus any explicit --bootnode-enr flags, and runs until
// the timeout expires. It reports the final CrawlerStats to stdout.
//
// This is an opt-in live test — it requires network access and is NOT
// wired into the CTest suite.
#include <boost/asio/spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/redirect_error.hpp>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include <discv5/discv5_client.hpp>
#include <discv5/discv5_bootnodes.hpp>
#include <discv5/discv5_constants.hpp>
#include <rlpx/crypto/ecdh.hpp>
#include <base/rlp-logger.hpp>
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
namespace
{
static const std::unordered_map<std::string, discv5::ChainId> kChainNameMap =
{
{ "ethereum", discv5::ChainId::kEthereumMainnet },
{ "mainnet", discv5::ChainId::kEthereumMainnet },
{ "sepolia", discv5::ChainId::kEthereumSepolia },
{ "holesky", discv5::ChainId::kEthereumHolesky },
{ "polygon", discv5::ChainId::kPolygonMainnet },
{ "amoy", discv5::ChainId::kPolygonAmoy },
{ "bsc", discv5::ChainId::kBscMainnet },
{ "bsc-testnet", discv5::ChainId::kBscTestnet },
{ "base", discv5::ChainId::kBaseMainnet },
{ "base-sepolia",discv5::ChainId::kBaseSepolia },
};
// ---------------------------------------------------------------------------
// CLI parsing
// ---------------------------------------------------------------------------
struct CliArgs
{
discv5::ChainId chain = discv5::ChainId::kEthereumMainnet;
std::vector<std::string> extra_enrs{};
uint16_t bind_port = discv5::kDefaultUdpPort;
uint32_t timeout_sec = 60U;
std::string log_level = "info";
};
void print_usage(const char* argv0)
{
std::cerr
<< "Usage: " << argv0 << " [options]\n"
<< "\nOptions:\n"
<< " --chain <name> ethereum|sepolia|holesky|polygon|amoy|bsc|bsc-testnet|base|base-sepolia\n"
<< " --bootnode-enr <uri> Add extra ENR or enode URI (may repeat)\n"
<< " --port <udp-port> Local UDP bind port (default: " << discv5::kDefaultUdpPort << ")\n"
<< " --timeout <secs> Stop after N seconds (default: 60)\n"
<< " --log-level <level> trace|debug|info|warn|error (default: info)\n"
<< " --help Show this message\n";
}
std::optional<CliArgs> parse_args(int argc, char** argv)
{
CliArgs args;
for (int i = 1; i < argc; ++i)
{
const std::string_view flag(argv[i]);
if (flag == "--help" || flag == "-h")
{
print_usage(argv[0]);
return std::nullopt;
}
auto require_next = [&](std::string_view name) -> const char*
{
if (i + 1 >= argc)
{
std::cerr << "Error: " << name << " requires an argument\n";
print_usage(argv[0]);
return nullptr;
}
return argv[++i];
};
if (flag == "--chain")
{
const char* val = require_next("--chain");
if (!val) { return std::nullopt; }
const auto it = kChainNameMap.find(std::string(val));
if (it == kChainNameMap.end())
{
std::cerr << "Error: unknown chain '" << val << "'\n";
print_usage(argv[0]);
return std::nullopt;
}
args.chain = it->second;
}
else if (flag == "--bootnode-enr")
{
const char* val = require_next("--bootnode-enr");
if (!val) { return std::nullopt; }
args.extra_enrs.emplace_back(val);
}
else if (flag == "--port")
{
const char* val = require_next("--port");
if (!val) { return std::nullopt; }
args.bind_port = static_cast<uint16_t>(std::stoul(val));
}
else if (flag == "--timeout")
{
const char* val = require_next("--timeout");
if (!val) { return std::nullopt; }
args.timeout_sec = static_cast<uint32_t>(std::stoul(val));
}
else if (flag == "--log-level")
{
const char* val = require_next("--log-level");
if (!val) { return std::nullopt; }
args.log_level = val;
}
else
{
std::cerr << "Error: unknown option '" << flag << "'\n";
print_usage(argv[0]);
return std::nullopt;
}
}
return args;
}
enum class StopReason
{
kIoStopped,
kTimeout,
kSignal,
};
enum class RunStatus
{
kCallbackEmissionsSeen,
kSendFailuresOnly,
kPartialLiveTraffic,
kMeasuredWithoutReceive,
kErrorsOnly,
kNoLiveResponse,
};
const char* to_string(StopReason reason) noexcept
{
switch (reason)
{
case StopReason::kTimeout:
return "timeout";
case StopReason::kSignal:
return "signal";
case StopReason::kIoStopped:
default:
return "io_stopped";
}
}
const char* to_string(RunStatus status) noexcept
{
switch (status)
{
case RunStatus::kCallbackEmissionsSeen:
return "callback_emissions_seen";
case RunStatus::kSendFailuresOnly:
return "send_failures_only";
case RunStatus::kPartialLiveTraffic:
return "partial_live_traffic";
case RunStatus::kMeasuredWithoutReceive:
return "measured_without_receive";
case RunStatus::kErrorsOnly:
return "errors_only";
case RunStatus::kNoLiveResponse:
default:
return "no_live_response";
}
}
RunStatus classify_run(
size_t callback_discoveries,
size_t callback_errors,
size_t received_packets,
size_t send_failures,
const discv5::CrawlerStats& stats) noexcept
{
if (callback_discoveries > 0U || stats.discovered > 0U)
{
return RunStatus::kCallbackEmissionsSeen;
}
if (send_failures > 0U)
{
return RunStatus::kSendFailuresOnly;
}
if (received_packets > 0U)
{
return RunStatus::kPartialLiveTraffic;
}
if (stats.measured > 0U)
{
return RunStatus::kMeasuredWithoutReceive;
}
if (callback_errors > 0U)
{
return RunStatus::kErrorsOnly;
}
return RunStatus::kNoLiveResponse;
}
const char* interpret_run(
size_t callback_discoveries,
size_t callback_errors,
size_t received_packets,
size_t whoareyou_packets,
size_t send_failures,
const discv5::CrawlerStats& stats) noexcept
{
if (callback_discoveries > 0U || stats.discovered > 0U)
{
return "peers were emitted by the crawler callback path";
}
if (send_failures > 0U)
{
return "outbound FINDNODE send failures occurred before any discovery callback fired";
}
if (whoareyou_packets > 0U)
{
return "remote WHOAREYOU challenges were parsed, but outbound handshake and NODES decode are not implemented yet";
}
if (received_packets > 0U)
{
return "live packets arrived, but receive-side WHOAREYOU/HANDSHAKE/NODES decode is still missing";
}
if (stats.measured > 0U)
{
return "crawler marked peers as measured, but no inbound packets were classified or emitted";
}
if (callback_errors > 0U)
{
return "the run reported crawler errors and produced no discoveries";
}
return "no observable discv5 traffic reached the current harness during the bounded run";
}
const char* consistency_note(
size_t callback_discoveries,
size_t received_packets,
size_t send_failures,
const discv5::CrawlerStats& stats) noexcept
{
if (callback_discoveries != stats.discovered)
{
return "callback discovery count differs from crawler discovered count";
}
if (stats.measured > 0U && received_packets == 0U)
{
return "measured peers were recorded without any receive-loop packet classification";
}
if (stats.failed > 0U && send_failures == 0U)
{
return "some peers failed without a local FINDNODE send-failure being recorded";
}
if (received_packets > 0U && stats.discovered == 0U)
{
return "packets were received, but no peers reached the discovered callback path";
}
return "counters are internally consistent for the current partial discv5 harness";
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int main(int argc, char** argv)
{
const auto args_opt = parse_args(argc, argv);
if (!args_opt.has_value())
{
return EXIT_FAILURE;
}
const CliArgs& args = args_opt.value();
// -----------------------------------------------------------------------
// Configure logging.
// -----------------------------------------------------------------------
spdlog::set_level(spdlog::level::from_str(args.log_level));
auto logger = rlp::base::createLogger("discv5_crawl");
// -----------------------------------------------------------------------
// Build bootnode seed list.
// -----------------------------------------------------------------------
discv5::discv5Config cfg;
cfg.bind_port = args.bind_port;
cfg.query_interval_sec = 10U; // Probe every 10 s for the live demo
const auto keypair_result = rlpx::crypto::Ecdh::generate_ephemeral_keypair();
if (!keypair_result)
{
logger->error("Failed to generate local secp256k1 keypair for discv5_crawl");
return EXIT_FAILURE;
}
std::copy(
keypair_result.value().private_key.begin(),
keypair_result.value().private_key.end(),
cfg.private_key.begin());
std::copy(
keypair_result.value().public_key.begin(),
keypair_result.value().public_key.end(),
cfg.public_key.begin());
size_t chain_seed_count = 0U;
size_t extra_seed_count = args.extra_enrs.size();
// Load seeds from the chain registry.
auto chain_source = discv5::ChainBootnodeRegistry::for_chain(args.chain);
if (chain_source)
{
const auto seeds = chain_source->fetch();
chain_seed_count = seeds.size();
cfg.bootstrap_enrs.insert(cfg.bootstrap_enrs.end(), seeds.begin(), seeds.end());
logger->info("Chain: {} seed count: {}",
discv5::ChainBootnodeRegistry::chain_name(args.chain),
seeds.size());
}
// Append any manually specified bootstrap URIs.
for (const auto& uri : args.extra_enrs)
{
cfg.bootstrap_enrs.push_back(uri);
logger->info("Extra bootnode: {}", uri.substr(0U, 60U));
}
if (cfg.bootstrap_enrs.empty())
{
logger->error("No bootstrap nodes available. Use --bootnode-enr or --chain.");
return EXIT_FAILURE;
}
// -----------------------------------------------------------------------
// Build and start the client.
// -----------------------------------------------------------------------
boost::asio::io_context io;
discv5::discv5_client client(io, cfg);
// Track peers as they are discovered.
std::atomic<size_t> total_discovered{0U};
std::atomic<size_t> total_errors{0U};
client.set_peer_discovered_callback(
[&logger, &total_discovered](const discovery::ValidatedPeer& peer)
{
++total_discovered;
logger->debug("Discovered peer {} {}:{} eth_fork={}",
total_discovered.load(),
peer.ip,
peer.tcp_port,
peer.eth_fork_id.has_value() ? "yes" : "no");
});
client.set_error_callback(
[&logger, &total_errors](const std::string& msg)
{
++total_errors;
logger->warn("Crawler error: {}", msg);
});
{
const auto start_result = client.start();
if (!start_result.has_value())
{
logger->error("Failed to start discv5 client: {}",
discv5::to_string(start_result.error()));
return EXIT_FAILURE;
}
}
const uint16_t actual_bound_port = client.bound_port();
StopReason stop_reason = StopReason::kIoStopped;
logger->info("discv5_crawl started on UDP port {}. Running for {} s …",
actual_bound_port, args.timeout_sec);
// -----------------------------------------------------------------------
// Run the io_context with a timeout and Ctrl-C handler.
// -----------------------------------------------------------------------
boost::asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait(
[&client, &io, &stop_reason](const boost::system::error_code& /*ec*/, int /*sig*/)
{
stop_reason = StopReason::kSignal;
client.stop();
io.stop();
});
boost::asio::spawn(io,
[&io, &client, &stop_reason, timeout_sec = args.timeout_sec](boost::asio::yield_context yield)
{
boost::asio::steady_timer timer(io);
timer.expires_after(std::chrono::seconds(timeout_sec));
boost::system::error_code ec;
timer.async_wait(boost::asio::redirect_error(yield, ec));
if (ec == boost::asio::error::operation_aborted)
{
return;
}
stop_reason = StopReason::kTimeout;
client.stop();
io.stop();
});
io.run();
// -----------------------------------------------------------------------
// Print final stats.
// -----------------------------------------------------------------------
const discv5::CrawlerStats stats = client.stats();
const size_t received_packets = client.received_packet_count();
const size_t whoareyou_packets = client.whoareyou_packet_count();
const size_t handshake_packets = client.handshake_packet_count();
const size_t outbound_handshake_attempts = client.outbound_handshake_attempt_count();
const size_t outbound_handshake_failures = client.outbound_handshake_failure_count();
const size_t inbound_hs_reject_auth = client.inbound_handshake_reject_auth_count();
const size_t inbound_hs_reject_challenge = client.inbound_handshake_reject_challenge_count();
const size_t inbound_hs_reject_record = client.inbound_handshake_reject_record_count();
const size_t inbound_hs_reject_crypto = client.inbound_handshake_reject_crypto_count();
const size_t inbound_hs_reject_decrypt = client.inbound_handshake_reject_decrypt_count();
const size_t inbound_hs_seen = client.inbound_handshake_seen_count();
const size_t inbound_msg_seen = client.inbound_message_seen_count();
const size_t inbound_msg_decrypt_fail = client.inbound_message_decrypt_fail_count();
const size_t nodes_packets = client.nodes_packet_count();
const size_t dropped_undersized_packets = client.dropped_undersized_packet_count();
const size_t send_failures = client.send_findnode_failure_count();
const RunStatus run_status = classify_run(
total_discovered.load(),
total_errors.load(),
received_packets,
send_failures,
stats);
const char* interpretation = interpret_run(
total_discovered.load(),
total_errors.load(),
received_packets,
whoareyou_packets,
send_failures,
stats);
const char* counter_note = consistency_note(
total_discovered.load(),
received_packets,
send_failures,
stats);
const bool show_trace_diagnostics = (spdlog::get_level() <= spdlog::level::trace);
std::cout << "\n=== discv5_crawl results ===\n"
<< " chain : " << discv5::ChainBootnodeRegistry::chain_name(args.chain) << "\n"
<< " udp port : " << actual_bound_port << "\n"
<< " stop reason : " << to_string(stop_reason) << "\n"
<< " run status : " << to_string(run_status) << "\n"
<< " chain seeds : " << chain_seed_count << "\n"
<< " extra seeds : " << extra_seed_count << "\n"
<< " bootstrap seeds : " << cfg.bootstrap_enrs.size() << "\n"
<< " callback discoveries : " << total_discovered.load() << "\n"
<< " callback errors : " << total_errors.load() << "\n"
<< " packets received : " << received_packets << "\n"
<< " whoareyou packets : " << whoareyou_packets << "\n"
<< " handshake packets : " << handshake_packets << "\n"
<< " nodes packets : " << nodes_packets << "\n"
<< " undersized dropped : " << dropped_undersized_packets << "\n"
<< " findnode send failures : " << send_failures << "\n"
<< " discovered : " << stats.discovered << "\n"
<< " queued : " << stats.queued << "\n"
<< " measured : " << stats.measured << "\n"
<< " failed : " << stats.failed << "\n"
<< " duplicates : " << stats.duplicates << "\n"
<< " wrong_chain : " << stats.wrong_chain << "\n"
<< " no_eth_entry: " << stats.no_eth_entry << "\n"
<< " invalid_enr : " << stats.invalid_enr << "\n"
<< " interpretation: " << interpretation << "\n"
<< " counter consistency: " << counter_note << "\n"
<< " note : use --log-level trace to include detailed handshake/message diagnostics\n";
if (show_trace_diagnostics)
{
std::cout << " handshake attempts : " << outbound_handshake_attempts << "\n"
<< " handshake failures : " << outbound_handshake_failures << "\n"
<< " hs reject auth : " << inbound_hs_reject_auth << "\n"
<< " hs reject challenge : " << inbound_hs_reject_challenge << "\n"
<< " hs reject record : " << inbound_hs_reject_record << "\n"
<< " hs reject crypto : " << inbound_hs_reject_crypto << "\n"
<< " hs reject decrypt : " << inbound_hs_reject_decrypt << "\n"
<< " hs inbound seen : " << inbound_hs_seen << "\n"
<< " msg inbound seen : " << inbound_msg_seen << "\n"
<< " msg decrypt fail : " << inbound_msg_decrypt_fail << "\n";
}
std::cout
<< "===========================\n";
return EXIT_SUCCESS;
}
Updated on 2026-04-13 at 23:22:46 -0700