369 Commits
Author SHA1 Message Date
zeekayandHanzo Dev c1e4934ab7 The plugin says what it linked; it does not choose an EVM
luxfi/evm dropped in-process backend switching (2f9a3158): choosing an EVM
is choosing which plugin binary the host runs, and the switch it replaced
never ran a block — its executor returned nil, nil and its tagged build did
not compile. The callers stayed here, so this package no longer built
against evm at all: parallel.SetBackend, AutoEVM, GoEVM, ActiveBackend and
AvailableBackends are gone.

What is left is worth saying out loud, so one file reports it: the C++
library's ABI, the backends it offers, and the health battery each answered.
The cevm package carries its own build tags, so the cgo/nocgo split in the
caller went with them.

Two tests went too. They asserted that selection left a runnable lane
active and that it was idempotent, and there is no selection to be
idempotent about; that a build reports what it cannot execute is still
covered. The package went from listing no tests at all — it did not compile
— to running ten.

Co-authored-by: Hanzo Dev <[email protected]>
v1.7.45
2026-09-17 20:56:19 -07:00
zeekayandHanzo Dev f4d3133bb7 Retract v1.7.7
It does not name one body of code: the proxy and the checksum database hold
what it was published with and always will, GitHub serves something else,
and go.sum files in this org have held a third. Builds through the proxy and
builds that go direct disagree, and the direct ones only look fine because
GOPRIVATE skips the checksum database — a checksum SECURITY ERROR waiting
for the first consumer who builds without it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 20:56:19 -07:00
zeekayandHanzo Dev 5532c64501 Build on Go 1.27
Build and vet clean on 1.27.1.

Co-authored-by: Hanzo Dev <[email protected]>
v1.7.44
2026-09-17 14:22:13 -07:00
zeekayandHanzo Dev 2a95c62551 Workflows ask for the runner label that exists
One label per runner, spelled <os>-<arch>. The aliases these jobs named
were removed, and a job naming a label no runner carries waits instead of
failing.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 01:54:37 -07:00
zeekayandHanzo Dev c1c29c1e81 go 1.26.8
One toolchain across the estate. The tree carried 1.26.0, .1, .3, .4 and .5
at once, so which compiler built a binary depended on which repo it came
from.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 23:59:02 -07:00
zeekayandHanzo Dev 654f9c4a87 chain letters are lowercase
The primary-network chains answer to either case; one spelling beats two.
/v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 15:21:35 -07:00
zeekayandHanzo Dev f13260c45d ci: a runner label names the platform, not the org
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.

One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:08:06 -07:00
zeekay a0e796b1fe bridgevm: the network declares its bridge limits, not the operator
The caps decide which transfers a block may carry — admissible() refuses a
transfer over the per-transfer cap and admit() refuses one over the daily cap,
both inside block verification. Reading them from the operator's chain config
meant two validators could hold different numbers and disagree about whether a
transfer was admissible, and the first one they disagreed about is a transfer
the builder keeps proposing and everyone else keeps refusing. A cap each
operator sets for themselves is not a cap.

Limits (minConfirmations, the two spend caps, the signer bond, the set size)
move to Genesis, where the network declares them once and every validator reads
the same numbers. BridgeConfig keeps only ExternalChains — endpoints and KMS
paths, how THIS node reaches the chains it was told to bridge, which cannot
change which transfers are valid. The dead BridgeFee field goes.

MaxSigners keeps its default: it is a capacity, not a risk decision.
v1.7.43
2026-09-07 00:06:37 -07:00
zeekay 6a9888847c B/M: a security crossing is its own act
M threshold-signs a security crossing the way it signs a fungible
transfer, and B carries the attestation to the destination register.
Same leaderless ceremony, same authorisation, same group key travelling
with the result so B verifies without asking M anything. What differs is
the message, and therefore the digest.

What B does here is narrower than what it does for a fungible asset. The
fungible path releases by minting: a confirmed lock authorises N units
to an account and nothing else has to be true. Whether a SECURITY may
exist on the destination at all is the destination register's question —
is this holder verified, do their claims satisfy the token, does a
holding period follow the paper across — and the token asks it in the
block that lands the position. B never decides a security may land. It
decides the attestation is genuine and says what it claims to say.

SecurityRequest is separate from BridgeRequest rather than a flag on it.
The two are released by different calls onto the destination, and a mint
against a register nobody asked is exactly the failure this path exists
to prevent; one type that could be either leaves that difference to a
branch somebody has to remember to write.

Two fields the destination cannot look up for itself travel with the
crossing — the holder's ONCHAINID on the source register, and when they
first received there. Without the second, an arriving position anchors
its clock to now, and a holding period a crossing resets is one a
crossing washes off.

A short identity is refused rather than read as an absence: treating it
as absent lets a truncated field pass for "this holder had none". An
absent one travels as zero, which is honest.

Quantity is a uint256. A billion shares at eighteen decimals is an
ordinary size for a register and does not fit the uint64 the fungible
path carries; a truncated quantity is a position that arrives smaller
than it left, so it is refused rather than truncated.

