mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
Before this there were three SHA-256s in the C++ tree, two builds of the same secp256k1, and a keccak compiled twice. blue/gpu-seam states the primitives once — hashes, the RFC 6962 fold, and public-key recovery — behind one interface per language, with a complete CPU backend and an optional kernel plugin behind the same door. A node built with nothing installed is a whole node, not a degraded one; that is what `make gpu-differential` runs first, with the library deliberately out of sight. Where the seam meets the node's core. The branch had each chain add gpu/cpp itself, which was right when each chain also compiled its own crypto. Core now holds what every chain shares, so core links the seam and a chain that links core has it. `lux::core::sha256` is `lux::gpu::sha256`; the private compiles of sha256.cpp and ripemd160.cpp under core, of `lux_pvm_crypto` and of `xvm_crypto` are gone. The Q-chain came along: it had its own SHA-256 with its own test vectors, and it identifies transactions with it. Its two callers ask the seam now — the streaming one folds its four pieces into the buffer the run of updates already meant. TWO DIVERGENCES, both found by tests the gate cannot see. A malleated signature — `s` negated, recovery parity flipped — names the same key, and Go spends under it: `RecoverPubkey` checks the length and `v < 4`, and the low-`s` refusal lives in `VerifySignature`, which no spend path calls. The first-party C++ curve recovers under it too. `k256` alone refuses a high `s`, and the P-chain's own code used to put the pair back in the form k256 reads — so routing it at the seam made the Rust backend the only one of three that would not spend an output Go spends, which is a chain split anyone can build by negating one scalar. The normalization moved into the seam, where the rule now lives once for every chain, and `a_malleated_signature_recovers_the_same_key` pins it beside the recovery-id vector. The X-chain gives two refusals here and the seam gives one. A byte that is not a recovery id at all is a different complaint from a recovery attempted and failed, and the reference makes that distinction; `Option` cannot carry it. The seam keeps the curve's rule and the chain keeps its own wording. What the seam already closed, and is why it is worth having: masking the recovery byte with `v & 1` read 2 as 0 and recovered the address of a DIFFERENT signature — one XOR of one byte produced a transaction C++ accepted and Go and Rust refused. 2 and 3 are named and refused now, in one place. Verified: 725 vectors, four implementations, agreed on every compared field, exit 0. C++ P-chain 23/23, X-chain 19/19, Q-chain 13/13; the seam's own suite 369 checks in C++, 16 in Rust, Go green; Rust P-chain 438 and X-chain 310 over the seam.
228 lines
10 KiB
CMake
228 lines
10 KiB
CMake
# Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
|
|
# SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
#
|
|
# platformvm — the P-chain, in C++.
|
|
#
|
|
# Two reused trees, found where they actually sit, matched on a FILE rather than
|
|
# a directory name so a same-named sibling cannot be mistaken for one:
|
|
#
|
|
# luxcpp/blst the BLS12-381 backend a proof of possession is checked on
|
|
# lux-cpp/node the VM seam this chain is registered through, and the
|
|
# lux-cpp/consensus id type that seam is written in
|
|
#
|
|
# blst is REQUIRED. A validator set with no admission control is what makes this
|
|
# chain a public good, and admission is a real pairing — so a build that cannot
|
|
# perform one does not produce a P-chain at all. There is no mode in which the
|
|
# proof check is skipped.
|
|
|
|
cmake_minimum_required(VERSION 3.20)
|
|
project(lux_platformvm 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()
|
|
|
|
set(PLATFORMVM_SANITIZE "" CACHE STRING "Sanitizer for platformvm code only (e.g. address,undefined)")
|
|
|
|
# The wire this chain speaks comes from the published SDK, not from here.
|
|
include(${CMAKE_CURRENT_SOURCE_DIR}/../zap.cmake)
|
|
|
|
# The wire's offsets, readers and builders: schema/wire.zap in,
|
|
# include/lux/platformvm/gen out.
|
|
foreach(_s wire genesis warp warpmsg)
|
|
zap_schema(${_s}-schema
|
|
${CMAKE_CURRENT_SOURCE_DIR}/schema/${_s}.zap
|
|
${CMAKE_CURRENT_SOURCE_DIR}/include/lux/platformvm/gen)
|
|
endforeach()
|
|
|
|
# find_checkout(VAR MARKER candidate...) — first candidate holding MARKER wins.
|
|
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()
|
|
|
|
# node2 sits at ~/work/lux/node2; this file at chains/cpp/platformvm. The C++
|
|
# node and its consensus checkout live one level up from the lux checkout, under
|
|
# lux-cpp, and blst under luxcpp.
|
|
set(_up "${CMAKE_CURRENT_SOURCE_DIR}/../../..")
|
|
|
|
find_checkout(_luxcpp "blst/bindings/blst.h"
|
|
"${_up}/../../luxcpp" "${_up}/../luxcpp" "${_up}/.." "$ENV{HOME}/work/luxcpp")
|
|
find_checkout(_node "include/lux/node/vm.hpp"
|
|
"${_up}/../../lux-cpp/node" "${_up}/../lux-cpp/node" "${_up}/../node" "$ENV{HOME}/work/lux-cpp/node")
|
|
find_checkout(_consensus "include/lux/consensus/quorum_cert_engine.hpp"
|
|
"${_up}/../../lux-cpp/consensus" "${_up}/../../luxcpp/consensus" "${_up}/../lux-cpp/consensus"
|
|
"${_up}/../consensus" "$ENV{HOME}/work/lux-cpp/consensus")
|
|
|
|
set(LUXCPP_ROOT "${_luxcpp}" CACHE PATH "luxcpp checkout root (blst)")
|
|
set(NODE_DIR "${_node}" CACHE PATH "lux-cpp/node checkout (the VM seam)")
|
|
set(CONSENSUS_DIR "${_consensus}" CACHE PATH "lux-cpp/consensus checkout (the id type)")
|
|
set(BLST_DIR "${LUXCPP_ROOT}/blst" CACHE PATH "blst source tree")
|
|
|
|
# The first-party secp256k1 recovery and ripemd160 live in the same luxcpp
|
|
# checkout as blst. They are what makes the fx signature check real: an address
|
|
# is ripemd160(sha256(compressed recovered key)), so both are REQUIRED for the
|
|
# same reason blst is — a chain that cannot check who signed does not have a
|
|
# spending rule.
|
|
set(CRYPTO_DIR "${LUXCPP_ROOT}/crypto" CACHE PATH "luxcpp/crypto checkout (secp256k1, ripemd160)")
|
|
|
|
foreach(_p "${BLST_DIR}/bindings/blst.h" "${BLST_DIR}/src/server.c" "${BLST_DIR}/build/assembly.S")
|
|
if(NOT EXISTS "${_p}")
|
|
message(FATAL_ERROR "platformvm: blst is required and was not found: ${_p}\n"
|
|
"set -DLUXCPP_ROOT=/path/to/luxcpp")
|
|
endif()
|
|
endforeach()
|
|
|
|
# blst ISA selection, mirroring blst/build.sh: without -D__ADX__ every field
|
|
# multiplication takes the slower generic path on a host that has adx.
|
|
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()
|
|
|
|
# keccak comes along because the recovery's batch pipeline composes an EVM
|
|
# address from a recovered key; the P-chain never asks for that address, but the
|
|
# symbol is in the same translation unit.
|
|
foreach(_p "${CRYPTO_DIR}/secp256k1/cpp/ecrecover.cpp" "${CRYPTO_DIR}/ripemd160/cpp/ripemd160.cpp"
|
|
"${CRYPTO_DIR}/keccak/cpp/keccak.cpp" "${CRYPTO_DIR}/include/lux/crypto/secp256k1.h")
|
|
if(NOT EXISTS "${_p}")
|
|
message(FATAL_ERROR "platformvm: the first-party crypto is required and was not found: ${_p}\n"
|
|
"set -DCRYPTO_DIR=/path/to/luxcpp/crypto")
|
|
endif()
|
|
endforeach()
|
|
|
|
# core — the names, the store and the primitive seam this chain shares with
|
|
# every other one here. Core links gpu/cpp, so the hashes, keccak and signature
|
|
# recovery all arrive through the one interface a chain asks a primitive
|
|
# through; they used to be compiled here and again in xvm, and the P-chain
|
|
# carried a third SHA-256 of its own in a header. A primitive with three builds
|
|
# has three places to drift.
|
|
set(LUXCPP_CRYPTO_DIR "${CRYPTO_DIR}" CACHE PATH "luxcpp/crypto checkout")
|
|
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../core" "${CMAKE_CURRENT_BINARY_DIR}/core")
|
|
|
|
if(NOT TARGET blst)
|
|
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")
|
|
endif()
|
|
|
|
add_library(lux_platformvm STATIC
|
|
src/error.cpp
|
|
src/signer.cpp
|
|
src/components.cpp
|
|
src/txs_wire.cpp
|
|
src/txs.cpp
|
|
src/txs_parse.cpp
|
|
src/block.cpp
|
|
src/state.cpp
|
|
src/fx.cpp
|
|
src/flow.cpp
|
|
src/executor.cpp
|
|
src/vm.cpp
|
|
src/complexity.cpp
|
|
src/validators.cpp
|
|
src/warp.cpp
|
|
src/warpmsg.cpp
|
|
src/l1.cpp
|
|
src/genesis.cpp
|
|
src/staking.cpp
|
|
src/adopt.cpp
|
|
src/mempool.cpp
|
|
src/persist.cpp)
|
|
target_include_directories(lux_platformvm PUBLIC include)
|
|
target_link_libraries(lux_platformvm PUBLIC blst zap::zap lux_core)
|
|
# -Wpedantic is deliberately absent: the wide integer the reward curve needs is
|
|
# __int128, a compiler extension every target this ships on has.
|
|
target_compile_options(lux_platformvm PRIVATE -Wall -Wextra)
|
|
|
|
# The VM seam is header-only; a chain that registers through it needs both the
|
|
# node's include tree and the consensus id it is written in.
|
|
if(NODE_DIR AND CONSENSUS_DIR)
|
|
target_include_directories(lux_platformvm PUBLIC "${NODE_DIR}/include" "${CONSENSUS_DIR}/include")
|
|
target_compile_definitions(lux_platformvm PUBLIC LUX_PLATFORMVM_HAS_NODE_SEAM=1)
|
|
else()
|
|
message(FATAL_ERROR
|
|
"platformvm: the node VM seam was not found.\n"
|
|
" NODE_DIR=${NODE_DIR}\n CONSENSUS_DIR=${CONSENSUS_DIR}\n"
|
|
"set -DNODE_DIR=/path/to/lux-cpp/node -DCONSENSUS_DIR=/path/to/lux-cpp/consensus")
|
|
endif()
|
|
|
|
if(PLATFORMVM_SANITIZE)
|
|
target_compile_options(lux_platformvm PRIVATE -fsanitize=${PLATFORMVM_SANITIZE} -fno-omit-frame-pointer)
|
|
target_link_options(lux_platformvm PUBLIC -fsanitize=${PLATFORMVM_SANITIZE})
|
|
endif()
|
|
|
|
# The node's own validator-set commitment, compiled here so the test suite can
|
|
# check this chain's answer against the NODE's rather than against itself. Two
|
|
# implementations of one hash that only agree with themselves is exactly the
|
|
# failure the commitment exists to prevent.
|
|
if(NOT TARGET lux_node_validators AND EXISTS "${NODE_DIR}/src/validators.cpp"
|
|
AND EXISTS "${CRYPTO_DIR}/sha256/cpp/sha256.cpp")
|
|
# SHA-256 comes from the seam, not from a second compile of the same file:
|
|
# two objects for one hash in one link is a duplicate symbol at best and two
|
|
# answers at worst.
|
|
add_library(lux_node_validators STATIC "${NODE_DIR}/src/validators.cpp")
|
|
target_include_directories(lux_node_validators PUBLIC
|
|
"${NODE_DIR}/include" "${CONSENSUS_DIR}/include" "${CRYPTO_DIR}")
|
|
target_link_libraries(lux_node_validators PUBLIC lux_core)
|
|
target_compile_options(lux_node_validators PRIVATE -O2)
|
|
endif()
|
|
|
|
option(PLATFORMVM_BUILD_TESTS "Build the ported Go test suite" ON)
|
|
if(PLATFORMVM_BUILD_TESTS)
|
|
enable_testing()
|
|
add_library(pvm_test_main STATIC test/harness.cpp)
|
|
target_include_directories(pvm_test_main PUBLIC test include "${CRYPTO_DIR}/secp256k1/cpp" "${BLST_DIR}/bindings")
|
|
target_link_libraries(pvm_test_main PUBLIC lux_platformvm)
|
|
if(TARGET lux_node_validators)
|
|
target_link_libraries(pvm_test_main PUBLIC lux_node_validators)
|
|
target_compile_definitions(pvm_test_main PUBLIC LUX_PLATFORMVM_HAS_NODE_SET_ROOT=1)
|
|
endif()
|
|
|
|
foreach(_t ids reward components txs syntactic block state flow fee warp genesis staking adopt mempool executor l1 validators vm)
|
|
add_executable(pvm_${_t}_test test/${_t}_test.cpp)
|
|
target_link_libraries(pvm_${_t}_test PRIVATE pvm_test_main)
|
|
target_compile_options(pvm_${_t}_test PRIVATE -Wall -Wextra)
|
|
add_test(NAME pvm_${_t} COMMAND pvm_${_t}_test)
|
|
endforeach()
|
|
|
|
# The durability proof stands apart from the ported suite: it needs argv,
|
|
# because the process that writes the state is this same binary re-EXECed and
|
|
# then killed where it stands. See the source.
|
|
add_executable(pvm_durability_test test/durability_test.cpp)
|
|
target_link_libraries(pvm_durability_test PRIVATE lux_platformvm)
|
|
target_compile_options(pvm_durability_test PRIVATE -Wall -Wextra)
|
|
add_test(NAME pvm_durability COMMAND pvm_durability_test)
|
|
endif()
|
|
|
|
# The C++ P-chain's evaluator for the cross-language chain differential. It is
|
|
# not a test: it prints this chain's answer for every vector in
|
|
# conformance/corpus/vectors.tsv and says nothing about whether that answer is
|
|
# right. Deciding that is the runner's job, and it needs all three answers to
|
|
# do it. Built unconditionally — a differential that quietly lost one of its
|
|
# three voices would report agreement among whoever was left.
|
|
add_executable(pvm_conformance test/conformance.cpp)
|
|
target_link_libraries(pvm_conformance PRIVATE lux_platformvm)
|
|
target_include_directories(pvm_conformance PRIVATE include)
|
|
target_compile_options(pvm_conformance PRIVATE -Wall -Wextra)
|