From f47ca0a34d092667c73cd64a8783978b754b8612 Mon Sep 17 00:00:00 2001 From: turuslan Date: Wed, 22 Oct 2025 11:00:29 +0500 Subject: [PATCH 01/17] genesis Signed-off-by: turuslan --- src/app/configurator.cpp | 3 +- src/commands/generate_genesis.hpp | 104 ++++++++++++++++++ .../key_generate_node_key.hpp} | 0 src/executable/lean_node.cpp | 17 ++- src/serde/enr.cpp | 92 +++++++++++++--- src/serde/enr.hpp | 14 ++- src/utils/sample_peer.hpp | 27 +++-- vcpkg-overlay/leanp2p/portfile.cmake | 4 +- 8 files changed, 224 insertions(+), 37 deletions(-) create mode 100644 src/commands/generate_genesis.hpp rename src/{executable/cmd_key_generate_node_key.hpp => commands/key_generate_node_key.hpp} (100%) diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index e2e91b9..a6dd6fd 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -176,7 +176,8 @@ namespace lean::app { std::cout << "Lean-node version " << buildVersion() << '\n'; std::cout << cli_options_ << '\n'; std::println(std::cout, "Other commands:"); - std::println(std::cout, " qlean key generate-node-key"); + std::println(std::cout, " {} key generate-node-key", argv_[0]); + std::println(std::cout, " {} generate-genesis", argv_[0]); return true; } diff --git a/src/commands/generate_genesis.hpp b/src/commands/generate_genesis.hpp new file mode 100644 index 0000000..53e2941 --- /dev/null +++ b/src/commands/generate_genesis.hpp @@ -0,0 +1,104 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#include + +#include "utils/sample_peer.hpp" + +inline int cmdGenerateGenesis(auto &&getArg) { + auto cmd = [](std::filesystem::path genesis_directory, + size_t validator_count, + bool shadow) { + auto build_yaml = [](std::filesystem::path path, auto &&build) { + std::ofstream file{path}; + YAML::Node yaml; + build(yaml); + file << yaml << "\n"; + file.close(); + }; + + auto now = shadow + ? std::chrono::seconds{946684800} + : std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()); + uint64_t genesis_time = (now + std::chrono::seconds{3}).count(); + + std::filesystem::create_directories(genesis_directory); + + build_yaml(genesis_directory / "config.yaml", [&](YAML::Node &yaml) { + yaml["GENESIS_TIME"] = genesis_time; + yaml["VALIDATOR_COUNT"] = validator_count; + }); + + auto node_id = [](size_t index) { return std::format("node_{}", index); }; + std::vector peers; + for (size_t index = 0; index < validator_count; ++index) { + peers.emplace_back(index, shadow); + } + + for (auto &peer : peers) { + std::ofstream node_key{genesis_directory + / std::format("{}.key", node_id(peer.index))}; + std::println(node_key, + "{}", + qtils::ByteView{peer.keypair.privateKey.data}.toHex()); + node_key.close(); + } + + build_yaml(genesis_directory / "nodes.yaml", [&](YAML::Node &yaml) { + for (auto &peer : peers) { + yaml.push_back(peer.enr); + } + }); + + build_yaml(genesis_directory / "validators.yaml", [&](YAML::Node &yaml) { + for (auto &peer : peers) { + yaml[node_id(peer.index)].push_back(peer.index); + } + }); + + build_yaml(genesis_directory / "validator-config.yaml", + [&](YAML::Node &yaml) { + yaml["shuffle"] = "roundrobin"; + for (auto &peer : peers) { + YAML::Node yaml_peer; + yaml_peer["name"] = node_id(peer.index); + yaml_peer["privkey"] = + qtils::ByteView{peer.keypair.privateKey.data}.toHex(); + auto yaml_enr = yaml_peer["enrFields"]; + yaml_enr["ip"] = lean::enr::toString(peer.enr_ip); + yaml_enr["quic"] = peer.port; + yaml_peer["count"] = 1; + yaml["validators"].push_back(yaml_peer); + } + }); + }; + if (auto arg_2 = getArg(2)) { + std::filesystem::path genesis_directory{*arg_2}; + if (auto arg_3 = getArg(3)) { + size_t validator_count = std::stoul(std::string{*arg_3}); + if (validator_count != 0) { + auto arg_4 = getArg(4); + auto shadow = arg_4 == "shadow"; + if (not arg_4 or shadow) { + cmd(genesis_directory, validator_count, shadow); + return EXIT_SUCCESS; + } + } + } + } + std::println(std::cerr, + "Usage: {} generate-genesis (genesis_directory) " + "(validator_count) (shadow?)", + getArg(0).value()); + return EXIT_FAILURE; +} diff --git a/src/executable/cmd_key_generate_node_key.hpp b/src/commands/key_generate_node_key.hpp similarity index 100% rename from src/executable/cmd_key_generate_node_key.hpp rename to src/commands/key_generate_node_key.hpp diff --git a/src/executable/lean_node.cpp b/src/executable/lean_node.cpp index b59ce3a..7ac932d 100644 --- a/src/executable/lean_node.cpp +++ b/src/executable/lean_node.cpp @@ -17,7 +17,8 @@ #include "app/application.hpp" #include "app/configuration.hpp" #include "app/configurator.hpp" -#include "executable/cmd_key_generate_node_key.hpp" +#include "commands/generate_genesis.hpp" +#include "commands/key_generate_node_key.hpp" #include "injector/node_injector.hpp" #include "loaders/loader.hpp" #include "log/logger.hpp" @@ -130,14 +131,12 @@ int main(int argc, const char **argv, const char **env) { return EXIT_FAILURE; } - if (getArg(1) == "key") { - if (getArg(2) == "generate-node-key") { - cmdKeyGenerateNodeKey(); - return EXIT_SUCCESS; - } - std::println(std::cerr, "Expected one of following commands:"); - std::println(std::cerr, " qlean key generate-node-key"); - return EXIT_FAILURE; + if (getArg(1) == "key" and getArg(2) == "generate-node-key") { + cmdKeyGenerateNodeKey(); + return EXIT_SUCCESS; + } + if (getArg(1) == "generate-genesis") { + return cmdGenerateGenesis(getArg); } auto app_configurator = diff --git a/src/serde/enr.cpp b/src/serde/enr.cpp index d1cc935..d6936f2 100644 --- a/src/serde/enr.cpp +++ b/src/serde/enr.cpp @@ -8,7 +8,10 @@ #include +#include #include +#include +#include #include #include @@ -252,20 +255,36 @@ namespace lean::rlp { namespace lean::enr { enum class Error { + EXPECTED_SECP256K1_KEYPAIR, INVALID_PREFIX, INVALID_ID, + SIGNATURE_VERIFICATION_FAILED, }; Q_ENUM_ERROR_CODE(Error) { using E = decltype(e); switch (e) { + case E::EXPECTED_SECP256K1_KEYPAIR: + return "Expected secp256k1 keypair"; case E::INVALID_PREFIX: return "Invalid ENR prefix"; case E::INVALID_ID: return "Invalid ENR id"; + case E::SIGNATURE_VERIFICATION_FAILED: + return "Signature verification failed"; } abort(); } + Ip makeIp(uint32_t i) { + Ip ip; + boost::endian::store_big_u32(ip.data(), i); + return ip; + } + + std::string toString(const Ip &ip) { + return std::format("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]); + } + libp2p::PeerId Enr::peerId() const { return libp2p::peerIdFromSecp256k1(public_key); } @@ -279,11 +298,8 @@ namespace lean::enr { libp2p::Multiaddress Enr::connectAddress() const { auto &ip = this->ip.value(); return libp2p::Multiaddress::create( - std::format("/ip4/{}.{}.{}.{}/udp/{}/quic-v1/p2p/{}", - ip[0], - ip[1], - ip[2], - ip[3], + std::format("/ip4/{}/udp/{}/quic-v1/p2p/{}", + toString(ip), port.value(), peerId().toBase58())) .value(); @@ -293,6 +309,24 @@ namespace lean::enr { return {peerId(), {connectAddress()}}; } + inline void encodeContent(rlp::Encoder &rlp, const Enr &enr) { + rlp.uint(enr.sequence); + rlp.str("id"); + rlp.str("v4"); + rlp.str("ip"); + rlp.bytes(enr.ip.value()); + rlp.str("quic"); + rlp.uint(enr.port.value()); + rlp.str("secp256k1"); + rlp.bytes(enr.public_key); + } + + qtils::ByteVec Enr::signable() const { + rlp::Encoder rlp; + encodeContent(rlp, *this); + return rlp.list(); + } + outcome::result decode(std::string_view str) { constexpr std::string_view s_enr{"enr:"}; if (not str.starts_with(s_enr)) { @@ -333,22 +367,48 @@ namespace lean::enr { BOOST_OUTCOME_TRY(enr.port, kv_quic->second.uint()); } + libp2p::crypto::secp256k1::Secp256k1ProviderImpl secp256k1{nullptr}; + BOOST_OUTCOME_TRY( + auto valid_signature, + secp256k1.verifyCompact(libp2p::Keccak::hash(enr.signable()), + enr.signature, + enr.public_key)); + if (not valid_signature) { + return Error::SIGNATURE_VERIFICATION_FAILED; + } + return enr; } - std::string encode(const Secp256k1PublicKey &public_key, Port port) { - Enr enr{Secp256k1Signature{}, 1, public_key, Ip{127, 0, 0, 1}, port}; + outcome::result encode(const libp2p::crypto::KeyPair &keypair, + Ip ip, + Port port) { + if (keypair.privateKey.type != libp2p::crypto::Key::Type::Secp256k1) { + return Error::EXPECTED_SECP256K1_KEYPAIR; + } + if (keypair.publicKey.type != libp2p::crypto::Key::Type::Secp256k1) { + return Error::EXPECTED_SECP256K1_KEYPAIR; + } + BOOST_OUTCOME_TRY(auto private_key, + Secp256k1PrivateKey::fromSpan(keypair.privateKey.data)); + BOOST_OUTCOME_TRY(auto public_key, + Secp256k1PublicKey::fromSpan(keypair.publicKey.data)); + + Enr enr{ + .sequence = 1, + .public_key = public_key, + .ip = ip, + .port = port, + }; + + libp2p::crypto::secp256k1::Secp256k1ProviderImpl secp256k1{nullptr}; + enr.signature = Secp256k1Signature{ + secp256k1.signCompact(libp2p::Keccak::hash(enr.signable()), private_key) + .value()}; + rlp::Encoder rlp; rlp.bytes(enr.signature); - rlp.uint(enr.sequence); - rlp.str("id"); - rlp.str("v4"); - rlp.str("ip"); - rlp.bytes(enr.ip.value()); - rlp.str("quic"); - rlp.uint(enr.port.value()); - rlp.str("secp256k1"); - rlp.bytes(enr.public_key); + encodeContent(rlp, enr); return "enr:" + cppcodec::base64_url_unpadded::encode(rlp.list()); } } // namespace lean::enr diff --git a/src/serde/enr.hpp b/src/serde/enr.hpp index 096a273..743cb1f 100644 --- a/src/serde/enr.hpp +++ b/src/serde/enr.hpp @@ -12,13 +12,21 @@ #include #include +namespace libp2p::crypto { + struct KeyPair; +} // namespace libp2p::crypto + namespace lean::enr { using Secp256k1Signature = qtils::ByteArr<64>; using Sequence = uint64_t; + using Secp256k1PrivateKey = qtils::ByteArr<32>; using Secp256k1PublicKey = qtils::ByteArr<33>; using Ip = qtils::ByteArr<4>; using Port = uint16_t; + Ip makeIp(uint32_t i); + std::string toString(const Ip &ip); + struct Enr { Secp256k1Signature signature; Sequence sequence; @@ -30,9 +38,13 @@ namespace lean::enr { libp2p::Multiaddress listenAddress() const; libp2p::Multiaddress connectAddress() const; libp2p::PeerInfo connectInfo() const; + + qtils::ByteVec signable() const; }; outcome::result decode(std::string_view str); - std::string encode(const Secp256k1PublicKey &public_key, Port port); + outcome::result encode(const libp2p::crypto::KeyPair &keypair, + Ip ip, + Port port); } // namespace lean::enr diff --git a/src/utils/sample_peer.hpp b/src/utils/sample_peer.hpp index f1cbf34..0da73b7 100644 --- a/src/utils/sample_peer.hpp +++ b/src/utils/sample_peer.hpp @@ -12,14 +12,25 @@ namespace lean { struct SamplePeer : libp2p::SamplePeer { - SamplePeer(size_t index) : libp2p::SamplePeer{makeSecp256k1(index)} {} - - std::string enr() const { - enr::Secp256k1PublicKey public_key; - assert(keypair.publicKey.data.size() == public_key.size()); - memcpy( - public_key.data(), keypair.publicKey.data.data(), public_key.size()); - return enr::encode(public_key, port); + static enr::Ip makeIp(size_t index, bool shadow) { + return shadow ? enr::makeIp((10 << 24) + index) : enr::Ip{127, 0, 0, 1}; } + + SamplePeer(size_t index, bool shadow) + : libp2p::SamplePeer{ + index, + enr::toString(makeIp(index, shadow)), + samplePort(index), + Secp256k1, + }, + enr_ip{makeIp(index,shadow)}, + enr{enr::encode( + keypair, + enr_ip, + port + ).value()} {} + + enr::Ip enr_ip; + std::string enr; }; } // namespace lean diff --git a/vcpkg-overlay/leanp2p/portfile.cmake b/vcpkg-overlay/leanp2p/portfile.cmake index ad30b84..e95dbaf 100644 --- a/vcpkg-overlay/leanp2p/portfile.cmake +++ b/vcpkg-overlay/leanp2p/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/leanp2p - REF refs/tags/v0.0.2 - SHA512 72a82d313ca84fe0095c755daa733cfdd09609080a6a9f627587355d57d0bf4a34448884282f29ebd1e23fcc21d8f5181ada63ff3d87772e060e5e9f554406a2 + REF 56c1859237f454e6e029a54b0d71f3d6fb420404 + SHA512 19fce0c1463e85aff685634f2c3d8da17ff8899c2490e7d9f579818006fedd505b26a4b6b42eb5378fe0c494aceb4a7826a52fee6c29c9f97a6bcedb5a4307a5 ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") vcpkg_cmake_install() From ca9a1ba0589debc99109214c11163b0ef17d48e8 Mon Sep 17 00:00:00 2001 From: turuslan Date: Wed, 22 Oct 2025 13:20:58 +0500 Subject: [PATCH 02/17] fix test Signed-off-by: turuslan --- tests/unit/serde/enr_test.cpp | 52 ++++++++++++----------------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/tests/unit/serde/enr_test.cpp b/tests/unit/serde/enr_test.cpp index 7a4d969..dc8c041 100644 --- a/tests/unit/serde/enr_test.cpp +++ b/tests/unit/serde/enr_test.cpp @@ -13,33 +13,19 @@ #include +#include "utils/sample_peer.hpp" + using lean::enr::Enr; using lean::enr::Ip; using lean::enr::Port; using lean::enr::Secp256k1PublicKey; using lean::enr::Secp256k1Signature; -namespace { - - // Known valid compressed secp256k1 public key: generator point G - static constexpr std::array kCompressedG{ - 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, - 0x62, 0x95, 0xCE, 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, - 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17, 0x98}; - - Secp256k1PublicKey makeKey() { - Secp256k1PublicKey k{}; - std::copy(kCompressedG.begin(), kCompressedG.end(), k.begin()); - return k; - } - -} // namespace - TEST(EnrTest, EncodeDecodeRoundTrip) { - auto pub = makeKey(); - Port port = 9000; + lean::SamplePeer peer{0, false}; - auto encoded = lean::enr::encode(pub, port); + ASSERT_OUTCOME_SUCCESS( + encoded, lean::enr::encode(peer.keypair, peer.enr_ip, peer.port)); // Encoded string must start with "enr:" ASSERT_TRUE(encoded.rfind("enr:", 0) == 0) << encoded; @@ -47,12 +33,7 @@ TEST(EnrTest, EncodeDecodeRoundTrip) { // Basic fields EXPECT_EQ(enr.sequence, 1u); - EXPECT_EQ(enr.public_key, pub); - - // Signature is all-zero (not actually signed) - EXPECT_TRUE(std::all_of(enr.signature.begin(), - enr.signature.end(), - [](uint8_t b) { return b == 0; })); + EXPECT_EQ(SpanAdl{enr.public_key}, peer.keypair.publicKey.data); // IP and port present and correct ASSERT_TRUE(enr.ip.has_value()); @@ -61,7 +42,7 @@ TEST(EnrTest, EncodeDecodeRoundTrip) { EXPECT_EQ(enr.ip.value(), expected_ip); ASSERT_TRUE(enr.port.has_value()); - EXPECT_EQ(enr.port.value(), port); + EXPECT_EQ(enr.port.value(), peer.port); // PeerId must be derivable and non-empty auto pid = enr.peerId().toBase58(); @@ -73,20 +54,23 @@ TEST(EnrTest, EncodeDecodeRoundTrip) { } TEST(EnrTest, DeterministicEncoding) { - auto pub = makeKey(); - Port port = 12345; - auto e1 = lean::enr::encode(pub, port); - auto e2 = lean::enr::encode(pub, port); + lean::SamplePeer peer{0, false}; + lean::SamplePeer peer2{1, false}; + + ASSERT_OUTCOME_SUCCESS( + e1, lean::enr::encode(peer.keypair, peer.enr_ip, peer.port)); + ASSERT_OUTCOME_SUCCESS( + e2, lean::enr::encode(peer.keypair, peer.enr_ip, peer.port)); EXPECT_EQ(e1, e2); // Changing port changes ENR - auto e3 = lean::enr::encode(pub, static_cast(port + 1)); + ASSERT_OUTCOME_SUCCESS( + e3, lean::enr::encode(peer.keypair, peer.enr_ip, peer2.port)); EXPECT_NE(e1, e3); // Changing key changes ENR - auto pub2 = pub; - pub2[32] ^= 0x01; // tweak - auto e4 = lean::enr::encode(pub2, port); + ASSERT_OUTCOME_SUCCESS( + e4, lean::enr::encode(peer2.keypair, peer.enr_ip, peer.port)); EXPECT_NE(e1, e4); } From 35858344e4d85bf6a3205d48af8a571088f752dd Mon Sep 17 00:00:00 2001 From: turuslan Date: Wed, 22 Oct 2025 13:59:37 +0500 Subject: [PATCH 03/17] leanp2p tag Signed-off-by: turuslan --- vcpkg-overlay/leanp2p/portfile.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vcpkg-overlay/leanp2p/portfile.cmake b/vcpkg-overlay/leanp2p/portfile.cmake index e95dbaf..c8a34e3 100644 --- a/vcpkg-overlay/leanp2p/portfile.cmake +++ b/vcpkg-overlay/leanp2p/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/leanp2p - REF 56c1859237f454e6e029a54b0d71f3d6fb420404 - SHA512 19fce0c1463e85aff685634f2c3d8da17ff8899c2490e7d9f579818006fedd505b26a4b6b42eb5378fe0c494aceb4a7826a52fee6c29c9f97a6bcedb5a4307a5 + REF refs/tags/v0.0.3 + SHA512 da5ef7a621f2758e588972b958fc35706ddfd9a0ee9fd378c113cbe0348a28788acc09fa7e23b6c663f9f5b4d1448b245e7610f07ff02b80be4a074a9e27a102 ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") vcpkg_cmake_install() From 7ab2a31b0423bbd9a8c1c2f47da3166068fae5a4 Mon Sep 17 00:00:00 2001 From: kamilsa Date: Thu, 23 Oct 2025 09:53:52 +0500 Subject: [PATCH 04/17] Add script to generate Shadow network YAML from genesis directory --- scripts/gen_shadow_yaml.sh | 190 +++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100755 scripts/gen_shadow_yaml.sh diff --git a/scripts/gen_shadow_yaml.sh b/scripts/gen_shadow_yaml.sh new file mode 100755 index 0000000..7b284fb --- /dev/null +++ b/scripts/gen_shadow_yaml.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +# Generate a Shadow network YAML from a genesis folder (e.g., genesis_test) +# Default output name: shadow_network.yaml +# Usage: +# gen_shadow_yaml.sh -g [-o ] [options] +# Options: +# -g GENESIS_DIR Path to the genesis directory containing config.yaml, validators.yaml, nodes.yaml, node_*.key (required) +# -o OUTPUT_YAML Output YAML path (default: ./shadow_network.yaml) +# -t STOP_TIME Shadow stop_time value (default: 60s) +# -u UDP_BASE Base UDP port for --listen-addr (default: 9000) +# -p PROM_BASE Base Prometheus port (default: 9100) +# -i IP_BASE_LAST_OCTET Base last octet for IPs starting at 10.0.0.X (default: 10) +# -x QLEAN_PATH Path to qlean executable (default: /build/src/executable/qlean) +# -m MODULES_DIR Path to modules dir (default: /build/src/modules) +# -r PROJECT_ROOT Project root to use for defaults (default: parent dir of this script) +# +# Notes: +# - Node count is inferred from node_*.key files in GENESIS_DIR. +# - Ports increment by +index per node. +# - IPs are assigned as 10.0.0.(IP_BASE_LAST_OCTET + index) +# - Paths are emitted literally in YAML and quoted; override with -x/-m if needed. + +set -euo pipefail + +print_usage() { + sed -n '1,45p' "$0" | sed 's/^# \{0,1\}//' +} + +# Defaults +OUTPUT_YAML="shadow_network.yaml" +STOP_TIME="60s" +UDP_BASE=9000 +PROM_BASE=9100 +IP_BASE_LAST_OCTET=10 +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT_DEFAULT="$(cd "$SCRIPT_DIR/.." && pwd)" +PROJECT_ROOT="$PROJECT_ROOT_DEFAULT" +QLEAN_PATH_DEFAULT="$PROJECT_ROOT/build/src/executable/qlean" +MODULES_DIR_DEFAULT="$PROJECT_ROOT/build/src/modules" +QLEAN_PATH="$QLEAN_PATH_DEFAULT" +MODULES_DIR="$MODULES_DIR_DEFAULT" +GENESIS_DIR="" + +while getopts ":g:o:t:u:p:i:x:m:r:h" opt; do + case $opt in + g) GENESIS_DIR="$OPTARG" ;; + o) OUTPUT_YAML="$OPTARG" ;; + t) STOP_TIME="$OPTARG" ;; + u) UDP_BASE="$OPTARG" ;; + p) PROM_BASE="$OPTARG" ;; + i) IP_BASE_LAST_OCTET="$OPTARG" ;; + x) QLEAN_PATH="$OPTARG" ;; + m) MODULES_DIR="$OPTARG" ;; + r) PROJECT_ROOT="$OPTARG" ; QLEAN_PATH_DEFAULT="$PROJECT_ROOT/build/src/executable/qlean"; MODULES_DIR_DEFAULT="$PROJECT_ROOT/build/src/modules" ;; + h) print_usage; exit 0 ;; + :) echo "Error: Option -$OPTARG requires an argument" >&2; print_usage; exit 2 ;; + \?) echo "Error: Invalid option -$OPTARG" >&2; print_usage; exit 2 ;; + esac +done + +# Apply defaults that depend on PROJECT_ROOT if user didn't override +if [[ "$QLEAN_PATH" == "$QLEAN_PATH_DEFAULT" && ! -x "$QLEAN_PATH_DEFAULT" ]]; then + # Keep default even if not built yet; just warn + echo "Warning: qlean not found at $QLEAN_PATH_DEFAULT; ensure you build it or pass -x" >&2 +fi +if [[ "$MODULES_DIR" == "$MODULES_DIR_DEFAULT" && ! -d "$MODULES_DIR_DEFAULT" ]]; then + echo "Warning: modules dir not found at $MODULES_DIR_DEFAULT; ensure you build modules or pass -m" >&2 +fi + +# Validate genesis dir +if [[ -z "$GENESIS_DIR" ]]; then + echo "Error: -g GENESIS_DIR is required" >&2 + print_usage + exit 2 +fi +if [[ ! -d "$GENESIS_DIR" ]]; then + echo "Error: GENESIS_DIR '$GENESIS_DIR' does not exist or is not a directory" >&2 + exit 2 +fi + +# Resolve absolute paths using Python for macOS portability (realpath -f is not standard) +py_abspath() { python3 - "$1" <<'PY' +import os,sys +p=sys.argv[1] +print(os.path.abspath(p)) +PY +} + +GENESIS_DIR_ABS="$(py_abspath "$GENESIS_DIR")" +QLEAN_PATH_ABS="$(py_abspath "$QLEAN_PATH")" +MODULES_DIR_ABS="$(py_abspath "$MODULES_DIR")" +OUTPUT_YAML_ABS="$(py_abspath "$(dirname "$OUTPUT_YAML")")/$(basename "$OUTPUT_YAML")" + +CONFIG_YAML="$GENESIS_DIR_ABS/config.yaml" +VALIDATORS_YAML="$GENESIS_DIR_ABS/validators.yaml" +NODES_YAML="$GENESIS_DIR_ABS/nodes.yaml" + +for f in "$CONFIG_YAML" "$VALIDATORS_YAML" "$NODES_YAML"; do + if [[ ! -f "$f" ]]; then + echo "Error: required file missing in genesis dir: $f" >&2 + exit 2 + fi +done + +# Collect node keys (portable, numerically sorted by index) +export GENESIS_DIR_ABS +NODE_KEY_FILES=() +while IFS= read -r line; do + NODE_KEY_FILES+=("$line") +done < <(python3 - <<'PY' +import os, re, sys +root = os.environ.get('GENESIS_DIR_ABS') +pat = re.compile(r'^node_(\d+)\.key$') +items = [] +for name in os.listdir(root): + m = pat.match(name) + if m: + items.append((int(m.group(1)), os.path.join(root, name))) +for _, path in sorted(items, key=lambda x: x[0]): + print(path) +PY +) +NODE_COUNT=${#NODE_KEY_FILES[@]} +if [[ "$NODE_COUNT" -eq 0 ]]; then + echo "Error: no node_*.key files found in $GENESIS_DIR_ABS" >&2 + exit 2 +fi + +# Try to read validator count and warn on mismatch +if VAL_COUNT=$(grep -E '^\s*VALIDATOR_COUNT\s*:' "$CONFIG_YAML" | awk -F: '{gsub(/ /,"",$2); print $2}'); then + if [[ -n "$VAL_COUNT" && "$VAL_COUNT" != "$NODE_COUNT" ]]; then + echo "Warning: VALIDATOR_COUNT ($VAL_COUNT) != number of node_*.key files ($NODE_COUNT)" >&2 + fi +fi + +# Helper: YAML double-quoted string escape +yaml_escape() { + local s="$1" + s="${s//\\/\\\\}" # escape backslashes + s="${s//\"/\\\"}" # escape double quotes + printf '%s' "$s" +} + +# Start writing YAML +mkdir -p "$(dirname "$OUTPUT_YAML_ABS")" +{ + printf "general:\n" + printf " stop_time: %s\n" "$STOP_TIME" + printf " model_unblocked_syscall_latency: true\n" + printf "experimental:\n" + printf " native_preemption_enabled: true\n" + printf "network:\n" + printf " graph:\n" + printf " type: 1_gbit_switch\n" + printf "hosts:\n" + + for ((i=0; i "$OUTPUT_YAML_ABS" + +echo "Wrote $OUTPUT_YAML_ABS with $NODE_COUNT node(s)." From b330b55f873b3325cb4edd9d8ef6db8a900f484e Mon Sep 17 00:00:00 2001 From: kamilsa Date: Thu, 23 Oct 2025 10:53:18 +0300 Subject: [PATCH 05/17] Enhance validator configuration parsing in shadow YAML generation --- scripts/gen_shadow_yaml.sh | 71 +++++++++++++++++++++++++-- src/modules/networking/networking.cpp | 2 +- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/scripts/gen_shadow_yaml.sh b/scripts/gen_shadow_yaml.sh index 7b284fb..bc6b261 100755 --- a/scripts/gen_shadow_yaml.sh +++ b/scripts/gen_shadow_yaml.sh @@ -93,8 +93,16 @@ OUTPUT_YAML_ABS="$(py_abspath "$(dirname "$OUTPUT_YAML")")/$(basename "$OUTPUT_Y CONFIG_YAML="$GENESIS_DIR_ABS/config.yaml" VALIDATORS_YAML="$GENESIS_DIR_ABS/validators.yaml" +VALIDATOR_CONFIG_YAML="$GENESIS_DIR_ABS/validator-config.yaml" NODES_YAML="$GENESIS_DIR_ABS/nodes.yaml" +# Prefer validator-config.yaml for parsing enrFields (it's used in some genesis folders). +if [[ -f "$VALIDATOR_CONFIG_YAML" ]]; then + PARSE_VALIDATOR_FILE="$VALIDATOR_CONFIG_YAML" +else + PARSE_VALIDATOR_FILE="$VALIDATORS_YAML" +fi + for f in "$CONFIG_YAML" "$VALIDATORS_YAML" "$NODES_YAML"; do if [[ ! -f "$f" ]]; then echo "Error: required file missing in genesis dir: $f" >&2 @@ -133,6 +141,51 @@ if VAL_COUNT=$(grep -E '^\s*VALIDATOR_COUNT\s*:' "$CONFIG_YAML" | awk -F: '{gsub fi fi +# Parse validators.yaml to extract enrFields.ip and enrFields.quic per validator (if present). +# We prefer these values over generated defaults so shadow uses the same IPs/ports as the genesis. +VALIDATOR_IPS=() +VALIDATOR_QUICS=() +# This python snippet prints one line per node index: " " (empty strings if not found) +while IFS= read -r _line; do + VALIDATOR_IPS+=("$(echo "$_line" | awk '{print $1}')") + VALIDATOR_QUICS+=("$(echo "$_line" | awk '{print $2}')") +done < <(python3 - "$PARSE_VALIDATOR_FILE" "$NODE_COUNT" <<'PY' +import sys, re +path = sys.argv[1] +node_count = int(sys.argv[2]) +mapping = {} +name = None +in_enr = False +with open(path) as f: + for raw in f: + line = raw.rstrip('\n') + m = re.match(r'^\s*-\s*name:\s*(\S+)', line) + if m: + name = m.group(1) + in_enr = False + continue + if re.match(r'^\s*enrFields:\s*', line): + in_enr = True + continue + if in_enr and name is not None: + m_ip = re.match(r'^\s*ip:\s*(\S+)', line) + if m_ip: + mapping.setdefault(name, {})['ip'] = m_ip.group(1) + continue + m_quic = re.match(r'^\s*quic:\s*(\S+)', line) + if m_quic: + mapping.setdefault(name, {})['quic'] = m_quic.group(1) + continue +# Emit ip and quic for node_0 .. node_{N-1} +for i in range(node_count): + nm = f'node_{i}' + ent = mapping.get(nm, {}) + ip = ent.get('ip', '') + quic = str(ent.get('quic', '')) + print(ip + ' ' + quic) +PY +) + # Helper: YAML double-quoted string escape yaml_escape() { local s="$1" @@ -157,9 +210,21 @@ mkdir -p "$(dirname "$OUTPUT_YAML_ABS")" for ((i=0; i + if [[ -n "${VALIDATOR_IPS[$i]}" ]]; then + ip="${VALIDATOR_IPS[$i]}" + else + ip_last=$((IP_BASE_LAST_OCTET + i)) + ip="10.0.0.$ip_last" + fi + + # Prefer the quic port from validators.yaml if present; otherwise use UDP_BASE + index + if [[ -n "${VALIDATOR_QUICS[$i]}" ]]; then + udp_port="${VALIDATOR_QUICS[$i]}" + else + udp_port=$((UDP_BASE + i)) + fi + prom_port=$((PROM_BASE + i)) # Build args string diff --git a/src/modules/networking/networking.cpp b/src/modules/networking/networking.cpp index 303e7e8..4722313 100644 --- a/src/modules/networking/networking.cpp +++ b/src/modules/networking/networking.cpp @@ -320,7 +320,7 @@ namespace lean::modules { res.error()); return; } - SL_INFO(self->logger_, + SL_DEBUG(self->logger_, "Received vote for target {}", signed_vote.data.target); }); From 113cb406bd4db00e39f9a620b052cf3e81131594 Mon Sep 17 00:00:00 2001 From: kamilsa Date: Fri, 24 Oct 2025 10:38:37 +0300 Subject: [PATCH 06/17] Add network graph generation to shadow YAML output --- scripts/gen_shadow_yaml.sh | 56 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/scripts/gen_shadow_yaml.sh b/scripts/gen_shadow_yaml.sh index bc6b261..2b0cb7e 100755 --- a/scripts/gen_shadow_yaml.sh +++ b/scripts/gen_shadow_yaml.sh @@ -17,7 +17,7 @@ # Notes: # - Node count is inferred from node_*.key files in GENESIS_DIR. # - Ports increment by +index per node. -# - IPs are assigned as 10.0.0.(IP_BASE_LAST_OCTET + index) +# - IPs are assigned as 10.0.0.(base+idx) # - Paths are emitted literally in YAML and quoted; override with -x/-m if needed. set -euo pipefail @@ -41,6 +41,12 @@ QLEAN_PATH="$QLEAN_PATH_DEFAULT" MODULES_DIR="$MODULES_DIR_DEFAULT" GENESIS_DIR="" +# Network/graph defaults (host/switched bandwidth, latency, packet loss) +BANDWIDTH_HOST="100 Mbit" +BANDWIDTH_SWITCH="1 Gbit" +LINK_LATENCY="1 ms" +PACKET_LOSS="0.0" + while getopts ":g:o:t:u:p:i:x:m:r:h" opt; do case $opt in g) GENESIS_DIR="$OPTARG" ;; @@ -203,8 +209,52 @@ mkdir -p "$(dirname "$OUTPUT_YAML_ABS")" printf "experimental:\n" printf " native_preemption_enabled: true\n" printf "network:\n" + # Emit an inline GML graph: create one node per host with 100 Mbit and a central switch with 1 Gbit printf " graph:\n" - printf " type: 1_gbit_switch\n" + printf " type: gml\n" + printf " inline: |\n" + printf " graph [\n" + printf " directed 0\n" + + # Print node entries for each host + for ((i=0; i Date: Fri, 24 Oct 2025 12:15:08 +0300 Subject: [PATCH 07/17] Fix fork choice logic to include the genesis time in slot calculations --- src/blockchain/fork_choice.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/blockchain/fork_choice.cpp b/src/blockchain/fork_choice.cpp index 808021c..a0d744d 100644 --- a/src/blockchain/fork_choice.cpp +++ b/src/blockchain/fork_choice.cpp @@ -294,7 +294,7 @@ namespace lean { auto time_since_genesis = now_sec - config_.genesis_time; std::vector> result{}; - while (time_ < time_since_genesis) { + while (time_ <= time_since_genesis) { Slot current_slot = time_ / INTERVALS_PER_SLOT; if (current_slot == 0) { // Skip actions for slot zero, which is the genesis slot @@ -401,11 +401,6 @@ namespace lean { const Checkpoint &root, const ForkChoiceStore::Votes &latest_votes, uint64_t min_score) { - // If no votes, return the starting root immediately - if (latest_votes.empty()) { - return root.root; - } - // For each block, count the number of votes for that block. A vote for // any descendant of a block also counts as a vote for that block std::unordered_map vote_weights; From 95edadc7309028eab6277cacbc1f2a04403da86f Mon Sep 17 00:00:00 2001 From: kamilsa Date: Fri, 24 Oct 2025 12:15:18 +0300 Subject: [PATCH 08/17] Improve bootnode selection and logging in networking module --- src/modules/networking/networking.cpp | 65 +++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/src/modules/networking/networking.cpp b/src/modules/networking/networking.cpp index 4722313..681cdd4 100644 --- a/src/modules/networking/networking.cpp +++ b/src/modules/networking/networking.cpp @@ -7,8 +7,10 @@ #include "modules/networking/networking.hpp" +#include #include #include +#include #include #include @@ -169,16 +171,35 @@ namespace lean::modules { // Add bootnodes from chain spec if (!bootnodes.empty()) { - SL_INFO(logger_, - "Adding {} bootnodes to address repository", - bootnodes.size()); + SL_INFO(logger_, "Found {} bootnodes in chain spec", bootnodes.size()); auto &address_repo = host->getPeerRepository().getAddressRepository(); - for (const auto &bootnode : bootnodes.getBootnodes()) { - if (bootnode.peer_id == peer_id) { + // Collect candidates (exclude ourselves) + auto all_bootnodes = bootnodes.getBootnodes(); + std::vector candidates; + candidates.reserve(all_bootnodes.size()); + for (const auto &b : all_bootnodes) { + if (b.peer_id == peer_id) { continue; } + candidates.push_back(b); + } + + // If more than 20 candidates, shuffle and pick first 20 + size_t max_take = 20; + if (candidates.size() > max_take) { + std::random_device rd; + std::mt19937 gen(rd()); + std::shuffle(candidates.begin(), candidates.end(), gen); + candidates.erase(candidates.begin() + max_take, candidates.end()); + } + + SL_INFO(logger_, + "Adding {} bootnodes to address repository", + candidates.size()); + + for (const auto &bootnode : candidates) { std::vector addresses{bootnode.address}; // Add bootnode addresses with permanent TTL @@ -221,8 +242,9 @@ namespace lean::modules { SL_DEBUG(logger_, "No bootnodes configured"); } + // Restore peer connection handlers and protocol startup auto on_peer_connected = - [weak_self{weak_from_this()}]( + [host, weak_self{weak_from_this()}]( std::weak_ptr weak_connection) { auto connection = weak_connection.lock(); @@ -243,8 +265,32 @@ namespace lean::modules { connection]() -> libp2p::Coro { std::ignore = co_await status_protocol->connect(connection); }); + } else { + // Non-initiator: record remote address in peer repository + auto addr_res = connection->remoteMultiaddr(); + if (!addr_res.has_value()) { + SL_WARN(self->logger_, "remoteMultiaddr() failed: {}", addr_res.error()); + return; + } + + std::vector addrs; + addrs.emplace_back(addr_res.value()); + + if (auto result = host->getPeerRepository() + .getAddressRepository() + .addAddresses( + peer_id, + std::span(addrs), + libp2p::peer::ttl::kRecentlyConnected); + not result.has_value()) { + SL_WARN(self->logger_, + "Failed to add addresses for peer {}: {}", + peer_id, + result.error()); + } } }; + auto on_peer_disconnected = [weak_self{weak_from_this()}](libp2p::PeerId peer_id) { auto self = weak_self.lock(); @@ -254,6 +300,7 @@ namespace lean::modules { self->loader_.dispatch_peer_disconnected( qtils::toSharedPtr(messages::PeerDisconnectedMessage{peer_id})); }; + on_peer_connected_sub_ = host->getBus() .getChannel() @@ -321,8 +368,8 @@ namespace lean::modules { return; } SL_DEBUG(self->logger_, - "Received vote for target {}", - signed_vote.data.target); + "Received vote for target {}", + signed_vote.data.target); }); io_thread_.emplace([io_context{io_context_}] { @@ -418,7 +465,7 @@ namespace lean::modules { void NetworkingImpl::receiveBlock(std::optional from_peer, SignedBlock &&signed_block) { auto slot_hash = signed_block.message.slotHash(); - SL_DEBUG(logger_, + SL_INFO(logger_, "receiveBlock slot {} hash {} parent {}", slot_hash.slot, slot_hash.hash, From 23268324f15dfe6ed0523dcdacd114e0b810a821 Mon Sep 17 00:00:00 2001 From: turuslan Date: Fri, 24 Oct 2025 22:00:17 +0500 Subject: [PATCH 09/17] request blocks after gossip Signed-off-by: turuslan --- src/modules/networking/networking.cpp | 118 +++++++++++++------------- vcpkg-overlay/leanp2p/portfile.cmake | 4 +- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/modules/networking/networking.cpp b/src/modules/networking/networking.cpp index 681cdd4..82c7df5 100644 --- a/src/modules/networking/networking.cpp +++ b/src/modules/networking/networking.cpp @@ -39,8 +39,6 @@ #include "modules/networking/types.hpp" namespace lean::modules { - // TODO(turuslan): gossip [from,seqno,signature,key]=None - inline auto gossipTopic(std::string_view type) { return std::format("/leanconsensus/devnet0/{}/ssz_snappy", type); } @@ -243,53 +241,53 @@ namespace lean::modules { } // Restore peer connection handlers and protocol startup - auto on_peer_connected = - [host, weak_self{weak_from_this()}]( - std::weak_ptr - weak_connection) { - auto connection = weak_connection.lock(); - if (not connection) { - return; - } - auto self = weak_self.lock(); - if (not self) { - return; - } - auto peer_id = connection->remotePeer(); - self->loader_.dispatch_peer_connected( - qtils::toSharedPtr(messages::PeerConnectedMessage{peer_id})); - if (connection->isInitiator()) { - libp2p::coroSpawn( - *self->io_context_, - [status_protocol{self->status_protocol_}, - connection]() -> libp2p::Coro { - std::ignore = co_await status_protocol->connect(connection); - }); - } else { - // Non-initiator: record remote address in peer repository - auto addr_res = connection->remoteMultiaddr(); - if (!addr_res.has_value()) { - SL_WARN(self->logger_, "remoteMultiaddr() failed: {}", addr_res.error()); - return; - } + auto on_peer_connected = [host, weak_self{weak_from_this()}]( + std::weak_ptr< + libp2p::connection::CapableConnection> + weak_connection) { + auto connection = weak_connection.lock(); + if (not connection) { + return; + } + auto self = weak_self.lock(); + if (not self) { + return; + } + auto peer_id = connection->remotePeer(); + self->loader_.dispatch_peer_connected( + qtils::toSharedPtr(messages::PeerConnectedMessage{peer_id})); + if (connection->isInitiator()) { + libp2p::coroSpawn(*self->io_context_, + [status_protocol{self->status_protocol_}, + connection]() -> libp2p::Coro { + std::ignore = + co_await status_protocol->connect(connection); + }); + } else { + // Non-initiator: record remote address in peer repository + auto addr_res = connection->remoteMultiaddr(); + if (!addr_res.has_value()) { + SL_WARN( + self->logger_, "remoteMultiaddr() failed: {}", addr_res.error()); + return; + } - std::vector addrs; - addrs.emplace_back(addr_res.value()); - - if (auto result = host->getPeerRepository() - .getAddressRepository() - .addAddresses( - peer_id, - std::span(addrs), - libp2p::peer::ttl::kRecentlyConnected); - not result.has_value()) { - SL_WARN(self->logger_, - "Failed to add addresses for peer {}: {}", - peer_id, - result.error()); - } - } - }; + std::vector addrs; + addrs.emplace_back(addr_res.value()); + + if (auto result = + host->getPeerRepository().getAddressRepository().addAddresses( + peer_id, + std::span(addrs), + libp2p::peer::ttl::kRecentlyConnected); + not result.has_value()) { + SL_WARN(self->logger_, + "Failed to add addresses for peer {}: {}", + peer_id, + result.error()); + } + } + }; auto on_peer_disconnected = [weak_self{weak_from_this()}](libp2p::PeerId peer_id) { @@ -344,16 +342,20 @@ namespace lean::modules { identify_->start(); gossip_blocks_topic_ = gossipSubscribe( - "block", [weak_self{weak_from_this()}](SignedBlock &&block) { + "block", + [weak_self{weak_from_this()}]( + SignedBlock &&block, std::optional received_from) { auto self = weak_self.lock(); if (not self) { return; } block.message.setHash(); - self->receiveBlock(std::nullopt, std::move(block)); + self->receiveBlock(received_from, std::move(block)); }); gossip_votes_topic_ = gossipSubscribe( - "vote", [weak_self{weak_from_this()}](SignedVote &&signed_vote) { + "vote", + [weak_self{weak_from_this()}](SignedVote &&signed_vote, + std::optional) { auto self = weak_self.lock(); if (not self) { return; @@ -405,12 +407,12 @@ namespace lean::modules { libp2p::coroSpawn( *io_context_, [this, type, topic, f{std::move(f)}]() -> libp2p::Coro { - while (auto raw_result = co_await topic->receive()) { + while (auto raw_result = co_await topic->receiveMessage()) { auto &raw = raw_result.value(); - if (auto uncompressed_res = snappyUncompress(raw)) { + if (auto uncompressed_res = snappyUncompress(raw.data)) { auto &uncompressed = uncompressed_res.value(); if (auto r = decode(uncompressed)) { - f(std::move(r.value())); + f(std::move(r.value()), raw.received_from); } } } @@ -466,10 +468,10 @@ namespace lean::modules { SignedBlock &&signed_block) { auto slot_hash = signed_block.message.slotHash(); SL_INFO(logger_, - "receiveBlock slot {} hash {} parent {}", - slot_hash.slot, - slot_hash.hash, - signed_block.message.parent_root); + "receiveBlock slot {} hash {} parent {}", + slot_hash.slot, + slot_hash.hash, + signed_block.message.parent_root); auto remove = [&](auto f) { std::vector queue{slot_hash.hash}; while (not queue.empty()) { diff --git a/vcpkg-overlay/leanp2p/portfile.cmake b/vcpkg-overlay/leanp2p/portfile.cmake index c8a34e3..f715c38 100644 --- a/vcpkg-overlay/leanp2p/portfile.cmake +++ b/vcpkg-overlay/leanp2p/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/leanp2p - REF refs/tags/v0.0.3 - SHA512 da5ef7a621f2758e588972b958fc35706ddfd9a0ee9fd378c113cbe0348a28788acc09fa7e23b6c663f9f5b4d1448b245e7610f07ff02b80be4a074a9e27a102 + REF badeae740d8e279bc6450eb0ed85d5e21f413470 + SHA512 aa86b1bc16728289a780467d40b92988b5b9bcd7ff9ab8629e9348b0b48d179778a0bc414c0919dabba97aee252c887a15e69f32661ae4182b762932c42d08bd ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") vcpkg_cmake_install() From 92331b2ddc9ea2779aaa213549536881f0eccc4d Mon Sep 17 00:00:00 2001 From: kamilsa Date: Mon, 27 Oct 2025 21:02:07 +0500 Subject: [PATCH 10/17] Update node ENRs and fix sed command syntax in README for genesis configuration --- example/0-single/README.md | 2 +- example/1-network/README.md | 10 +++++----- example/1-network/genesis/nodes.yaml | 9 ++++----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/example/0-single/README.md b/example/0-single/README.md index 7d7b529..f5ef5ee 100644 --- a/example/0-single/README.md +++ b/example/0-single/README.md @@ -17,7 +17,7 @@ Before starting the node, update the GENESIS_TIME in the genesis config file to ```bash future_time=$(( $(date +%s) + 20 )) -sed -i "s/GENESIS_TIME: .*/GENESIS_TIME: $future_time/" example/0-single/genesis/config.yaml +sed -i '' "s/GENESIS_TIME: .*/GENESIS_TIME: $future_time/" example/0-single/genesis/config.yaml ``` Example CLI command: diff --git a/example/1-network/README.md b/example/1-network/README.md index 15c3300..78eebef 100644 --- a/example/1-network/README.md +++ b/example/1-network/README.md @@ -17,7 +17,7 @@ Before starting nodes, set `GENESIS_TIME` in `example/1-network/genesis/config.y ```bash future_time=$(( $(date +%s) + 20 )) -sed -i "s/GENESIS_TIME: .*/GENESIS_TIME: $future_time/" example/1-network/genesis/config.yaml +sed -i '' "s/GENESIS_TIME: .*/GENESIS_TIME: $future_time/" example/1-network/genesis/config.yaml ``` ## Start the 4 validators @@ -33,7 +33,7 @@ Node 0: --genesis example/1-network/genesis/config.yaml \ --validator-registry-path example/1-network/genesis/validators.yaml \ --node-id node_0 \ - --node-key cb920fbda3b96e18f03e22825f4a5a61343ec43c7be1c8c4a717fffee2f4c4ce \ + --node-key 0000000000000000010000000000000002000000000000000300000000000000 \ --listen-addr /ip4/0.0.0.0/udp/9000/quic-v1 \ --prometheus-port 9100 ``` @@ -47,7 +47,7 @@ Node 1: --genesis example/1-network/genesis/config.yaml \ --validator-registry-path example/1-network/genesis/validators.yaml \ --node-id node_1 \ - --node-key a87e7d23bb1de4613b67002b700bce41e031f4ab1529a3436bd73c893ea039b3 \ + --node-key 0100000000000000020000000000000003000000000000000400000000000000 \ --listen-addr /ip4/0.0.0.0/udp/9001/quic-v1 \ --prometheus-port 9101 ``` @@ -61,7 +61,7 @@ Node 2: --genesis example/1-network/genesis/config.yaml \ --validator-registry-path example/1-network/genesis/validators.yaml \ --node-id node_2 \ - --node-key f2f53f6acf312c5e92c2a611bbca7a1932b4db0b9e0c43bec413badca9b76760 \ + --node-key 0200000000000000030000000000000004000000000000000500000000000000 \ --listen-addr /ip4/0.0.0.0/udp/9002/quic-v1 \ --prometheus-port 9102 ``` @@ -75,7 +75,7 @@ Node 3: --genesis example/1-network/genesis/config.yaml \ --validator-registry-path example/1-network/genesis/validators.yaml \ --node-id node_3 \ - --node-key fa5ddbec80f964d17d28221c2c5bac0f4a3f9cfcf4b86674e605f459e195a1c4 \ + --node-key 0300000000000000040000000000000005000000000000000600000000000000 \ --listen-addr /ip4/0.0.0.0/udp/9003/quic-v1 \ --prometheus-port 9103 ``` diff --git a/example/1-network/genesis/nodes.yaml b/example/1-network/genesis/nodes.yaml index 1439119..807a631 100644 --- a/example/1-network/genesis/nodes.yaml +++ b/example/1-network/genesis/nodes.yaml @@ -1,5 +1,4 @@ -- enr:-IW4QIh9cSo0CPOcsTq5T6SAYr0HrGFMYekZjrgC7ZTgdsMhBKjIQgzCfgqsxulCO4O1TXyjRLZ3BYc4GgqVRvl3d1sBgmlkgnY0gmlwhAoAAAqEcXVpY4IjKIlzZWNwMjU2azGhAmVoFLEuozqtJ7uxbf6RlL8ow-UlDKSYLzxKcbpZ13Zg -- enr:-IW4QG_-KOB94fI18bEqxW8B_lKgG3cGVdvKIIKdZJBLftoaZE5y3Vg4PFQQIPmIRpeD1QawVKrd_6HDd1D2K7WLsLQBgmlkgnY0gmlwhAoAAAuEcXVpY4IjKYlzZWNwMjU2azGhA8PFJzjZs3Nmzn34yVzbnN5Mo5RhzwiWDxLnmoW1U7AV -- enr:-IW4QDEHjhkVEcEX5GS4qAjAHbiqCevwjwFd6ce1SYxLEIgYXDmozUjm8ao4Nl1YoFVhNBs1cn8zW2kwb6yaJpgVDLkBgmlkgnY0gmlwhAoAAAyEcXVpY4IjKolzZWNwMjU2azGhAsQeX5os8a2pG4v2cGuMMXZYY2B-yzYLcZM3yEHa3_kW -- enr:-IW4QEgn7uYKIhbom8qWeFGWBTOh_WZGKjLCfoJay5PHND9yAG359yxsK84DBxfOWm86U5zvVF_UbCO5n1Uz6P2tG28BgmlkgnY0gmlwhAoAAA2EcXVpY4IjK4lzZWNwMjU2azGhAhXhXn0CrP2llJ7PNcWcimcUJ31GeGVMfk0MF2lnH4Ri - +- enr:-IW4QF83vMklTZR8wrXxsNiA14P7xNmk79zsy1OsqblVCiG5ZEzT2TMVbtkARM5q0QfHbYU2jaVfk5xCNXEv01LkZ1QBgmlkgnY0gmlwhH8AAAGEcXVpY4InEIlzZWNwMjU2azGhAyUAddLEC14kKi9ziAKt47wyvzX_jvJqFbbD-HkxKQak +- enr:-IW4QBj8XATzGHrMHeYuQG1PLXkH7CujKi0knmbeo8AAGNBqVXM24zrEElpLqYOaPdMiZzjeZ-KKOrZfBy24rvh9QaIBgmlkgnY0gmlwhH8AAAGEcXVpY4InEYlzZWNwMjU2azGhA3x5MQ79P8UyJ8JrEcHbbJp-CbX4AgyV6cZHlYb6uJrl +- enr:-IW4QPpY9L4EHFiGk3FXbGIe_pKSaORsxN-Kij2avIpg4V_jdHCnHYhEc_jyi6CLpwRnbHJvIxduC7AUU3d10P1ZoKoBgmlkgnY0gmlwhH8AAAGEcXVpY4InEolzZWNwMjU2azGhA9GSOWDRYKpxNbaRn_Nctl2E3GBReM6XsPumdACuwJvR +- enr:-IW4QANPPIHjaXKMePVjOIUvqtOUg89t6pWuey9hBcQhywX_dg8QekjUpigHkUvvBO8O7j3-4iRradZCf5puJ4hVDuMBgmlkgnY0gmlwhH8AAAGEcXVpY4InE4lzZWNwMjU2azGhAhbcwnDGuwqTSa-lfSuqVS2F6m3SLNP24pLUPHv6QT8E From ccf26576a2eaec04e94c5d49204ea4a306cc295d Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 4 Nov 2025 11:23:52 +0500 Subject: [PATCH 11/17] update libp2p Signed-off-by: turuslan --- vcpkg-overlay/leanp2p/portfile.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vcpkg-overlay/leanp2p/portfile.cmake b/vcpkg-overlay/leanp2p/portfile.cmake index f715c38..5da83a5 100644 --- a/vcpkg-overlay/leanp2p/portfile.cmake +++ b/vcpkg-overlay/leanp2p/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/leanp2p - REF badeae740d8e279bc6450eb0ed85d5e21f413470 - SHA512 aa86b1bc16728289a780467d40b92988b5b9bcd7ff9ab8629e9348b0b48d179778a0bc414c0919dabba97aee252c887a15e69f32661ae4182b762932c42d08bd + REF refs/tags/v0.0.4 + SHA512 9ab9ab55b08306198d2bdaf3b80086126b06b2ac04e6f795e22cb1ba7a44f4ae8d8505183622ba91abc1d0c4e65f3148939e5ccdbef7829b5b625a8bf87139fd ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") vcpkg_cmake_install() From 4ae4dd895dc598ef51ede795730e61e2edc4e14d Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 4 Nov 2025 11:37:57 +0500 Subject: [PATCH 12/17] max bootnodes config Signed-off-by: turuslan --- src/app/configuration.cpp | 4 ++++ src/app/configuration.hpp | 2 ++ src/app/configurator.cpp | 5 +++++ src/modules/networking/networking.cpp | 21 +++++++++------------ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/app/configuration.cpp b/src/app/configuration.cpp index 02a2834..3425265 100644 --- a/src/app/configuration.cpp +++ b/src/app/configuration.cpp @@ -60,6 +60,10 @@ namespace lean::app { return node_key_.value(); } + const std::optional &Configuration::maxBootnodes() const { + return max_bootnodes_; + } + const Configuration::DatabaseConfig &Configuration::database() const { return database_; } diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp index faf49d7..16cfdf9 100644 --- a/src/app/configuration.hpp +++ b/src/app/configuration.hpp @@ -48,6 +48,7 @@ namespace lean::app { [[nodiscard]] virtual const std::optional & listenMultiaddr() const; [[nodiscard]] virtual const libp2p::crypto::KeyPair &nodeKey() const; + [[nodiscard]] virtual const std::optional &maxBootnodes() const; [[nodiscard]] virtual const DatabaseConfig &database() const; @@ -66,6 +67,7 @@ namespace lean::app { std::filesystem::path genesis_config_path_; std::optional listen_multiaddr_; std::optional node_key_; + std::optional max_bootnodes_; DatabaseConfig database_; MetricsConfig metrics_; diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index a6dd6fd..de792cb 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -117,6 +117,7 @@ namespace lean::app { ("name,n", po::value(), "Set name of node.") ("node-id", po::value(), "Node id from validator registry (genesis/validators.yaml).") ("node-key", po::value(), "Set secp256k1 node key as hex string (with or without 0x prefix).") + ("max-bootnodes", po::value(), "Max bootnodes count to connect to.") ("log,l", po::value>(), "Sets a custom logging filter.\n" "Syntax: =, e.g., -llibp2p=off.\n" @@ -414,6 +415,10 @@ namespace lean::app { fail = true; } }); + if (auto max_bootnodes = + find_argument(cli_values_map_, "max-bootnodes")) { + config_->max_bootnodes_ = *max_bootnodes; + } find_argument( cli_values_map_, "base-path", [&](const std::string &value) { config_->base_path_ = value; diff --git a/src/modules/networking/networking.cpp b/src/modules/networking/networking.cpp index 82c7df5..2ca7122 100644 --- a/src/modules/networking/networking.cpp +++ b/src/modules/networking/networking.cpp @@ -184,13 +184,13 @@ namespace lean::modules { candidates.push_back(b); } - // If more than 20 candidates, shuffle and pick first 20 - size_t max_take = 20; - if (candidates.size() > max_take) { - std::random_device rd; - std::mt19937 gen(rd()); - std::shuffle(candidates.begin(), candidates.end(), gen); - candidates.erase(candidates.begin() + max_take, candidates.end()); + // Randomly choose no more than `maxBootnodes` bootnodes to connect to. + if (config_->maxBootnodes().has_value() + and candidates.size() > *config_->maxBootnodes()) { + std::ranges::shuffle(candidates, std::default_random_engine{}); + // `resize` doesn't work without default constructor + candidates.erase(candidates.begin() + *config_->maxBootnodes(), + candidates.end()); } SL_INFO(logger_, @@ -409,11 +409,8 @@ namespace lean::modules { [this, type, topic, f{std::move(f)}]() -> libp2p::Coro { while (auto raw_result = co_await topic->receiveMessage()) { auto &raw = raw_result.value(); - if (auto uncompressed_res = snappyUncompress(raw.data)) { - auto &uncompressed = uncompressed_res.value(); - if (auto r = decode(uncompressed)) { - f(std::move(r.value()), raw.received_from); - } + if (auto r = decodeSszSnappy(raw.data)) { + f(std::move(r.value()), raw.received_from); } } }); From 650f626dff54d922ea0accc489170745ca149a47 Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 4 Nov 2025 11:52:28 +0500 Subject: [PATCH 13/17] fix test Signed-off-by: turuslan --- tests/unit/blockchain/fork_choice_test.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/unit/blockchain/fork_choice_test.cpp b/tests/unit/blockchain/fork_choice_test.cpp index 7c20d1c..4b98ce5 100644 --- a/tests/unit/blockchain/fork_choice_test.cpp +++ b/tests/unit/blockchain/fork_choice_test.cpp @@ -267,16 +267,22 @@ TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_with_votes) { EXPECT_EQ(head, target.hash()); } -// Test get_fork_choice_head with no votes returns the root. -TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_no_votes) { +/** + * Test fork choice algorithm with no attestations walks to the leaf. + * + * With no attestations, fork choice should walk down the tree and select the + * leaf block (the furthest descendant), breaking ties by lexicographic hash. + */ +TEST(TestForkChoiceHeadFunction, test_fork_choice_no_attestations) { auto blocks = makeBlocks(3); auto &root = blocks.at(0); + auto &leaf = blocks.at(2); ForkChoiceStore::Votes empty_votes; auto head = getForkChoiceHead( makeBlockMap(blocks), Checkpoint::from(root), empty_votes, 0); - EXPECT_EQ(head, root.hash()); + EXPECT_EQ(head, leaf.hash()); } // Test get_fork_choice_head respects minimum score. From a042f700a44ff07ca42e7a0c69bb1318c542aab2 Mon Sep 17 00:00:00 2001 From: Ruslan Tushov Date: Tue, 4 Nov 2025 13:37:24 +0500 Subject: [PATCH 14/17] fix --max-nodes type --- src/app/configurator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index de792cb..93c5490 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -117,7 +117,7 @@ namespace lean::app { ("name,n", po::value(), "Set name of node.") ("node-id", po::value(), "Node id from validator registry (genesis/validators.yaml).") ("node-key", po::value(), "Set secp256k1 node key as hex string (with or without 0x prefix).") - ("max-bootnodes", po::value(), "Max bootnodes count to connect to.") + ("max-bootnodes", po::value(), "Max bootnodes count to connect to.") ("log,l", po::value>(), "Sets a custom logging filter.\n" "Syntax: =, e.g., -llibp2p=off.\n" From a220b4cf377eec716cd847b9b2e1c1f4c1eecb50 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 10 Nov 2025 15:33:19 +0500 Subject: [PATCH 15/17] exe name --- src/app/configurator.cpp | 5 +++-- src/commands/generate_genesis.hpp | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index 93c5490..942991a 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -174,11 +174,12 @@ namespace lean::app { } if (vm.contains("help")) { + auto exe = std::filesystem::path{argv_[0]}.filename().string(); std::cout << "Lean-node version " << buildVersion() << '\n'; std::cout << cli_options_ << '\n'; std::println(std::cout, "Other commands:"); - std::println(std::cout, " {} key generate-node-key", argv_[0]); - std::println(std::cout, " {} generate-genesis", argv_[0]); + std::println(std::cout, " {} key generate-node-key", exe); + std::println(std::cout, " {} generate-genesis", exe); return true; } diff --git a/src/commands/generate_genesis.hpp b/src/commands/generate_genesis.hpp index 53e2941..a912117 100644 --- a/src/commands/generate_genesis.hpp +++ b/src/commands/generate_genesis.hpp @@ -96,9 +96,10 @@ inline int cmdGenerateGenesis(auto &&getArg) { } } } + auto exe = std::filesystem::path{getArg(0).value()}.filename().string(); std::println(std::cerr, "Usage: {} generate-genesis (genesis_directory) " "(validator_count) (shadow?)", - getArg(0).value()); + exe); return EXIT_FAILURE; } From bdae76bae486ddd40bac2c2e7de371f6443b1770 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 10 Nov 2025 16:10:20 +0500 Subject: [PATCH 16/17] enr ip to string --- src/serde/enr.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/serde/enr.cpp b/src/serde/enr.cpp index d6936f2..4f49972 100644 --- a/src/serde/enr.cpp +++ b/src/serde/enr.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -282,7 +283,9 @@ namespace lean::enr { } std::string toString(const Ip &ip) { - return std::format("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]); + auto ip_host_endian = boost::endian::load_big_u32(ip.data()); + auto ip_asio = boost::asio::ip::make_address_v4(ip_host_endian); + return ip_asio.to_string(); } libp2p::PeerId Enr::peerId() const { From 5826da54ec9a00d3543ac75bad5c2f6d7dac1306 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 10 Nov 2025 16:30:22 +0500 Subject: [PATCH 17/17] sample peer make ip --- src/serde/enr.cpp | 5 +++++ src/serde/enr.hpp | 1 + src/utils/sample_peer.hpp | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/serde/enr.cpp b/src/serde/enr.cpp index 4f49972..809e12d 100644 --- a/src/serde/enr.cpp +++ b/src/serde/enr.cpp @@ -282,6 +282,11 @@ namespace lean::enr { return ip; } + Ip makeIp(std::string_view base, uint32_t i) { + auto ip_host_endian = boost::asio::ip::make_address_v4(base).to_uint(); + return makeIp(ip_host_endian + i); + } + std::string toString(const Ip &ip) { auto ip_host_endian = boost::endian::load_big_u32(ip.data()); auto ip_asio = boost::asio::ip::make_address_v4(ip_host_endian); diff --git a/src/serde/enr.hpp b/src/serde/enr.hpp index 743cb1f..1a48852 100644 --- a/src/serde/enr.hpp +++ b/src/serde/enr.hpp @@ -25,6 +25,7 @@ namespace lean::enr { using Port = uint16_t; Ip makeIp(uint32_t i); + Ip makeIp(std::string_view base, uint32_t i); std::string toString(const Ip &ip); struct Enr { diff --git a/src/utils/sample_peer.hpp b/src/utils/sample_peer.hpp index 0da73b7..7abd68c 100644 --- a/src/utils/sample_peer.hpp +++ b/src/utils/sample_peer.hpp @@ -13,7 +13,10 @@ namespace lean { struct SamplePeer : libp2p::SamplePeer { static enr::Ip makeIp(size_t index, bool shadow) { - return shadow ? enr::makeIp((10 << 24) + index) : enr::Ip{127, 0, 0, 1}; + if (shadow) { + return enr::makeIp("10.0.0.0", index); + } + return enr::makeIp("127.0.0.1", 0); } SamplePeer(size_t index, bool shadow)