Signer ids convert to strings at the boundary so a verifier on the far
side needs nothing of M's type graph. The crossing type itself is
aliased from internal/bridgeattest rather than copied — attest_bridge.go
keeps its own copy of BridgeTransfer, and two definitions kept in step
by hand is a way for a digest to drift across a boundary whose whole
value is that it cannot.
v1.7.42
2026-09-06 12:30:50 -07:00
zeekay e081ccef5f bridgeattest: the attested message for a security crossing
A fungible transfer says move N units of an asset to an account. A
security crossing cannot be said that way: the destination has to run
the register before the position exists there — whether the recipient is
verified, what act this is (an arrival is not a mint), and whose clock
the position brings with it.

None of that fits BridgeTransfer, whose layout is frozen and whose
Amount is a uint64 — too small for an eighteen-decimal position above
about 18.4 tokens. So SecurityCrossing is a sibling message with its own
domain tag rather than a widening of that one. The frozen digest stays
frozen, and a fungible attestation can never be replayed as a security
one because the tag they commit to differs in the first bytes of the
preimage. That property is asserted, not assumed.

Two fields carry what the destination cannot ask the source for:

  Identity  the holder's ONCHAINID on the SOURCE register. The register's
            clocks belong to the person, not the key, and the
            destination has no way to look it up.
  Acquired  when the holder first received on the source. Without it an
            arriving position anchors to now, and a holding period a
            crossing resets is a holding period a crossing washes off.

Both are zero where the source had none — an honest absence the
destination weighs as it likes, rather than a default that reads as an
identity of zero.

Quantity is 32 bytes big-endian for the full uint256.

VerifyAgainst takes the expected group key as an argument, following the
fix already recorded on the fungible verifier: reading the key off the
struct being verified answers "was this signed by whoever signed it",
which an attacker's own key satisfies. Asserted here too.

Every field is bound — each of the seven, mutated one at a time, stops
the signature verifying.
2026-09-06 12:30:49 -07:00
zeekayandHanzo Dev 77fa2901e2 Take the fee types from their permanent home, and let go of the node
luxfi/node is being retired. Six of the seven packages chains once reached
into it for already come from their new homes; the fee types were the last,
and they were held here only by the oraclevm and relayvm feegate tests.
Those two assert on the policy value that luxfi/oracle and luxfi/relay hand
back, so they could not move before those modules did. oracle v1.1.3 and
relay v1.1.2 build against luxfi/vm/vms/types/fee, so the tests now compare
against the same type the VM returns rather than a look-alike from the node.

With that, no source or requirement in chains names luxfi/node.

Co-authored-by: Hanzo Dev <[email protected]>
v1.7.41
2026-09-06 03:48:46 -07:00
zeekay 2f6257af69 build the plugin images on both native pools
Dockerfile.plugin compiles nothing: it copies build/<vm>, the binary the
step above it produces with `go build` on the runner, so the architecture
of the image is the architecture of the machine that ran that build. Asking
one amd64 runner for two platforms would stamp an arm64 manifest onto an
amd64 ELF — an image that pulls on an arm64 host and dies with `exec format
error`, which is worse than the amd64-only manifest it replaces, because
the manifest promises it will run.

One job per architecture compiles natively (evm's CGO included, no
cross-toolchain, no emulation) and pushes <image>:sha-<sha7>-<arch>. A
merge job joins the pair under the release names and refuses to be green
unless every one of them carries both platforms.
2026-09-05 19:08:03 -07:00
zeekayandHanzo Dev 0d4846533e build the VMs without the daemon in their closure
The plugin-SDK surface the VMs programmed against lived in the daemon:
the admission fee policy, the VM factory alias, version, and artifacts.
Twelve VMs now take it from chains/fee, chains/artifacts, luxfi/vm and
luxfi/version instead, so a plugin no longer drags the daemon it runs
under. oraclevm and relayvm still do, through luxfi/oracle and
luxfi/relay; they clear when those two are cut.

go build/vet/test exit 0, 37 packages ok. go list -deps reports zero
daemon packages for all twelve, confirmed against the built binaries.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-04 01:34:11 -07:00
zeekay a93f6e3dec build the VMs without the daemon in their closure
A plugin is its own binary; the daemon that loads it has no business being
linked into it. Twelve VMs reached into github.com/luxfi/node for four things,
and each one now programs against a home that is actually its own:

- fee admission (Policy, FlatPolicy, NoUserTxPolicy, Validate) joins the
  settlement half already in chains/fee. One fee package, and the nodefee alias
  that marked the split is gone.
- the cross-chain artifact types move to chains/artifacts.
- the factory the daemon registers is luxfi/vm/manager.Factory, and the version
  types are luxfi/version. Both are ordinary modules the daemon reads too, so a
  VM imports them directly rather than through anything of ours.

The GPU switch is deleted, not moved. It was a global set during daemon startup
that a separate process could never observe, so every plugin read its default;
FHEOptions.Backend is the switch that was really there.

go list -deps names no luxfi/node for aivm, bridgevm, dexvm, evm, fhevm,
graphvm, identityvm, keyvm, mpcvm, quantumvm, schain or zkvm, and the built
plugin binaries carry no such string. oraclevm and relayvm still do: they are
re-export shims over luxfi/oracle and luxfi/relay, which build their fee
policies upstream. Their tests keep naming the daemon's fee package because
that is the type their value has.
2026-09-03 21:10:40 -07:00
zeekayandHanzo Dev 50c64a1068 Ask every endpoint what chain it is on, and which chain that id names
Ported onto the current tree from work stranded on a copy a hundred commits
behind it. The dial-time check asked endpoints[0] for its chain id and trusted
the rest; every endpoint answers for itself now, and one that disagrees with the
configuration or cannot answer refuses the client by name.

