diff --git a/README.md b/README.md index a49bd70..eaaeac6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,125 @@ # qlean-mini -Lean Ethereum PQ devnet client implementation in C++ + +Lean Ethereum consensus client for devnets, implemented in modern C++. + +qlean-mini follows the Lean Ethereum devnet specification: +- Spec: https://github.com/leanEthereum/leanSpec/ + +This repository builds a small, modular node binary called `qlean` and a set of loadable modules. It uses CMake, Ninja, and vcpkg (manifest mode) for dependency management. + + +## Quick start + +### 0) Clone the repository and set system dependencies + +```bash +git clone https://github.com/qdrvm/qlean-mini.git +cd qlean-mini +``` + +Prerequisites: +- CMake ≥ 3.25 +- Ninja +- A C++23-capable compiler (AppleClang/Clang/GCC) +- Python 3 (required by the CMake build) +- vcpkg (manifest mode) + +Note: Dependencies are also listed in the `.ci/.env` file for both macOS and Linux. To install on Debian Linux: + +```bash +source .ci/.env +sudo apt update && sudo apt install -y $(echo $LINUX_PACKAGES) +``` + +Make sure gcc-$GCC_VERSION is default compiler (where GCC_VERSION is defined in .ci/.env): + +```bash +sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-$GCC_VERSION 100 +sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-$GCC_VERSION 100 +``` + +For macOS: + +```bash +source .ci/.env +brew install $(echo $MACOS_PACKAGES) +``` + +### 1) Install and bootstrap vcpkg + +```bash +# Clone vcpkg somewhere on your machine (e.g. in your home directory) +git clone https://github.com/microsoft/vcpkg.git "$HOME/vcpkg" + +# Bootstrap vcpkg +"$HOME/vcpkg"/bootstrap-vcpkg.sh + +# Export VCPKG_ROOT for the current shell session (and add to ~/.zshrc if use zsh for convenience) +export VCPKG_ROOT="$HOME/vcpkg" +``` + +Notes: +- This project uses vcpkg in manifest mode with custom overlay ports from `vcpkg-overlay/` and the registry settings in `vcpkg-configuration.json`. +- CMake presets expect `CMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake`. + +### 2) Configure and build + +Use the provided CMake preset (generator: Ninja): + +```bash +# From the repository root +cmake --preset default +cmake --build build -j +``` + +You can also build the project's Docker images (builder + runtime) with: + +```bash +make docker_build_all +``` + +See the `Makefile` for more Docker targets. + +This will: +- Configure the project into `./build/` +- Build the main node executable at `./build/src/executable/qlean` + +### 3) Ensure build was successful +Print help: + +```bash +./build/src/executable/qlean --help +``` + +## Run the node + +For step-by-step instructions to run a local single-node devnet, see `example/0-single/README.md`. It includes the exact CLI command and an explanation of all flags. + + +## Generate a node key + +The binary includes a helper subcommand to generate a node key and corresponding PeerId: + +```bash +./build/src/executable/qlean key generate-node-key +``` + +This prints two lines: +1) The private key (hex) +2) The PeerId (base58) + + +## Tests + +If `TESTING` is enabled (default ON in top-level CMakeLists), tests are built and can be run with CTest: + +```bash +# After building +ctest --test-dir build --output-on-failure +``` + +Individual test binaries are placed under `build/test_bin/`. + +## License + +SPDX-License-Identifier: Apache-2.0 — Copyright Quadrivium LLC diff --git a/example/0-single/README.md b/example/0-single/README.md new file mode 100644 index 0000000..7d7b529 --- /dev/null +++ b/example/0-single/README.md @@ -0,0 +1,60 @@ +# 0-single — run a single-node devnet + +This example shows how to start a one-node network using the `qlean` binary built from this repository. + +Files in this folder: +- `genesis/config.yaml` — genesis settings (e.g., `GENESIS_TIME`, `VALIDATOR_COUNT`) +- `genesis/validators.yaml` — validator registry for this example +- `genesis/nodes.yaml` — static bootnodes list used at startup + +Prerequisites: +- Build the project first. See the root README “Quick start” for setup and build steps. +- Run commands from the repository root so relative paths resolve correctly. + +## Start the node + +Before starting the node, update the GENESIS_TIME in the genesis config file to a future Unix timestamp (e.g., current time + 20 seconds) to allow the node to bootstrap the network. For example: + +```bash +future_time=$(( $(date +%s) + 20 )) +sed -i "s/GENESIS_TIME: .*/GENESIS_TIME: $future_time/" example/0-single/genesis/config.yaml +``` + +Example CLI command: + +```bash +./build/src/executable/qlean \ + --modules-dir ./build/src/modules \ + --bootnodes example/0-single/genesis/nodes.yaml \ + --validator-registry-path example/0-single/genesis/validators.yaml \ + --node-id node_0 \ + --genesis example/0-single/genesis/config.yaml \ + --node-key cb920fbda3b96e18f03e22825f4a5a61343ec43c7be1c8c4a717fffee2f4c4ce \ + --listen-addr /ip4/0.0.0.0/udp/9000/quic-v1 +``` + +## What each flag means + +- `--modules-dir ./build/src/modules` + - Where the node looks for loadable modules built by this repository. The default build places them under `./build/src/modules`. +- `--bootnodes example/0-single/genesis/nodes.yaml` + - A YAML file with a list of peers (ENRs or multiaddrs) used for initial connectivity. +- `--validator-registry-path example/0-single/genesis/validators.yaml` + - The validator registry for this devnet. For a single-node network, it typically contains a single validator. +- `--node-id node_0` + - A human-friendly identifier used in logs and for distinguishing nodes when running multiple instances. +- `--genesis ` + - Genesis configuration describing initial chain parameters, such as `GENESIS_TIME` and the number of validators. +- `--node-key ` + - Hex-encoded libp2p private key. Using a fixed key gives a stable PeerId across restarts. You can generate one with: + + ```bash + ./build/src/executable/qlean key generate-node-key + ``` +- `--listen-addr /ip4/0.0.0.0/udp/9000/quic-v1` + - libp2p multiaddress to bind the QUIC transport. Adjust the port if `9000` is taken, or bind to `127.0.0.1` if you want local-only access. + +## Tips + +- If the binary cannot find modules, double-check you built the project and the `--modules-dir` path is correct. +- You can run multiple nodes by copying this command and changing `--node-id`, `--node-key`, ports in `--listen-addr`, and using appropriate bootnodes. diff --git a/example/0-single/genesis/config.yaml b/example/0-single/genesis/config.yaml new file mode 100644 index 0000000..2719856 --- /dev/null +++ b/example/0-single/genesis/config.yaml @@ -0,0 +1,5 @@ +# Genesis Settings +GENESIS_TIME: 946684803 + +# Validator Settings +VALIDATOR_COUNT: 1 diff --git a/example/0-single/genesis/nodes.yaml b/example/0-single/genesis/nodes.yaml new file mode 100644 index 0000000..fb68f81 --- /dev/null +++ b/example/0-single/genesis/nodes.yaml @@ -0,0 +1,2 @@ +- enr:-IW4QIh9cSo0CPOcsTq5T6SAYr0HrGFMYekZjrgC7ZTgdsMhBKjIQgzCfgqsxulCO4O1TXyjRLZ3BYc4GgqVRvl3d1sBgmlkgnY0gmlwhAoAAAqEcXVpY4IjKIlzZWNwMjU2azGhAmVoFLEuozqtJ7uxbf6RlL8ow-UlDKSYLzxKcbpZ13Zg + diff --git a/example/0-single/genesis/validators.yaml b/example/0-single/genesis/validators.yaml new file mode 100644 index 0000000..57add34 --- /dev/null +++ b/example/0-single/genesis/validators.yaml @@ -0,0 +1,2 @@ +node_0: + - 0 diff --git a/example/0-smoke/genesis/nodes.yaml b/example/0-smoke/genesis/nodes.yaml deleted file mode 100644 index 5c4a8b5..0000000 --- a/example/0-smoke/genesis/nodes.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- enr:-IW4QNVvHz0m0tyHur-rUg88u20O7T-b_ZlJo_tFAtKXYIuQSD7RbE1ihpmf5gGzjvE7FqVAW2pYZhUsikiskVakTHYBgmlkgnY0gmlwhH8AAAGEcXVpY4IjKIlzZWNwMjU2azGhAmVoFLEuozqtJ7uxbf6RlL8ow-UlDKSYLzxKcbpZ13Zg -- enr:-IW4QMa4Ldb_hgv2P6Py91wQQ_7aIRnPSMyGnjM-yXocWe8VUo8yYyh43XBoEx549dYxzxfuwSrQhP7vVWrHsdiI_kABgmlkgnY0gmlwhH8AAAGEcXVpY4IjKYlzZWNwMjU2azGhA8PFJzjZs3Nmzn34yVzbnN5Mo5RhzwiWDxLnmoW1U7AV -- enr:-IW4QB0ZNaL8jHw14Gzb43ReLLwCGsLdWsxGHSKuGptmSC98NE6HwUco-8-o4Jb_VJpNqX8bgFS0eQveJ6KNNWyjKUUBgmlkgnY0gmlwhH8AAAGEcXVpY4IjKolzZWNwMjU2azGhAsQeX5os8a2pG4v2cGuMMXZYY2B-yzYLcZM3yEHa3_kW -- enr:-IW4QKG5cm3dR-7nNlHlEZnvOIDLQv_Hrqi4HDZqZOcOqnW4VF2eE_loEABlC-9iUB5wW-D-fq-kN-sdjQkxw6FfiOUBgmlkgnY0gmlwhH8AAAGEcXVpY4IjK4lzZWNwMjU2azGhAhXhXn0CrP2llJ7PNcWcimcUJ31GeGVMfk0MF2lnH4Ri diff --git a/example/1-network/README.md b/example/1-network/README.md new file mode 100644 index 0000000..15c3300 --- /dev/null +++ b/example/1-network/README.md @@ -0,0 +1,87 @@ +# 1-network — run a 4-validator devnet + +This example shows how to start a 4-validator network using the `qlean` binary built from this repository. + +Files in this folder: +- `genesis/config.yaml` — genesis settings (e.g., `GENESIS_TIME`, `VALIDATOR_COUNT`) +- `genesis/validators.yaml` — validator registry for this network +- `genesis/nodes.yaml` — bootnodes list used at startup + +Prerequisites: +- Build the project first. See the root README “Quick start” for setup and build steps. +- Run commands from the repository root so relative paths resolve correctly. + +## Prepare genesis time + +Before starting nodes, set `GENESIS_TIME` in `example/1-network/genesis/config.yaml` to a future Unix timestamp so the chain can start. For example, current time + 20 seconds: + +```bash +future_time=$(( $(date +%s) + 20 )) +sed -i "s/GENESIS_TIME: .*/GENESIS_TIME: $future_time/" example/1-network/genesis/config.yaml +``` + +## Start the 4 validators + +Open four terminals (or run in background) and launch each node with its own key and ports. + +Node 0: + +```bash +./build/src/executable/qlean \ + --modules-dir ./build/src/modules \ + --bootnodes example/1-network/genesis/nodes.yaml \ + --genesis example/1-network/genesis/config.yaml \ + --validator-registry-path example/1-network/genesis/validators.yaml \ + --node-id node_0 \ + --node-key cb920fbda3b96e18f03e22825f4a5a61343ec43c7be1c8c4a717fffee2f4c4ce \ + --listen-addr /ip4/0.0.0.0/udp/9000/quic-v1 \ + --prometheus-port 9100 +``` + +Node 1: + +```bash +./build/src/executable/qlean \ + --modules-dir ./build/src/modules \ + --bootnodes example/1-network/genesis/nodes.yaml \ + --genesis example/1-network/genesis/config.yaml \ + --validator-registry-path example/1-network/genesis/validators.yaml \ + --node-id node_1 \ + --node-key a87e7d23bb1de4613b67002b700bce41e031f4ab1529a3436bd73c893ea039b3 \ + --listen-addr /ip4/0.0.0.0/udp/9001/quic-v1 \ + --prometheus-port 9101 +``` + +Node 2: + +```bash +./build/src/executable/qlean \ + --modules-dir ./build/src/modules \ + --bootnodes example/1-network/genesis/nodes.yaml \ + --genesis example/1-network/genesis/config.yaml \ + --validator-registry-path example/1-network/genesis/validators.yaml \ + --node-id node_2 \ + --node-key f2f53f6acf312c5e92c2a611bbca7a1932b4db0b9e0c43bec413badca9b76760 \ + --listen-addr /ip4/0.0.0.0/udp/9002/quic-v1 \ + --prometheus-port 9102 +``` + +Node 3: + +```bash +./build/src/executable/qlean \ + --modules-dir ./build/src/modules \ + --bootnodes example/1-network/genesis/nodes.yaml \ + --genesis example/1-network/genesis/config.yaml \ + --validator-registry-path example/1-network/genesis/validators.yaml \ + --node-id node_3 \ + --node-key fa5ddbec80f964d17d28221c2c5bac0f4a3f9cfcf4b86674e605f459e195a1c4 \ + --listen-addr /ip4/0.0.0.0/udp/9003/quic-v1 \ + --prometheus-port 9103 +``` + +## Notes and tips + +- Start Node 0 first so others can discover it via the bootnodes list; the rest can follow in any order. +- Ports (9000–9003) must be free; adjust if they’re taken. Prometheus ports (9100–9103) are optional and can be changed or omitted. +- For an explanation of the common flags, see `example/0-single/README.md` (the meanings are the same). diff --git a/example/0-smoke/genesis/config.yaml b/example/1-network/genesis/config.yaml similarity index 70% rename from example/0-smoke/genesis/config.yaml rename to example/1-network/genesis/config.yaml index 5cfae3b..7935746 100644 --- a/example/0-smoke/genesis/config.yaml +++ b/example/1-network/genesis/config.yaml @@ -1,5 +1,5 @@ # Genesis Settings -GENESIS_TIME: 1759469684 +GENESIS_TIME: 946684803 # Validator Settings VALIDATOR_COUNT: 4 diff --git a/example/1-network/genesis/nodes.yaml b/example/1-network/genesis/nodes.yaml new file mode 100644 index 0000000..1439119 --- /dev/null +++ b/example/1-network/genesis/nodes.yaml @@ -0,0 +1,5 @@ +- enr:-IW4QIh9cSo0CPOcsTq5T6SAYr0HrGFMYekZjrgC7ZTgdsMhBKjIQgzCfgqsxulCO4O1TXyjRLZ3BYc4GgqVRvl3d1sBgmlkgnY0gmlwhAoAAAqEcXVpY4IjKIlzZWNwMjU2azGhAmVoFLEuozqtJ7uxbf6RlL8ow-UlDKSYLzxKcbpZ13Zg +- enr:-IW4QG_-KOB94fI18bEqxW8B_lKgG3cGVdvKIIKdZJBLftoaZE5y3Vg4PFQQIPmIRpeD1QawVKrd_6HDd1D2K7WLsLQBgmlkgnY0gmlwhAoAAAuEcXVpY4IjKYlzZWNwMjU2azGhA8PFJzjZs3Nmzn34yVzbnN5Mo5RhzwiWDxLnmoW1U7AV +- enr:-IW4QDEHjhkVEcEX5GS4qAjAHbiqCevwjwFd6ce1SYxLEIgYXDmozUjm8ao4Nl1YoFVhNBs1cn8zW2kwb6yaJpgVDLkBgmlkgnY0gmlwhAoAAAyEcXVpY4IjKolzZWNwMjU2azGhAsQeX5os8a2pG4v2cGuMMXZYY2B-yzYLcZM3yEHa3_kW +- enr:-IW4QEgn7uYKIhbom8qWeFGWBTOh_WZGKjLCfoJay5PHND9yAG359yxsK84DBxfOWm86U5zvVF_UbCO5n1Uz6P2tG28BgmlkgnY0gmlwhAoAAA2EcXVpY4IjK4lzZWNwMjU2azGhAhXhXn0CrP2llJ7PNcWcimcUJ31GeGVMfk0MF2lnH4Ri + diff --git a/example/0-smoke/genesis/validators.yaml b/example/1-network/genesis/validators.yaml similarity index 50% rename from example/0-smoke/genesis/validators.yaml rename to example/1-network/genesis/validators.yaml index 54a1b4d..28c9bd8 100644 --- a/example/0-smoke/genesis/validators.yaml +++ b/example/1-network/genesis/validators.yaml @@ -1,8 +1,8 @@ node_0: - - 0 + - 0 node_1: - - 1 + - 1 node_2: - - 2 + - 2 node_3: - - 3 + - 3 diff --git a/shadow/README.md b/shadow/README.md new file mode 100644 index 0000000..adac152 --- /dev/null +++ b/shadow/README.md @@ -0,0 +1,28 @@ +Shadow example +=============== + +Quick steps + +1) Install Shadow + +Follow the official guide: https://github.com/shadow/shadow + +2) Generate shadow.yaml from template + +Generate the `shadow.yaml` file from the template by replacing the `@PROJECT_ROOT@` placeholder with your project root: + +```bash +cd /path/to/qlean-mini +sed "s|@PROJECT_ROOT@|$(pwd)|g" shadow/shadow.yaml.in > shadow/shadow.yaml +``` + +3) Run the example + +```bash +rm -rf shadow.data && shadow --progress true --parallelism $(nproc) shadow/shadow.yaml +``` + +Notes +- The `shadow.yaml.in` file is a template with `@PROJECT_ROOT@` placeholders that need to be replaced with actual paths. +- Ensure the `qlean` executable path points to your built binary (build it if needed). +- If you run Shadow from a different directory, make all paths in `shadow/shadow.yaml` absolute. diff --git a/shadow/shadow.yaml.in b/shadow/shadow.yaml.in new file mode 100644 index 0000000..1882aa7 --- /dev/null +++ b/shadow/shadow.yaml.in @@ -0,0 +1,40 @@ +general: + stop_time: 60s + model_unblocked_syscall_latency: true +experimental: + native_preemption_enabled: true +network: + graph: + type: 1_gbit_switch +hosts: + node0: + network_node_id: 0 + ip_addr: 10.0.0.10 + processes: + - path: build/src/executable/qlean + args: --modules-dir @PROJECT_ROOT@/build/src/modules --bootnodes @PROJECT_ROOT@/example/1-network/genesis/nodes.yaml --genesis @PROJECT_ROOT@/example/1-network/genesis/config.yaml --validator-registry-path @PROJECT_ROOT@/example/1-network/genesis/validators.yaml --node-id node_0 --node-key cb920fbda3b96e18f03e22825f4a5a61343ec43c7be1c8c4a717fffee2f4c4ce --listen-addr /ip4/0.0.0.0/udp/9000/quic-v1 --prometheus-port 9100 + expected_final_state: running + + node1: + network_node_id: 0 + ip_addr: 10.0.0.11 + processes: + - path: build/src/executable/qlean + args: --modules-dir @PROJECT_ROOT@/build/src/modules --bootnodes @PROJECT_ROOT@/example/1-network/genesis/nodes.yaml --genesis @PROJECT_ROOT@/example/1-network/genesis/config.yaml --validator-registry-path @PROJECT_ROOT@/example/1-network/genesis/validators.yaml --node-id node_1 --node-key a87e7d23bb1de4613b67002b700bce41e031f4ab1529a3436bd73c893ea039b3 --listen-addr /ip4/0.0.0.0/udp/9001/quic-v1 --prometheus-port 9101 + expected_final_state: running + + node2: + network_node_id: 0 + ip_addr: 10.0.0.12 + processes: + - path: build/src/executable/qlean + args: --modules-dir @PROJECT_ROOT@/build/src/modules --bootnodes @PROJECT_ROOT@/example/1-network/genesis/nodes.yaml --genesis @PROJECT_ROOT@/example/1-network/genesis/config.yaml --validator-registry-path @PROJECT_ROOT@/example/1-network/genesis/validators.yaml --node-id node_2 --node-key f2f53f6acf312c5e92c2a611bbca7a1932b4db0b9e0c43bec413badca9b76760 --listen-addr /ip4/0.0.0.0/udp/9002/quic-v1 --prometheus-port 9102 + expected_final_state: running + + node3: + network_node_id: 0 + ip_addr: 10.0.0.13 + processes: + - path: build/src/executable/qlean + args: --modules-dir @PROJECT_ROOT@/build/src/modules --bootnodes @PROJECT_ROOT@/example/1-network/genesis/nodes.yaml --genesis @PROJECT_ROOT@/example/1-network/genesis/config.yaml --validator-registry-path @PROJECT_ROOT@/example/1-network/genesis/validators.yaml --node-id node_3 --node-key fa5ddbec80f964d17d28221c2c5bac0f4a3f9cfcf4b86674e605f459e195a1c4 --listen-addr /ip4/0.0.0.0/udp/9003/quic-v1 --prometheus-port 9103 + expected_final_state: running diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index 340e9ee..e2e91b9 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -134,9 +134,9 @@ namespace lean::app { po::options_description metrics_options("Metric options"); metrics_options.add_options() - ("prometheus_disable", "Set to disable OpenMetrics.") - ("prometheus_host", po::value(), "Set address for OpenMetrics over HTTP.") - ("prometheus_port", po::value(), "Set port for OpenMetrics over HTTP.") + ("prometheus-disable", "Set to disable OpenMetrics.") + ("prometheus-host", po::value(), "Set address for OpenMetrics over HTTP.") + ("prometheus-port", po::value(), "Set port for OpenMetrics over HTTP.") ; // clang-format on @@ -714,8 +714,9 @@ namespace lean::app { bool fail; fail = false; + // support new kebab-case option name find_argument( - cli_values_map_, "prometheus_host", [&](const std::string &value) { + cli_values_map_, "prometheus-host", [&](const std::string &value) { boost::beast::error_code ec; auto address = boost::asio::ip::make_address(value, ec); if (!ec) { @@ -725,7 +726,7 @@ namespace lean::app { config_->metrics_.enabled = true; } } else { - std::cerr << "Option --prometheus_host has invalid value\n" + std::cerr << "Option --prometheus-host has invalid value\n" << "Try run with option '--help' for more information\n"; fail = true; } @@ -736,7 +737,7 @@ namespace lean::app { fail = false; find_argument( - cli_values_map_, "prometheus_port", [&](const uint16_t &value) { + cli_values_map_, "prometheus-port", [&](const uint16_t &value) { if (value > 0 and value <= 65535) { config_->metrics_.endpoint = {config_->metrics_.endpoint.address(), static_cast(value)}; @@ -744,7 +745,7 @@ namespace lean::app { config_->metrics_.enabled = true; } } else { - std::cerr << "Option --prometheus_port has invalid value\n" + std::cerr << "Option --prometheus-port has invalid value\n" << "Try run with option '--help' for more information\n"; fail = true; } @@ -753,7 +754,7 @@ namespace lean::app { return Error::CliArgsParseFailed; } - if (find_argument(cli_values_map_, "prometheus_disabled")) { + if (find_argument(cli_values_map_, "prometheus-disable")) { config_->metrics_.enabled = false; }; if (not config_->metrics_.enabled.has_value()) {