47 Commits
Author SHA1 Message Date
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.
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.
2026-09-06 12:30:50 -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]>
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 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 2026-09-01 02:38:03 -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 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 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.
2026-08-28 15:04:51 -07:00
zeekay 3035c643f0 bridgevm: the plugin reports the version the VM has
It printed a string literal beside bridgevm.Version. Bump either and the
binary an operator asks reports a version it is not. Splitting run() out of
main() makes what the plugin does separable from how a process reports it,
and testable.
2026-08-28 12:05:31 -07:00
zeekay 6595c4ff2e bridgevm: an empty group is not a key, and the arms nothing can reach are gone
A CMP group with no shares sums to the identity point, which marshals to a
well-formed thirty-three bytes. Handed back as the custody key it made an
empty config look like a finished keygen: the node reported itself ready to
attest and bridge_getInfo agreed, on a chain that could verify nothing.

The rest is subtraction. A settlement record now holds only the fact that
the transfer is settled, because the block that settled it is already named
by the chain. movedToday reports what it read or why it could not, instead
of reporting zero. RemoveSigner returns what happened rather than an error
it never had. Marshal cannot fail, so Bytes has no failure to handle. The
gorilla-rpc registration was a second way to serve one API. The route table
NewHTTPHandler built was a second copy of the one CreateHandlers names. The
logger is set at Initialize, so guarding every log call against its absence
was guarding against ourselves.

How often the watcher polls is data on the watcher, as the releaser's
backoff already is.
2026-08-28 11:59:59 -07:00
zeekay 168838436e bridgevm: drive the release path, the EVM client and the plugin ABI
The EVM client had no test at all, so the thing that broadcasts a release
was described rather than exercised; it now runs against a JSON-RPC
endpoint. The release worker's retry policy is data on the worker rather
than literals in its loop.

The GPU plugin loader is driven against a shared object built for the
test, which is the only thing that can catch the Go and C sides
disagreeing about where a field sits — the drift its own comments say
would silently corrupt every state root. The skip it replaced could not
fail.
2026-08-28 11:52:11 -07:00
zeekay a8dbf849b4 bridgevm: B-Chain boots, and what it settles it remembers
The VM never built its chain store, so Initialize dereferenced a nil
store and B-Chain could not start at all.

What a block settles — the transfers, and the day's running total per
destination — is now written in the same commit as the block. Both lived
in maps on the heap, so a restart forgot every transfer the chain had
released and handed back the whole day's allowance: a restart was a free
reset of the cap that bounds what this bridge can move. The counter is
cut from the block's own timestamp, so every node closes the window on
the same block, and it only ever grew before, which stopped the chain for
good once total volume passed the limit.

A request's id is now the digest of the transfer it carries, and Verify
refuses one where it is not. The id was a label the proposer chose while
the replay guard, the daily counter and the release worker all keyed off
it, so one deposit could ride in under any number of names.

Verify now resolves the parent, refuses one beneath the tip, requires the
height and the timestamp to follow it, and registers what it accepted so
a follower can verify the second block of a run. It read none of that
before. Blocks are bounded and canonically encoded, the chain id is
hashed into every block id, and assembly admits exactly what Verify
admits, so an uncarryable transfer no longer wedges the proposer.

The signer set's threshold is ⌈2n/3⌉ in integers rather than a float that
asked for unanimity at three signers and for two of one at one, the bond
it declares is enforced where a signer registers, and a replacement takes
the slot it replaces.

Deleted: the block and request MPC signature carriage, which nothing
produced and whose deserializer could not decode a canonical secp256k1
signature, so it could only ever reject; the chain-name list beside the
routing table; the confirmation depth and observation time that each node
read for itself and then wrote to the wire; a worker pool, a party id and
an engine channel nobody read.
2026-08-28 11:42:07 -07:00
zeekay 2dee7c2aac chains: answer for the height index that is already written
GetBlockIDAtHeight said "not implemented" on I, Z and B while returning a real
answer on K, F, Q, G, S and M. The store writes the height entry in the same
commit as the block, so on these three it was a stub in front of an index that
already existed — and the engine resolves blocks by height when it bootstraps
and when it state-syncs.

Each is now one line onto that index, which cannot name a block the chain did
not accept because the two are the same commit. A test on each proves it: a
block whose commit is refused has no height entry, and gets one when it lands.
2026-08-28 09:01:29 -07:00
zeekay eaf8e8258b chains: gofmt the files this pass touched 2026-08-28 08:59:56 -07:00
zeekay 0ff6430a7b bridgevm: a transfer is released only once its block is durable
Accept credited the daily volume, completed the requests, advanced
lastAcceptedID, dropped the block from the in-flight set and handed the
transfers to the release worker — which broadcasts on an external chain — and
only then wrote the block. Every one of those survived a failed write, so a
release could be in flight for a transfer no block records.

lastAcceptedID was never written at all. The tip lived in memory, reset to
genesis on every restart, and the daily volume that bounds the bridge limit
went with it.

The block now commits through the shared store, which writes it with its height
entry and the tip in one batch, and its effects — including the handoff to the
release worker — happen only afterwards. A proposal follows the engine's
preference, which the store now holds, rather than always the accepted tip.

The bridge's own state keeps its own lock. The transfers in flight, the signer
set, the chain clients and the release plumbing are not the chain's state, and
folding them into the chain's lock would say they were. The order is the
chain's lock, then the bridge's, then the registry's, and it is written down
where the locks are.

Still in memory only, and NOT changed here because it needs its own design: the
completed-bridge registry and the daily volume. A restart still forgets both,
which re-opens the daily bridge limit.
2026-08-28 08:42:29 -07:00
zeekayandHanzo Dev f377c29746 chains: comments state the contract, not the history behind it
Across these files the comments explained a rule by recounting what happened
without it — which key was mistaken for which, which payer a full pool released,
what a verifier accepted, what a chain did with work it never announced. That
tells a reader what changed rather than what holds, and it dates: the account
stays in the file long after anyone needs it, while the rule it was meant to
convey has to be inferred back out.

They now state the rule. A circuit is judged on its own key. A witness is
exactly as long as the key's circuit says. A count describes the records it is
read from. A full pool releases what pays least. Custody membership costs stake
because an identifier can be aimed and stake cannot. Consensus builds when
WaitForEvent returns, so accepting work and reporting it are one step.

Behaviour is unchanged; the diff is comments, with one exception worth naming:
gofmt realigned a block of one-line methods in quantumvm/feegate_test.go that
was already unformatted before this commit.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-26 13:17:16 -07:00
zeekayandHanzo Dev ea1b0efe60 graphvm: say that this chain builds no blocks, and bridgevm: one status type
G-Chain indexes what other chains have already agreed, so it holds no state of
its own to agree on and never advances past genesis. That was written down in a
comment on its state and nowhere the code could be read from: BuildBlock
answered a generic "not implemented", which reads as an omission, and
WaitForEvent's comment said it would one day wait for queued schema updates,
which invites exactly the latch the other chains just got. Both now name the
reason. A block here would carry a query result, which a peer recomputes from
state, or a schema, which is local configuration; neither is something the
network votes on. A test pins it, so the difference between a chain with nothing
to say and a chain with no way to say it stays visible.

BridgeRequest.Status was a string whose valid values lived in a comment beside
it, while the same lifecycle already had a type and eight constants on
BridgeRequestRecord. One concept, two representations, and the one the watcher
wrote to was the untyped one. It now uses the type.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-25 13:09:57 -07:00
zeekayandHanzo Dev 68a2d33eb9 bridgevm: serve the API that exists, and answer health with what is true
The node served three handlers that answered the same JSON whatever the state:
/status said "operational" always, /validators said none always, and /bridge
took nothing. Meanwhile the bridge's real API — estimate a fee, submit a
request, ask after one, read the signer set, seventeen methods in rpc.go — was
written and never reached CreateHandlers, so nothing served it. Same shape as
the lock reader: the code was there and nobody called it. CreateHandlers now
returns it and the three canned handlers are gone.

HealthCheck answered Healthy: true unconditionally, which routes transfers at a
node that cannot carry them. It now reports what the node can do. A node with no
chains wired is not a relayer and stays healthy — that is a configuration, not a
fault, and calling it unhealthy would take validators that never meant to relay
out of rotation. A node that IS configured to relay but has no threshold key to
attest with, or is not reading the chains it relays from, says so.

One function decides this and both surfaces read it, so bridge_health and the
node's own check cannot disagree about the same node.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-25 12:29:04 -07:00
zeekayandHanzo Dev 9a898cb620 bridgevm: read the source chains, so a lock becomes a block
B routes an attested release after value is locked on the source chain, and it
learns of that lock from the chain's own Locked events. Nothing read them.
FetchLockEvents was written, was not on the ChainClient contract, and had no
callers, so nothing outside a test ever wrote to the pending queue: BuildBlock
answered "no pending bridge requests" forever and B could not leave genesis
however much was bridged through the gateways. WaitForEvent waited only on the
context, so even a full queue would not have been built from.

The watcher polls each configured chain, reads the range it has not read yet,
and turns each lock into a request. It is keyed by the transfer's canonical
digest — what M signs and what the destination gateway keys its replay guard by
— so two chains reusing a nonce cannot collide and the same lock read twice is
the same request rather than a second one.

Three things it will not do. It reads twelve blocks behind the head, so a lock a
reorg takes back never becomes a request. It bounds one pass, so a node that has
been down asks for its backlog in pieces rather than in a query no endpoint will
answer. And a chain it cannot reach keeps its cursor, so an endpoint having a bad
minute does not skip that chain's blocks.

The depth a lock is buried at rides along as its confirmation count, which is
what BuildBlock already gates on, and the source transaction rides along with it.

Tested end to end against a fake chain: a lock is read, consensus is told, and
the block that gets built carries that transfer and no other.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-25 12:24:07 -07:00
zeekayandHanzo Dev 8781ffbd7a bridgevm: the release path answers to the key it was configured with
VerifyAgainst refuses a signature made by a key it was not told to expect, and a
test proves it. Nothing proved the CALLER passes the right key. Handing it
att.GroupPubKey instead of vm.mpcGroupPublicKey() -- the exact pre-fix shape, the
one that let a self-consistent forgery through -- left the whole bridgevm suite
green, because no test had ever entered releaseTransfer.

Nothing could. releaseTransfer routes through evmByChainID, typed to
*evmChainClient, and that type dials RPC endpoints in its constructor. The
package already declares ChainClient and already asserts evmChainClient satisfies
it; the sibling map chainClients is already typed to it. Only the id-keyed index
reached past the interface to the implementation. Widening it and moving
IsProcessed onto the contract leaves every object and every call unchanged and
lets the funds path be given a double.

Three refusals now hold: an attestation signed by a key it supplied itself, a
genuine attestation on a chain whose custody keygen has not completed, and no
attestation at all. Each asserts that the destination gateway received zero
broadcasts, because broadcasting is the release. An already-processed transfer is
the positive control, proving the rig reaches the gateway and the refusals are
decided where they claim to be.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-20 19:35:27 -07:00
zeekayandHanzo Dev fbfeb9a49d bridgeattest: verify against the key we trust, not the one supplied
Verify() checked the signature against a.GroupPubKey — a field of the very struct
being verified. That answers "was this signed by whoever signed it", which every
attestation satisfies, including one an attacker minted with its own key and
shipped alongside. B-Chain called it immediately before releasing funds, so a
self-consistent forgery passed the one check standing between a request and a
real transfer.

The expected key is an argument now, because it is the whole check: a signature
is evidence only when the verifier already knows whose signature it will accept.
The caller has it — B-Chain holds the group public point from M-Chain's keygen
and never holds a custody secret. A nil or empty expected key fails closed, which
is the state of a chain whose keygen has not completed, and exactly when
releasing funds would be worst.