A chain id is not an identity: a fork inherits the id of the chain it left, so a
forked endpoint answers eth_chainId correctly and the id check cannot see it.
ExternalChainConfig gains Genesis, the hash of block 0, and when it is set every
endpoint must produce it — the same commitment platformvm's adopt record calls
Identity. A malformed hash is refused at startup rather than folded into one
that can never match. Left unset, the question is unanswered and startup says
so.

The confirmation read stays the least advanced answer of every endpoint, as it
already was here; the port re-applies it over the older file it arrived in.
The wrong-network test now asserts the claim in the refusal rather than the
words around it, since the refusal names the endpoint.

(cherry picked from commit 678800144052032009a58472b61873aa4b20a52c)

Co-authored-by: Hanzo Dev <[email protected]>
v1.7.40
2026-09-03 21:02:46 -07:00
zeekayandHanzo Dev 27672d9e4d An unobserved lock releases nothing
releaseOnce checked the source lock's depth only when it happened to have a
client for the source chain and the request happened to name a transaction:
`if src != nil && req.SourceTxID != ids.Empty`. Both of those arrive in the
event data a request is built from, so naming a chain this node has no client
for, or naming no transaction at all, skipped the check and went on to request
the custody threshold signature — the one that authorizes the mint at the
destination gateway. A caller could choose to be unchecked. Both cases now
refuse before anything reaches M.

What the check establishes is narrower than it reads, and now says so: it takes
the receipt's status and block number, never decodes the lock event, and
compares nothing to this transfer. It shows the named transaction succeeded and
is buried, not that it is this transfer's lock.

The depth itself came from endpoints[0]. Verifying every endpoint at dial time
and then trusting one of them afterwards buys nothing for the decision that
gates the release: one endpoint could present a lock 1 block deep as 101. It is
now the least advanced answer of every endpoint, so an endpoint can hold a
release back and none can manufacture one — and a client with no endpoints
refuses rather than returning the seed the loop starts from, which is the
maximum and would clear any minimum.

(cherry picked from commit 91230c4d48dd9c8ac01e24301163573d0da6f315)

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-03 21:02:45 -07:00
zeekayandHanzo Dev 9a568c2bbb feat(fhevm): add plugin entrypoint for binary plugin build
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-03 13:21:35 -07:00
zeekayandHanzo Dev 0d9fec309a chore: symlink AGENTS.md + GEMINI.md + CLAUDE.md -> LLM.md
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 13:42:24 -07:00
zeekayandHanzo Dev 17bdadba30 chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases
already ship it. The deployed gateways still answer the old prefix, so remote
reads break until they are redeployed; the estate moves together rather than
sitting half converted.

Lines that record an observation - a probe, a status code, a past release -
keep the prefix they were written against, as do URLs on hosts that are not
ours. Avalanche endpoints move to /ext/bc, which is the path that host serves.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 08:19:10 -07:00
zeekay 4aa7898664 Include string.h once v1.7.39 2026-09-01 02:38:03 -07:00
zeekay b00b12408a Ask for the plugin directory once 2026-09-01 02:10:25 -07:00
zeekay c49dae1bb9 Close the library mpcvm just refused to trust 2026-09-01 02:03:02 -07:00
zeekay 56e81b4132 Retract v1.7.37, which supersedes a release it does not contain 2026-09-01 01:55:11 -07:00
zeekay b1b67ee46c Join the v1.7.37 line, whose one unique fix is already here
v1.7.37 branched from 964b6b0, which is BEFORE v1.7.36, so it carries a higher
number than a release it does not contain. Three of its four commits are
accounted for: the hero-card change is here by patch-id as 60b7b3e, the codec
fix as af7b1dc with internal/codec byte-identical, and the GPU plugin identity
work is cherry-picked in 114440f — the one thing on that line no other line
had.

The tree is not taken. Its go.mod retracts v1.7.36 for serving zkvm's routes
flat; this line serves them the same way, so taking that directive would
retract one release for what its successors also do. The route shape is left
for whoever owns it.
2026-09-01 01:53:40 -07:00
zeekay a7f17a965f Join the parallel line into this history
luxfi/chains was developed twice from the same starting point: two roots with
the same subject and date, no commit in common, 41 commits on one side and 288
on the other. This records that the two are one project again.

The tree is this line's. Eighteen of the parallel line's commits are already
here by patch-id and seventeen more by an exact-subject twin the re-creation
re-hashed. What remains unique to it is one comment-only commit, applied in
f849e8f.

Its tree is not taken. The files it holds and this line does not were deleted
here on purpose and with reasons: the D-Chain proxy that answered nothing
(e4d04be, which added an AST check against its return), the vestigial BLS
custody subsystem (b0cc6d8), thresholdvm after the rename to mpcvm (51a7797).
Merging its tree would undo four deliberate removals and trip the check the
first one installed.
2026-09-01 01:53:01 -07:00
zeekayandHanzo Dev d7199487d2 Build the stub plugins the loader is now allowed to trust
The three GPU suites compiled a library exporting launcher names and nothing
else, which is exactly what the identity check refuses, so eighteen tests
failed against a loader that had started doing its job. The stubs now export
lux_gpu_backend_init answering with this build's ABI version and vtable size,
compiled against the real header so an ABI bump moves them with it.

