mirror of
https://github.com/lux-cpp/node.git
synced 2026-10-30 14:48:15 +00:00
A stranger cannot hold the door (test/acceptor_test.cpp). The slot clock counts from the later of the tip's time and kReach windows before this validator took the height up, so a stale parent opens at most kReach + SKEW windows. Any verified certificate on its own tip lifts its import gate, the rule the engine applies at start. LLM.md records where lux-rs/node has the same flaw (engine.rs:786). Co-authored-by: Hanzo Dev <[email protected]>
332 lines
18 KiB
CMake
332 lines
18 KiB
CMake
# Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
|
||
# SPDX-License-Identifier: BSD-3-Clause-Eco
|
||
#
|
||
# node — a running consensus node: the consensus engine + a ZAP vote mesh over
|
||
# real TCP sockets. Layering (each layer below is REUSED unmodified):
|
||
#
|
||
# blst → bls_signature → consensus (the gate + Node, and its BLS domain)
|
||
# [all built by the consensus checkout's CMakeLists, added as a subdirectory]
|
||
# lux::zap wire codec (zap-cpp-core, header-only)
|
||
# ──────────────────────── NEW in node ────────────────────────
|
||
# FrameReader (non-blocking reassembler, header-only)
|
||
# MeshVoteTransport (VoteTransport over N TCP sockets, header-only)
|
||
# Node2Host (listen/accept/dial mesh + drive the Node) ← the one .cpp
|
||
#
|
||
# Reused, NOT vendored: the consensus checkout (the engine) and the luxcpp
|
||
# checkout (blst, cevm::crypto::bls, zap-cpp-core). Both are found automatically
|
||
# where they actually sit; -DCONSENSUS_DIR / -DLUXCPP_ROOT override.
|
||
|
||
cmake_minimum_required(VERSION 3.20)
|
||
project(node LANGUAGES C CXX ASM)
|
||
|
||
set(CMAKE_CXX_STANDARD 23)
|
||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||
if(NOT CMAKE_BUILD_TYPE)
|
||
set(CMAKE_BUILD_TYPE Release)
|
||
endif()
|
||
|
||
# Sanitizer for node's + consensus's OWN code (NOT the reused blst/bls assembly
|
||
# crypto). TSan here is load-bearing: the mesh setup is multi-threaded and the gate
|
||
# mutex is exercised across threads. Usage: -DNODE_SANITIZE=thread (or address,undefined)
|
||
set(NODE_SANITIZE "" CACHE STRING "Sanitizer for node+consensus code (e.g. thread | address,undefined)")
|
||
# Propagate to the consensus subdir so the reused gate is instrumented to match,
|
||
# while blst/bls_signature stay clean (consensus scopes it the same way).
|
||
if (NODE_SANITIZE AND NOT CONSENSUS_SANITIZE)
|
||
set(CONSENSUS_SANITIZE "${NODE_SANITIZE}" CACHE STRING "" FORCE)
|
||
endif()
|
||
|
||
# find_checkout(VAR MARKER candidate...) — first candidate holding MARKER wins.
|
||
# One search, used for both reused trees, so "where does X live" is stated once.
|
||
function(find_checkout out marker)
|
||
foreach(_root ${ARGN})
|
||
get_filename_component(_abs "${_root}" ABSOLUTE)
|
||
if(EXISTS "${_abs}/${marker}")
|
||
set(${out} "${_abs}" PARENT_SCOPE)
|
||
return()
|
||
endif()
|
||
endforeach()
|
||
set(${out} "${out}-NOTFOUND" PARENT_SCOPE)
|
||
endfunction()
|
||
|
||
# consensus — the C++ consensus lives in the luxcpp checkout, so look there first.
|
||
# The sibling candidate stays for a side-by-side checkout, but it is LAST and it is
|
||
# why the marker is a header rather than a directory name: beside node in the lux
|
||
# checkout sits luxfi/consensus, the GO implementation, under the very same name.
|
||
# Matching on include/lux/consensus/node.hpp is what tells the two apart.
|
||
find_checkout(_consensus "include/lux/consensus/node.hpp"
|
||
"${CMAKE_SOURCE_DIR}/../../luxcpp/consensus"
|
||
"${CMAKE_SOURCE_DIR}/../luxcpp/consensus"
|
||
"${CMAKE_SOURCE_DIR}/../consensus")
|
||
|
||
# cevm — the C++ EVM that executes the C-Chain's blocks. Found the same way the
|
||
# other reused trees are, and matched on a source file rather than the directory
|
||
# name for the same reason: `cevm` is also the name of a Go binding and of a
|
||
# Conan package. lib/evm/state/processor.hpp is what identifies the checkout.
|
||
find_checkout(_cevm "lib/evm/state/processor.hpp"
|
||
"${CMAKE_SOURCE_DIR}/../cevm"
|
||
"${CMAKE_SOURCE_DIR}/../../luxcpp/cevm"
|
||
"${CMAKE_SOURCE_DIR}/../luxcpp/cevm")
|
||
|
||
# luxcpp — blst + cevm::crypto::bls + zap-cpp-core all live under one root.
|
||
find_checkout(_luxcpp "blst/bindings/blst.h"
|
||
"${CMAKE_SOURCE_DIR}/../../luxcpp"
|
||
"${CMAKE_SOURCE_DIR}/../luxcpp"
|
||
"${CMAKE_SOURCE_DIR}/..")
|
||
|
||
|
||
set(CONSENSUS_DIR "${_consensus}" CACHE PATH "consensus checkout")
|
||
set(CEVM_DIR "${_cevm}" CACHE PATH "cevm checkout")
|
||
set(LUXCPP_ROOT "${_luxcpp}" CACHE PATH "luxcpp checkout root")
|
||
set(ZAP_DIR "${LUXCPP_ROOT}/zap-cpp-core/include" CACHE PATH "zap-cpp-core include root")
|
||
|
||
foreach(_p "${CONSENSUS_DIR}/CMakeLists.txt"
|
||
"${CEVM_DIR}/lib/evm/state/processor.hpp"
|
||
"${ZAP_DIR}/lux/zap/wire.hpp"
|
||
"${LUXCPP_ROOT}/blst/bindings/blst.h"
|
||
"${LUXCPP_ROOT}/crypto/bls/cpp/bls_signature.cpp")
|
||
if(NOT EXISTS "${_p}")
|
||
message(FATAL_ERROR "node: required reused asset not found: ${_p}\n"
|
||
"searched for consensus and luxcpp beside this checkout; "
|
||
"set -DCONSENSUS_DIR=/path/to/consensus -DLUXCPP_ROOT=/path/to/luxcpp")
|
||
endif()
|
||
endforeach()
|
||
message(STATUS "node: consensus = ${CONSENSUS_DIR}")
|
||
message(STATUS "node: luxcpp = ${LUXCPP_ROOT}")
|
||
message(STATUS "node: cevm = ${CEVM_DIR}")
|
||
|
||
# cevm's dependencies (intx, nlohmann_json, lux-crypto, bls, lux-zap-core) come
|
||
# from Conan, so node is configured with the toolchain Conan generated for it.
|
||
# Saying so here — rather than failing 200 lines later inside cevm's
|
||
# find_package(intx) — is the difference between a build you can fix and one you
|
||
# have to bisect.
|
||
if(NOT TARGET intx::intx AND NOT CMAKE_TOOLCHAIN_FILE)
|
||
message(FATAL_ERROR
|
||
"node: cevm's dependencies come from Conan and no toolchain was given.\n"
|
||
" conan install ${CEVM_DIR} -pr ${CEVM_DIR}/.github/conan/manylinux-relax.profile \\\n"
|
||
" -s build_type=Release -s compiler.cppstd=gnu20 \\\n"
|
||
" --output-folder=${CEVM_DIR}/build-node --build=missing\n"
|
||
" cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=${CEVM_DIR}/build-node/build/Release/generators/conan_toolchain.cmake")
|
||
endif()
|
||
|
||
find_package(Threads REQUIRED)
|
||
|
||
# ── AWS-LC — the ONLY TLS stack on this machine offering luxd's mandatory
|
||
# X25519MLKEM768 hybrid group (`network/peer/tls_config.go`'s single-
|
||
# element CurvePreferences list). System OpenSSL here is 3.0.13, which
|
||
# predates the group entirely; AWS-LC 0.45.0 (the exact version node.rs's
|
||
# own `aws-lc-rs` dependency resolves to) has it at the SAME wire codepoint
|
||
# Go's crypto/tls assigns (`SSL_GROUP_X25519_MLKEM768 = 0x11EC`). Built
|
||
# standalone from its own CMakeLists (its source ships inside the
|
||
# `aws-lc-sys` crate; this repo depends on neither Cargo nor that crate,
|
||
# only on the C library it vendors) — see LLM.md for the exact command.
|
||
# Not a Conan package: nothing in this stack packages it.
|
||
# Found the way the other two reused trees are found, rather than named on
|
||
# the command line: it is a checkout beside them and it was the one
|
||
# dependency that made configuring this repo a two-step job. -DAWSLC_SRC
|
||
# and -DAWSLC_BUILD_DIR still override.
|
||
set(AWSLC_SRC "" CACHE PATH "AWS-LC source tree (has include/openssl/ssl.h)")
|
||
set(AWSLC_BUILD_DIR "" CACHE PATH "AWS-LC build dir (has crypto/libcrypto.a, ssl/libssl.a)")
|
||
if(NOT AWSLC_SRC)
|
||
find_checkout(AWSLC_SRC "include/openssl/ssl.h"
|
||
"${CMAKE_SOURCE_DIR}/../aws-lc"
|
||
"${CMAKE_SOURCE_DIR}/../../luxcpp/aws-lc"
|
||
"${CMAKE_SOURCE_DIR}/../luxcpp/aws-lc")
|
||
endif()
|
||
if(NOT AWSLC_BUILD_DIR AND AWSLC_SRC)
|
||
# The build is a directory under the source, and it counts only if the
|
||
# library is actually in it: a configured-but-unbuilt tree is not an answer.
|
||
find_checkout(AWSLC_BUILD_DIR "ssl/libssl.a"
|
||
"${AWSLC_SRC}/build"
|
||
"${AWSLC_SRC}/build/Release")
|
||
endif()
|
||
if(NOT AWSLC_SRC OR NOT EXISTS "${AWSLC_SRC}/include/openssl/ssl.h" OR
|
||
NOT AWSLC_BUILD_DIR OR NOT EXISTS "${AWSLC_BUILD_DIR}/ssl/libssl.a")
|
||
message(FATAL_ERROR
|
||
"node: AWS-LC not found. Build it once — see LLM.md \"Building AWS-LC\" —\n"
|
||
"then pass -DAWSLC_SRC=<...>/aws-lc -DAWSLC_BUILD_DIR=<build dir>.")
|
||
endif()
|
||
message(STATUS "node: aws-lc = ${AWSLC_BUILD_DIR}")
|
||
add_library(awslc_crypto STATIC IMPORTED)
|
||
set_target_properties(awslc_crypto PROPERTIES IMPORTED_LOCATION "${AWSLC_BUILD_DIR}/crypto/libcrypto.a")
|
||
add_library(awslc_ssl STATIC IMPORTED)
|
||
set_target_properties(awslc_ssl PROPERTIES
|
||
IMPORTED_LOCATION "${AWSLC_BUILD_DIR}/ssl/libssl.a"
|
||
INTERFACE_LINK_LIBRARIES awslc_crypto
|
||
INTERFACE_INCLUDE_DIRECTORIES "${AWSLC_SRC}/include")
|
||
|
||
# ── Reuse consensus's build (brings the blst / bls_signature / consensus targets
|
||
# with correct include + link propagation). One build definition for the reused
|
||
# layers — no duplication here.
|
||
add_subdirectory("${CONSENSUS_DIR}" consensus-build)
|
||
|
||
# ── Reuse cevm's build the same way. EXCLUDE_FROM_ALL because node links four of
|
||
# its libraries and does not want its 40-odd test and benchmark binaries; they
|
||
# stay buildable by name. Static, because luxd is one process: cevm defaults
|
||
# to a shared library so it can be loaded as an EVMC plugin, which is not how
|
||
# it is used here.
|
||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||
set(CEVM_TESTING OFF CACHE BOOL "" FORCE)
|
||
add_subdirectory("${CEVM_DIR}" cevm-build EXCLUDE_FROM_ALL)
|
||
|
||
# ── node — the NEW host library (the only compiled unit; the rest is header-only)
|
||
# libluxcrypto — the ML-DSA-65 and ML-KEM-768 the post-quantum handshake signs
|
||
# and encapsulates with. It is a Go build output, `make dist` in luxfi/crypto,
|
||
# and it is found the way the other reused trees are rather than named.
|
||
find_checkout(_luxcrypto "libluxcrypto.a"
|
||
"${CMAKE_SOURCE_DIR}/../../lux/crypto/dist"
|
||
"${CMAKE_SOURCE_DIR}/../lux/crypto/dist"
|
||
"$ENV{LUX_LIB_DIR}")
|
||
if(_luxcrypto)
|
||
message(STATUS "node: luxcrypto = ${_luxcrypto}")
|
||
add_library(luxcrypto STATIC IMPORTED)
|
||
set_target_properties(luxcrypto PROPERTIES
|
||
IMPORTED_LOCATION "${_luxcrypto}/libluxcrypto.a"
|
||
INTERFACE_INCLUDE_DIRECTORIES "${_luxcrypto}")
|
||
else()
|
||
message(FATAL_ERROR
|
||
"node: no libluxcrypto.a. Build it with `make dist` in luxfi/crypto, or set "
|
||
"LUX_LIB_DIR to the directory holding it.")
|
||
endif()
|
||
|
||
add_library(node STATIC
|
||
src/mesh.cpp # the socket dance
|
||
src/windower.cpp # who proposes at a height and a slot: Go's proposervm windower
|
||
src/statement.cpp # what a validator signs — a vote or an anchor — and α of them
|
||
src/journal.cpp # one statement a height, on the device before it is signed
|
||
src/ledger.cpp # the certificates accepted on, the import gate, the held block
|
||
src/engine.cpp # the height rule: windows, one signature, the converged winner
|
||
src/network.cpp # which chain aliases this node's network owns
|
||
src/rpc.cpp # HTTP JSON-RPC, and the relay to a chain's own server
|
||
src/run.cpp # a node on one network's spec
|
||
src/validators.cpp # the set-root commitment a vote binds
|
||
src/committee.cpp # the network description all three daemons read
|
||
src/plugin.cpp # a chain in another process, over ZAP
|
||
src/signer.cpp # this validator's own name and voting key
|
||
src/staking.cpp # a luxd peer's identity: P-256 cert + NodeID + BLS + ML-DSA
|
||
src/peer_tls.cpp # TLS 1.3 + X25519MLKEM768, over AWS-LC
|
||
src/keccak.cpp # SHAKE256/cSHAKE256 and the SP 800-185 encodings under them
|
||
src/pq_handshake.cpp # ML-DSA-65 identity + ML-KEM-768 session, the strict-PQ link
|
||
src/peer.cpp) # the p2p Handshake/PushQuery/vote path to a live luxd
|
||
target_include_directories(node PUBLIC include "${ZAP_DIR}")
|
||
target_link_libraries(node PUBLIC consensus Threads::Threads awslc_ssl luxcrypto)
|
||
# A Go archive brings its own runtime, which wants the platform frameworks it
|
||
# calls into. Whole-archive so the runtime's initializer survives dead-strip:
|
||
# nothing in C++ calls it, and a linker that keeps only what is called drops it.
|
||
if(APPLE)
|
||
target_link_libraries(node PUBLIC
|
||
"-Wl,-force_load,${_luxcrypto}/libluxcrypto.a"
|
||
"-framework CoreFoundation" "-framework Security" resolv)
|
||
else()
|
||
target_link_libraries(node PUBLIC -Wl,--whole-archive luxcrypto -Wl,--no-whole-archive)
|
||
endif()
|
||
# evm-state carries the StateDB, process_block and the Merkle-Patricia-Trie; evm
|
||
# is the interpreter behind evmc_create_cevm. lux::crypto-secp256k1 is what
|
||
# recovers a transaction's sender, and nlohmann_json is the RPC's codec.
|
||
# find_package again at THIS level, not because cevm forgot to: an imported
|
||
# target a subdirectory creates is scoped to that subdirectory, so node has to
|
||
# ask for the two it links by name itself. Same packages, same Conan-generated
|
||
# configs, resolved a second time — not a second source of truth.
|
||
find_package(crypto CONFIG REQUIRED)
|
||
find_package(nlohmann_json CONFIG REQUIRED)
|
||
target_link_libraries(node PUBLIC evm-state evm lux::crypto-secp256k1 lux::crypto-sha256 nlohmann_json::nlohmann_json)
|
||
target_compile_options(node PRIVATE -Wall -Wextra -Wpedantic)
|
||
|
||
# ── luxd — this node on a local Lux chain
|
||
add_executable(luxd src/luxd.cpp)
|
||
target_link_libraries(luxd PRIVATE node)
|
||
target_compile_options(luxd PRIVATE -Wall -Wextra)
|
||
|
||
# ── lux-join — a validator on a REAL, external luxd network (not this node's
|
||
# own self-referential mesh): PQ-gated TLS, the p2p Handshake, and a signed
|
||
# Quasar vote on whatever luxd proposes. See LLM.md.
|
||
add_executable(lux-join src/lux_join_main.cpp)
|
||
target_link_libraries(lux-join PRIVATE node)
|
||
target_compile_options(lux-join PRIVATE -Wall -Wextra)
|
||
|
||
# ── tests — one registration, one list
|
||
# frame_reader the reassembler in isolation (fragmentation, batching, limits)
|
||
# wire_vector the bytes on the wire, written from the spec and read off a socket
|
||
# mesh_transport what one hostile or dead socket can do to this node
|
||
# mesh_formation mesh setup is bounded, and a missing validator is not a dead cluster
|
||
# acceptor a greeting has one deadline, runs off the accepting thread,
|
||
# and is capped in all and per source: a stranger cannot hold
|
||
# the door (the review's probe, asserting)
|
||
# windower Go's proposer schedule, held to windower_test.go's vectors
|
||
# statement the vote and the anchor, their domain, the journal both go
|
||
# through, the certificate rule, the ledger and held records
|
||
# live five validators import Zoo mainnet's tip, anchor it and
|
||
# build on it; one seat down; one that imported another tip;
|
||
# one that restarts and catches up
|
||
# hostile_decode what a stranger can send the two decoders that take his bytes
|
||
# set_root the validator-set commitment, against Go's encoding
|
||
# committee the network description: the parse, the refusals, the root,
|
||
# and the possession proof that is checked rather than trusted
|
||
# chain_route every spelling of a chain's path reaches that one chain
|
||
# archive_proxy what a relayed answer's status is, when this node is a proxy
|
||
# pq_handshake the strict-PQ peer handshake: Go's own answers to the three
|
||
# derivations, then a full exchange over a real socket pair
|
||
# pq_vector the validator link held to a handshake the GO node produced:
|
||
# its frames, its names, its session key (LP-10602)
|
||
# engine one validator against a scripted peer: a proposal from the
|
||
# wrong window is never parsed, a refused block is never
|
||
# signed, a gated node signs only its anchor, a chain that
|
||
# refuses to build does not stop it
|
||
# run what a node refuses before it starts: a spec whose genesis
|
||
# names another chain, a network no spec has
|
||
# plugin cevm's EVM plugin, started the way the Go node starts one
|
||
# and driven through a height over ZAP
|
||
enable_testing()
|
||
|
||
set(NODE_TESTS frame_reader wire_vector mesh_transport mesh_formation acceptor hostile_decode set_root committee chain_route archive_proxy pq_handshake pq_vector run windower statement live engine)
|
||
foreach(_t ${NODE_TESTS})
|
||
add_executable(${_t}_test test/${_t}_test.cpp)
|
||
target_link_libraries(${_t}_test PRIVATE node)
|
||
target_compile_options(${_t}_test PRIVATE -Wall -Wextra)
|
||
add_test(NAME ${_t} COMMAND ${_t}_test)
|
||
# cevm is a subdirectory and registers its own tests, which this build does
|
||
# not compile; the label is what says which tests are this repository's.
|
||
set_tests_properties(${_t} PROPERTIES LABELS node)
|
||
endforeach()
|
||
|
||
# The plugin test drives THIS build's EVM plugin — cevm's, the one the node runs
|
||
# its chain in — the way the node does, so it is built with the test.
|
||
add_executable(plugin_test test/plugin_test.cpp)
|
||
target_link_libraries(plugin_test PRIVATE node)
|
||
target_compile_options(plugin_test PRIVATE -Wall -Wextra)
|
||
add_dependencies(plugin_test cevm-bin)
|
||
add_test(NAME plugin COMMAND plugin_test $<TARGET_FILE:cevm-bin>)
|
||
set_tests_properties(plugin PROPERTIES LABELS node)
|
||
|
||
# The tool scripts/cluster.sh builds its export with: the local chain, built by
|
||
# the cevm plugin alone, written as luxfi/state writes an export.
|
||
add_executable(local_export test/local/export.cpp)
|
||
target_link_libraries(local_export PRIVATE node)
|
||
add_dependencies(local_export cevm-bin)
|
||
|
||
# The committee test reads REAL FILES — the same files the Rust node reads, so
|
||
# "both accept this" is a fact about one artifact rather than about two string
|
||
# literals that were typed twice.
|
||
target_compile_definitions(committee_test PRIVATE
|
||
COMMITTEE_FIXTURES="${CMAKE_CURRENT_SOURCE_DIR}/test/committee")
|
||
|
||
# Same discipline for the link: the vector is a file, and it came off a wire
|
||
# with a Go node at the other end of it.
|
||
target_compile_definitions(pq_vector_test PRIVATE
|
||
PQ_VECTORS="${CMAKE_CURRENT_SOURCE_DIR}/test/pq")
|
||
|
||
# Apply NODE_SANITIZE to node's own targets only (the consensus subdir scopes
|
||
# its own; blst/bls stay clean). One place for the whole node surface.
|
||
if (NODE_SANITIZE)
|
||
set(_n2_san -fsanitize=${NODE_SANITIZE} -fno-omit-frame-pointer)
|
||
set(_n2_targets node luxd)
|
||
foreach(_t ${NODE_TESTS})
|
||
list(APPEND _n2_targets ${_t}_test)
|
||
endforeach()
|
||
foreach(_t ${_n2_targets})
|
||
target_compile_options(${_t} PRIVATE ${_n2_san})
|
||
target_link_options(${_t} PRIVATE ${_n2_san})
|
||
endforeach()
|
||
endif()
|