mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
main
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a93f6e3dec |
build the VMs without the daemon in their closure
A plugin is its own binary; the daemon that loads it has no business being linked into it. Twelve VMs reached into github.com/luxfi/node for four things, and each one now programs against a home that is actually its own: - fee admission (Policy, FlatPolicy, NoUserTxPolicy, Validate) joins the settlement half already in chains/fee. One fee package, and the nodefee alias that marked the split is gone. - the cross-chain artifact types move to chains/artifacts. - the factory the daemon registers is luxfi/vm/manager.Factory, and the version types are luxfi/version. Both are ordinary modules the daemon reads too, so a VM imports them directly rather than through anything of ours. The GPU switch is deleted, not moved. It was a global set during daemon startup that a separate process could never observe, so every plugin read its default; FHEOptions.Backend is the switch that was really there. go list -deps names no luxfi/node for aivm, bridgevm, dexvm, evm, fhevm, graphvm, identityvm, keyvm, mpcvm, quantumvm, schain or zkvm, and the built plugin binaries carry no such string. oraclevm and relayvm still do: they are re-export shims over luxfi/oracle and luxfi/relay, which build their fee policies upstream. Their tests keep naming the daemon's fee package because that is the type their value has. |
||
|
|
9a568c2bbb |
feat(fhevm): add plugin entrypoint for binary plugin build
Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
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. |
||
|
|
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. |