gpu

package
v1.37.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: BSD-3-Clause Imports: 10 Imported by: 0

README

gpu — the one place a Lux chain asks for a primitive

A chain needs a hash and a signature check. This directory is where it asks for one, in all three languages, and where the choice between computing the answer and handing the question to an installed kernel library is made — once, in one place, with one rule.

The boundary

Kernels are private and no kernel source is here. They live in their own repository (lux-gpu/gpu, checked out on this machine at ~/work/luxcpp/gpu). What crosses the line into this public tree is a C ABI: four symbol names.

lux_gpu_create()             -> LuxGPU*
lux_gpu_destroy(LuxGPU*)
lux_gpu_backend_name(LuxGPU*) -> const char*
lux_gpu_keccak256_batch(LuxGPU*, const uint8_t* inputs, uint8_t* out,
                        const size_t* input_lens, size_t n) -> LuxError

Nothing is linked at build time. The library is dlopen'd, so a build machine that has never heard of it produces the same binary as one that has, and a binary that never finds it is a whole node.

The rule

The CPU backend is complete. Every primitive has a CPU body, always compiled, and it is the DEFINITION: the plugin does not get to have its own opinion. Two backends that disagree about a hash disagree about a block, so a plugin answer is either byte-identical to the CPU answer or a bug.

The plugin is a strict positive overlay. It may only ever be faster. When it is absent, when it declines a batch, or when it errors, the CPU answers — a device's bad day never propagates into consensus.

The knob

One environment variable, read once, at first use.

LUX_GPU
off CPU only. Nothing is opened.
on Plugin for batch work when a library is installed. Default, and it degrades to off on its own when nothing is installed.
verify Compute both and stop on the first byte that differs.

LUX_GPU_LIB names the library path when it is not on the loader's path.

LUX_GPU=verify is how "we believe they agree" becomes "we checked". It is what make gpu-differential sets.

What actually has two paths

operation CPU plugin
keccak256_batch yes, the definition yes — lux_gpu_keccak256_batch
merkle_root (RFC 6962 fold) yes yes, through the batch: a level of the tree is a batch of independent hashes, which is the only shape a device can help with
keccak256 (one input) yes no, deliberately. One hash is not a batch; a dispatch costs more than the answer
sha256 yes none exists. The plugin ABI's hash surface is keccak256, SHA3-256, SHAKE256, BLAKE3 and Poseidon2. SHA3-256 is not SHA-256
ripemd160 yes none exists
recover (secp256k1) yes present but not applicable. lux_gpu_ecrecover_batch answers a different question: it returns an Ethereum address, keccak(Q.x‖Q.y)[12:]. A Lux address is ripemd160(sha256(compressed Q)), and the recovered key never leaves that call, so its answer cannot be turned into this one's
BLS proof of possession yes (blst / blst crate) none exists. The ABI has a raw op_bls12_381_pairing, not a verify that knows the BLS_POP_… domain tag; composing one here would mean writing hash-to-curve twice

No operation diverges between the CPU and the plugin — "What was measured" below says exactly what that claim rests on, and what it does not. A cross-LANGUAGE divergence is a different thing, and consolidating the primitives turned one up; it is the last section.

The three homes

gpu/gpu.go, plugin_cgo.go, plugin_nocgo.go   Go     package github.com/luxfi/node2/gpu
gpu/rust/                                    Rust   crate lux-gpu
gpu/cpp/                                     C++    CMake target lux_gpu

Each is the same shape: a cpu half that is always there, a plugin half behind dlopen, one LUX_GPU policy, and one Backend() that names which is answering. In Go the plugin half is //go:build cgo — a CGO_ENABLED=0 build has no way to open a shared library, so it never has one. That is not a missing feature; it is the build make luxd RUNTIME=go produces, and it is complete.

The C++ target also compiles the reused luxcpp/crypto bodies its CPU half delegates to. Those used to be compiled twice, once into xvm and once into platformvm, and the P-chain carried a third SHA-256 of its own in a header — three builds of one hash in one tree, and three places for the chains to drift.

Who goes through it

Every chain, for every primitive it is defined over.

