Sign now asks the key for its ML-DSA form instead of rebuilding one per
call. A key that arrives as bytes parses on first use and reuses that
key for every signature after; a freshly generated key already holds its
parsed form. A secret that will not parse fails closed on every call.
Takes crypto v1.20.5, where a private key copies the secret it is given
and hands back a copy, so the bytes a caller persists out of a generated
key stay readable for as long as the caller holds them.
Co-authored-by: Hanzo Dev <[email protected]>
The accelerated batch path re-implemented two things the sequential path already
did, and got both wrong.
1. It re-listed the block-correspondence fields itself and omitted ReceiptsRoot,
which verifyStampSync checks. A stamp carrying a forged receipts root was
therefore accepted whenever accel was available and the batch cleared
DilithiumBatchThreshold, and refused otherwise — acceptance decided by host
hardware. Both paths now call stampMatchesBlock, the one definition.
2. The hybrid post-quantum leg read signDataSlice[0] for every stamp:
signDataSlice is indexed by BATCH POSITION while indices maps position to stamp
index, and the loop discarded the position. Every hybrid stamp's SLH-DSA
signature was verified against the FIRST batch entry's signing bytes. That both
false-rejects honest stamps and, since a batch submitter chooses what entry zero
contains, accepts a signature made over data the stamp does not commit to.
Proven against the pre-fix logic: a correctly signed hybrid stamp at batch
position 1 failed; a stamp carrying a signature over another stamp's bytes passed;
a forged ReceiptsRoot passed the batch field list.
Tests: TestHybridBatchVerifiesEachStampAgainstItsOwnData,
TestHybridBatchRejectsSignatureOverAnotherStampsData,
TestStampMatchesBlockCoversReceiptsRoot.
Co-authored-by: Hanzo Dev <[email protected]>
AddCoronaSignature and AddBLSSignature appended caller-supplied signatures with no
signer check, and TryFinalize compares len(signatures) against q.threshold. One
peer resending the same signature therefore drove the count to the threshold on
its own. Proven: three calls carrying one validator's signature left three entries
in the list.
The Corona leg is the sharp end: TryFinalize finalizes it on count alone and
verifies none of the signatures, unlike the BLS leg which aggregates and verifies.
So a single peer satisfied the post-quantum half of quantum finality by itself.
Insertion for both legs now goes through addBLS/addCorona, which record one
signature per ValidatorID, and SignBlock's own contribution goes through the same
door instead of appending directly.
Tests: TestOneValidatorCannotReachCoronaThreshold,
TestDistinctValidatorsReachCoronaThreshold, TestOneValidatorCannotReachBLSThreshold.
Co-authored-by: Hanzo Dev <[email protected]>
Completes the "one and only one way" wire migration: every VM in this repo now
serializes blocks and txs through luxfi/zap struct-is-wire at fixed field
offsets — no encoding/json, no hand-rolled big-endian, no cursor codec.
- aivm: Block + AIVertex + nested CIntent native-encoded. Fixes a latent
data-loss bug — AIVertex had unexported fields, so encoding/json silently
marshalled it as {}; the native encoder round-trips every field.
- keyvm: Transaction (signing preimage = SigningBytes(), excludes Auth/Sig;
full wire = signing || sig object, so the signed bytes stay a genuine prefix
and authenticate() re-derives a byte-identical preimage) + Block.
- quantumvm: Block + BaseTransaction. Block wire is the quantum-signature
preimage (deterministic fixed offsets, ordered tx list).
- dexvm: block envelope (txs were already ZAP). Preserves UnixNano timestamps,
deterministic tx order, and content-addressed carried fills.
Hardening from the adversarial crypto review (must-holds all verified: no
keyvm signature malleability/replay, no aivm CIntent id-forgery/escrow tamper,
no dexvm ordering/fill tamper, all parsers fail closed):
- keyvm (M1): make the wire strictly canonical. zap follows the root offset
and ignores unreferenced padding inside a message's declared size, so a twin
buffer could decode to identical fields with a different hash (id-malleability).
ParseTransaction now binds the id to the re-serialized (canonical) form and
rejects any non-canonical input at the door — exactly one byte-string
authenticates per logical tx. Regression test: the padded twin is rejected.
- quantumvm (M2): derive block + tx ids as sha256(Bytes()), matching the other
three VMs. The prior ids.ToID(parent||height) over 40 bytes (and ids.ToID over
the >=32-byte tx wire) silently yielded ids.Empty for every block/tx,
collapsing the block store and tx pool to a single slot. The block id is no
longer stored in the wire, so the invariant id == sha256(Bytes()) holds
unconditionally. Regression tests: ids are non-Empty and collision-distinct.
Not in scope (tracked separately, fail-closed today): aivm RewardPerOperator is
escrowed but not bound by ComputeIntentID — a pre-existing design gap that needs
a coordinated A-side + C-side intent-id preimage bump before any non-fail-closed
CCommitVerifier ships.
Builds + tests green under the canonical CGO=0 -mod=readonly recipe; go.mod/go.sum
are the go mod tidy canonical set (indirect deps resolved forward, all within
major). Cryptographer verdict: SAFE TO PUBLISH.
Co-authored-by: Hanzo Dev <[email protected]>
Flip the Q-Chain fee gate from FlatPolicy (user-fee metering) to the
committee-only fee.NoUserTxPolicy sentinel. Finality-cert inclusion is
a validator obligation paid via P-Chain reward distribution; a fee
market on Q would make finality hostage to blockspace pricing — the
exact failure mode LP-0130 §6 eliminates. Consensus-internal cert
aggregation reaches txPool.AddTransaction directly and is unaffected.
Tests updated: policy is the NoUserTxPolicy sentinel and IssueTx
refuses every user tx (zero-fee, floor-fee, and 1000x floor) with
ErrChainAcceptsNoUserTxs. go test ./quantumvm/ passes; build + vet
clean.
Closes the top divergence from the 2026-07-03 LP-vs-code audit.
Co-authored-by: Hanzo Dev <[email protected]>
InitializeDualThreshold called consensus quasar.GenerateDualKeys, a
trusted-dealer keygen (one process mints+holds every validator's BLS+Corona
share, defeating threshold security). Consensus relocated that helper to
test-only (keygen_testsupport_test.go) as corona-genesis hardening, so
production chains no longer compiles against it.
The method was dead: zero callers anywhere, and it already discarded the
generated signer (_ = signer, used only for two log lines) and inited fake
v0/v1/v2 validator IDs. The live Quasar bridge signs through the consensus
core with per-validator keys (ML-DSA fallback on the VM paths); real
validators join via AddValidator. Removing it is a no-op at runtime and
restores the build against consensus v1.25.35. Any future per-epoch group
key must come from a dealerless DKG, never a trusted dealer.
quantumvm/vm.go: remove GetEngine() so the chain manager routes Q-Chain through
the linear consensuschain cert path (the same deterministic 2/3-stake BFT
certificate finality as C/D/P), not the undriven DAG engine. quantumvm already
implements the full chain.ChainVM/BlockBuilder surface; dropping GetEngine is the
whole change. Adds the _ chain.ChainVM + _ consensuschain.BlockBuilder asserts.
dexvm/factory.go: correct the stale 'Quasar consensus' comment — dexvm finalizes
via the linear cert path (Photon->Wave->Focus + 2/3-stake cert), same as C-Chain.
OSS public chains module must not reference the private GPU plugin
implementation by repo name or filesystem path. Two leak types fixed:
1. Hard compile-time dependency in quantumvm:
`#cgo CFLAGS: -I${SRCDIR}/../../../lux-private/gpu-kernels/include`
Vendor the public ABI v14 header (backend_plugin.h + crypto.h, 1475
LOC total) into chains/internal/luxgpu/include/lux/gpu/ and point
CFLAGS at the in-tree path. Plugin DSO is still dlopen'd at runtime
— only the public C ABI surface is vendored.
2. Soft leakage — `LUX_PRIVATE_GPU_KERNELS_DIR` env vocabulary, dev-tree
probe paths to ~/work/lux-private/gpu-kernels/build/..., prose
mentions of "lux-private/gpu-kernels" in package comments.
All replaced with `LUX_GPU_PLUGIN_DIR` (universal env) and generic
"GPU plugin" prose.
Bridges affected: aivm, bridgevm, dexvm, quantumvm, thresholdvm.
All 139 tests PASS post-scrub under both build tags. No functional
behavior change — only path / env / prose.
Adds four Go files to chains/quantumvm so the Q-Chain VM can call into
the lux-private/gpu-kernels plugin's batched ML-DSA verify, ML-DSA sign,
and SLH-DSA verify slots without a build-time pkg-config dependency:
* backend.go cross-build types: Backend enum, GPUBackend
interface, MLDSAMode + MLDSA65* size constants,
SLHDSAVariant enum + per-variant pk/sig widths
(FIPS 204 / FIPS 205), ErrGPUNotAvailable,
AutoBackend(). Build-tag-free so vm.go can
reference the surface regardless of CGO_ENABLED.
* quantumvm_gpu.go cgo dlopen probe at init() in order
(cgo) cuda -> hip -> metal -> vulkan -> webgpu,
resolving lux_gpu_backend_init and verifying
the ABI v14 cookies (abi_version + vtbl_size)
plus the load-bearing op_mldsa_verify_batch
slot before opening a LuxBackendContext. Three
vtbl-slot wrappers dispatch through C
trampolines into op_mldsa_verify_batch,
op_mldsa_sign_batch, and the SHAKE-128f /
SHAKE-192f variants of op_slhdsa_verify_batch.
* quantumvm_gpu_nocgo.go symmetric noGPUBackend stub returning
ErrGPUNotAvailable on every method so the
package surface is identical under !cgo.
* quantumvm_gpu_test.go round-trip test: dlopens the configured
plugin (LUX_GPU_PLUGIN_DIR override or default
loader path), generates an ML-DSA-65 keypair +
signs a 64-byte digest via circl, then submits
the triple through MLDSAVerifyBatch and asserts
results[0] == true. Cleanly SKIPs when no
plugin loaded or when the backend returns
NOT_SUPPORTED for op_mldsa_verify_batch (e.g.
plugins built without LUX_GPU_LINK_CRYPTO).
The probe is header-only: cgo CFLAGS points at
${SRCDIR}/../../../lux-private/gpu-kernels/include for the v14 vtbl
layout, but the .dylib / .so is dlopen'd at runtime - no pkg-config,
no static link, no luxcpp/install dependency. Existing quantumvm
vm.go, quantum/signer.go, and quantumvm/quasar.go are untouched;
consumers opt in explicitly when ready. Both 'go build ./quantumvm/...'
and 'CGO_ENABLED=1 go build ./quantumvm/...' are green.
Test output with LUX_GPU_PLUGIN_DIR pointing at a freshly-built
libluxgpu_backend_vulkan.dylib:
--- PASS: TestAutoBackend_Probes (0.00s) [backend=vulkan]
--- PASS: TestMLDSAVerifyBatch_RoundTrip (5.56s) [backend=vulkan,
op_mldsa_verify_batch honest verify -> true]
--- PASS: TestMLDSAVerifyBatch_LengthMismatch (0.00s)
--- PASS: TestMLDSAVerifyBatch_NonMLDSA65Skips (0.00s)
Add Fee() to the Transaction interface and introduce VM.IssueTx as
the canonical user-tx admission point. The FeePolicy gate refuses
zero-fee tx before they reach the parallel quantum verification
pool. Consensus-internal callers bypass via txPool.AddTransaction.
Detailed invention disclosures are sensitive pre-filing material. Moved to
lux-private/patents to preserve international filing rights (EU/JP/KR/CN
absolute-novelty rule). Cross-ref: github.com/lux-private/patents/bundles/
VM consolidation (one module, one place only):
- Removed per-VM go.mod/go.sum/VERSION from {aivm,bridgevm,dexvm,evm,
graphvm,identityvm,keyvm,oraclevm,quantumvm,relayvm,thresholdvm,zkvm}
- All VMs now compile under github.com/luxfi/chains root module
- aivm/, dexvm/, identityvm/, keyvm/, oraclevm/ factory.go + vm.go updated
for AccelHost wiring
cevm Go-side (evm/cevm/):
- cevm.go, cevm_cgo.go, cevm_nocgo.go: ExecuteBlockV4(backend, threads,
txs, ctx, state) with StateAccount snapshot
- cevm_secp256k1.go: BatchRecoverSenders Metal LDFLAGS
- cevm_secp256k1_metal_anchor_darwin.{c,go}: anchor to keep linker from
dropping libsecp256k1_metal.a
- cevm_secp256k1_nocgo.go: stub for non-cgo builds
- plugin.go, fetch-luxcpp.sh, luxcpp-SHA256SUMS, LUXCPP_VERSION:
pinned luxcpp consumption
- evm/cevm/parallel/parallel.go: BlockExecutor adapter calls
cevm.ExecuteBlockV4 with state snapshot built from txs+statedb
Quantum/ZK witness:
- quantumvm/quasar_witness.go, zkvm/zwitness.go
go.mod: removed dead `replace github.com/ethereum/go-ethereum => luxfi/geth`
directive (caused dual-path conflict after math semver bump; no remaining
ethereum/go-ethereum imports in source). go mod tidy clean.
Tests: TestBatchRecoverSenders PASS through Metal anchor (cevm package).
- evm: bump precompile to v0.5.12 + fhe to v1.8.0 + gpu to v0.31.1
(gpu v0.30.0 had unresolved C.mlx_* CGO symbols)
- aivm, bridgevm, quantumvm: go mod tidy
- thresholdvm, zkvm: bump genproto to v0.0.0-20260217215200-42d3e9bedb6d
to resolve ambiguous googleapis/api/annotations import
All 12 chains now build and tests pass with GOWORK=off.
Refs #248
- dexvm.VMID: [32]byte → ids.ID (consistent with other 10 VMs)
- quantumvm RPC service: "qvm" → "quantumvm" (matches package + dir)
Removes last vestiges of the old short-name convention.
VMs now live in ONE place: chains/<name>/ owns the full implementation.
Previously split across node/vms/<name>/ (impl) + chains/<name>/main.go
(plugin wrapper). Now consolidated.
New layout:
chains/<name>vm/*.go — package <name>vm (VM code)
chains/<name>vm/cmd/plugin/main.go — package main (plugin binary)
chains/<name>vm/go.mod — module github.com/luxfi/chains/<name>vm
Moved 11 VMs: aivm, bridgevm, dexvm, graphvm, identityvm, keyvm,
oraclevm, quantumvm, relayvm, thresholdvm, zkvm.
External callers now import github.com/luxfi/chains/<name> directly.
Each VM builds as a standalone binary that the Lux node loads via
--plugin-dir. Node no longer bakes optional VMs in-process.
VMs: evm, dexvm, aivm, bridgevm, graphvm, identityvm, keyvm,
oraclevm, quantumvm, relayvm, servicenodevm, teleportvm,
thresholdvm, zkvm.
All 13 node-dependent VMs compile and report version correctly.
EVM requires CGo (luxcpp) — builds in CI only.