F-Chain existed as a reserved vmID and a spec. luxfi/constants declares FHEVMID and node/node/vms.go lists fhevm in OptionalVMs, so luxd scanned --plugin-dir for a binary nothing produced. This builds it. F is the chain of record for encrypted values it never holds. It stores a ciphertext's 32-byte handle, the digest of the off-chain body, its owner, the capability permits granted over it, and the threshold decryptions asked for and answered — and no ciphertext body, no FHE key, no share. invariant_test.go proves that by walking the type graph and scanning the package source for the runtime's secret-bearing entry points. Six operations, each fee-settled through consensus on the shared chains/fee primitive and priced by scheme and ring dimension, because a TFHE boolean and a CKKS n=2^15 vector cost the committee amounts that differ by an order of magnitude. Two of them are threshold decisions: a decryption completes when Threshold distinct members attest the same result handle, and an epoch installs when Threshold members of the sitting committee attest the same successor. Votes are tallied per value and a member votes once, so one member naming a false result delays nothing and pays for the attempt; the deciding transaction carries the proposal, so nothing pending is ever stored. It WRAPS mpcvm/fhe rather than restating it — the records embed that package's types and the public parameters come from its threshold configuration — but owns its persistence, because that package's Registry stamps time.Now(). That is right for the off-chain daemon it was written for and wrong for a state root: two validators replaying one block would write different bytes. Every timestamp F writes comes from the accepting block. Shape follows keyvm: versiondb with a single commit and a rollback that reloads caches, fail-closed verification, a nonce replay guard, and a per-payer cumulative fee check within a block. Two things go further. GetBlockIDAtHeight answers from a real index written in the block's own commit rather than refusing. And a transaction names the state entry it writes, so admission refuses a second claim on it and consensus refuses a peer's block carrying both — without that, one payer's nonces n and n+1 can each pass every check and only collide in Accept, and a rejected block requeues, so the pair rebuilds indefinitely. The Makefile names its targets instead of globbing the directory: the glob swept in dexvm, fee, ownership, internal and cmd, and stopped at the first with no plugin entrypoint, so `make` built nothing at all.
3.9 KiB
Pluggable chain VMs
luxfi/chains hosts the luxd plugin shells — each subdirectory's
cmd/plugin/main.go is the binary that luxd loads. The actual VM
implementations live in dedicated repos so they can also run as standalone
operator daemons (no luxd validator required), the same way mpcd and
kms do.
Repository ownership
| chain VM | luxd plugin (this repo) | canonical implementation | standalone daemon |
|---|---|---|---|
| R-Chain (relay) | chains/relayvm/ (shim) |
luxfi/relay/vm/ |
luxfi/relay/cmd/relayd |
| O-Chain (oracle) | chains/oraclevm/ (shim) |
luxfi/oracle/vm/ |
luxfi/oracle/cmd/oracled |
| M-Chain (MPC threshold) | luxfi/chains/mpcvm/ (canonical, MPC mode) |
(same) | n/a — runs in luxd |
| F-Chain (FHE) | luxfi/chains/fhevm/ (canonical) |
(same) | n/a — runs in luxd |
| C-Chain (EVM) | chains/evm/ |
uses chains/evm/cevm (C++/GPU FFI shim) or pure-Go (default) via build tags |
n/a — runs in luxd |
| A-Chain (AI) | chains/aivm/ |
luxfi/ai |
n/a |
| I-Chain (identity) | chains/identityvm/ |
luxfi/id |
n/a |
M and F are both canonical here — they are written in this repo, not shimmed
from another. They divide the work LP-134 took off T-Chain: M does threshold
signing and bridge custody, F coordinates confidential compute. F wraps the FHE
runtime library at mpcvm/fhe/, which stays where it is because mpcvm also
uses it; F reuses that package's types and threshold parameters and owns its own
persistence, since the runtime's Registry stamps time.Now() and cannot back
a state root.
The shim pattern is uniform:
// chains/<name>vm/<name>vm.go
package <name>vm
import canon "github.com/luxfi/<repo>/vm"
type Factory = canon.Factory
type VM = canon.VM
// … type aliases for every public type
External imports of github.com/luxfi/chains/<name>vm keep working
unchanged. The plugin's cmd/plugin/main.go doesn't need to be touched.
Pluggable EVM (cevm vs Go-evm)
chains/evm/cevm is an FFI shim around ~/work/luxcpp/cevm (C++ / GPU EVM),
not a standalone VM — it has no operator daemon, it lives in luxd. It folds
into this module rather than shipping as a separate luxfi/cevm repo. Two
implementations behind build tags:
| Tag | File | Implementation |
|---|---|---|
cgo (default if cgo enabled) |
cevm/cevm_cgo.go |
C++ / GPU-accelerated EVM via CGO |
!cgo (CGO_ENABLED=0) |
cevm/cevm_nocgo.go |
Pure-Go fallback (drops back to luxfi/geth) |
Operators choose at build time:
# C++/GPU EVM (default for production validators with cgo + GPU)
go build -o luxd ./cmd/luxd
# Pure-Go EVM (small footprint, no cgo dependency)
CGO_ENABLED=0 go build -o luxd ./cmd/luxd
# Hanzo node uses revm via the same plugin contract
# (separate luxd build flag; uses chains/evm/cevm at the API layer, REVM under the hood)
The plugin contract (chain.ChainVM from luxfi/vm/chain) is shared, so
ANY of these EVM kernels drops in behind the same import. Different
operators can run different kernels in the same network as long as state
roots match — which they do, because all kernels are EVM-spec compliant.
Standalone daemons (operator side)
relayd and oracled are operator-side processes — they observe source
data (chain logs / Bitcoin RPC / OP_NET indexer / external feeds), sign
with the operator key, and submit to the chain VM via JSON-RPC. The chain
VM is the security boundary; the daemons are couriers.
Same shape as mpcd (FROST/CGGMP21 threshold signing) and kms
(Hashicorp-style secret management): single binary, HTTP API, NATS
subscription, persistent local state.
Adding a new pluggable chain VM
- Create
luxfi/<name>repo withvm/package +cmd/<name>ddaemon. - Make
chains/<name>vm/<name>vm.gore-exportluxfi/<name>/vmtypes. - Update
chains/<name>vm/go.modto require the canonical repo. - Plugin
cmd/plugin/main.gokeeps its existing import path unchanged.