chains/rust/xvm/src/hash.rs              names the seam's primitives, defines none
chains/rust/xvm/src/fx/secp256k1.rs      recover_public_key
chains/rust/platformvm/src/ids.rs        hash256
chains/rust/platformvm/src/sign.rs       address, recover
chains/cpp/xvm/src/id.cpp                sha256, pubkey_to_address
chains/cpp/xvm/src/root.cpp              keccak, the tags, the fold
chains/cpp/xvm/src/fx.cpp                recover_address
chains/cpp/platformvm/include/.../sha256.hpp   sha256
chains/cpp/platformvm/src/fx.cpp         address_of_compressed_key, recover_address

Signing is deliberately outside: a chain verifies signatures and never makes them, so k256's SigningKey stays in the chains' test and tooling halves. The seam exists to make VERIFICATION agree across backends.

What was measured

Run make gpu-differential. It builds all three seams and runs each one's tests twice — once with no library visible, once with LUX_GPU=verify against an installed one — and every input is asked of both backends and compared byte for byte.

On the machine this was written on:

  • plugin:cpu, not plugin:cuda. lux_backend_available reports 1 for the plugin's own SIMD CPU backend and 0 for Metal, CUDA and Dawn. The only device backend built here, libluxgpu_backend_webgpu.so, does not load: it needs libwgpu_native.so, which is not on this machine. So the differential proves the dispatch and the ABI agree with the CPU definition, on every batch shape tried; it does not exercise a device kernel, because there is no device backend installed to exercise. Backend() reports the plugin's own name for exactly this reason — a host that says plugin:cpu is telling you it found the library and no device.
  • Every seam's test carries a negative control on the verify comparison itself: it is handed a deliberately flipped byte and must refuse. A comparison nothing has ever seen fail is a comparison nobody has checked can fail.

A one-byte fork, closed by consolidating

Recovery ids 2 and 3. Go accepts all four (luxfi/crypto/secp256k1 checkSignature refuses only v >= 4) and recovers correctly for each; Rust's k256 does the same. Ids 2 and 3 mean R.x = r + n, and the first-party C++ curve refuses r >= n at parse time, so it has no path to them.

Before this seam the two C++ chains disagreed about that, with each other and with Go:

  • chains/cpp/xvm accepted them, by masking v & 1. That turns v = 2 into v = 0 and recovers the key of a different id — so take any valid signature and move its recovery byte from 0 to 2, and a C++ node accepted a transaction Go and Rust both refuse. A fork anyone could build in one byte.
  • chains/cpp/platformvm refused them, which matched Go for every such input.

Both refuse now, and that closes the exploitable direction. Measured on the KAT in gpu/cpp/test/gpu_test.cpp and gpu/rust/tests/differential.rs — the same signature asked of both seams:

v Go / Rust C++ before C++ now
0 the signer's key the signer's key the signer's key
1 the other candidate the other candidate the other candidate
2, 3 no key (r + n is not on the curve) the id-0/1 key no key
≥ 4 no key no key no key

The residue is one case: a deliberately built r where r + n is a valid x coordinate. There Go recovers a key and C++ still refuses — C++ refusing what Go accepts, never the reverse. It belongs to whoever owns the P/X differential, and closing it properly means teaching the C++ curve the r + n case.

Building the kernel library

make gpu builds lux-gpu/gpu's luxgpu_core_static target in place. It is not needed to build or run a node; it is needed to have something for LUX_GPU_LIB to point at.

Documentation

Overview

Package gpu is the one place a Lux chain asks for a primitive.

The boundary

Kernels are private and live in their own repository. This package contains no kernel source and never will: what crosses the line is a C ABI — four symbol names — opened at run time if the library happens to be installed.

The rule

The CPU backend is COMPLETE. A node with nothing installed computes every primitive itself and is a full node, not a degraded one. The plugin is a strict positive overlay: it may only ever be faster, never different. Two backends that disagree about a hash disagree about a block, so every plugin answer is either byte-identical to the CPU answer or a bug.

The knob

LUX_GPU picks the policy, once, at first use:

off      CPU only; nothing is opened.
on       plugin for batch work when a library is installed. The default,
         and it degrades to off on its own when nothing is installed.
verify   compute BOTH and stop on the first byte that differs.

LUX_GPU_LIB names the library path when it is not on the loader's path.

What actually has two paths

  • Keccak256Batch — CPU and plugin (lux_gpu_keccak256_batch), and so MerkleRoot, which is a batch of keccaks per level.
  • Sha256, Ripemd160 — CPU only. The plugin ABI has no such op: its hash surface is keccak256, SHA3-256, SHAKE256, BLAKE3, Poseidon2, and SHA3-256 is not SHA-256.
  • Recover — CPU only. The plugin's lux_gpu_ecrecover_batch answers a different question: it returns an Ethereum address, keccak(Q.x‖Q.y)[12:], where a Lux address is ripemd160(sha256(compressed Q)). The recovered key never leaves that call, so its answer cannot be turned into this one's.

