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