Files
consensus/CMakeLists.txt
zeekayandHanzo Dev 34103e6d07 pop: pin the corpus this repo cannot regenerate to its digest
Seven of the eight files in vectors/ this repo's own scripts/oracle writes; a
regeneration is how they stay true. pop.json is the exception. It comes from
luxfi/conformance, generated there by oracle/pop.go over validator/pop, and is
carried here verbatim so the C++ harness needs no Go — which means nothing in
this repo can rederive it, and a carried copy can fall behind its source without
anything saying so. That is the one failure a shared corpus exists to rule out:
each side asserts against its own copy, both go green, and neither can tell you
they were asked different questions.

Counting cases does not close it. A count catches a copy that lost a case; it
passes a copy of the right length carrying the wrong bytes, which is the same
silence in a better disguise. So the bytes are pinned. luxfi/consensus carries
the same file for the Rust runner and the Go oracle re-emits it exactly, all at
sha256 1e09e832…4173cb: three languages, one corpus, one number, and a
divergence becomes a diff someone reads instead of a question nobody asked.

The check reads whatever CONFORMANCE_DIR resolves to, so pointing the harness at
another checkout is covered too — that cache entry invites exactly the stale copy
this catches. And the corpus is a configure dependency, so editing it re-runs the
check on the next build rather than at whatever configure happens next.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-02 04:03:06 -07:00

329 lines
17 KiB
CMake

# Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause-Eco
#
# consensus — the finality GATE, in pure C++23. Builds:
# blst (static, from source) → bls_signature (the reused domain-free bodies)
# → consensus (the gate + the Lux consensus vote domain) → the tests
# → luxconsensus_quasar (+ _c, the cgo ABI) — the witness verifier, a
# SIBLING of the gate on the same blst
#
# Reused, unmodified, from the luxcpp checkout (NOT vendored here):
# ${LUXCPP_ROOT}/blst — blst BLS12-381 backend
# ${LUXCPP_ROOT}/crypto/bls/cpp — cevm::crypto::bls (eth2 POP ciphersuite).
# consensus uses it ONLY for the operations with no hash-to-curve; the vote
# domain is BLS_SIG_..._RO_NUL_ and lives in src/bls.cpp. See bls.hpp.
cmake_minimum_required(VERSION 3.20)
project(consensus_seed 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()
# Sanitizers, scoped to consensus's OWN code (the gate + Node + tests) — NOT the
# reused blst/bls_signature, which is audited assembly crypto we keep unmodified
# and whose instrumentation is both forbidden by policy and pathologically slow.
# Usage: -DCONSENSUS_SANITIZE=address,undefined or -DCONSENSUS_SANITIZE=thread
set(CONSENSUS_SANITIZE "" CACHE STRING "Sanitizer set for consensus code only (e.g. address,undefined | thread)")
# Locate the reused luxcpp assets. The default is the parent of THIS repo, which
# is the luxcpp root when the repo sits where it belongs. CURRENT_SOURCE_DIR, not
# SOURCE_DIR: added as a subdirectory, SOURCE_DIR is the outer project and the
# default would point at the wrong tree — masked today only because node2 sets
# LUXCPP_ROOT before adding us.
get_filename_component(_default_luxcpp "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
set(LUXCPP_ROOT "${_default_luxcpp}" CACHE PATH "Path to the luxcpp checkout root")
set(BLST_DIR "${LUXCPP_ROOT}/blst" CACHE PATH "blst source tree")
set(BLS_DIR "${LUXCPP_ROOT}/crypto/bls/cpp" CACHE PATH "cevm::crypto::bls source dir")
foreach(_p "${BLST_DIR}/bindings/blst.h" "${BLS_DIR}/bls_signature.cpp")
if(NOT EXISTS "${_p}")
message(FATAL_ERROR "consensus seed: required reused asset not found: ${_p}\n"
"set -DLUXCPP_ROOT=/path/to/luxcpp")
endif()
endforeach()
# blst ISA selection. blst/build.sh adds -D__ADX__ when the host reports adx;
# without it assembly.S compiles the mulq path and every field multiplication —
# so every pairing — runs on the slower generic code. Measured on evo: 24% on a
# verification. BLST_ISA=portable keeps both paths and dispatches at runtime.
set(BLST_ISA "auto" CACHE STRING "blst ISA: auto | adx | portable | generic")
set(BLST_ISA_FLAGS "")
if(BLST_ISA STREQUAL "portable")
set(BLST_ISA_FLAGS -D__BLST_PORTABLE__)
elseif(BLST_ISA STREQUAL "adx")
set(BLST_ISA_FLAGS -D__ADX__)
elseif(BLST_ISA STREQUAL "auto")
if(EXISTS /proc/cpuinfo)
file(READ /proc/cpuinfo _cpuinfo)
if(_cpuinfo MATCHES "[ \t]adx[ \t\n]")
set(BLST_ISA_FLAGS -D__ADX__)
endif()
endif()
endif()
# ── blst (BLS12-381 backend) — unity build mirroring blst/build.sh ────────────
add_library(blst STATIC
"${BLST_DIR}/src/server.c"
"${BLST_DIR}/build/assembly.S")
target_compile_options(blst PRIVATE -O2 -fno-builtin ${BLST_ISA_FLAGS})
target_include_directories(blst PUBLIC "${BLST_DIR}/bindings")
# ── cevm::crypto::bls — the REUSED genuine BLS sign/verify/aggregate surface ───
add_library(bls_signature STATIC "${BLS_DIR}/bls_signature.cpp")
target_include_directories(bls_signature PUBLIC "${BLS_DIR}")
target_link_libraries(bls_signature PUBLIC blst)
# ── consensus — the NEW finality gate (the seed) ─────────────────────────────
add_library(consensus STATIC
src/threshold.cpp src/bls.cpp src/cert.cpp src/registration.cpp src/quorum_cert_engine.cpp
src/wave.cpp src/node.cpp)
target_include_directories(consensus PUBLIC include)
# bls_signature carries the reused domain-free bodies; blst carries the curve. The
# consensus vote domain (src/bls.cpp) needs blst's headers directly — see bls.hpp.
target_link_libraries(consensus PUBLIC bls_signature blst)
target_compile_options(consensus PRIVATE -Wall -Wextra -Wpedantic)
# ── quasar — the witness verifier and its cgo ABI ─────────────────────────────
# The C++ hot-path port of Go protocol/quasar: Verify + AggregateThresholdSignatures.
# A SIBLING of the gate, not a layer of it: no gate source calls lux::quasar, and
# linking them would fabricate a dependency neither one has. Same blst, same NUL
# DST as the vote domain; it rides the from-source blst target above, so this tree
# has exactly ONE blst.
add_library(luxconsensus_quasar STATIC src/quasar_verify.cpp src/quasar_witness.cpp)
target_include_directories(luxconsensus_quasar PUBLIC include)
target_link_libraries(luxconsensus_quasar PUBLIC blst)
target_compile_options(luxconsensus_quasar PRIVATE -Wall -Wextra)
# The extern "C" surface, built as its own shared object so cgo consumers load the
# four lux_quasar_* symbols and nothing else. The name is a PUBLISHED CONTRACT:
# lux-private/multichain-cpp find_library's luxconsensus_quasar and find_path's
# lux/quasar.h. Do not rename either.
add_library(luxconsensus_quasar_c SHARED src/quasar_ffi.cpp)
target_link_libraries(luxconsensus_quasar_c PRIVATE luxconsensus_quasar blst)
target_include_directories(luxconsensus_quasar_c PUBLIC include)
target_compile_options(luxconsensus_quasar_c PRIVATE -Wall -Wextra)
set_target_properties(luxconsensus_quasar_c PROPERTIES OUTPUT_NAME luxconsensus_quasar)
# The gate is consumed by add_subdirectory (node2) and is never installed; the
# quasar pair is the one surface an outside build links against, so it is the one
# thing with an install rule.
include(GNUInstallDirs)
install(TARGETS luxconsensus_quasar luxconsensus_quasar_c
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES include/lux/quasar.h include/lux/quasar.hpp
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/lux)
# ── tests ─────────────────────────────────────────────────────────────────────
# ONE list of test names, ONE loop. A new test is one line here: its source is
# test/<name>_test.cpp, its target <name>_test, its ctest name <name>. The
# sanitizer block below reads the SAME list, so a test can never be instrumented
# in one place and forgotten in another.
enable_testing()
set(CONSENSUS_TESTS
toy_stub_killer # the acceptance gate: no toy stub survives it
poll_driver
engine_integration
multi_node
batch_verify_attack
no_double_finalize # agreement: no two conflicting blocks finalize at a height
liveness
out_of_turn # the acceptance window
tier # the two finality rungs: Nova (accept) and Quasar (export)
conformance # the Go corpus: message bytes, DST, floors, thresholds
pop_conformance # the node-bound proof of possession, byte-for-byte vs Go & Rust
derived_authority # a certificate states its quorum; it does not choose it
registration # the admission door: possession, one key one node, one node one key
party # the door wired to the participant: one admission, gate + registry
quasar_verify # the witness verifier vs the Go fixtures, C++ AND C ABI
quasar_witness) # threshold-share aggregation on Go's (index || sig) wire form
# The ZAP vote transport rides zap-cpp-core, so it is gated on -DZAP_DIR; core
# consensus has no ZAP dependency.
if (ZAP_DIR)
list(APPEND CONSENSUS_TESTS zap_vote_transport)
endif()
foreach(_t ${CONSENSUS_TESTS})
add_executable(${_t}_test test/${_t}_test.cpp)
target_link_libraries(${_t}_test PRIVATE consensus)
target_compile_options(${_t}_test PRIVATE -Wall -Wextra)
add_test(NAME ${_t} COMMAND ${_t}_test)
endforeach()
if (ZAP_DIR)
target_include_directories(zap_vote_transport_test PRIVATE "${ZAP_DIR}")
endif()
# The quasar tests exercise the witness surface and its C ABI, so they link the
# quasar pair on top of the gate. Their fixtures resolve from __FILE__/../../testdata
# — the corpus at this repo's root — so they need no argument.
foreach(_t quasar_verify quasar_witness)
target_link_libraries(${_t}_test PRIVATE luxconsensus_quasar luxconsensus_quasar_c)
endforeach()
# The conformance harness reads the Go-generated corpus. CONFORMANCE_DIR points at
# whichever checkout holds it; the vectors shipped here are the default.
set(CONFORMANCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vectors" CACHE PATH
"Directory holding the Go-generated conformance corpus")
target_compile_definitions(conformance_test PRIVATE
CONSENSUS_VECTORS="${CONFORMANCE_DIR}")
target_compile_definitions(pop_conformance_test PRIVATE
CONSENSUS_VECTORS="${CONFORMANCE_DIR}")
# Seven of those vectors this repo's own scripts/oracle writes. pop.json it does
# not: that one is generated by luxfi/conformance (oracle/pop.go, over
# validator/pop) and carried here verbatim so the C++ harness needs no Go. A
# carried copy can fall behind its source in silence, which is the one failure a
# shared corpus exists to rule out — so the bytes are pinned, not just the count.
# A stale copy of the right length passes a count; it does not pass this.
# luxfi/consensus carries the same file for the Rust runner, at the same digest:
# three languages, one corpus, one number, and a divergence is a visible diff.
# Pin what this repo cannot regenerate.
set(POP_CORPUS_SHA256 1e09e8320b9315609834b74c2886a6233ca9537116b57de5ba81236fb169d1b1)
file(SHA256 "${CONFORMANCE_DIR}/pop.json" POP_CORPUS_READ)
if(NOT POP_CORPUS_READ STREQUAL POP_CORPUS_SHA256)
message(FATAL_ERROR
"the frozen pop corpus has drifted:\n"
" read ${CONFORMANCE_DIR}/pop.json\n"
" sha256 ${POP_CORPUS_READ}\n"
" frozen ${POP_CORPUS_SHA256}\n"
"It is luxfi/conformance vectors/pop.json, the same bytes as luxfi/consensus "
"pkg/rust/tests/vectors/pop.json. Copy it from either. If the corpus was "
"regenerated on purpose, the three copies and this digest move together.")
endif()
# A corpus that is edited, or a CONFORMANCE_DIR re-pointed at another checkout,
# re-runs the check on the next build rather than at whatever configure happens
# next.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CONFORMANCE_DIR}/pop.json")
# Consensus's OWN targets — the gate, the quasar pair, and every test. Written
# once because two things instrument it (the sanitizer below, coverage after it)
# and a target present in one list and missing from the other is instrumented in
# one build and silently not in the other. blst and bls_signature are NOT here:
# audited assembly crypto we keep unmodified, whose instrumentation is both
# forbidden by policy and pathologically slow.
set(CONSENSUS_OWN_TARGETS consensus luxconsensus_quasar luxconsensus_quasar_c)
foreach(_t ${CONSENSUS_TESTS})
list(APPEND CONSENSUS_OWN_TARGETS ${_t}_test)
endforeach()
# Apply the requested sanitizer to consensus's OWN targets only — see
# CONSENSUS_SANITIZE above.
if (CONSENSUS_SANITIZE)
set(_san -fsanitize=${CONSENSUS_SANITIZE} -fno-omit-frame-pointer)
foreach(_t ${CONSENSUS_OWN_TARGETS})
target_compile_options(${_t} PRIVATE ${_san})
target_link_options(${_t} PRIVATE ${_san})
endforeach()
endif()
# ── coverage ──────────────────────────────────────────────────────────────────
# What the tests actually reach, MEASURED. -DCONSENSUS_COVERAGE=ON instruments
# consensus's own code and adds one target:
#
# cmake -S . -B build-cov -DCMAKE_BUILD_TYPE=Debug -DCONSENSUS_COVERAGE=ON \
# -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
# cmake --build build-cov --target coverage
#
# Clang's source-based coverage, not gcov, for two reasons. It counts REGIONS,
# so a line carrying two clauses reports both and a half-tested predicate cannot
# read as covered — which is the only measure worth quoting about a gate whose
# clauses are the rule. And it is the same instrumentation `cargo llvm-cov`
# reports the Rust crate with, so the C++ number and the Rust number mean the
# same thing and can be put in one table. A build with any other compiler says
# so rather than quietly measuring nothing.
option(CONSENSUS_COVERAGE "Instrument consensus's own code and add the `coverage` target" OFF)
if (CONSENSUS_COVERAGE)
if (NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR
"CONSENSUS_COVERAGE needs Clang's source-based coverage; this build uses "
"${CMAKE_CXX_COMPILER_ID}. Re-run with "
"-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++")
endif()
find_program(LLVM_PROFDATA NAMES llvm-profdata llvm-profdata-18 llvm-profdata-17 REQUIRED)
find_program(LLVM_COV NAMES llvm-cov llvm-cov-18 llvm-cov-17 REQUIRED)
foreach(_t ${CONSENSUS_OWN_TARGETS})
target_compile_options(${_t} PRIVATE -fprofile-instr-generate -fcoverage-mapping)
target_link_options(${_t} PRIVATE -fprofile-instr-generate -fcoverage-mapping)
endforeach()
set(_cov "${CMAKE_BINARY_DIR}/coverage")
# One profile per test, named for the test — from the SAME list that declared
# them, so a test cannot be run by ctest and missed by the report. Named
# rather than globbed (%p) so the merge input is exact and a stale profile
# from an earlier run cannot slip into the number.
set(_run "")
set(_raw "")
list(GET CONSENSUS_TESTS 0 _first_test)
foreach(_t ${CONSENSUS_TESTS})
list(APPEND _run COMMAND ${CMAKE_COMMAND} -E env
"LLVM_PROFILE_FILE=${_cov}/${_t}.profraw" $<TARGET_FILE:${_t}_test>)
list(APPEND _raw "${_cov}/${_t}.profraw")
endforeach()
# Read from the binaries the profiles came out of — llvm-cov cannot open a
# static archive, and an archive's members alone lose the header inline
# functions, which are emitted into whichever translation unit uses them and
# are as much the gate as anything in src/. The shared quasar object joins
# them because its ABI (src/quasar_ffi.cpp) lives only there.
#
# test/ is filtered back out: the subject is the gate, and test files scoring
# themselves would only dilute the number. (llvm-cov warns that some
# functions have mismatched data — the per-test `main`s, one per binary and
# each a different shape under one name. They are in test/, so the filter
# drops them; every src/ figure is identical whether the report is read from
# the binaries or from the library objects alone.)
set(_objects $<TARGET_FILE:${_first_test}_test>)
foreach(_t ${CONSENSUS_TESTS})
if (NOT _t STREQUAL _first_test)
list(APPEND _objects -object $<TARGET_FILE:${_t}_test>)
endif()
endforeach()
list(APPEND _objects -object $<TARGET_FILE:luxconsensus_quasar_c>)
# Absolute paths, so /test/ names the directory and nothing else. Kept free of
# shell metacharacters: the generator hands this to /bin/sh verbatim.
set(_only "-ignore-filename-regex=/test/")
add_custom_target(coverage
DEPENDS ${CONSENSUS_OWN_TARGETS}
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_cov}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_cov}"
${_run}
COMMAND ${LLVM_PROFDATA} merge -sparse -o "${_cov}/consensus.profdata" ${_raw}
COMMAND ${LLVM_COV} report ${_objects}
"-instr-profile=${_cov}/consensus.profdata"
"-show-region-summary" "${_only}"
USES_TERMINAL
COMMENT "consensus: running every test under instrumentation, then reporting what it reached")
# The same profile, per line, for reading the misses rather than counting them.
add_custom_target(coverage-show
DEPENDS coverage
COMMAND ${LLVM_COV} show ${_objects}
"-instr-profile=${_cov}/consensus.profdata"
"-show-line-counts-or-regions" "-show-branches=count" "${_only}"
"-output-dir=${_cov}/html" "-format=html"
COMMAND ${CMAKE_COMMAND} -E echo "coverage: ${_cov}/html/index.html"
USES_TERMINAL
COMMENT "consensus: per-line coverage, for reading the misses")
endif()
# ── bench_trilang — the C++ leg of the three-language CPU comparison ──────────
# Off by default: it needs google-benchmark, and a build that cannot find it
# should still produce the library and the tests. -DCONSENSUS_BENCH=ON to build.
option(CONSENSUS_BENCH "Build the three-language comparison benchmark" OFF)
if(CONSENSUS_BENCH)
find_package(benchmark REQUIRED)
add_executable(bench_trilang bench/bench_trilang.cpp)
target_link_libraries(bench_trilang PRIVATE consensus benchmark::benchmark)
target_compile_options(bench_trilang PRIVATE -O3 -Wall -Wextra)
endif()