Adversarial review built thirteen attacks and landed thirteen. The worst was a permanent chain halt for the price of two transactions, and it came from the function written to prevent exactly that. effect() named an epoch-advance vote by the PROPOSAL. One member voting for two different committees therefore produced two different effects: both passed admission, both passed Verify against committed state, and Accept applied the first and refused the second. Verify is deterministic, so every validator certifies that block; Accept is deterministic, so no validator can apply it, and the engine stops at that height by design. The decision a member votes on is the epoch, of which exactly one is ever open, so the vote is named by the member alone. That class runs deeper than one function. checkAuth reads state an earlier transaction in the same block can change, so ANY block-time authorization verdict can differ from the application-time one. So authorization is no longer asked in Verify at all: it is decided once, in Accept, and a transaction that fails it REVERTS — burns its fee, consumes its nonce, changes nothing. Verify asks only that a block is well-formed, authentic, ordered and paid for. The queue could be poisoned for free. Admission took any nonce above the committed one; Verify demanded exactly the next; and a Verify-failed block is DISCARDED without Reject, so the transaction vanished while its effect claim did not. Register's effect is payer-independent, so any funded account could permanently block any handle, unlimited handles, zero cost, taking the rest of the mempool along each round. Admission now applies the same nonce rule consensus does, counted over what is queued; BuildBlock SELECTS from the mempool rather than draining it, so a discarded proposal costs nothing; and one shared rule (batch.admit) both proposes and verifies, so a proposer cannot build a block it would itself reject. Chain time was whatever the proposer said. Nothing bounded a block's timestamp or height against its parent, and chain time drives every expiry — so a block could be back-dated to revive an expired permit or stamped year 36812 to expire everything at once. Height is consecutive now, time is monotone, and the future is capped; a proposer whose clock trails its own tip declines to propose rather than emit a block nobody can verify. What remains is the gap since the last block, which is what a chain can promise about time, and a test measures it rather than leaving it unexamined. "Structural, not a matter of discipline" was discipline. Gas had no length term, nothing bounded Payload or Scheme, and encoding/json ignores what it does not recognise — so a megabyte of ciphertext body rode on for the price of the handle meant to replace it, and came back out of the block store. Payloads decode as EXACTLY their schema, bulk is bounded before it is decoded, stored bytes are priced by the byte, and the persisted field list is pinned so a new field has to be written down. A committee deduplicated NodeID while voting identity is addressOf(PublicKey): n seats could share one key, seating a threshold one account could never reach, for decryption or for rotating itself out — and it reported Healthy. And a vote was spent by being cast, so a split committee could never converge; at unanimity one member wedged rotation alone, and an honest DKG race did it with nobody attacking. Seats are deduplicated by the identity that votes, and a member's latest vote replaces its earlier one. pendingBlocks had two mutexes and one map, which in Go is a fatal throw. It has one owner. Fixing it surfaced the same bug in HealthCheck, which read height and the burned counter after releasing the lock. Revocation did not reach an in-flight decryption: the committee answered anyway and the plaintext went to the callback. It reaches it now. Expiry deliberately does not — it bounds the ask, not the answer. Also: block parsing walked past bytes no length covered, so a block had many encodings and one id; the type walk skipped interfaces and exempted every 32-byte array from its own name check. Two things F still does not have, for one reason, now written down rather than implied: no state root, and no verification of a child of a block not yet accepted. Both need a state layer per in-flight block. A root checked only at Accept would be worse than none — a wrong one would pass Verify everywhere and fail Accept everywhere, which is the halt this commit is mostly about. Until that layer exists, determinism is enforced from outside consensus by replaying a chain onto independently built nodes on different clocks and requiring byte-identical databases. The package doc claimed a state root as the reason F owns its persistence. It named a mechanism the chain does not have. The decision stands; the justification is now the true one.
Lux Chains
Independent VM plugin binaries for the Lux Network.
Each directory builds to a standalone binary that the Lux node loads as a plugin via --plugin-dir.
Build
make # build all VMs
make evm # build one VM
make test # test all
Install
lpm install evm
lpm install dexvm
Or copy binaries to ~/.lux/plugins/<vmid>.
VMs
make's target list is named in the Makefile, and every name in it is a
directory here that ships a plugin entrypoint (<vm>/cmd/plugin, or evm/main.go).
dexvm/ is the exception in the table below: it is a library the EVM plugin
embeds — it has no entrypoint of its own, so make builds no dexvm binary.
The other directories here (fee/, ownership/, internal/, cmd/) are
libraries and a tool, and are not VMs.
Live means the VM has a chain registered on Lux mainnet 96369
(platform.getBlockchains).
| VM | Chain | Live on 96369 | Purpose |
|---|---|---|---|
| evm | C-Chain | ✅ | EVM smart contracts |
| dexvm | D-Chain | ✅ | Decentralized exchange |
| aivm | A-Chain | ✅ | AI/ML inference |
| bridgevm | B-Chain | ✅ | Cross-chain bridge — also owns all cross-chain messaging / relay / teleport (LP-6000); there is no separate teleportvm |
| graphvm | G-Chain | ✅ | GraphQL data layer |
| keyvm | K-Chain | ✅ | Key management |
| quantumvm | Q-Chain | ✅ | Post-quantum consensus signing (Pulsar) |
| zkvm | Z-Chain | ✅ | Groth16 over BLS12-381 (rolls N × ML-DSA-65 sigs into 192-byte proof) |
| identityvm | I-Chain | ❌ built, not deployed | Decentralized identity |
| oraclevm | O-Chain | ❌ built, not deployed | Oracle/off-chain data |
| relayvm | R-Chain | ❌ built, not deployed | Cross-chain relay |
| mpcvm | M-Chain | ❌ built, not deployed | MPC ceremonies (CGGMP21, FROST, Pulsar-general) — bridge custody for external wallets (LP-7100) |
| fhevm | F-Chain | ❌ built, not deployed | Confidential compute (LP-8200): ciphertext handles, access permits, threshold decryption. Records the public coordinates of encrypted values and holds none of the encrypted part — no ciphertext body, no FHE key, no share. Wraps the FHE runtime at mpcvm/fhe/ |
| schain | S-Chain | ❌ built, not deployed | S3-style object storage (on-chain metadata / off-chain blob). Declares its own VMID = ids.ID{'s','c','h','a','i','n'} in schain/factory.go and is not in the canonical luxfi/constants VM-ID registry |
P-Chain (platformvm) and X-Chain (xvm) are in-process core VMs registered by
luxfi/node, not plugins built here. Together with the eight ✅ rows above they
are the ten chains live on 96369.
Not in this repo
| Name | Status |
|---|---|
teleportvm / T-Chain |
Removed. LP-134 dissolves T-Chain with zero remainder: threshold signing → M-Chain, FHE → F-Chain, cross-chain messaging/teleport → B-Chain (bridgevm). LP-5013 and LP-7330 are deprecated by LP-134; see LP-7050 for the migration map. No teleportvm VM ID exists in luxfi/constants. |
servicenodevm |
Never existed. The service-node registry was never built under this name; the S-Chain directory here is schain/ and is object storage, not a registry. |
Plugin filenames
luxd resolves a CreateChainTx's vmID to an implementation by looking for a file
of that name in --plugin-dir, so a plugin installed under any other name is
invisible and its chain silently never starts. The filename is the CB58 of the
vmID — n6sSsSfbpQBrU9sY4R29U6z8VrmnTo2CntW6da4rRS7qmnGdv for fhevm,
qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS for mpcvm. Each VM pins its
own in a test, because a vmID cannot change once a chain has been created with it.