Files
zeekay 7c2aed1f93 One place to ask for a primitive, and the answer three languages agree on
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.
2026-09-06 18:56:36 -07:00

173 lines
7.6 KiB
CMake

# Copyright (C) 2026, Lux Industries Inc. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause-Eco
#
# xvm — the Lux X-Chain (the UTXO DAG and asset transfers) in C++, behind the
# node's VM seam.
#
# Layering, each layer below REUSED unmodified:
#
# luxcpp/crypto sha256, ripemd160, keccak256, secp256k1 (recover+sign)
# lux-cpp/consensus the Id type the seam speaks (header-only here)
# lux-cpp/node include/lux/node/vm.hpp — the VM seam itself
# zap-proto/cpp the ZAP protocol SDK (published, pinned in zap.cmake)
# ─────────────────────────── NEW in xvm ────────────────────────────
# wire the (TypeKind, ShapeKind) fx envelopes
# fx the fx primitives and the three fx families
# txs the five transactions and the UTXO vocabulary
# state the occupied set, and a diff over it
# root the execution root a validator signs
# executor syntax → semantics → apply
# block the block
# vm the seam implementation
#
# Reused, NOT vendored. The checkouts are found where they actually sit;
# -DLUXCPP_CRYPTO_DIR / -DNODE_DIR / -DCONSENSUS_DIR override.
cmake_minimum_required(VERSION 3.20)
project(xvm LANGUAGES C CXX)
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(XVM_SANITIZE "" CACHE STRING "Sanitizer for xvm's own code (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 accessors: schema/wire.zap in, include/lux/xvm/gen out.
zap_schema(wire-schema
${CMAKE_CURRENT_SOURCE_DIR}/schema/wire.zap
${CMAKE_CURRENT_SOURCE_DIR}/include/lux/xvm/gen)
# find_checkout(VAR MARKER candidate...) — first candidate holding MARKER wins.
# The marker is a FILE, not a directory name, because names are ambiguous here:
# `consensus` beside node in the lux checkout is the GO implementation, and only
# the header tells the two apart.
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()
find_checkout(_crypto "include/lux/crypto/secp256k1.h"
"${LUXCPP_CRYPTO_DIR}"
"$ENV{HOME}/work/luxcpp/crypto"
"${CMAKE_SOURCE_DIR}/../../../../luxcpp/crypto")
if(NOT _crypto)
message(FATAL_ERROR
"luxcpp/crypto not found (looked for include/lux/crypto/secp256k1.h). "
"Pass -DLUXCPP_CRYPTO_DIR=/path/to/luxcpp/crypto.")
endif()
message(STATUS "xvm: reusing crypto at ${_crypto}")
find_checkout(_node "include/lux/node/vm.hpp"
"${NODE_DIR}"
"$ENV{HOME}/work/lux-cpp/node"
"${CMAKE_SOURCE_DIR}/../../../../lux-cpp/node")
find_checkout(_consensus "include/lux/consensus/quorum_cert_engine.hpp"
"${CONSENSUS_DIR}"
"$ENV{HOME}/work/lux-cpp/consensus"
"${CMAKE_SOURCE_DIR}/../../../../lux-cpp/consensus")
# The seam is the point of the port, so its absence is a hard error rather than
# a quiet feature-off: a chain built against no seam is a library nothing runs.
if(NOT _node OR NOT _consensus)
message(FATAL_ERROR
"the VM seam was not found — node=${_node} consensus=${_consensus}. "
"Pass -DNODE_DIR=/path/to/lux-cpp/node -DCONSENSUS_DIR=/path/to/lux-cpp/consensus.")
endif()
message(STATUS "xvm: VM seam at ${_node}, Id from ${_consensus}")
# ── core: the names, the store and the primitive seam, shared with every other
# chain 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 platformvm; a primitive with two builds has two
# places to drift.
set(LUXCPP_CRYPTO_DIR "${_crypto}" CACHE PATH "luxcpp/crypto checkout")
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../core ${CMAKE_CURRENT_BINARY_DIR}/core)
# ── xvm itself ───────────────────────────────────────────────────────────────
add_library(xvm STATIC
src/address.cpp
src/wire.cpp
src/fx.cpp
src/txs.cpp
src/state.cpp
src/root.cpp
src/executor.cpp
src/block.cpp
src/genesis.cpp
src/mempool.cpp
src/vm.cpp
)
target_include_directories(xvm PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${_node}/include
${_consensus}/include
)
target_link_libraries(xvm PUBLIC zap::zap lux_core)
target_compile_options(xvm PRIVATE -Wall -Wextra)
# ── tests ────────────────────────────────────────────────────────────────────
# wire the bytes, against the Go X-Chain's own (golden.hpp)
# root the execution root, against the cross-language KAT
# projection chain state -> leaves, and the fold at every edge
# txs the tx vocabulary: verify, sort, round-trip
# fx the three fx families' spend and operation gates
# syntactic every syntactic rejection, ported case for case
# semantic the chain's own agreement, incl. import/export
# executor what applying a tx does to the UTXO set
# address bech32, against Go's own strings
# state the diff, and what acceptance means
# block block bytes, ids, and parsing
# mempool the admission gate, the pool, and the bloom filter
# vm build → verify → accept → REJECT over the node's seam
enable_testing()
set(XVM_TESTS wire root projection syntactic txs fx executor address state block genesis
mempool durability semantic vm)
foreach(_t ${XVM_TESTS})
add_executable(${_t}_test test/${_t}_test.cpp)
target_link_libraries(${_t}_test PRIVATE xvm)
target_include_directories(${_t}_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/test)
target_compile_options(${_t}_test PRIVATE -Wall -Wextra)
add_test(NAME ${_t} COMMAND ${_t}_test)
endforeach()
# The C++ X-chain's evaluator for the cross-language chain differential. It is
# not a test: it prints this chain's answer for every X 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(xvm_conformance test/conformance.cpp)
target_link_libraries(xvm_conformance PRIVATE xvm)
target_compile_options(xvm_conformance PRIVATE -Wall -Wextra)
# Everything that links the sanitized library is sanitized, the evaluator
# included — it is declared above this block for exactly that reason. Left out,
# it failed to LINK under -DXVM_SANITIZE while ctest still reported fourteen
# passes, so the differential was the one voice a sanitized run silently lost.
if(XVM_SANITIZE)
set(_san -fsanitize=${XVM_SANITIZE} -fno-omit-frame-pointer)
set(_targets xvm xvm_conformance)
foreach(_t ${XVM_TESTS})
list(APPEND _targets ${_t}_test)
endforeach()
foreach(_t ${_targets})
target_compile_options(${_t} PRIVATE ${_san})
target_link_options(${_t} PRIVATE ${_san})
endforeach()
endif()