A note on CGO

A CGO_ENABLED=0 build has no way to open a shared library, so it never has a plugin. That is the point: it is still a whole node.

Index

Constants

View Source
const (
	LeafTag byte = 0x00
	NodeTag byte = 0x01
)

The RFC 6962 domain tags. leaf(d) = keccak(0x00 ‖ d), node(L,R) = keccak(0x01 ‖ L ‖ R). They separate the domains, so a leaf preimage can never be read as a node's.

View Source
const SignatureLen = 65

SignatureLen is a secp256k1 signature: 64 bytes of (r,s) plus the recovery byte.

Variables

This section is empty.

Functions

func Backend

func Backend() string

Backend names which backend is answering: "cpu", or "plugin:<name>" naming the device the installed library selected for itself.

This is a diagnostic, and it is the honest one: a host with the library installed but no device driver reports "plugin:cpu", not "plugin:cuda".

func Recover

func Recover(h Hash256, sig [SignatureLen]byte) ([]byte, bool)

Recover returns the compressed public key that signed h, or false.

The 65 bytes are r ‖ s ‖ recovery_id, and the answer is the 33-byte compressed SEC1 encoding — the shape a Lux address is derived from, which is not the shape the EVM ecrecover precompile returns.

func Same

func Same(got, want []Hash256) bool

Same is the comparison LUX_GPU=verify makes before it stops. Exported so a test can hand it a wrong answer and watch it say so — a comparison nothing has ever seen fail is a comparison nobody has checked can fail.

Types

type Hash160

type Hash160 = [20]byte

Hash160 is a 160-bit digest — the width of an address.

func PubkeyToAddress

func PubkeyToAddress(key []byte) Hash160

PubkeyToAddress is the address of a public key: RIPEMD-160 of its SHA-256.

func Ripemd160

func Ripemd160(b []byte) Hash160

Ripemd160 is the second half of an address.

type Hash256

type Hash256 = [32]byte

Hash256 is a 256-bit digest.

func CPUKeccak256

func CPUKeccak256(parts ...[]byte) Hash256

CPUKeccak256 is Keccak256 with the plugin never consulted.

func CPUKeccak256Batch

func CPUKeccak256Batch(inputs [][]byte) []Hash256

CPUKeccak256Batch is Keccak256Batch with the plugin never consulted.

func EmptyRoot

func EmptyRoot() Hash256

EmptyRoot is keccak256("") — the root of nothing.

func Keccak256

func Keccak256(parts ...[]byte) Hash256

Keccak256 is Ethereum Keccak-256 — the 0x01 pad, NOT FIPS-202 SHA3 — over the concatenation of the parts.

One hash is not a batch: there is nothing to parallelise and a dispatch costs more than the answer. This is the CPU backend, always.

func Keccak256Batch

func Keccak256Batch(inputs [][]byte) []Hash256

Keccak256Batch is one Keccak-256 per input — the shape that has somewhere else to go.

func LeafHash

func LeafHash(d Hash256) Hash256

LeafHash is keccak256(0x00 ‖ d) — a tagged leaf.

func MerkleRoot

func MerkleRoot(leaves []Hash256) Hash256

MerkleRoot folds a dense, already-compacted, ascending leaf-digest list. A lone right node is promoted unchanged rather than paired with itself, which is what keeps two different leaf sets from folding to one root.

A level is a batch: every node on it is independent of every other, which is the only reason a device can help at all. The preimages are built here and hashed together, so the whole level makes one dispatch.

func NodeHash

func NodeHash(l, r Hash256) Hash256

NodeHash is keccak256(0x01 ‖ L ‖ R) — a tagged internal node.

func Sha256

func Sha256(b []byte) Hash256

Sha256 names things: a transaction id is the hash of its signed bytes and a block id the hash of its block bytes.

type Policy

type Policy int

Policy is what LUX_GPU selected.

const (
	// Off means CPU only. Nothing is opened.
	Off Policy = iota
	// On means plugin when installed, CPU otherwise.
	On
	// Verify means both, compared, every call.
	Verify
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL