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.
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.
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]>
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]>
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.