Three assertions pinned the behaviour that was removed — aivm expecting the
bare filename, mpcvm the bare leaves, bridgevm the working directory — and now
pin its absence.

TestALibraryThatOnlyExportsTheNames is new in each package: it plants the
unidentified build, watches the loader refuse it, then opens the identified
build from the same place so the refusal is the identity check rather than a
stub that never worked.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 01:52:39 -07:00
zeekay c0d4022c71 Cite the specs by the numbers they carry
LP-134 was renumbered LP-1340 (lp-1340-lux-chain-topology); 21 citations
across 16 files still named the old number, which resolves to nothing.

Corona rests on Module-LWE, not Ring-LWE — LP-4805 says so in three places
and b17e086 already corrected one file, leaving evm/main.go and
schain/SECURITY_review.md claiming the wrong hardness assumption. The
two-family argument in LP-4900 depends on which lattice problem this is.

P3Q at 0x012205 is kind-byte signature dispatch; STARK/FRI is starkfri at
0x012220 (LP-7050). Naming P3Q for a scheme at another slot describes a
precompile that is not the one being imported.

Carries the substance of 2f22fa2, the one commit the parallel line held that
this history did not.
2026-09-01 01:40:34 -07:00
zeekayandHanzo Dev 6aa5b77a44 Identify a GPU plugin before trusting it, and load only from a named place
A GPU plugin replaces a VM's state transitions wholesale. Three of the four
seams reached one by dlsym'ing launcher names out of a library nothing had
identified, and all four would fall back to a bare filename — which makes the
dynamic loader's default search path, and in bridgevm's case the working
directory of whoever started the node, the thing that decides which code
computes consensus state.

A library exporting the five bridgevm launcher names and nothing else,
compiled from twenty lines of C that return 0:

  before, LUX_GPU_PLUGIN_DIR=/tmp/rogue   AutoBackend = hip
  before, no env, cwd holds the .so       AutoBackend = hip
  after,  either                          AutoBackend = none

The plugin header says a loader must check abi_version and vtbl_size. aivm,
bridgevm and mpcvm now do so before resolving a single launcher: a name was
the only thing being matched, and a name is not a contract.

Bare filenames and the CWD entry are gone. What is left is LUX_GPU_PLUGIN_DIR
and LUXCPP_PREFIX — places an operator named. Unset means no plugin, which is
the configuration every box without a GPU is already in.

Cherry-picked from 634df5e, whose quantumvm hunk is dropped: that seam's file
was deleted in 409d0e4 and the chain no longer loads a plugin.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 01:36:45 -07:00
zeekay 1eb35500ef Merge the tip and Seed fixes into the current line
chain/store.go gains ErrNotOnTip, so Accept re-checks that the block still
extends the accepted tip under the same lock that commits it, and Seed records
the tip it wrote. Open already refuses to read any error as a fresh chain;
a Seed that left no tip was the remaining way to look fresh after writing.

Carries the zkvm, identityvm and graphvm work the fixes were built on.
2026-09-01 01:32:34 -07:00
zeekayandHanzo Dev a5edffeba2 Restore the chain prefix the running nodes answer
The rename to /v1/chain is real in luxd source, but the deployed fleet has not
taken it yet: probed just now, api.lux.network, api.lux-test.network,
api.zoo.network and api.hanzo.network each answer 200 on /v1/bc/C/rpc and 404 on
/v1/chain/C/rpc. Composing /v1/chain against those hosts reaches nothing, which
is the failure the rename set out to prevent.

The move belongs with the node redeploy, not ahead of it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 21:21:32 -07:00
zeekayandHanzo Dev 6c2b79d070 chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain; the /v1/bc prefix it replaced is
gone, so anything composing it reaches nothing. Lines that record an
observation - a probe, a status code, a past release - keep the prefix they
were written against, as do URLs on hosts that are not ours.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 20:42:54 -07:00
zeekay cce073153d docs: chain routes are /v1/chain, health probes are /v1/health/ops
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181), and the health service moved onto typed
ops. Published addresses now match what a node answers on:

  /v1/bc/<alias>/...    -> /v1/chain/<alias>/...
  /ext/bc/<alias>/...   -> /v1/chain/<alias>/...
  /v1/health            -> /v1/health/ops/health     (full report)
  /v1/health/liveness   -> /v1/health/ops/liveness
  /v1/health/readiness  -> /v1/health/ops/readiness

Observations keep their original addresses: a dated note or a recorded status
code is a fact about when it was written, not an instruction to follow.
2026-08-30 17:31:37 -07:00
zeekay 51430e25c1 Name Zen in the hero card's font stack
The social card asked for Inter first. That is not a face this estate
licenses, ships or ever drew, so a renderer with Inter installed painted
the wrong face and one without it silently fell through to system-ui.
Zen leads now; the same tail follows it.
v1.7.38
2026-08-30 15:47:44 -07:00
zeekayandHanzo Dev b11515d169 chains: retract v1.7.36, whose zkvm routes answer 404
v1.7.36 serves the zkvm handlers one per flat route, so a caller of /rpc,
/privacy or /proof receives 404. Node's TestChainHandlerServesItsOwnPaths
names /rpc/getStatus and fails against it, and passes against v1.7.33,
against v1.7.35 and against this line.

The tag stays where it is. retract says it was published in error without
rewriting history.

Co-authored-by: Hanzo Dev <[email protected]>
v1.7.37
2026-08-30 15:16:24 -07:00
zeekayandHanzo Dev 0ac784c33a codec, quantumvm: a lower-case name on the wire reaches an exported Go method
quantumvm registered node's JSON-RPC codec — the one that raises the initial
of the function named after the dot. That codec is now internal/codec, so the
spelling quantumvm serves, quantumvm.getBlock for Service.GetBlock, is decided
in this module.

The spelling is unchanged: an initial that is already upper-case is refused,
and an argument that will not read reports one message. identityvm, keyvm and
bridgevm register gorilla's plain codec and keep it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 14:54:14 -07:00
zeekayandHanzo Dev 8f0ec4b5fc codec, quantumvm: a lower-case name on the wire reaches an exported Go method
quantumvm registered node's JSON-RPC codec — the one that raises the initial
of the function named after the dot. That codec is now internal/codec, so the
spelling quantumvm serves, quantumvm.getBlock for Service.GetBlock, is decided
in this module.

The spelling is unchanged: an initial that is already upper-case is refused,
and an argument that will not read reports one message. identityvm, keyvm and
bridgevm register gorilla's plain codec and keep it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 14:45:34 -07:00
zeekay 3abc34922f gpu: identify a plugin before trusting it, and load only from a named place
A GPU plugin replaces a VM's state transitions wholesale. Three of the four
seams reached one by dlsym'ing launcher names out of a library nothing had
identified, and all four would fall back to a bare filename — which makes the
dynamic loader's default search path, and in bridgevm's case the working
directory of whoever started the node, the thing that decides which code
computes consensus state.

That is not theoretical. A library exporting the five bridgevm launcher names
and nothing else, compiled from twenty lines of C that return 0:

  before, LUX_GPU_PLUGIN_DIR=/tmp/rogue   AutoBackend = hip
  before, no env, cwd holds the .so       AutoBackend = hip
  after,  either                          AutoBackend = none

Two changes, neither of them new machinery.

The plugin header says a loader MUST check abi_version and vtbl_size, and
quantumvm already does, through lux_gpu_backend_init. aivm, bridgevm and mpcvm
now do the same before resolving a single launcher: a name is what was being
matched, and a name is not a contract.

Bare filenames and the CWD entry are gone from all four. What is left is
LUX_GPU_PLUGIN_DIR and LUXCPP_PREFIX — places an operator named. No new
environment variable: unset means no plugin, which is already the supported
configuration and the one every box without a GPU is in.

Verified with the real HIP plugin from lux-private/gpu-kernels: aivm, bridgevm,
mpcvm and quantumvm all build, vet and test green, aivm TestRoundTripAttestation
passes through the loaded plugin, and the rogue library is refused.
2026-08-29 19:04:26 -07:00
zeekay 85e238e525 Format what the tip change touched 2026-08-29 13:05:58 -07:00
zeekay e82c509a4e Decide the tip under the lock that commits
Every adopter asked whether its block still extended the tip, released
that reading, and only then asked the store for the lock. The tip moves
in between and the commit lands anyway, writing the height index and the
tip pointer for a block on an abandoned branch -- which every peer
bootstrapping from that index is then served as canonical. The lock is
not reentrant, so the check cannot come from the caller and be held
across the commit: it comes from the store or it is not held at all.

Parent joins Height on the Block interface because a place in the
sequence takes both, and height alone is the reading that lets an old
accepted block pass. Two concurrent accepts fall to the same move, not
just the Verify-to-Accept window: the tip a block is judged against is
now read inside the critical section that commits it, so the second is
refused rather than serialised behind a reading taken before the first
landed. The store also exempts nothing, where identityvm exempted height
zero -- and its Verify skips every parent check at height zero, so a peer
could hand a live chain a second genesis carrying records of its choosing
and send the tip and the height back to the start.

mpcvm keeps its root comparison, which answers a different question --
whether applying this block reaches the root the network agreed on -- and
stays true of a node whose state moved for any reason. That a moved tip
also showed up there is a consequence of that chain refusing empty
blocks, not what the comparison is for.

Seed records the tip in that same commit, because seeding is what makes
a chain no longer fresh. Recording none left Open, for which a missing
tip is a fresh chain, reporting a seeded node fresh on every boot and
asking for the allocation again: a duplicate its own state refuses, and
a node that cannot restart until it produces a block it cannot produce.
2026-08-28 20:57:03 -07:00
zeekay 9aed2192a6 Point the citations at the renumbered specs
Corona is Module-LWE, not Ring-LWE, and 0x012205 verifies the P3Q
threshold-signature family rather than a STARK -- the FRI verifier is a
separate precompile at 0x012220.
2026-08-28 20:24:10 -07:00
zeekay fc9a87c1fe zkvm: a prune test that could not see the prune
TestAcceptDrainsWhatTheChainHasPassed accepted one block carrying BOTH
transactions, so Publish removed the expired one as an accepted one and
the pool emptied whether PruneExpired ran or not — deleting the call left
it green. The pool now holds a transaction that never fits in a block,
because a better-paying one keeps taking the slot, and the assertion is
that the chain moving past its expiry is what frees it.

Formatted the two plugin mains.
2026-08-28 15:44:18 -07:00
zeekay a20d44bf52 zkvm: decoding a proof in order to refuse it is not a verifier
PLONK's verification equation was never written: verifyPLONKPairing
refused every proof unconditionally, and above it sat some 200 lines
decoding the proof and the key — exact sizes, subgroup checks, points at
infinity — whose result that refusal discarded. The refusal is the whole
of what the path does, so it is now the whole of what it says.

The aggregate block proof went the same way. VerifyBlockProof re-verified
the very transactions Verify had verified one line above, so it could
only ever agree with them; nothing built a block carrying one; and the
field stood on the wire and in the block id where a reader would take an
aggregate guarantee to be.

Two more second implementations: Verify type-asserted its parent to
*Block after GetBlock had already refused anything that is not one, and
Shutdown asked whether its logger was disabled before logging, which is
what a logger decides.

Coverage 95.5% -> 99.1%. What is left cannot be reached: four bn254.Pair
error returns on one-element slices, two IsInSubGroup refusals on a curve
whose G1 cofactor is 1 and whose Unmarshal already checks G2, and three
boot gates — a store opened through a versiondb view, and fee.Validate on
the floor chain.Floor returns.
2026-08-28 15:39:17 -07:00
zeekay 9f4636b05c identityvm: drive the signatures, the wire and the RPC
Coverage 74.4% -> 99.7%. The two statements left are boot gates that
cannot fail by construction: RegisterService on a service type this
package defines, and fee.Validate on the floor chain.Floor returns.

The tests hold keys, because the chain now requires signatures: a party
signs its own record and the harness submits it, which is the shape a
client has to follow. Each defect the rewrite closed has a test named for
it — an attacker's block claiming a victim's DID, a revocation naming a
public id, a credential naming an issuer it cannot sign for, an allowlist
that admits nobody it does not name, a signature made for another chain,
a restart that must hold what the chain accepted, and a block whose
records reference each other.

Health and getStatus answered an error HealthCheck cannot return.
2026-08-28 15:32:38 -07:00
zeekay f9a999746c identityvm: the chain agrees on what changes its state, and a key authorizes it
Every authorization on I-Chain was "the request said so". RevokeCredential
compared a caller-supplied revokerID against the credential's issuer and
subject, both of which GetCredential publishes — so revoking anyone's
credential was pasting their id into the request. RegisterIssuer checked
nothing and stamped Status "active", while TrustedIssuers was loaded and
never read: an allowlist that allowed everything. VerifyCredential's one
test on a proof was len(ZKProof) != 0. Nothing in the package verified a
signature; nothing was signed.

Records now carry an ML-DSA-65 signature over their content, checked
against the key the chain holds, with the chain's binding —
sha256(ChainID ‖ NetworkID), not on the wire — as the signing context. An
identity's id is the hash of its key and an issuer's is too, so a record
names a key its holder proved; a credential's is the hash of what it says.

A block's Identities and Revocations were verified by NOTHING. Verify
looped over Credentials alone, and Publish applied all three — so a peer's
block naming {victim's id, attacker's key} took over the victim's DID and
one naming any credential revoked it. Nothing else produced either list,
and Write persisted neither, so the takeover was invisible on disk.

Underneath that: CreateIdentity, RegisterIssuer and RevokeCredential wrote
straight to the base database on whichever node received the call, so no
two nodes held the same identity set — and verifyCredential consulted that
node-local issuer map, which makes block validity a property of which node
you ask. All four kinds are now block-carried, verified by one predicate
that assembly and consensus both ask, written in the block's commit, and
rebuilt from the records at boot. Nothing rebuilt them before: a restarted
node held only what genesis named and answered "unknown identity" for
every identity it had itself accepted.

Also:

  A credential's lifetime came from a caller-supplied int64 added to the
  wall clock with no sign check, and Take(0) returns the whole queue, and
  Reject does not drop from it. One negative TTL therefore rode in every
  block the node proposed, was refused by every node including itself, and
  could not be removed: a permanent halt for one fee-paying call.

  Verify required height == parent+1 and nothing more, so a block whose
  parent was a buried ancestor verified; Accept re-checks the tip now,
  because the tip moves between the two.

  ParseBlock never tracked what it parsed and SetPreference discarded it,
  so a follower could verify the first block of a run and not the second.

  ParseGenesis stamped a genesis naming no timestamp with the wall clock —
  a different genesis id per node and per restart — directly under the
  comment explaining why that must not happen.

  Nested frames did not reject trailing bytes, so one logical block had
  unboundedly many ids; unpackStrings truncated silently on a bad length;
  unpackMetadata dropped an unpaired string; parseClaims answered "no
  claims" for a blob that would not decode, which passes the claims bound
  trivially.

  ResolveIdentity scanned every identity and returned whichever of two
  matching DIDs the map reached first. A DID is derived from the id now,
  so it is a lookup.

  GetCredential wrote cred.Status under a READ lock. A credential's state
  is derived from the revocation set and its expiry; it is not a field.

Gone with it: Controllers, Services, Updated and StateRoot, none of which
anything wrote; a CredentialProof carried on the wire whose only check was
its length; and a Status the chain kept in step with two other answers.
2026-08-28 15:27:18 -07:00
zeekay 15fc140211 zkvm: drive the consensus surface, the endpoints and the pairing
Coverage 76.7% -> 95.5%, and three more things the tests found on the way.

A chain that seeds its genesis allocation and has not yet accepted a
block reports itself fresh on every boot, because the store records a tip
when it accepts a block and seeding is not accepting one. Answering that
with a second allocation ends in "UTXO already exists" — a node that
cannot restart until it produces a block. Whether the allocation has been
applied is now asked of the state: Finalize is the last thing it does, so
a committed root that is no longer the empty one is the allocation saying
so.

Three Marshal methods returned an error they never produced, so every
caller wrote an unreachable branch for it — Block.Bytes answered nil in
its one, which is a block with no encoding.

spendTx built every fixture with the same all-zero commitment, so two
different spends created the same output and the set refused the second.
An output commitment now follows what the transaction spends.

The proof tests construct a key and a proof that SATISFY the pairing
equation, so the arithmetic reaches a verified verdict rather than only
refusals — and then show that the same proof bytes on a transaction
spending different notes do not reach it.
2026-08-28 15:12:18 -07:00
zeekay 95a5c2e478 bridgevm: recover the plugin test that .gitignore was hiding
A bare `plugin` line in .gitignore matched every <vm>/cmd/plugin DIRECTORY, so
`git add` inside one printed a hint and staged nothing. This file was written
and passing and was not in the repository. Another lane anchored the pattern to
/plugin; this is the file that was stranded behind it.

Work that is written, green, and invisible is the worst of the three states: it
looks done to whoever wrote it and does not exist to anyone else.
v1.7.36
2026-08-28 15:04:51 -07:00
zeekay eb02967a9a aivm: one staging layer, one commit, one chain
A-Chain kept the tip only in memory, staged every pending block's writes
in one shared versiondb, and accepted a block by committing that layer
before storing the block. So a restart came back at genesis, accepting A
published B's intent, rejecting B discarded A's, and a failed store left
the state durable and the block gone.

Verify and BuildBlock now decide against an overlay over committed state
and throw it away, so a block that is proposed or checked moves nothing.
Accept stages the transition, the block, its height entry and the tip
through one view and commits them once. Custody moves into engine state,
so balances commit with the stake records that depend on them instead of
living beside them in memory.

Verify also gains the lineage, time and size checks it never had: the
parent must be the tip or a block in flight above it, the height must
follow, the timestamp must sit between the parent's and the local clock
plus a skew allowance, and the encoding must be one a peer will accept.
Accept re-checks the tip under the lock that commits, because it moves
between the two calls.

A block is now named under its chain id, which never travels, so the same
bytes name different blocks on different chains. Nested payloads must
account for their blob exactly, id lists must be whole ids, and an
element the encoder cannot write is reported rather than dropped. A
merkle proof's index must be spent by the fold, so an out-of-range index
no longer verifies on its low bits.

Also: the running flag is read under the lock that writes it, a vertex no
longer moves the block chain's head, and eligibleSet says what its quorum
is a quorum of.

The ten probe files that proved all this are converted to regression
tests, not deleted.
2026-08-28 15:03:32 -07:00
zeekay 787284d140 mpcvm/fhe: one device round trip, three kernels
BatchNTTForward, BatchNTTInverse and BatchPolyMul were three copies of one
loop — snapshot the session, allocate operand tensors, run a kernel, sync, read
back, copy out, and on any failure at any step close what is open and do the
operation on the CPU instead. ~45 statements each, differing in how many
polynomials an element reads, which kernel runs, and what the CPU does instead.
Three copies of a resource lifecycle are three places to forget a Close, and a
fourth kernel would have been a fourth copy.

Now one offload plus one element, and three wrappers that name their three
differences. 101 lines go, no behaviour with them.

The floors are deliberately NOT unified: forward NTT reaches the device at 64
polynomials and ring degree 8192, the other two at 4 and any degree, and the
reasoning written on the forward path applies to all three. That reads as
drift, but changing a performance threshold needs a device to measure on and
this host has none, so the numbers are carried through as parameters and the
disagreement is stated where the next reader will see it.

Also drops the count == 0 guard, which count < minBatch already subsumes for
every floor; reverting it changed no test result, which is what redundant
means.
2026-08-28 15:02:56 -07:00
zeekay 6919ac8229 zkvm: a transaction's identity is its content, and one predicate decides
The proof cache was keyed on tx.ID as read off the wire, plus the proof
type, the proof and its public inputs — all four the sender's to choose.
Copy an accepted transaction's four onto a transaction spending entirely
different notes and the key matched; the hit returned nil from the cache,
ahead of verifyPublicInputs, which is the only thing tying those inputs to
what the transaction spends. Shielded value, backed by a proof that
attests to nothing about it.

The id is no longer on the wire. parseTransaction derives it, the cache is
keyed on ComputeID(), and a block's id folds in ComputeID() rather than
whatever id arrived — which also ends the block and vertex id malleability
that one flipped byte bought.

