batchVerifyBlockSignatures had two answers to "is this signature valid", chosen
by accel.Available():
- the CPU loop verified against blockHash[:] (accel_verify.go:69) while the GPU
layout verified against sha256(blockHash[:]) (accel_verify.go:96,107). Same
signature set, two different signed messages, so a node with an accelerator
and a node without reached opposite verdicts on the same block —
bridgevm/block.go:219 turns validSignatures into accept or reject, so this is
an accept/reject split decided by host hardware.
- a signature shorter than 64 bytes, or a public key that would not marshal,
was silently zero-filled (`if len >= sigSize`, `if mErr != nil { continue }`)
while the entry kept its row in the result vector, and validCount was taken
straight from the kernel's output for that row. The CPU path rejects those
entries outright via deserializeSignature. Correctness of the count therefore
rested on an out-of-tree kernel refusing degenerate input, and one
spuriously-true row is enough to admit a block.
The predicate is now verifyBlockSig, and the CPU loop is its only evaluator.
packBlockSigBatch lays out the accelerator's rows over the same message bytes and
returns an error for any row it cannot represent exactly, which sends the batch to
that one authority instead of guessing.
Tests: TestBatchSignsTheBlockHashItself, TestBatchRefusesRowsItCannotRepresent,
TestVerifyBlockSigIsTheOneAuthority, TestBatchVerifyCountsNoSignatureWithoutConfig.
Co-authored-by: Hanzo Dev <[email protected]>
BuildBlock collected the block's bridge requests by ranging vm.pendingBridges, a
map. Go randomises map iteration, and that iteration decided two things that must
be functions of the pending set:
- the ORDER of BridgeRequests, which is hashed into the block id
(block.go computeID writes each req.ID in slice order) and serialised to the
wire. The same pending set therefore produced a different block on every
build. The sibling map in the same wire format is already sorted by NodeID
(wire.go:140) — BridgeRequests was the one that was missed.
- WHICH requests the block carries, because the 100-request cap broke out of
the range. With more than 100 ready, the included subset was a random draw,
so a request could be passed over indefinitely for no reason.
Proven: two builds over one 24-request pending set produced different orders, and
the capped build over 140 requests carried a random subset.
Candidates are now sorted by request id and the cap takes a prefix of that order.
Tests: TestBuildBlockOrdersRequestsDeterministically,
TestBuildBlockCapIsADeterministicPrefix.
Co-authored-by: Hanzo Dev <[email protected]>
Completes the bridge cross-chain release leg the recon flagged as missing.
- internal/bridgeattest: the ONE canonical definition of the bridge-transfer
attestation seam between B (bridgevm) and M (mpcvm). M threshold-signs a
domain-bound bridge-transfer digest; B and every EVM gateway holding the group
key's address verifies before mint/release. Domain-bound + replay-safe; test
proves a signature over one transfer verifies for THAT transfer and no other.
- evmclient.go: the concrete EVM ChainClient B instantiates per external EVM
(Zoo 200201, Lux testnet C 96368) — chainClients was never populated because
the interface had SendTransaction but no concrete EVM client; B could not
broadcast a release. Now wired via evmByChainID in factory.
- release.go: on a confirmed lock/burn in an accepted block, B builds the
destination release call, obtains + verifies M's threshold attestation, and
broadcasts to the destination gateway. Both directions (Zoo<->Lux).
- attest.go: injects the two runtime deps (Warp->M, KMS relayer key) as clean
interface boundaries rather than hard-wiring transports.
Co-authored-by: Hanzo Dev <[email protected]>
Block + BridgeRequest get native zap Marshal/Parse (offset objects; requests
as packed length-prefixed wire objects; MPC sig map as sorted nodeID blob +
length-prefixed sig blob). Genesis stays JSON at the config boundary (vm.go).
Deleted bridgevm/codec.go (linearcodec via node/vms/pcodecs). Parse rejects
trailing bytes (canonical). No pcodecs, no reflection, no codec registry.
Build + tests green. 1 of 3 chains VMs (zkvm/mpcvm remain) that block deleting
the node's pcodecs package.
Co-authored-by: Hanzo Dev <[email protected]>
B-Chain does NOT do threshold custody keygen/signing. Custody is
M-Chain's job: dealerless CGGMP21/FROST distributed keygen via
luxfi/threshold (cmp.Keygen/frost.Keygen — multi-round protocols over a
message router; no trusted dealer, the group secret is never assembled
by any validator). B-Chain requests keygen/reshare/sign from M-Chain
over Warp (CrossChainMPCRequest) and only VERIFIES the resulting CMP
threshold signatures (block.Verify → mpcConfig, accel_verify.go).
bridgevm carried a SECOND, parallel MPC subsystem — a BLS MPCKeyManager
that did its own in-VM keygen (until this session, via a trusted dealer)
feeding BridgeSigner/DeliveryConfirmationSigner/BridgeMessageValidator.
It duplicated custody, used the wrong scheme (BLS vs the live CMP path),
and was never in block.Verify. Deleted entirely — one and one way.
Removed:
- mpc.go (MPCKeyManager, SigningSession)
- bridge_signing.go (BridgeSigner, DeliveryConfirmationSigner,
BridgeMessageValidator, MPCCoordinator, BridgeMessage/DeliveryConfirmation)
- vm_mpc_integration.go (InitializeMPCKeys, TriggerKeygen, ProcessBridgeMessage)
- vm.go: the 5 BLS fields + their init block (no key material is
generated on B-Chain now)
- feegate.go: gateUserBridgeMessage (BLS-message fee gate) → replaced by
gateUserBridgeFee(uint64) on the live path (LP-0130 §8)
Relocated / repointed:
- ErrInvalidBridgeSignature → accel_verify.go (the live CMP verify path)
- rpc.go MPCReady/MPCPublicKey → vm.mpcGroupPublicKey() (reads the CMP
custody group point from mpcConfig, i.e. M-Chain's key), never a
B-Chain-held secret
Build + vet + full bridgevm tests green.
Co-authored-by: Hanzo Dev <[email protected]>
B-Chain is a public permissionless chain; custody keygen must never
run through a trusted dealer. GenerateKeys now runs a dealerless
Pedersen-VSS DKG (no party holds the group secret) instead of
scheme.NewTrustedDealer. Build + bridgevm MPC tests pass.
This is the one production chains/node path that USED a trusted dealer
(the dealer remains available as a library option in crypto/threshold
per CTO direction — it is simply never used by chains/node).
Follow-up (threshold-architecture consolidation): route this keygen
through luxfi/dkg, the canonical dealerless DKG engine, once B-Chain
custody's signature scheme (classical BLS vs PQ corona/pulsar) is
fixed.
Co-authored-by: Hanzo Dev <[email protected]>
Standardize all of chains on github.com/luxfi/warp v1.23.0 (the standalone
ZAP-native warp that precompile/evm/bridgevm already use). zkvm/fhe and
thresholdvm/fhe are migrated OFF github.com/luxfi/node/vms/platformvm/warp;
there are now ZERO platformvm/warp imports in chains.
The build->sign->wrap sequence (previously copy-pasted in three places and
split across two warp libraries) now lives in exactly one place:
internal/warpmsg.BuildSigned(signer, networkID, sourceChainID, payload)
-> (*warp.Envelope, error). It builds a content Message (NewMessage), signs
it over the Beam domain with the node BLS key, and wraps the signature at
validator bit index 0 into a single-signer Envelope. All three send sites
call it:
- bridgevm/vm.go reshare-request gossip
- zkvm/fhe/coprocessor FHE task-result callback
- thresholdvm/fhe/relayer decryption fulfillment
v1.23.0 split the signature from the Message: the signed/transmittable unit
is now *warp.Envelope (Message + Beam BitSetSignature + PQ lanes), not a
combined Message. The receive path moves with it: onMessage callbacks and
WarpHandler.HandleMessage now carry *warp.Envelope and read authenticated
content from env.Message. MPC/BLS single-signer semantics are preserved
exactly (bit-0 signer, Beam-domain BLS, receiver aggregates/verifies the
BitSetSignature against the canonical validator set via warp.VerifyEnvelope).
CGO_ENABLED=0 build/vet clean; bridgevm/zkvm/thresholdvm test suites green.
bridgevm runs as a standalone rpcchainvm plugin; mpc.go called
threshold.GetScheme(SchemeBLS) but never imported crypto/threshold/bls,
whose init() registers it. The full node binary pulled it in via
platformvm, but the v1.30.16 plugin build didn't -> B-Chain failed VM
init ('threshold: scheme not registered'). Blank-import to self-register.
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.
Mirrors the evm/cevm pattern (cevm_cgo.go / cevm_nocgo.go +
backend_cgo.go). Three new files in bridgevm/:
backend.go — Backend enum, layout structs (byte-equal to
ops/bridgevm/cuda/bridgevm_kernels_common.cuh),
ErrGPUNotAvailable, GPUBackend interface,
unsafe.Sizeof asserts in init().
bridgevm_gpu.go cgo — direct dlopen(3) + dlsym(3) for the per-backend
plugin libluxgpu_backend_<bk>.{so,dylib}.
Probes cuda → hip → metal → vulkan → webgpu in
order; first plugin exposing all five
lux_<bk>_bridgevm_* launchers wins.
bridgevm_gpu_nocgo.go — stub returning ErrGPUNotAvailable on every method.
Direct dlopen (NOT pkg-config) so the public chains module builds without
the private gpu-kernels repo present. Search path: \$LUX_GPU_PLUGIN_DIR,
\$LUXCPP_PREFIX/lib/lux-gpu, \$LUXCPP_PREFIX/lib, the dev tree under
~/work/lux-private/gpu-kernels/build/backends/<bk>, CWD, and finally the
system loader's default.
Five launcher signatures dispatched via cgo trampolines (one per shape) —
matches the C ABI in include/lux/gpu/bridgevm.h. Layout structs include
explicit '_pad*' fields to keep Go sizeof() byte-equal to the C++
alignas(16) layout; the init() sizeof asserts catch any drift at process
start with a per-struct panic message.
vm.go / bridge_signing.go / mpc.go are NOT touched — this is a thin
substrate; the round applier opts in later when it's ready to route
through ActiveGPUBackend().
bridgevm_gpu_test.go — four tests under //go:build cgo:
TestActiveBackend — AutoBackend() returns a known tag.
TestStubReturnsErrGPUNotAvailable
— BackendNone path returns the sentinel.
TestLayoutSizesMatchHeader — runtime-equivalent of the C++ static_asserts.
TestSignerApplyZeroFixture — dlopen + dlsym + zero-fixture round trip
through lux_<best>_bridgevm_signer_apply.
Builds verified under both CGO_ENABLED=0 (nocgo path active) and
CGO_ENABLED=1 -tags cgo (cgo path active). All four cgo-tagged tests
pass on the local dev box (Vulkan plugin auto-detected from the
gpu-kernels build tree).
The luxfi/bridge daemon's bchain client (internal/bchain.Client) calls
bridge_getInfo / bridge_getSupportedChains / bridge_getChainConfig /
bridge_getSignature against the B-Chain RPC for permissionless discovery
+ MPC signature retrieval. Until now BridgeVM only implemented the
settlement subset (estimateFee, submitRequest, getStatus, cancelRequest,
health, getMPCPublicKey) — daemons fell back to assumed local values
for the discovery surface, a centralization vector.
This commit:
- Adds GetBridgeInfo, GetSupportedChains, GetChainConfig, GetSignature
to bridgevm.Service.
- Wires all four into the JSON-RPC dispatcher (bridge_* and bridge.*
aliases).
- GetSignature reads from the swap-store record (populated by the MPC
signing pipeline once the threshold quorum aggregates shares).
Distinguishes "no such swap" from "still signing" via separate errors
so callers can poll without dispatching on a generic error code.
- GetChainConfig matches the ChainID case-insensitively and surfaces
the requested id in the error message when the chain is not in
SupportedChains.
- Adds 6 table-driven tests covering happy paths, not-found, empty
params, and the not-yet-available signature poll.
After this lands the luxfi/bridge daemon stops needing any cached
fallback for these four endpoints — the B-Chain VM is authoritative
end-to-end. Daemon code itself needs no change; its bchain.Client
already calls these method names.
Tests: go test ./bridgevm/... — 36/36 pass (30 pre-existing + 6 new).
The bridge is permissionless and non-custodial. Authoritative quote
math and swap state must live where the validator quorum runs
consensus — in this VM — not in any single daemon's local cache.
This change moves both into bridgevm and exposes them via JSON-RPC
so any client can reach the same settlement decisions.
New RPC methods:
bridge_estimateFee — authoritative quote computation
bridge_submitRequest — create + quote-snapshot
bridge_getStatus — read swap by id
bridge_cancelRequest — idempotent cancel
bridge_health — liveness probe
bridge_getMPCPublicKey — threshold group public key
Architecture:
- QuoteEngine (quote.go) StaticPriceFeed → quorum oracle later
- SwapStore (swap_store.go) in-memory genesis impl; block-persistence next
- Service methods (rpc.go) JSON-RPC dispatch (snake + dot aliases)
Attestation aggregation already lived in bridge_signing.go via
BridgeSigner + BridgeMessageValidator — no changes needed there.
LP-333 signer-set methods unchanged.
InitiateBridgeTransfer is the canonical user-tx admission point on
B-Chain — it drives MPC signing capacity. Add a Fee uint64 field to
BridgeMessage and attach a FlatPolicy at MinTxFeeFloor in
Initialize. The fee gate refuses zero-fee transfers before any MPC
signing capacity is consumed. Consensus-internal callers bypass
via pendingBridges / bridgeSigner direct access.
Update Go package doc comments and code comments in thresholdvm/,
bridgevm/, and zkvm/ to reflect LP-134 split: M-Chain (MPC ceremonies
for bridge custody of external wallets) and F-Chain (FHE compute).
Legacy T-Chain naming clarified inline.
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
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.