The attestation still carries its own key so a holder can see which group signed,
but a disagreement with the key we trust is a refusal, not a preference for
theirs.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-20 17:39:48 -07:00
zeekayandHanzo Dev 3e9fd77e98 rpc: /ext/ -> /v1/ on our own hosts
luxd serves /v1/*; /ext/* 404s. Third-party chains keep their own paths.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 21:15:23 -07:00
zeekayandHanzo Dev bcf93b97db bridgevm: one acceptance predicate for a block MPC signature
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]>
2026-07-29 16:48:10 -07:00
zeekayandHanzo Dev 9a2e4fa7d3 bridgevm: build a block from its inputs, not from map iteration order
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]>
2026-07-29 16:48:10 -07:00
zeekayandHanzo Dev 4ba9544b51 bridgevm: B<->M release path + canonical attestation seam
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]>
2026-07-12 16:44:17 -07:00
zeekayandHanzo Dev 3a21d0cf4a bridgevm: native ZAP struct-is-wire — kill node/vms/pcodecs dependency
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]>
2026-07-12 16:44:17 -07:00
zeekayandHanzo Dev 38e66384f1 bridgevm: rip out vestigial BLS custody subsystem — custody is M-Chain
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]>
2026-07-03 13:57:33 -07:00
zeekayandHanzo Dev 844c0d5d71 bridgevm: remove trusted dealer from B-Chain custody keygen
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]>
2026-07-03 12:47:21 -07:00
zeekay 3e8821a3d5 decomplect(warp): one warp (luxfi/warp v1.23.0) + one signed-message helper across bridgevm/zkvm/thresholdvm; deps evm v1.99.48 + precompile v0.16.0
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.
2026-06-27 21:49:08 -07:00
zeekay 8437d0eb7a fix(bridgevm): register BLS threshold scheme in the plugin process
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.
2026-06-19 20:12:33 -07:00
zeekay 4a9b1b3456 chains: migrate bridgevm/zkvm/thresholdvm to node/vms/pcodecs
Wave 2D codec-rip extension for chains repo:
- bridgevm/codec.go: codec.Manager + linearcodec.NewDefault → pcodecs
- zkvm/codec.go: same pattern
- thresholdvm/codec.go: same pattern
- Bump luxfi/warp v1.18.6 → v1.19.3 (closes warp→ids→codec/wrappers transitive)
- Bump luxfi/ids v1.2.14 → v1.2.15 (drops codec/wrappers from Prefix/Append)

luxfi/codec remains as // indirect via proto/zap_codec → codec/zapcodec
(the actual ZAP wire impl — physical relocation to luxfi/zap/codec
queued separately). Tests pass for all three VMs.
2026-06-06 15:40:20 -07:00
zeekay b8a2a5c4dc chains: scrub private-repo leakage from GPU bridges
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.
2026-06-05 15:46:00 -07:00
zeekay 430d46baf5 bridgevm: wire to GPU plugin substrate via dlopen + dlsym
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).
2026-06-05 14:53:18 -07:00
zeekayandGitHub f345620ee9 bridgevm: close daemon-discovery RPC gap (getInfo, getSupportedChains, getChainConfig, getSignature) (#5)
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).
2026-05-30 20:06:31 -07:00
zeekayandGitHub c4ba2592ac bridgevm: promote quote engine + swap store from cmd/bridge daemon (#3)
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.
2026-05-28 09:05:11 -07:00
zeekay 7485edd06c bridgevm: gate user tx on FeePolicy (MinTxFee floor)
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.
2026-05-21 17:07:06 -07:00
zeekay b8b8b42f2f chains: M/F/Z-Chain canonical naming per LP-134
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.
2026-05-05 16:28:53 -07:00
zeekay 38a694bde4 Consolidate VMs into single chains module; cevm Metal anchor + V4 dispatch
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).
2026-05-05 10:17:49 -07:00
zeekay 2a86eaedde fix(submodules): resolve build/test failures across 6 chains
- 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
2026-04-28 02:51:06 -07:00
zeekay 8045450815 feat: move all VM implementations into chains/
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.
2026-04-13 05:27:00 -07:00
zeekay b74f5eba49 refactor: rename packages bvm/gvm/qvm/tvm/zvm to match dir names
Consistent naming: directory = package = binary.
- bvm → bridgevm
- gvm → graphvm
- qvm → quantumvm
- tvm → thresholdvm
- zvm → zkvm

Also: delete teleportvm/ and servicenodevm/ chain wrappers.
- teleportvm was duplicate of bridgevm+relayvm+oraclevm (separated chains)
- servicenodevm was renamed to sessionvm and lives at github.com/luxfi/session

One name, one package, one place.
2026-04-13 05:15:42 -07:00
zeekay ea6e37dc24 feat: VM plugin monorepo — 14 independent binaries
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.
2026-04-12 13:05:45 -07:00