Every block and vertex id, and the first public input of every proof, now
open with sha256(ChainID ‖ NetworkID). It is not on the wire, so a block
from another chain has no parent here and its proofs do not verify, rather
than passing a check someone can forget to write.

Also on the consensus surface:

  Verify required height == parent+1 and nothing more, so a block whose
  parent was a long-accepted ancestor verified, and accepting it rewound
  the tip and left the height index naming an orphan to every peer that
  bootstraps from it. Verify now requires the parent to be the tip or a
  block above it, and Accept re-checks against the tip, which moves
  between the two.

  Vertex.Verify checked its transactions and nothing else — no parent, no
  height — so a vertex naming no parent at height 1<<40 verified, and
  accepting it set the store's height to 1<<40 and left the chain unable
  to propose again. Its decoder also ignored trailing bytes, so one
  logical vertex had unboundedly many encodings under one id, each of
  which the store writes to disk.

  BuildBlock skipped the ValidateBasic that Verify runs, so a transaction
  with an out-of-range Type — read straight off the wire — was assembled
  into every block and refused by every node including the proposer, and
  nothing evicted it. admit is now that one predicate, assembly drops what
  it cannot build, and a transaction names an expiry so a bounded pool
  drains.

  BuildBlock stamped an unclamped time.Now(), which is below a parent
  legally up to maxClockSkew ahead — a block its own Verify refuses.

  ParseBlock never tracked what it parsed and SetPreference discarded it,
  so a follower could verify the first block of a run and not the second.

  A genesis naming no timestamp was stamped with the wall clock, hashed
  into the genesis id: one genesis file, a different chain per node and
  per restart.

  VerifyBlockProof took a GPU path on accel.Available() — a second, inline
  copy of the Groth16 checks that consulted neither the cache nor the
  dummy-key refusal — so whether a block was accepted turned on whether
  the node had an accelerator.

Reads that failed were reported as absence: the spent set answered "not
spent" for a set it could not read, which is the whole double-spend gate;
the state root answered "empty" at boot AND on rollback, where it also
returned nil; the UTXO store answered "not found". Each now reports the
failure.

Deleted, none of it reachable: a node-side wallet holding plaintext
spending keys behind an HTTP endpoint, with a loadAddresses that returned
nil so the keys were unusable anyway; note construction and the package's
only panic; an FHE processor whose verifier tested len(proof) >= 128 and
whose "encryption" wrote the plaintext into the first eight bytes; a
zwitness carrying a TODO; a 256-level sparse Merkle tree that computed no
root and wrote its cache under a read lock; six endpoints answering "Not
implemented"; a Signature field nothing verified; and eight config knobs
nothing read. The endpoints that remain answer at the path the node mounts
them on, rather than behind a mux that dispatches on a path it never sees.

Coverage 58.9% -> 76.7% on a denominator 539 statements smaller.
2026-08-28 15:02:32 -07:00
zeekay ec074f11aa evm, oraclevm, relayvm: a vmID nobody compared, and a policy no rollback could reach
The two shims re-exported luxfi/oracle's and luxfi/relay's private vmID
literals. constants.OracleVMID and constants.RelayVMID exist, agree with them,
and nothing made them agree. This repo builds the plugin binary whose FILENAME
is that id, so the shims now name the constant and their tests compare all
three; a drift upstream fails here instead of producing a chain no node starts.

cevm gets ErrNotLinked, so a caller can tell "this build has no C++ EVM" from
"this input is bad". cevm/parallel's sender-recovery step returned a hard error
for the first, which in a build without the library is every block, forever,
and CEVM_STRICT=0 -- the documented rollback to the Go EVM -- did not reach
that line. It now leaves by declineBlock like every other capability refusal.

Its receipt gate asked whether a transaction carried calldata. A contract
creation with empty init code carries none, so it was receipted as a value
transfer: no ContractAddress, and 21000 intrinsic gas against creation's 53000,
into the cumulative total the header commits to.

ExecuteBlock splits into shape / dispatch / assemble so the parity-critical
half needs no library to exercise. strictGPUEVM loses the sync.Once that fixed
its answer at the first decline and left both branches unreachable from one
process.

The native build referenced gpu_execute_block and gpu_free_result, which ABI v6
removed, so nothing behind lux_cevm_native compiled at all.

The three plugins print the version the VM reports rather than a second literal
beside it, and the vmID the binary must be installed under: a plugin under the
wrong filename is invisible and the chain simply never appears.

cevm/plugin.go is gone. It located a binary at ~/work/luxcpp/evm/build/bin/cevm,
a path that has never existed -- the tree is luxcpp/cevm -- claimed the lux CLI
and universe Makefiles used it, which neither does, and returned "cevm" from a
function called VMID. Its two tests skipped and asserted nothing.

.gitignore's bare `plugin` matched every <vm>/cmd/plugin DIRECTORY, so a new
file under one could not be added: git printed a hint and staged nothing.
mpcvm's and bridgevm's plugin tests are written, pass, and are not in the
repository.

Coverage: evm 0.0 -> 85.0, evm/cevm 71.1 -> 100.0, evm/cevm/parallel 0.0 -> 86.1,
oraclevm 0.0 -> 90.0, oraclevm/cmd/plugin 0.0 -> 72.2, relayvm/cmd/plugin 0.0 -> 72.2.
2026-08-28 15:01:34 -07:00