mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
9f4636b05cdcec9016e43f0bf2892eb40c0fe92c
286
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f4636b05c |
identityvm: drive the signatures, the wire and the RPC
Coverage 74.4% -> 99.7%. The two statements left are boot gates that cannot fail by construction: RegisterService on a service type this package defines, and fee.Validate on the floor chain.Floor returns. The tests hold keys, because the chain now requires signatures: a party signs its own record and the harness submits it, which is the shape a client has to follow. Each defect the rewrite closed has a test named for it — an attacker's block claiming a victim's DID, a revocation naming a public id, a credential naming an issuer it cannot sign for, an allowlist that admits nobody it does not name, a signature made for another chain, a restart that must hold what the chain accepted, and a block whose records reference each other. Health and getStatus answered an error HealthCheck cannot return. |
||
|
|
f9a999746c |
identityvm: the chain agrees on what changes its state, and a key authorizes it
Every authorization on I-Chain was "the request said so". RevokeCredential
compared a caller-supplied revokerID against the credential's issuer and
subject, both of which GetCredential publishes — so revoking anyone's
credential was pasting their id into the request. RegisterIssuer checked
nothing and stamped Status "active", while TrustedIssuers was loaded and
never read: an allowlist that allowed everything. VerifyCredential's one
test on a proof was len(ZKProof) != 0. Nothing in the package verified a
signature; nothing was signed.
Records now carry an ML-DSA-65 signature over their content, checked
against the key the chain holds, with the chain's binding —
sha256(ChainID ‖ NetworkID), not on the wire — as the signing context. An
identity's id is the hash of its key and an issuer's is too, so a record
names a key its holder proved; a credential's is the hash of what it says.
A block's Identities and Revocations were verified by NOTHING. Verify
looped over Credentials alone, and Publish applied all three — so a peer's
block naming {victim's id, attacker's key} took over the victim's DID and
one naming any credential revoked it. Nothing else produced either list,
and Write persisted neither, so the takeover was invisible on disk.
Underneath that: CreateIdentity, RegisterIssuer and RevokeCredential wrote
straight to the base database on whichever node received the call, so no
two nodes held the same identity set — and verifyCredential consulted that
node-local issuer map, which makes block validity a property of which node
you ask. All four kinds are now block-carried, verified by one predicate
that assembly and consensus both ask, written in the block's commit, and
rebuilt from the records at boot. Nothing rebuilt them before: a restarted
node held only what genesis named and answered "unknown identity" for
every identity it had itself accepted.
Also:
A credential's lifetime came from a caller-supplied int64 added to the
wall clock with no sign check, and Take(0) returns the whole queue, and
Reject does not drop from it. One negative TTL therefore rode in every
block the node proposed, was refused by every node including itself, and
could not be removed: a permanent halt for one fee-paying call.
Verify required height == parent+1 and nothing more, so a block whose
parent was a buried ancestor verified; Accept re-checks the tip now,
because the tip moves between the two.
ParseBlock never tracked what it parsed and SetPreference discarded it,
so a follower could verify the first block of a run and not the second.
ParseGenesis stamped a genesis naming no timestamp with the wall clock —
a different genesis id per node and per restart — directly under the
comment explaining why that must not happen.
Nested frames did not reject trailing bytes, so one logical block had
unboundedly many ids; unpackStrings truncated silently on a bad length;
unpackMetadata dropped an unpaired string; parseClaims answered "no
claims" for a blob that would not decode, which passes the claims bound
trivially.
ResolveIdentity scanned every identity and returned whichever of two
matching DIDs the map reached first. A DID is derived from the id now,
so it is a lookup.
GetCredential wrote cred.Status under a READ lock. A credential's state
is derived from the revocation set and its expiry; it is not a field.
Gone with it: Controllers, Services, Updated and StateRoot, none of which
anything wrote; a CredentialProof carried on the wire whose only check was
its length; and a Status the chain kept in step with two other answers.
|
||
|
|
15fc140211 |
zkvm: drive the consensus surface, the endpoints and the pairing
Coverage 76.7% -> 95.5%, and three more things the tests found on the way. A chain that seeds its genesis allocation and has not yet accepted a block reports itself fresh on every boot, because the store records a tip when it accepts a block and seeding is not accepting one. Answering that with a second allocation ends in "UTXO already exists" — a node that cannot restart until it produces a block. Whether the allocation has been applied is now asked of the state: Finalize is the last thing it does, so a committed root that is no longer the empty one is the allocation saying so. Three Marshal methods returned an error they never produced, so every caller wrote an unreachable branch for it — Block.Bytes answered nil in its one, which is a block with no encoding. spendTx built every fixture with the same all-zero commitment, so two different spends created the same output and the set refused the second. An output commitment now follows what the transaction spends. The proof tests construct a key and a proof that SATISFY the pairing equation, so the arithmetic reaches a verified verdict rather than only refusals — and then show that the same proof bytes on a transaction spending different notes do not reach it. |
||
|
|
95a5c2e478 |
bridgevm: recover the plugin test that .gitignore was hiding
A bare `plugin` line in .gitignore matched every <vm>/cmd/plugin DIRECTORY, so `git add` inside one printed a hint and staged nothing. This file was written and passing and was not in the repository. Another lane anchored the pattern to /plugin; this is the file that was stranded behind it. Work that is written, green, and invisible is the worst of the three states: it looks done to whoever wrote it and does not exist to anyone else.v1.7.36 |
||
|
|
eb02967a9a |
aivm: one staging layer, one commit, one chain
A-Chain kept the tip only in memory, staged every pending block's writes in one shared versiondb, and accepted a block by committing that layer before storing the block. So a restart came back at genesis, accepting A published B's intent, rejecting B discarded A's, and a failed store left the state durable and the block gone. Verify and BuildBlock now decide against an overlay over committed state and throw it away, so a block that is proposed or checked moves nothing. Accept stages the transition, the block, its height entry and the tip through one view and commits them once. Custody moves into engine state, so balances commit with the stake records that depend on them instead of living beside them in memory. Verify also gains the lineage, time and size checks it never had: the parent must be the tip or a block in flight above it, the height must follow, the timestamp must sit between the parent's and the local clock plus a skew allowance, and the encoding must be one a peer will accept. Accept re-checks the tip under the lock that commits, because it moves between the two calls. A block is now named under its chain id, which never travels, so the same bytes name different blocks on different chains. Nested payloads must account for their blob exactly, id lists must be whole ids, and an element the encoder cannot write is reported rather than dropped. A merkle proof's index must be spent by the fold, so an out-of-range index no longer verifies on its low bits. Also: the running flag is read under the lock that writes it, a vertex no longer moves the block chain's head, and eligibleSet says what its quorum is a quorum of. The ten probe files that proved all this are converted to regression tests, not deleted. |
||
|
|
787284d140 |
mpcvm/fhe: one device round trip, three kernels
BatchNTTForward, BatchNTTInverse and BatchPolyMul were three copies of one loop — snapshot the session, allocate operand tensors, run a kernel, sync, read back, copy out, and on any failure at any step close what is open and do the operation on the CPU instead. ~45 statements each, differing in how many polynomials an element reads, which kernel runs, and what the CPU does instead. Three copies of a resource lifecycle are three places to forget a Close, and a fourth kernel would have been a fourth copy. Now one offload plus one element, and three wrappers that name their three differences. 101 lines go, no behaviour with them. The floors are deliberately NOT unified: forward NTT reaches the device at 64 polynomials and ring degree 8192, the other two at 4 and any degree, and the reasoning written on the forward path applies to all three. That reads as drift, but changing a performance threshold needs a device to measure on and this host has none, so the numbers are carried through as parameters and the disagreement is stated where the next reader will see it. Also drops the count == 0 guard, which count < minBatch already subsumes for every floor; reverting it changed no test result, which is what redundant means. |
||
|
|
6919ac8229 |
zkvm: a transaction's identity is its content, and one predicate decides
The proof cache was keyed on tx.ID as read off the wire, plus the proof type, the proof and its public inputs — all four the sender's to choose. Copy an accepted transaction's four onto a transaction spending entirely different notes and the key matched; the hit returned nil from the cache, ahead of verifyPublicInputs, which is the only thing tying those inputs to what the transaction spends. Shielded value, backed by a proof that attests to nothing about it. The id is no longer on the wire. parseTransaction derives it, the cache is keyed on ComputeID(), and a block's id folds in ComputeID() rather than whatever id arrived — which also ends the block and vertex id malleability that one flipped byte bought. Every block and vertex id, and the first public input of every proof, now open with sha256(ChainID ‖ NetworkID). It is not on the wire, so a block from another chain has no parent here and its proofs do not verify, rather than passing a check someone can forget to write. Also on the consensus surface: Verify required height == parent+1 and nothing more, so a block whose parent was a long-accepted ancestor verified, and accepting it rewound the tip and left the height index naming an orphan to every peer that bootstraps from it. Verify now requires the parent to be the tip or a block above it, and Accept re-checks against the tip, which moves between the two. Vertex.Verify checked its transactions and nothing else — no parent, no height — so a vertex naming no parent at height 1<<40 verified, and accepting it set the store's height to 1<<40 and left the chain unable to propose again. Its decoder also ignored trailing bytes, so one logical vertex had unboundedly many encodings under one id, each of which the store writes to disk. BuildBlock skipped the ValidateBasic that Verify runs, so a transaction with an out-of-range Type — read straight off the wire — was assembled into every block and refused by every node including the proposer, and nothing evicted it. admit is now that one predicate, assembly drops what it cannot build, and a transaction names an expiry so a bounded pool drains. BuildBlock stamped an unclamped time.Now(), which is below a parent legally up to maxClockSkew ahead — a block its own Verify refuses. ParseBlock never tracked what it parsed and SetPreference discarded it, so a follower could verify the first block of a run and not the second. A genesis naming no timestamp was stamped with the wall clock, hashed into the genesis id: one genesis file, a different chain per node and per restart. VerifyBlockProof took a GPU path on accel.Available() — a second, inline copy of the Groth16 checks that consulted neither the cache nor the dummy-key refusal — so whether a block was accepted turned on whether the node had an accelerator. Reads that failed were reported as absence: the spent set answered "not spent" for a set it could not read, which is the whole double-spend gate; the state root answered "empty" at boot AND on rollback, where it also returned nil; the UTXO store answered "not found". Each now reports the failure. Deleted, none of it reachable: a node-side wallet holding plaintext spending keys behind an HTTP endpoint, with a loadAddresses that returned nil so the keys were unusable anyway; note construction and the package's only panic; an FHE processor whose verifier tested len(proof) >= 128 and whose "encryption" wrote the plaintext into the first eight bytes; a zwitness carrying a TODO; a 256-level sparse Merkle tree that computed no root and wrote its cache under a read lock; six endpoints answering "Not implemented"; a Signature field nothing verified; and eight config knobs nothing read. The endpoints that remain answer at the path the node mounts them on, rather than behind a mux that dispatches on a path it never sees. Coverage 58.9% -> 76.7% on a denominator 539 statements smaller. |
||
|
|
ec074f11aa |
evm, oraclevm, relayvm: a vmID nobody compared, and a policy no rollback could reach
The two shims re-exported luxfi/oracle's and luxfi/relay's private vmID literals. constants.OracleVMID and constants.RelayVMID exist, agree with them, and nothing made them agree. This repo builds the plugin binary whose FILENAME is that id, so the shims now name the constant and their tests compare all three; a drift upstream fails here instead of producing a chain no node starts. cevm gets ErrNotLinked, so a caller can tell "this build has no C++ EVM" from "this input is bad". cevm/parallel's sender-recovery step returned a hard error for the first, which in a build without the library is every block, forever, and CEVM_STRICT=0 -- the documented rollback to the Go EVM -- did not reach that line. It now leaves by declineBlock like every other capability refusal. Its receipt gate asked whether a transaction carried calldata. A contract creation with empty init code carries none, so it was receipted as a value transfer: no ContractAddress, and 21000 intrinsic gas against creation's 53000, into the cumulative total the header commits to. ExecuteBlock splits into shape / dispatch / assemble so the parity-critical half needs no library to exercise. strictGPUEVM loses the sync.Once that fixed its answer at the first decline and left both branches unreachable from one process. The native build referenced gpu_execute_block and gpu_free_result, which ABI v6 removed, so nothing behind lux_cevm_native compiled at all. The three plugins print the version the VM reports rather than a second literal beside it, and the vmID the binary must be installed under: a plugin under the wrong filename is invisible and the chain simply never appears. cevm/plugin.go is gone. It located a binary at ~/work/luxcpp/evm/build/bin/cevm, a path that has never existed -- the tree is luxcpp/cevm -- claimed the lux CLI and universe Makefiles used it, which neither does, and returned "cevm" from a function called VMID. Its two tests skipped and asserted nothing. .gitignore's bare `plugin` matched every <vm>/cmd/plugin DIRECTORY, so a new file under one could not be added: git printed a hint and staged nothing. mpcvm's and bridgevm's plugin tests are written, pass, and are not in the repository. Coverage: evm 0.0 -> 85.0, evm/cevm 71.1 -> 100.0, evm/cevm/parallel 0.0 -> 86.1, oraclevm 0.0 -> 90.0, oraclevm/cmd/plugin 0.0 -> 72.2, relayvm/cmd/plugin 0.0 -> 72.2. |
||
|
|
d1de30ce04 |
graphvm: a chain that builds no block admits no block
G-Chain declines every BuildBlock, and then ParseBlock accepted 56 bytes
of well-formed ZAP naming any height and any parent, Verify passed it,
and Accept moved lastAccepted to it — after which GetBlock(LastAccepted())
answered not-found and the node could not boot. One block off the wire,
one permanently unstartable node. The refusal is now one predicate that
admission, assembly, preference and consensus all answer with.
The block carried four payload slices no code ever wrote, so Accept's and
Verify's loops over them were vacuous, including the unlocked read of
vm.queries. Deleted, with the four types. The VM also held a second
GraphQL parser and resolver table with no callers, behind two endpoints
that answered "GraphQL endpoint ready" to every query; the real executor
is now what /graphql serves, and the placeholders are gone.
In that executor:
first: "-1" reached make([]*T, 0, -1) and panicked the process, and
mints and burns had no upper bound at all. Forty near-identical
resolvers are now two shapes, so the bound is a property of the shape.
where: {token0: "0x1"} arrived as the string "{token0" — the argument
list was split on commas — so every filtered query answered with the
whole table and reported no error.
{ a b } was one field named "a b": the selection set was split on
commas and newlines only.
MaxResultSize was stored and never read, and QueryTimeoutMs bounded
nothing because no resolver watched the deadline.
sortTokens returned !cmp to reverse, reporting i<j and j<i for every
tie, which left sort.Slice free to permute equal rows differently on
identical data.
RequireAuth with a configured "" admitted `Authorization: Bearer `,
because ConstantTimeCompare reports equal for two empty slices. The
chain refuses to boot on that config instead.
Coverage 31.8% -> 99.8%.
|
||
|
|
d5722bde31 |
mpcvm: a test that re-implements what it tests cannot fail with it
TestUnboundLabelIsNotCounted named the boot-time binding count and then wrote its own copy of the loop and asserted on that. It passed whatever Initialize did, including nothing. The property is held against the VM in TestOnlyAnEntryCarryingARealChainIdCountsAsBound, which boots a node on a table of labels and asks it who it authorizes; what stays here is the half that is local — a label does not parse as a chain id.v1.7.35 |
||
|
|
1393f91994 |
mpcvm: delete the docs for a design that was never built
docs/ and DESIGN.md described protocol/{cggmp21,frost,corona_general,
tfhe_keygen} — four interface-only packages with no implementation and no
importer, deleted this pass — and pointed at a chains/mchain/ tree that does
not exist, which LLM.md already says never did. Prose describing an unbuilt
system reads as a contract; the reader who believes it looks for code that
isn't there.
What is true moves into LLM.md: the constants that must stay constants because
Verify enforces them, the two checks that must read consensus state rather than
the document being checked, and the boot fault that made a share-holding
validator unable to restart.
|
||
|
|
6981a3cd6c |
mpcvm: M-Chain to 98.6%
The lane that did this was killed before it could commit; the tree it left is green and is preserved here rather than lost. Root package 51.5% at the start of the day, 93.3% at its last checkpoint, 98.6% now, with cert, fhe, types and cmd/plugin all passing. Includes the fault-injection suite it was building when it stopped: a node that cannot read its state does not start, and one that cannot enumerate its registry does not either. Not reviewed by me beyond build, vet, gofmt and the full suite under -count=1. The lane's own report is owed and did not arrive, so its mutation count and its verdict on the mpcvm/fhe duplication are unrecorded. |
||
|
|
75c971b172 |
chain: pin that only an absent tip is a fresh chain
The fix in aa4c375 was correct and unpinned -- reverting it left the suite green, which is the same failure this estate has now hit six times. Two tests close it: an unreadable tip must surface its cause and must not report a first run, and a tip of the wrong width must be refused as corruption rather than rounded down to absence. Restoring the old condition now fails both. The absent-tip half was already covered by TestOpenOnAnEmptyStoreStartsAtGenesisAndSaysSo, so it is not restated here. |
||
|
|
8b6d2c50d4 |
chain: only an ABSENT tip means a fresh chain
Open read any failure from the tip as "nothing recorded yet" and started the chain at genesis. A closed database, an unreadable volume or a short read therefore looked identical to a first run, and a live chain would begin building height 1 over state it could not see -- durably, with Initialize returning nil. Q-Chain and F-Chain each found and fixed this in their own code before the store existed. Five VMs now share the store, so the same defect was waiting for each of them again. Not-found starts at genesis; anything else refuses to open, because a chain that cannot read its own tip does not know where it is. A wrong-width tip is now refused rather than silently rounded to a fresh chain for the same reason: it is corruption, not absence. |
||
|
|
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. |
||
|
|
a927ce8a7e |
aivm: an error that says the opposite of what happened, and a guard that guarded nothing
Two of the fifteen defects a probe suite confirmed and nobody had fixed. Both are self-contained; the rest are one architectural change to the staging layer and belong together. WithdrawStake refused an operator that was NOT unbonding and reported ErrOperatorUnbonding -- the opposite of what had happened. An operator reading it would conclude it had already deregistered and wait out a cooldown that never started. Its own comment said "must Deregister first", which is what the error now says. The existing test asserted the wrong constant while its comment described the right behaviour, so the assertion was updated to match the comment rather than the code. Initialize guarded the runtime for one field and dereferenced it on the next line, so the guard read as defensive and the nil panic happened anyway. A VM with no runtime has no logger, no network and no identity, so it refuses. Still open, and deliberately not touched here: the tip is never persisted, the staging layer is shared across in-flight blocks so Accept commits a sibling's writes and Reject drops them, Accept commits engine state before storing the block, there is no tip check, no size bound, no chain binding, no timestamp bounds, and a Merkle proof verifies at an index outside its tree. Those are one job, not nine, and the shape is already settled in fhevm and chain/. |
||
|
|
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. |
||
|
|
e5fbbc5201 |
fhevm: a block extends the tip, a signature names one chain, and a failed read is not an answer
Four defects, each confirmed by executing it before it was fixed and by mutation after. A block whose parent was an OLD accepted block satisfied height == parent+1, so Verify passed it and Accept applied it: the chain at height 3 rewound to 2, the last-accepted pointer and the height index moved with it, and the index went on naming the abandoned block at 3. A peer bootstrapping from it was served an incoherent chain. The parent must now be the tip or a block verified above it; Accept decides again under its own hold of the lock, because the tip moves between the two. Nothing bound a transaction or a block to the chain it was meant for. An address is the hash of a public key, so the same payer exists on every F-Chain, and a captured transaction authenticated verbatim on all of them and burned a balance there. Every chain sharing a genesis timestamp also shared a genesis id, so one chain's block resolved its parent on the others. The chain id now enters the signing preimage and the block id without travelling on the wire: each side supplies its own, so a mismatch is a failed signature and an unresolvable parent rather than a check someone can forget. A read that FAILED was reported as a read that found NOTHING. An unreadable last-accepted pointer left a live chain looking like a fresh one, with Initialize returning nil and the node ready to build height 1 over height 2. The same conflation on the epoch pointer seats a committee nobody elected, and on a nonce it reopens the replay window a nonce exists to close. One reader now separates absent from unreadable, and nonceOf reports rather than returns zero. HealthCheck likewise stops reporting a ledger it could not read as zero burned. MaxBlockTxs was applied by Verify, and Verify runs after the parse, so an 8 MB message carrying 1,524 transactions parsed to completion before any check ran. Bytes and count are both bounded at the first byte, and neither implies the other; selection stops at whichever comes first, so a proposer cannot build what its own parser refuses. Deleted: the tx-blob accounting check, which the canonical-encoding rule subsumes exactly (a blob with unread bytes re-serializes shorter), and the empty-path branch in NewHTTPHandler, whose one producer returns a literal. fhevm 89.7% -> 98.9%. The twelve statements left are named unreachable in the report, not padded. The harness now holds constant what production holds constant: one chain id and one genesis across every node of a chain, rather than a fresh id per node. |
||
|
|
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. |
||
|
|
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. |
||
|
|
2b45e26ccc |
mpcvm: one way for a lane to reach its verifier, and no dead contracts
The lane registry had two paths to a verifier. Register refuses a lane that does not belong to the registry's owner, and its comment says that is what enforces M/F orthogonality. RegisterLegacyAlias then mapped any id at all onto a registered verifier, checking only that the TARGET existed and never that the legacy id belonged to the owner -- so the alias walked around the invariant Register exists to hold. The test demonstrated it: an arbitrary id 200, owned by neither chain, dispatching to an M-Chain verifier. It bought a grace window for LP-5013 T-Chain lane ids. LP-134 dissolved T-Chain with zero remainder, so those ids cannot arrive. Nothing outside the test ever registered an alias. Gone, with the aliases map and ClearAliases: a lane this chain does not own now has no verifier, which is the ownership rule still holding at dispatch. Deleting mpcvm/runtime's two adapter interfaces emptied the package. FChainAdapter and MChainAdapter were declared, never implemented and never called -- every reference to either was inside its own file. |
||
|
|
6419047cdf |
aivm: a vertex id names the work in it, and no two vertices share one
Two defects in the same function, both confirmed by execution. The transaction ids never reached the hash, so the payload was unauthenticated: two vertices carrying different work under the same job names had the same id, and a peer asking for one could be served the other with no way to tell. The job names were concatenated with nothing between them, so ["ab","c"] and ["a","bc"] hashed identical bytes -- a distinct vertex that every peer must treat as the first. Each variable-length part now carries its length ahead of it, which is what makes an encoding injective. The same framing bug turned up independently in schain's key builder, spelled with the same two strings. Mutation: 2 of 4 caught. The two survivors are the element COUNTS on the parent and job lists. Removing either leaves the encoding injective today, because the neighbouring field's count delimits it -- so no test can kill them without a collision I was unable to construct. They stay anyway: they are what keeps each field independently injective, so adding or reordering a field later cannot quietly reintroduce this. An identity hash is the wrong place to remove framing on the strength of not having found the collision yet. |
||
|
|
8cf453d4b0 |
chains: a signature of the wrong width is refused, not reshaped
warpmsg is the one place bridgevm, mpcvm and zkvm turn a payload into a signed cross-chain envelope. It copied whatever the signer returned into a fixed 96-byte array without checking the length. Signer is an interface, so that width is a runtime fact: a short signature was silently zero-padded and a long one silently truncated, and either way the envelope came out well-formed and unverifiable -- a message the sender believes it sent and no receiver can ever accept, with no error anywhere. oraclevm's accel host documented an error for a nil VM and never checked, deferring the failure to whichever method the operator called first. |
||
|
|
1b02c78541 |
quantumvm: a block off the wire is held to what a block it built must satisfy
Red confirmed by execution that the receive path was not the build path, and three of its findings were chain-integrity defects rather than gaps. Parse discarded the transaction set, so a block arriving over the network had none -- and the ML-DSA check was gated on having some. The check therefore ran only on blocks this node built itself. The same bytes verified in memory and passed off the wire, and a block carrying arbitrary transaction bytes was accepted and committed. Parse now reconstructs the full set, signatures included, and refuses a declared count its bytes cannot hold or lengths that do not partition the blob. Accept never asked whether the parent was the tip -- only that the height followed it. So an old sibling rewound a five-block chain and the height index went on serving the orphaned branch to bootstrapping peers as canonical. The tip reader answered the zero id for any failure, and the genesis seeder read that as a fresh chain: one transient read at boot committed genesis over a live tip, durably, and Initialize returned nil. Not-found is now distinguished from unreadable, and only the first means new. The harness that was supposed to defend the signer-identity fix could not see it: it gave each node its own ChainID, which production never does, so reverting the fix left the suite green. It now holds the chain constant and varies the node, which is the shape the defect lives in. Also: the proposer no longer builds a block its own Verify refuses when the clock trails the tip. 96.4% coverage. |
||
|
|
98b2a7fce7 |
fee: a fee that cannot be recorded is not a fee that was paid
Every VM in this repo burns through this package, and its fail-secure guards were the part with no tests: the overflow arms, the corrupt-state refusal, and every storage-failure path. 82.7% -> 100.0%. An unreadable balance is not a balance of zero. Reporting zero would make an unfunded account and an unreachable store indistinguishable, and the caller would charge the one it could not read. Same for a stored value of the wrong width: that is corruption, not a number, and decoding it invents a balance from whatever bytes are there. Burn's two writes are ordered on purpose. Either can fail, and whichever half stages alone is wrong -- the caller must abort, because the Balances contract puts both writes in the block's commit. But they are not equally wrong if one ever did commit: debit-then-record destroys value, record-then-debit invents it, and an overstated burn counter is unrecoverable because nobody can say which account should have paid. The survivable failure is the chosen one. Conservation is now asserted rather than assumed: across an interleaving of credits and burns, including refused ones, everything credited is still either spendable or burned -- never both, never neither. That is what makes this a ledger instead of two counters that happen to move together. 8/8 mutation checks caught, including the write order. |
||
|
|
f28e5d0e30 |
chain: keep only what something calls
Two pieces of the new package had no caller. Pool.Holds was the read side of a claim set that Add already enforces, so nothing ever needed to ask; it is gone, and the tests that used it now assert the same property through Add, which is the behaviour that matters. chain.Closed had no caller because M-Chain still declared its own NoUserTxPolicy. It declares chain.Closed() now — one fewer copy of a policy five chains state identically, and the one no-user-tx chain in this pass exercises the value that describes it. |
||
|
|
7ed6c6d644 | zkvm: drop the tip key the store now owns | ||
|
|
5fb41c5066 | chain: drop a lint directive for a method that is used | ||
|
|
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. |
||
|
|
87cb72352a |
schain: an authorization is spent once, and a block commits what it contains
An AllocateTx's nonce was covered by the owner's signature and compared to nothing. Every gate it passed the first time passed again for the identical bytes: the epoch was still current, the fingerprint still matched, the signer was still the range owner, the ML-DSA signature still verified. Anyone who saw an owner's allocate could resubmit it and consume the range's id space again, for free, as often as they liked. A range now carries the nonce of the allocation that last moved it, in the same key as its cursor, and takes only a strictly greater one. The state root covers it, so a chain cannot disagree about what has been spent and share a root. Accept committed whatever was staged. Every apply starts by dropping the version layer, so the layer belongs to whichever block was processed LAST — verify a sibling between a block's Verify and its Accept and the commit carried the sibling's writes under this block's id. Accept re-applies its own transactions from committed state, commits, and only then advances the chain in memory, under the lock it was mutating without. Block position is now checked before block content: the parent must be a block this node holds, the height must be parent+1, and the timestamp must not rewind or run past a skew ceiling floored at the parent. None of it was checked, so a block could claim any height — and the height it claimed became the chain's height and part of every state root computed after it. A proposer now leaves out what it cannot build rather than failing the whole build: one unauthorized allocate anyone could submit used to drain the mempool and destroy every manifest queued behind it. A verifier still refuses the whole block, which is the half that carries the safety property. Also: the height lookup reads an index instead of scanning the block map and returning whichever entry map iteration reached first; abandoned blocks are released; a rejected block leaves the index; boot no longer panics on a nil logger when the manager supplies no runtime; and the empty-set check in the owner gate is gone — pinning.Owner already answers that question, and answers the wider one the length check missed. Coverage 69.9% -> 97.9%, with 32 guards mutation-checked. The stale premise in SECURITY_review.md is recorded at its head: it reviewed a checkout that did not contain this VM. |
||
|
|
eaf8e8258b | chains: gofmt the files this pass touched | ||
|
|
6d786b8104 |
mpcvm: a refused block leaves no ceremony behind, so it can be applied again
Accept folded the operations into the key registry and the ceremony log with one Put each, and only THEN compared the root it reached against the one the block claims. Refusing there left every record it had already written — exactly the disagreeing state the check exists to refuse. That was worse than losing a block. PutKey and PutCeremony refuse a record they already hold, so the block that failed could never be applied again: the chain was wedged at that height, holding ceremonies no block records. PutBlock and SetLastAccepted were each two Puts with an early return between them, so a fault could also drop a height entry or advance the root under a stale tip. State now writes through the chain's view, and the block, its height entry, the tip and every record it wrote commit together. A refused block leaves nothing, and applying it again works — which the test proves by doing it. The staging queue becomes the shared pool: it was already a map keyed by ceremony id plus a slice for order, rebuilt from the map when entries went, which is what the pool derives. The store owns the tip, the height index, the blocks in flight and the engine's preference; State keeps the registry, the ceremony log and the root, which are M-Chain's own. One lock order, written where the locks are: the chain's, then the VM's, then the pool's. Verify resolves its parent before taking the VM's lock, because holding it across a block lookup takes the two in the order Accept does not. No existing test changed. |
||
|
|
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. |
||
|
|
d4c7d5440b |
quantumvm: an accepted block is one the node still holds after a restart
Accept wrote the block, the tip pointer and the height through the version layer and never committed, so a node that had accepted a thousand blocks came back from a restart naming genesis — while its peers held it to the tip it had already told them about. The three writes were also three early returns, so a failure part-way left the block stored under a tip that named its parent. They are now one commit with a rollback, alongside the height index that makes GetBlockIDAtHeight an answer rather than a refusal. Verify checked only that the parent existed, and only above height one. A height-one block could name a parent no node has ever seen; any block could name genesis as the parent of height 500, rewind chain time to revive expired quantum stamps, or jump forward and expire every stamp in flight. It now sits on its parent in height and in time, under a skew cap — and BuildBlock never stamps behind its own parent, so a node whose clock trails a slightly fast peer does not build blocks it will itself refuse. Verify held the VM's read lock and then called GetBlock, which takes it again. Go queues a waiting writer ahead of any later reader, so a build arriving between the two acquisitions deadlocked the chain with nothing to time out. The Quasar validator id was the CHAIN id — a field never assigned — so every node signed as the empty id, each peer's signature arrived as a duplicate of the first, and no threshold above one could ever be met. It is the node id. The default threshold was derived from a committee size that had not yet been defaulted, so a config naming neither got one-of-three. Cleanup released only FINALIZED entries, which are exactly the ones that do not accumulate. Corona keyed its PRF off the first 32 bytes of the message, so any shorter message through StampBlock killed the process; the signature counts were read outside the lock that appends them. A transaction whose quantum stamp has aged out never verifies again; left in the pool it held its slot for good, and enough of them stopped the chain accepting work. BuildBlock evicts what it could not verify, and re-arms the builder when the batch limit leaves work behind — the latch holds one signal, so the remainder had nothing to wake it. A wire too short to hold the header decoded to height 0, time 0 and the empty parent rather than failing, so every truncation named one value under as many ids as an attacker cared to send. Deleted, all proven unreferenced: the dlopen GPU plugin bridge (a second way to do what quantum.ParallelVerifyWithThreshold already does through accel), the stamper package (near-verbatim copy of the signer's accel path, with a mode-confusion bug the original does not have, and the only reason the tree needed geth/core), the Q-witness adapter, the NTT helpers (whose inverse did not invert), and a signature cache that was written and never read — and whose ids.ToID over a >32-byte input gave every entry the empty id, so all ten thousand slots were one. Coverage 20.0% -> 97.0%, with every load-bearing guard mutation-checked. |
||
|
|
17734274d2 |
keyvm: one predicate decides a transaction, so no layer can wedge another
Admission accepted any nonce above the committed one while Verify demanded exactly committed+1. That split was a free, permanent halt: one gapped transaction from any funded account entered the mempool, was drained into every block alongside honest work, failed Verify, was requeued by Reject, and the next build produced the same doomed block forever. Admission, assembly and consensus now decide through one checkTx, and assembly drops what it cannot build, so a block this node builds is a block this node verifies. Block position is now checked before block content. The parent must exist (a block claiming height 0 skipped the check outright), the height must be parent+1, and the timestamp must not rewind or run past a skew ceiling floored at the parent. Block time is the clock every authorization decision is made against, so an unbounded timestamp let a proposer revive a permit the chain had already retired. Also: payload bytes are priced (a megabyte of commitments rode on chain for the flat fee of an empty one); blocks are canonical on the wire, as transactions already were; the processing-block index lives under one mutex instead of two, which the race detector answers with a fatal throw; abandoned blocks are released rather than retained forever; in-memory state advances after the commit rather than before it; the height index is real, so GetBlockIDAtHeight answers; and checkAuth resolves the record Apply works from instead of Apply resolving and re-authorizing it a second time. Deleted: keyvm/config, 25 knobs of which one was ever read, including a ShareCacheSize for a VM whose defining invariant is that it never holds a share; the duplicate networkID the factory set and Initialize silently discarded; the duplicate state handle beside versdb; and a path-normalizing branch for a path that cannot occur. Coverage 58.9% -> 97.9%, with 30 guards mutation-checked. |
||
|
|
479d676264 |
zkvm: a note is spent with its block or not at all
Accept marked the block accepted and moved lastAccepted before writing anything, then issued a Put per nullifier and per output, each returning early. A failure partway left some notes spent and some outputs created under a tip the chain had already advanced. Worse, the spent-nullifier cache advanced as it went, and MarkNullifierSpent refuses a nullifier it already holds — so the block that failed could never be applied again. The chain was stuck at that height holding notes it believed spent with no block recording the spend. Everything a block writes — nullifiers, outputs, the state root — now stages through the store's view and commits in one batch with the block and the tip. On failure the view is rolled back and the three caches are rebuilt from what committed, so the block can simply be applied again. The DAG vertex had the same defect and goes through the same store. That is why the store's Block is not the engine's block interface: a vertex has several parents and no timestamp, so it cannot satisfy it, and it changes state exactly the same way. The store is generic in what a chain tracks, so Propose, Open and Block hand back the chain's own type and no caller asserts. Also here: genesis is seeded through Seed, which commits on its own, because a staged allocation would ride on whichever block landed first and vanish from a chain that never accepted one. StateTree.Finalize records the root before holding it, rather than after. Restoring the original Accept fails the atomicity test; removing the cache rebuild fails it on the line that says the cache must agree with the database. |
||
|
|
338d59e0cd |
identityvm: a credential is recorded with its block or not at all
Accept set the status and moved lastAccepted before writing anything, then issued one Put per credential with the error DROPPED on the credential writes themselves. A failure partway left earlier credentials on disk, the tip already advanced, and the chain believing a block it had only half applied. It now goes through the shared store: the credentials are staged, committed in one batch with the block and the tip, and the revocations, identities and status become visible only afterwards. A failure discards all of it and leaves the tip where it was. The dropped error is checked. Beside the fix, what this chain kept restating goes away: the pending queue becomes the shared pool, the blocks in flight and the tip come from the store under its one lock, the fee declaration is a value rather than a policy plus a network id, and the factory no longer half-populates a VM that Initialize rebuilds anyway. Two things change that were not behaviour anyone could rely on. Genesis was stamped with time.Now, so its id — the hash of its own fields — differed on every node and every restart; it now comes from genesis itself, as on K and F. And the per-VM GPU session the factory allocated was never read and never released. Restoring the original Accept fails all three atomicity tests. |
||
|
|
6220024728 |
chain: a block lands whole or not at all
Nine chains each write their own Accept, and five of them write it with individual Puts that return early. A later write failing leaves the earlier ones on disk and the chain already believing the block was accepted: state is half-applied and there is no path back. The discipline two chains already have becomes the only one there is. A block declares what it Writes and what it Publishes; the store stages the writes through a versiondb, commits them in ONE batch with the block, its height entry and the tip pointer, and on any failure rolls the view back and rebuilds the caches from committed state. Nothing the block claimed survives and the tip does not move. Publish runs after the commit, so there is no window in which the chain has advanced past state that is not on disk. Beside it: the blocks in flight, under the store's one lock, pruned at or below the accepted height because the engine may abandon a block without ever deciding it; the pool, whose claim set is derived from its queue so a claim cannot outlive the entry that made it; the fee a chain declares, which admits nothing until it is declared; and the factory, whose VM holds nothing until Initialize. Seven mutations of the store and three of the pool each fail at least one test. |
||
|
|
af188362b1 |
fhevm: a verified parent is findable, whoever built it
The in-flight block set was written in exactly one place — BuildBlock — so a block arriving from a peer was never findable by id and its child failed Verify with "parent not found". That is the ordinary shape whenever more than one block is in flight, so a follower could not follow. Both paths now register through trackVerified: the block we build, and the block we verify from a peer. The read lock a Verify holds cannot be upgraded, so the checks finish under it and the registration takes the write lock on its own — pendingBlocks keeps one owner, which is the property this VM already has a test for. trackVerified also prunes, because nothing else would. The engine may drop a block it never accepts and never rejects, so a set that only ever grew would leak the way an unreleased mempool claim did; anything at or below the last accepted height is decided or orphaned, which bounds it to what is actually in flight. This fixes the parent lookup and not the layer under it: Verify reads nonces from committed state, so two blocks in flight from the SAME payer still cannot both verify. Doing that needs a per-block state overlay. keyvm has the same limit, and the test says so rather than implying otherwise. |
||
|
|
2e7c1fb58c |
fhevm: a block every validator certifies, every validator can apply
Adversarial review built thirteen attacks and landed thirteen. The worst was a permanent chain halt for the price of two transactions, and it came from the function written to prevent exactly that. effect() named an epoch-advance vote by the PROPOSAL. One member voting for two different committees therefore produced two different effects: both passed admission, both passed Verify against committed state, and Accept applied the first and refused the second. Verify is deterministic, so every validator certifies that block; Accept is deterministic, so no validator can apply it, and the engine stops at that height by design. The decision a member votes on is the epoch, of which exactly one is ever open, so the vote is named by the member alone. That class runs deeper than one function. checkAuth reads state an earlier transaction in the same block can change, so ANY block-time authorization verdict can differ from the application-time one. So authorization is no longer asked in Verify at all: it is decided once, in Accept, and a transaction that fails it REVERTS — burns its fee, consumes its nonce, changes nothing. Verify asks only that a block is well-formed, authentic, ordered and paid for. The queue could be poisoned for free. Admission took any nonce above the committed one; Verify demanded exactly the next; and a Verify-failed block is DISCARDED without Reject, so the transaction vanished while its effect claim did not. Register's effect is payer-independent, so any funded account could permanently block any handle, unlimited handles, zero cost, taking the rest of the mempool along each round. Admission now applies the same nonce rule consensus does, counted over what is queued; BuildBlock SELECTS from the mempool rather than draining it, so a discarded proposal costs nothing; and one shared rule (batch.admit) both proposes and verifies, so a proposer cannot build a block it would itself reject. Chain time was whatever the proposer said. Nothing bounded a block's timestamp or height against its parent, and chain time drives every expiry — so a block could be back-dated to revive an expired permit or stamped year 36812 to expire everything at once. Height is consecutive now, time is monotone, and the future is capped; a proposer whose clock trails its own tip declines to propose rather than emit a block nobody can verify. What remains is the gap since the last block, which is what a chain can promise about time, and a test measures it rather than leaving it unexamined. "Structural, not a matter of discipline" was discipline. Gas had no length term, nothing bounded Payload or Scheme, and encoding/json ignores what it does not recognise — so a megabyte of ciphertext body rode on for the price of the handle meant to replace it, and came back out of the block store. Payloads decode as EXACTLY their schema, bulk is bounded before it is decoded, stored bytes are priced by the byte, and the persisted field list is pinned so a new field has to be written down. A committee deduplicated NodeID while voting identity is addressOf(PublicKey): n seats could share one key, seating a threshold one account could never reach, for decryption or for rotating itself out — and it reported Healthy. And a vote was spent by being cast, so a split committee could never converge; at unanimity one member wedged rotation alone, and an honest DKG race did it with nobody attacking. Seats are deduplicated by the identity that votes, and a member's latest vote replaces its earlier one. pendingBlocks had two mutexes and one map, which in Go is a fatal throw. It has one owner. Fixing it surfaced the same bug in HealthCheck, which read height and the burned counter after releasing the lock. Revocation did not reach an in-flight decryption: the committee answered anyway and the plaintext went to the callback. It reaches it now. Expiry deliberately does not — it bounds the ask, not the answer. Also: block parsing walked past bytes no length covered, so a block had many encodings and one id; the type walk skipped interfaces and exempted every 32-byte array from its own name check. Two things F still does not have, for one reason, now written down rather than implied: no state root, and no verification of a child of a block not yet accepted. Both need a state layer per in-flight block. A root checked only at Accept would be worse than none — a wrong one would pass Verify everywhere and fail Accept everywhere, which is the halt this commit is mostly about. Until that layer exists, determinism is enforced from outside consensus by replaying a chain onto independently built nodes on different clocks and requiring byte-identical databases. The package doc claimed a state root as the reason F owns its persistence. It named a mechanism the chain does not have. The decision stands; the justification is now the true one. |
||
|
|
2656b30487 |
fhevm: F-Chain coordinates confidential compute without holding it
F-Chain existed as a reserved vmID and a spec. luxfi/constants declares FHEVMID and node/node/vms.go lists fhevm in OptionalVMs, so luxd scanned --plugin-dir for a binary nothing produced. This builds it. F is the chain of record for encrypted values it never holds. It stores a ciphertext's 32-byte handle, the digest of the off-chain body, its owner, the capability permits granted over it, and the threshold decryptions asked for and answered — and no ciphertext body, no FHE key, no share. invariant_test.go proves that by walking the type graph and scanning the package source for the runtime's secret-bearing entry points. Six operations, each fee-settled through consensus on the shared chains/fee primitive and priced by scheme and ring dimension, because a TFHE boolean and a CKKS n=2^15 vector cost the committee amounts that differ by an order of magnitude. Two of them are threshold decisions: a decryption completes when Threshold distinct members attest the same result handle, and an epoch installs when Threshold members of the sitting committee attest the same successor. Votes are tallied per value and a member votes once, so one member naming a false result delays nothing and pays for the attempt; the deciding transaction carries the proposal, so nothing pending is ever stored. It WRAPS mpcvm/fhe rather than restating it — the records embed that package's types and the public parameters come from its threshold configuration — but owns its persistence, because that package's Registry stamps time.Now(). That is right for the off-chain daemon it was written for and wrong for a state root: two validators replaying one block would write different bytes. Every timestamp F writes comes from the accepting block. Shape follows keyvm: versiondb with a single commit and a rollback that reloads caches, fail-closed verification, a nonce replay guard, and a per-payer cumulative fee check within a block. Two things go further. GetBlockIDAtHeight answers from a real index written in the block's own commit rather than refusing. And a transaction names the state entry it writes, so admission refuses a second claim on it and consensus refuses a peer's block carrying both — without that, one payer's nonces n and n+1 can each pass every check and only collide in Accept, and a rejected block requeues, so the pair rebuilds indefinitely. The Makefile names its targets instead of globbing the directory: the glob swept in dexvm, fee, ownership, internal and cmd, and stopped at the first with no plugin entrypoint, so `make` built nothing at all. |
||
|
|
8daf8f12aa |
aivm: a task reaches its verdict because the chain advanced
Operators were selected, bonded, committed and revealed, and then nothing paid them. Settle — which tallies reveals, credits the honest majority, slashes withholders and folds a receipt into the root — ran only from the demonstration driver. The production block path imported intents, created tasks, and never gave any of them a verdict, so a task sat past its reveal deadline forever and the work done for it was credited to nobody. Open mining rests on the opposite: an operator that did the work is paid without anyone asking on its behalf. Every block now settles the tasks whose reveal window has closed. The set is a function of the state and the height alone, which is what lets a follower reach the same verdicts without being told: it applies the block's imports, runs the same pass at the same height, and compares receipt roots. A disagreement about who was paid changes the root, and verifyImported already refuses that — so this needs nothing recorded in the block. Settling requires enumerating tasks and engine state is keyed by slot hash with nothing to iterate, so tasks awaiting a verdict are held in an array in state: appended at creation, removed on reaching one. It holds only what is open, so a block's work is bounded by the tasks still outstanding rather than by everything the chain has ever done. An entry that cannot settle is dropped rather than retried, since leaving it would make every later block re-attempt the same refusal. The test drives the real lifecycle and then does nothing but produce blocks: it fails without this, reporting a task past its deadline with the work unpaid. Registration was already open by default; its comment now says so, and the permissioned variants are described as deployment policy. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
34226894ca |
zkvm: say why the PLONK precompile accepts nothing
The reason is the property, not a version that had it wrong: a verifier that does not evaluate the statement is not a verifier, since it would accept any well-formed blob. The equation is unimplemented here, so this accepts nothing. Comment only. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
729eedaffa |
docs: record how a chain here tells consensus there is work
Two sections. The first is the one way a VM reports pending work — vm.Latch, on the pool that learns of it rather than on the VM — and the two chains that deliberately do something else: schain and dexvm notify on the ToEngine channel, and graphvm builds no blocks because it indexes what other chains have already agreed. That distinction is worth writing down: a parked WaitForEvent is not automatically a defect, and the check is whether anything sends on toEngine. The second is where a B-Chain request comes from — the source chain's Locked events, keyed by the transfer digest the gateway replays against — and what the watcher will not do: read within reorg range of the head, ask for an unbounded backlog, or skip a chain it could not reach. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
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]> |
||
|
|
e900f2b6d2 |
zkvm: the same witness rule at the precompile's door
The 0x80 precompile takes both the verifying key and the public-input count from its caller's calldata, so a caller picks both and they have to agree. Its comment said so — "numInputs must match vk.K length minus 1" — while the code checked only that there were not too many. Too few leaves the trailing K points out of the linear combination and judges the proof against a smaller statement than the key describes, which is the same hole just closed in the VM's own verifier and reachable here by anyone who can call the precompile. The comment described the rule; now the code does. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
645aef166f |
zkvm: a witness is exactly as long as the key's circuit says
A verifying key holds one K point per public input plus a constant term, so it states how many inputs the circuit it was made for takes. The witness arrives with the transaction, so a peer picks its length, and only the upper end was checked. Too few is quieter and no better: the linear combination runs over the witness, so a short one leaves the trailing K points out and judges the proof against a smaller statement than the circuit was compiled for. The count is now exactly what the key states. That is read off the key, not assumed about the circuit — how many inputs a circuit takes is the key's to say, while what the trailing ones MEAN still needs whoever owns the circuit. They remain bound in count and unbound in value. 🪤 The test named for reaching the pairing kept passing after this change while no longer reaching it: it built a key stating three inputs, supplied two, and asserted only that the error said "verification failed" — which the new refusal also says. Its key now states the two it supplies, and it asserts the pairing's own verdict, so it fails if anything in front of the arithmetic answers first. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
dfdf4fb222 |
evm: the Metal anchor's C half carries the same build tag as its Go half
The Go side of the secp256k1 Metal anchor is built only under cgo && lux_cevm_native && darwin. Its C file carried no constraint at all, so every other build still saw a C source in a package it was compiling without cgo, and refused: "C source files not allowed when not using cgo or SWIG". That took out `go build ./...` for the whole repository, not just this package. A file and its other half now agree about when they exist. Under the tag the anchor is still compiled in — go list reports it in CFiles — and without it, neither half is. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
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]> |
||
|
|
6732e966d0 |
keyvm, mpcvm: one latch, not one per chain
Both had grown the same thing: a depth-one channel, a non-blocking send guarded by a select with an empty default, and a WaitForEvent selecting over it and the context. That is what vm.Latch is, so they use it. Four copies of the pattern become none, and the comment explaining why the send is non-blocking lives with the primitive rather than beside each caller. Co-authored-by: Hanzo Dev <[email protected]> |