mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
main
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1362566d34 |
Merge github/main into the hardening branch
Both sides carried work. Ours is the security pass: the stableswap amp=0 and empty-pool halts, the wei-per-swap leak, uint256 truncation in math, three verifiers that returned a verdict without verifying, the cgo/non-cgo consensus split in cggmp21, and the starkfri soundness floor. Theirs is the fhe parameter set, the mldsa crafted-length refusal, and the naming sweep. Resolved per hunk rather than by side. Where both fixed one defect the better fix survives alone, never two guards for one bug. |
||
|
|
3da812d7d9 |
contract: an unanchored signature regex accepts a selector no caller sends
CalculateFunctionSelector keccaks the whole string, so a signature carrying stray whitespace or a copy-paste artefact yields a selector nobody will ever send. The regex matched a substring, so the panic that exists to catch exactly that never fired. Anchored. Every caller is a package initialiser over a literal, so this is a boot failure on a constant, unreachable from calldata. RequireGasLimit's nil check was dead: a nil interface satisfies no interface, so the assertion below already answered it. |
||
|
|
fe4430cec6 |
dex, fhe, contract: a 256-bit word narrowed to a machine integer without saying so
big.Int.Uint64 and .Int64 are documented "if x cannot be represented, the result is undefined". On calldata that is not a lossy conversion to reason about, it is a value substitution, and the substitution happens twice in series: Uint64 drops everything above bit 63, and the Go conversion that follows it -- uint24(x), which is uint32(x) because uint24 is an alias -- drops everything above bit 31. A reader who checks only the first concludes the site is safe. contract.Unsigned and contract.Signed refuse a word that does not fit its declared width, once, where the bytes become a value. Six production sites use them; nothing downstream has to remember which of its arguments were bounded. DecodePoolKey read fee and tickSpacing as 24-bit fields and enforced neither. Above 2^32 the narrowing substituted: a tickSpacing word of 2^32+6 arrived as 6, and 2^32+60 produced the same pool id as 60. Between 2^24 and 2^32 the field held the caller's value faithfully while EncodePoolKeyABI wrote only its low three bytes, so Fee 3000 and Fee 2^24+3000 hash to the SAME id -- PoolKey.ID is not injective over its own Go type, and the decoder handed back a key whose fields are not the ones its id was derived from. The encoder is unchanged; changing it renames every pool that exists. What changed is that no such key can be built from calldata, which empties the collision class at the only boundary that produces PoolKeys from untrusted bytes. Same bound on the two tick fields in DecodeModifyLiquidityInput and decodePMLifecycle. Three engines narrowed INSIDE the comparison meant to bound the value, so the check did not merely miss, it inverted. A margin removal to 2^64x leverage read as leverage zero and passed; a perp open at 2^64 in hundredths did the same; increasePosition wrapped the quotient into a uint32 field that then set the liquidation price. Leverage is now compared at full width. CalculateFee clamped AFTER narrowing, so a violent enough move came out cheaper than a calm one; it clamps in 256 bits and narrows once the clamp proves it fits. GetTimeToRepayment saturates rather than wrapping a horizon into a small number of blocks. fhe declares euint128/160/256 and loads them through EncryptUint64, whose bit loop reads value>>i off a uint64 -- zero for every i above 63. The container is genuinely wider (tfheRandom fills all 256 bits), so the missing bits were not rounded away, they were encrypted AS ZERO: measured, asEuint256(2^200) decrypted to 0, and asEaddress on two addresses sharing their last eight bytes produced the same plaintext, so a homomorphic equality over encrypted addresses answered true for two different accounts. Casts to a narrower type still truncate, matching Solidity and the existing asEuint4/8/16/32 masks; what is refused is a plaintext wider than the loader, which is not a cast but a value the encryptor cannot carry. Widening it needs an EncryptBig upstream -- BitCiphertext's fields are unexported and it has no constructor from bits. Same bound on the scalar ops, once ahead of the switch rather than five times inside it. Consensus-visible: words that decoded now refuse. Fee or tickSpacing wider than 24 bits, a tick wider than int24, an FHE plaintext or scalar wider than 64 bits. Every one of them previously resolved to a different value than the caller sent. Four prior lanes had pinned three of these as characterisation tests naming themselves DEFECT, each instructing that the test be inverted when the bound arrived; they are inverted here rather than deleted, so the history of why stays legible. 24 of 24 mutations caught, no survivors. contract 100.0%, dex 99.1 -> 99.2. Both CGO settings, -count=1, -count=2 and -race. |
||
|
|
414aa8fa29 |
precompiles: parse calldata through one cursor, answer with one verdict
Class 1. Every parser walked its wire format with hand-written slice expressions, and the reason that is dangerous is not the one written in the old fixtures' comments. A precompile's input is the two-index slice Memory.GetPtr returns; nothing on the path to Run copies it. len is what the caller declared and paid for, cap is the rest of EVM memory, and the caller MSTOREd it. So s[a:b] past len does not panic and does not halt a validator -- it returns bytes the attacker chose and the verifier answers over them. Reverting one cursor read in frost and declaring the input one byte short returns verdict word 1 over a byte nobody sent: an accept, not a crash. contract.Cursor bounds every read against len, caps what it returns so a field cannot be re-sliced forward, and refuses a declared count before it sizes anything -- Fields divides rather than multiplying, because n*w wraps and a wrapped product compares as small. STARKVerifyPrecompile did make([]uint64, n) for an attacker-chosen uint32, up to 34 GB, and only then checked. Class 2. (bool, error) has four states and three meanings: (false, nil) is both "verified and rejected" and "nothing ran". That collision is why one verifier could fold its rounds, discard the result, return true, and read as ordinary code. contract.Verdict carries the missing bit, so its zero value denies and a refusal is distinguishable from a rejection. A positive can only come from Checked, and Checked(true) fails the build -- reintroduced in fflonkVerify, the compiler and go vet both pass it, the gate names the line. Nine parse functions across eight packages carry zero hand-written bounds now. The class rests on two checks in Cursor; deleting either fails every suite, including with fixtures degraded back to plain append -- which is what the class used to depend on. |
||
|
|
9cc8cc7bb7 |
contract: one cursor holds every calldata bound, one verdict cannot be faked
A precompile's input is the two-index slice Memory.GetPtr returns, and nothing on the path to Run copies it. len is what the caller declared and paid for; cap is the rest of EVM memory, which that same caller filled with MSTORE. So a parser reaching past its input does not panic and does not halt a validator -- it reads bytes the attacker chose and answers over them. Measured, not assumed: TestCalldataCarriesCallerChosenSpareCapacity drives the VM and reads back len=8 cap=256 with 0xA5 past the end. Cursor bounds every read against len, never against cap and never by letting a slice expression refuse, and caps what it returns so a field cannot be re-sliced forward. Verdict carries the bit (bool, error) was missing -- did a verifier run -- so "nothing checked this" stops borrowing the spelling of "this failed", and its zero value denies. Poisoned is the one definition of the adversarial fixture: append-built input has spare capacity too, but zeroed, so an over-read reads as a harmless run of zeros. p3q parses through the cursor. Its six hand-written bounds become zero; the class now rests on the single check in Cursor.Bytes, and deleting that check fails both suites -- including with fixtures degraded back to plain append, which is what the class used to depend on. |
||
|
|
2537155c73 |
contract: drop AtomicStager
An interface with no implementation and no caller. The staging capability it described is the LP-311 stateless-conduit design, which has never landed on main: nothing here asserts to it, nothing satisfies it, and no build or CI step names it. It is still used by the unmerged dex/stateless-conduit-carry-fills branch, which carries its own copy of this file. That branch has to restore or re-land the interface with the conduit that needs it — main does not hold a contract for code that is not here. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
b313c73bfc |
feat(precompile): enable-everything builder surface — drop strict-PQ refusal from verify-only precompiles
Public permissionless launch policy: enable basically every precompile for builder convenience, ESPECIALLY wallet-curve VERIFY so users sign natively on Lux from other chains (ed25519=Solana, sr25519=Polkadot, secp256r1=WebAuthn, secp256k1/ecrecover=Ethereum). Disable ONLY actual security risks. Lux's own consensus and identity stay PQ (quasar/p3q) — enforced in the consensus layer, never by refusing an EVM verifier a dapp asked for. Two layers, decomplected: - builder EVM precompile surface : enable-all-verify (this change) - chain consensus / finality : PQ-strict (consensus module, untouched) Removed the RefuseUnderStrictPQ gate from 17 verify-only / key-safe custom precompiles: ed25519, sr25519, secp256r1, bls12381 (EIP-2537, x7 ops), kzg4844 (EIP-4844), blake3, poseidon, pedersen, babyjubjub, pasta, ring, vrf, hpke, curve25519, x25519, cggmp21, frost, and the classical SNARK verifiers in zk (Groth16/PLONK/Halo2/KZG/IPA/range/batch/commitment). zk fflonk (0x03) stays DISABLED — but on its OWN forge-bug mechanism (ErrFflonkDisabled, returned at dispatch), NOT strict-PQ: verifyFflonk has a nil-vk soundness hole that forges any statement. Security disable, fully PQ-independent. The RefuseUnderStrictPQ helper + ErrClassicalForbiddenInPQ + StrictPQReporter had zero remaining code callers (evm uses a local structural interface) — deleted contract/strict_pq.go and its test. Rewrote zk's gate test to assert the new policy (classical ops enabled, fflonk disabled). Removed the now- orphaned isPedersenCommitment; fixed stale comments referencing deleted symbols. Build: full module green. Tests: contract + zk + all 17 edited packages pass. NOTE: the STANDARD eth precompiles (ecrecover, p256Verify, sha256, ripemd160, blake2f, bls12381, kzg) are still refused by LuxStrictPQ() in the evm plugin — a follow-up commit flips that to Permissive so ecrecover (every Ethereum dapp) works at launch. |
||
|
|
7ae03fe157 |
feat(precompile): enable-everything builder surface — drop strict-PQ refusal from verify-only precompiles
Public permissionless launch policy: enable basically every precompile for builder convenience, ESPECIALLY wallet-curve VERIFY so users sign natively on Lux from other chains (ed25519=Solana, sr25519=Polkadot, secp256r1=WebAuthn, secp256k1/ecrecover=Ethereum). Disable ONLY actual security risks. Lux's own consensus and identity stay PQ (quasar/p3q) — enforced in the consensus layer, never by refusing an EVM verifier a dapp asked for. Two layers, decomplected: - builder EVM precompile surface : enable-all-verify (this change) - chain consensus / finality : PQ-strict (consensus module, untouched) Removed the RefuseUnderStrictPQ gate from 17 verify-only / key-safe custom precompiles: ed25519, sr25519, secp256r1, bls12381 (EIP-2537, x7 ops), kzg4844 (EIP-4844), blake3, poseidon, pedersen, babyjubjub, pasta, ring, vrf, hpke, curve25519, x25519, cggmp21, frost, and the classical SNARK verifiers in zk (Groth16/PLONK/Halo2/KZG/IPA/range/batch/commitment). zk fflonk (0x03) stays DISABLED — but on its OWN forge-bug mechanism (ErrFflonkDisabled, returned at dispatch), NOT strict-PQ: verifyFflonk has a nil-vk soundness hole that forges any statement. Security disable, fully PQ-independent. The RefuseUnderStrictPQ helper + ErrClassicalForbiddenInPQ + StrictPQReporter had zero remaining code callers (evm uses a local structural interface) — deleted contract/strict_pq.go and its test. Rewrote zk's gate test to assert the new policy (classical ops enabled, fflonk disabled). Removed the now- orphaned isPedersenCommitment; fixed stale comments referencing deleted symbols. Build: full module green. Tests: contract + zk + all 17 edited packages pass. NOTE: the STANDARD eth precompiles (ecrecover, p256Verify, sha256, ripemd160, blake2f, bls12381, kzg) are still refused by LuxStrictPQ() in the evm plugin — a follow-up commit flips that to Permissive so ecrecover (every Ethereum dapp) works at launch. |
||
|
|
bd66095726 |
dex: LP-311 stateless 0x9999 conduit + host AtomicStager seam + real-StateDB reap-noop proof
Decomplect step 1 (LP-311 §5, the gate to live). The 0x9999 settlement conduit drifted into a SECOND venue: it kept an order book + a per-asset/per-account ledger + atomic-staging records in its OWN account storage. Because 0x9999 is AlwaysOn and dispatched by address, an ERC-20-only deposit leaves it EMPTY (nonce==0 ∧ balance==0 ∧ codeHash==∅; storage is NOT part of EIP-158 empty()), so geth Finalise(true) reaps the account AND its storage -> seamReserve stranded, zero fill. The shipped "fix" was a nonce marker on 0x9999 (preserves the complect). This adds the LP-311 STATELESS CONDUIT that dissolves the hazard at its cause: - contract/atomic_stager.go: AtomicStager optional host capability (StageExport/ ConsumeImport). The conduit moves cross-chain value objects through the HOST's non-empty staging account, never its own (reapable) account. - dex/conduit.go: conduitForwardCredit / conduitApplyRelease. Escrow is native balance or the TOKEN's trie; the ledger + book live on the D-Chain venue; cross- domain objects are staged via AtomicStager. ZERO durable SetState(0x9999). The nonce marker is unnecessary and unused on this path. Reuses the proven C<->D object codec (native_wire.go), DeriveIntentID, and the raw escrow helpers (native balance / pullERC20Terminal under-delivery guard). - dex/conduit_reap_test.go: the INVERSION of native_zap_realstate_test.go over a REAL *state.StateDB (EIP-158 on) + REAL cross-chain atomic.SharedMemory + a minimal-but- real D-Chain ledger venue. Drives ERC-20 deposit -> D-credit -> withdraw and proves: 0x9999 holds ZERO own storage, is reaped at Finalise(true) as a NO-OP, escrow (token trie) + claim (D ledger) intact + withdrawable, conservation holds (no mint/loss). Plus the crisp RED/GREEN discriminator: same record reaped on empty 0x9999, survives on the non-empty host account. Tests: TestConduit_OwnStorageReapIsNoOp_RealStateDB, TestConduit_ReapDiscriminator_HostAccountVsConduit — GREEN (GOWORK=off CGO_ENABLED=0); full dex suite green (no regressions). SYNC carry-fills no-fork primitive verified green in chains/dexvm (TestRED_PerValidatorRelay_SplitsConsensus: identical roots, 0 venue calls at Verify). Forward-only, patch-bump cascade. Old braided money path retained pending cutover (repoint dispatch + delete C-side ledger -> D, embedded-venue deletion). |
||
|
|
6ea3143281 |
dex: LP-311 stateless 0x9999 conduit + host AtomicStager seam + real-StateDB reap-noop proof
Decomplect step 1 (LP-311 §5, the gate to live). The 0x9999 settlement conduit drifted into a SECOND venue: it kept an order book + a per-asset/per-account ledger + atomic-staging records in its OWN account storage. Because 0x9999 is AlwaysOn and dispatched by address, an ERC-20-only deposit leaves it EMPTY (nonce==0 ∧ balance==0 ∧ codeHash==∅; storage is NOT part of EIP-158 empty()), so geth Finalise(true) reaps the account AND its storage -> seamReserve stranded, zero fill. The shipped "fix" was a nonce marker on 0x9999 (preserves the complect). This adds the LP-311 STATELESS CONDUIT that dissolves the hazard at its cause: - contract/atomic_stager.go: AtomicStager optional host capability (StageExport/ ConsumeImport). The conduit moves cross-chain value objects through the HOST's non-empty staging account, never its own (reapable) account. - dex/conduit.go: conduitForwardCredit / conduitApplyRelease. Escrow is native balance or the TOKEN's trie; the ledger + book live on the D-Chain venue; cross- domain objects are staged via AtomicStager. ZERO durable SetState(0x9999). The nonce marker is unnecessary and unused on this path. Reuses the proven C<->D object codec (native_wire.go), DeriveIntentID, and the raw escrow helpers (native balance / pullERC20Terminal under-delivery guard). - dex/conduit_reap_test.go: the INVERSION of native_zap_realstate_test.go over a REAL *state.StateDB (EIP-158 on) + REAL cross-chain atomic.SharedMemory + a minimal-but- real D-Chain ledger venue. Drives ERC-20 deposit -> D-credit -> withdraw and proves: 0x9999 holds ZERO own storage, is reaped at Finalise(true) as a NO-OP, escrow (token trie) + claim (D ledger) intact + withdrawable, conservation holds (no mint/loss). Plus the crisp RED/GREEN discriminator: same record reaped on empty 0x9999, survives on the non-empty host account. Tests: TestConduit_OwnStorageReapIsNoOp_RealStateDB, TestConduit_ReapDiscriminator_HostAccountVsConduit — GREEN (GOWORK=off CGO_ENABLED=0); full dex suite green (no regressions). SYNC carry-fills no-fork primitive verified green in chains/dexvm (TestRED_PerValidatorRelay_SplitsConsensus: identical roots, 0 venue calls at Verify). Forward-only, patch-bump cascade. Old braided money path retained pending cutover (repoint dispatch + delete C-side ledger -> D, embedded-venue deletion). |
||
|
|
a081f8489b |
dex(0x9999): decentralize halt authority + commit the value-path proof tests
HIGH-3 (centralized key): the 0x9999 emergency-halt authority was a single
hardcoded EOA (DefaultDAOTreasury 0x9011…94714), which per memory is the devnet
"Maker" derivable from LUX_MNEMONIC — so anyone with the dev mnemonic could
setHaltAsset(real,true) (censor) or setHaltGlobal(true) (DoS the whole DEX). It
also gated seedSeamReserve/creditPositionFee. A single admin key, not governance.
Fix: the halt + pot-seeding authority is now the per-NETWORK DEX governance
controller resolved at RUNTIME from contract.AtomicState.GovernanceController()
(the same seam networkID/cChainID/dChainID flow through) — a governance CONTRACT
the host binds from its deployment topology, NEVER a dev-mnemonic EOA.
- contract.AtomicState gains GovernanceController() common.Address.
- halt9999.go: new governanceController(state) helper + ErrSettleNoGovernance;
fail-closed when no AtomicState or the network configured no controller (zero
address) — every halt/seed reverts, no default key to abuse. Strictly safer
than a default key: an unset network can be neither censored nor DoS'd.
- settle_module.go: DELETE DefaultDAOTreasury (the mnemonic EOA) and the
SettleContract.protocolFeeController field; SettleContract is now stateless
w.r.t. authority. The 4 gates (setHaltGlobal/Scoped, seedSeamReserve,
creditPositionFee) resolve gov from runtime, then check caller == gov.
- access_observer.go: observingAccessibleState forwards GovernanceController to
the inner (fail-closed when non-atomic) so the L2 write-set wrapper preserves
the capability.
- Conservation is unchanged: only the authority resolution moved; value-movement
logic (observed-delta seed, the pots, checkHalt) is untouched.
HIGH-1/HIGH-2 (integrity): commit the RED #5/#8/#11 proof tests that were
untracked — gatec_red_probe_test.go + gatec_red_dos_test.go — so the value path's
behavior is reproducible at this commit.
LOW-1: swap_sync.go doc comment updated (pre-permissionless "registry admission"
wording → the permissionless canonical resolver) so doc + behavior agree.
TDD (halt_governance_test.go + dex_test.go rewrite, all green):
- the retired 0x9011 EOA can NO LONGER halt (ErrUnauthorized);
- an arbitrary EOA cannot halt;
- ONLY the configured governance controller can halt (global/asset/market) and
the halt BITES — TestHIGH3_HaltBitesARealFill: governance halts a real market,
a fillable swap reverts ErrMarketHalted, the retired EOA cannot lift it, only
governance lifts it, then the swap fills;
- with no governance configured, NOBODY can halt (fail-closed, ErrSettleNoGovernance);
- the retired EOA cannot seed pots either;
- RED CASE 6 (governance cannot mint/move funds) still passes under the new authority.
|
||
|
|
73f1772a64 |
dex(0x9999): decentralize halt authority + commit the value-path proof tests
HIGH-3 (centralized key): the 0x9999 emergency-halt authority was a single
hardcoded EOA (DefaultDAOTreasury 0x9011…94714), which per memory is the devnet
"Maker" derivable from LUX_MNEMONIC — so anyone with the dev mnemonic could
setHaltAsset(real,true) (censor) or setHaltGlobal(true) (DoS the whole DEX). It
also gated seedSeamReserve/creditPositionFee. A single admin key, not governance.
Fix: the halt + pot-seeding authority is now the per-NETWORK DEX governance
controller resolved at RUNTIME from contract.AtomicState.GovernanceController()
(the same seam networkID/cChainID/dChainID flow through) — a governance CONTRACT
the host binds from its deployment topology, NEVER a dev-mnemonic EOA.
- contract.AtomicState gains GovernanceController() common.Address.
- halt9999.go: new governanceController(state) helper + ErrSettleNoGovernance;
fail-closed when no AtomicState or the network configured no controller (zero
address) — every halt/seed reverts, no default key to abuse. Strictly safer
than a default key: an unset network can be neither censored nor DoS'd.
- settle_module.go: DELETE DefaultDAOTreasury (the mnemonic EOA) and the
SettleContract.protocolFeeController field; SettleContract is now stateless
w.r.t. authority. The 4 gates (setHaltGlobal/Scoped, seedSeamReserve,
creditPositionFee) resolve gov from runtime, then check caller == gov.
- access_observer.go: observingAccessibleState forwards GovernanceController to
the inner (fail-closed when non-atomic) so the L2 write-set wrapper preserves
the capability.
- Conservation is unchanged: only the authority resolution moved; value-movement
logic (observed-delta seed, the pots, checkHalt) is untouched.
HIGH-1/HIGH-2 (integrity): commit the RED #5/#8/#11 proof tests that were
untracked — gatec_red_probe_test.go + gatec_red_dos_test.go — so the value path's
behavior is reproducible at this commit.
LOW-1: swap_sync.go doc comment updated (pre-permissionless "registry admission"
wording → the permissionless canonical resolver) so doc + behavior agree.
TDD (halt_governance_test.go + dex_test.go rewrite, all green):
- the retired 0x9011 EOA can NO LONGER halt (ErrUnauthorized);
- an arbitrary EOA cannot halt;
- ONLY the configured governance controller can halt (global/asset/market) and
the halt BITES — TestHIGH3_HaltBitesARealFill: governance halts a real market,
a fillable swap reverts ErrMarketHalted, the retired EOA cannot lift it, only
governance lifts it, then the swap fills;
- with no governance configured, NOBODY can halt (fail-closed, ErrSettleNoGovernance);
- the retired EOA cannot seed pots either;
- RED CASE 6 (governance cannot mint/move funds) still passes under the new authority.
|
||
|
|
9cfae862cf |
dex(0x9999): always-on settlement precompile, runtime params, zero per-net config
0x9999 — the native C<->D atomic DEX settlement money path — is now ALWAYS-ON:
present + active on every chain that loads the EVM plugin, from genesis, with NO
dexSettleConfig genesis/upgrade entry. This is the production-launch design; the
per-net config-gate is killed. Forward-perfect, one activation way.
Activation: modules.Module gains an AlwaysOn flag (+ modules.AlwaysOnModules()).
SettleModule sets AlwaysOn=true. The Configurator is retained only to satisfy the
interface; for an always-on module the host never invokes Configure (no activating
config). SettleConfig collapses to a parameter-free {Upgrade} marker that always
Verifies — 0x9999 takes no per-net parameters.
Params resolved at RUNTIME, not config (Configure has no consensus context, so it
cannot resolve them):
- protocolFeeController: built-in DefaultDAOTreasury (set in init). One canonical
DAO governance authority across every network; not a compromised dev key.
- networkID / cChainID: already sourced at runtime via contract.AtomicState.
- dChainID (THE crux): contract.AtomicState gains DChainID(). The 4 native-client
sites that read loadDChainTarget(stateDB) now read atomicState.DChainID(), which
the host resolves from the chain topology (consensus-context "D"/dexvm alias).
ids.Empty (no dexvm on this network) closes the seam — fail-secure, no mint.
Money-path logic is byte-for-byte unchanged: DeriveIntentID/DerivePositionCommitID,
lock/stage/credit, conservation, exactly-once, rail discriminator, FIX-2 one-batch
flush, nonReentrant/custody guards all untouched. The ONLY change is HOW the
precompile activates and obtains its params (runtime, not config). The Empty-dChain
revert is preserved (strengthened: SubmitSwapIntent/SubmitPositionCommit now revert
on Empty instead of falling back to the C-Chain's own id).
Dead config machinery removed (one way, no dead code): SetSettle{ChainIdentity,
DChainTarget}, loadDChainTarget, loadSettleChainIdentity, cfg{Net,CChain,DChain}IDKey
slots, the STM read-set entries for them, and the now-unused kvAdapter Configure bridge.
Tests: full dex suite 335/335 PASS, 0 fail, 0 skip (CGO_ENABLED=0; the test binary's
only CGO dep is luxfi/accel, unrelated to the pure-Go money path). Harness feeds the
D peer via the runtime AtomicState capability, mirroring production exactly — proof
that the money path is intact with zero config.
go.sum: reconcile e2e threshold v1.9.4 to the origin-canonical hash (upstream re-tag).
|
||
|
|
91539c69bc |
dex(0x9999): always-on settlement precompile, runtime params, zero per-net config
0x9999 — the native C<->D atomic DEX settlement money path — is now ALWAYS-ON:
present + active on every chain that loads the EVM plugin, from genesis, with NO
dexSettleConfig genesis/upgrade entry. This is the production-launch design; the
per-net config-gate is killed. Forward-perfect, one activation way.
Activation: modules.Module gains an AlwaysOn flag (+ modules.AlwaysOnModules()).
SettleModule sets AlwaysOn=true. The Configurator is retained only to satisfy the
interface; for an always-on module the host never invokes Configure (no activating
config). SettleConfig collapses to a parameter-free {Upgrade} marker that always
Verifies — 0x9999 takes no per-net parameters.
Params resolved at RUNTIME, not config (Configure has no consensus context, so it
cannot resolve them):
- protocolFeeController: built-in DefaultDAOTreasury (set in init). One canonical
DAO governance authority across every network; not a compromised dev key.
- networkID / cChainID: already sourced at runtime via contract.AtomicState.
- dChainID (THE crux): contract.AtomicState gains DChainID(). The 4 native-client
sites that read loadDChainTarget(stateDB) now read atomicState.DChainID(), which
the host resolves from the chain topology (consensus-context "D"/dexvm alias).
ids.Empty (no dexvm on this network) closes the seam — fail-secure, no mint.
Money-path logic is byte-for-byte unchanged: DeriveIntentID/DerivePositionCommitID,
lock/stage/credit, conservation, exactly-once, rail discriminator, FIX-2 one-batch
flush, nonReentrant/custody guards all untouched. The ONLY change is HOW the
precompile activates and obtains its params (runtime, not config). The Empty-dChain
revert is preserved (strengthened: SubmitSwapIntent/SubmitPositionCommit now revert
on Empty instead of falling back to the C-Chain's own id).
Dead config machinery removed (one way, no dead code): SetSettle{ChainIdentity,
DChainTarget}, loadDChainTarget, loadSettleChainIdentity, cfg{Net,CChain,DChain}IDKey
slots, the STM read-set entries for them, and the now-unused kvAdapter Configure bridge.
Tests: full dex suite 335/335 PASS, 0 fail, 0 skip (CGO_ENABLED=0; the test binary's
only CGO dep is luxfi/accel, unrelated to the pure-Go money path). Harness feeds the
D peer via the runtime AtomicState capability, mirroring production exactly — proof
that the money path is intact with zero config.
go.sum: reconcile e2e threshold v1.9.4 to the origin-canonical hash (upstream re-tag).
|
||
|
|
49fe03803c |
dex: native C<->D atomic settlement seam (0x9999) + Red fixes 2/3/4
The 0x9999 money path: V4 ABI swap -> NativeDChainClient -> C staged -> shared-memory export/import -> D dexvm atomic import/export (Mode A async, no BLS). C SETTLES, C never matches. Value crosses ONLY as a primary-network atomic shared-memory object. The BLS cert / VerifierRegistry / DFillReceipt value path is ripped (quarantined under dex/_deprecated_bls/). Ship rule (structural, not trust): C credited ONLY by consuming a D->C object (ImportSettlement); D funded ONLY by consuming a C->D object (SubmitSwapIntent -> dexvm executeImport). Atomic ops are STAGED in EVM state during execution (revert-aware) and flushed to shared memory at block accept over the deterministic parent->current seq window. contract.AtomicState: the optional host capability a precompile type-asserts to reach atomic shared memory (the platformvm/dexvm import/export primitive). Red fixes applied to the seam: FIX 2 (consensus-liveness chain-halt): native_staging.go FlushAcceptedAtomicOps already takes a batch; the flush is now one-batch-atomic at the host (see evm block.go/vm.go) — the SM Apply and the seq-marker advance commit all-or- nothing, closing the crash-between-Apply-and-marker -> duplicate-Put -> halt window. Regression: native_haltwindow_test.go (re-accept after a committed window is a clean no-op; crash-before-commit re-applies cleanly; the marker must ride the Apply batch). FIX 3 (conservation blast-radius): native_state.go adds seamReserve[a], the seam's OWN pot, decoupled from the depositor pot (settleVault) and the maker pot (makerLockedVault). lockIntentInput funds seamReserve; creditSettlementOutput checks+debits seamReserve. A settlement credit can never raid a depositor's claim; a withdraw can never strand a backed settlement. Invariant realHolding == settleVault + makerLockedVault + seamReserve. Regression: native_conservation_test.go (both subsystems, same asset). FIX 4 (reentrancy defense-in-depth): SettleSwap now takes the same single custody guard slot as deposit/withdraw/modifyLiquidity (enterCustodyKV), so the whole 0x9999 money surface is single-in-flight and an ERC-20 transfer callback cannot re-enter. Regression: native_callindex_test.go (callIndex determinism + intent-id collision-freedom across two swaps + a reverted sub-frame + a STATICCALL; SettleSwap non-reentrant + guard released for sequential calls). 305 prior + 10 new dex tests pass (CGO_ENABLED=0). Live-EVM-call-tree and live-crash-injection tests are CI-only (CGO + luxfi/accel LUXCPP). |
||
|
|
51828edc63 |
dex: native C<->D atomic settlement seam (0x9999) + Red fixes 2/3/4
The 0x9999 money path: V4 ABI swap -> NativeDChainClient -> C staged -> shared-memory export/import -> D dexvm atomic import/export (Mode A async, no BLS). C SETTLES, C never matches. Value crosses ONLY as a primary-network atomic shared-memory object. The BLS cert / VerifierRegistry / DFillReceipt value path is ripped (quarantined under dex/_deprecated_bls/). Ship rule (structural, not trust): C credited ONLY by consuming a D->C object (ImportSettlement); D funded ONLY by consuming a C->D object (SubmitSwapIntent -> dexvm executeImport). Atomic ops are STAGED in EVM state during execution (revert-aware) and flushed to shared memory at block accept over the deterministic parent->current seq window. contract.AtomicState: the optional host capability a precompile type-asserts to reach atomic shared memory (the platformvm/dexvm import/export primitive). Red fixes applied to the seam: FIX 2 (consensus-liveness chain-halt): native_staging.go FlushAcceptedAtomicOps already takes a batch; the flush is now one-batch-atomic at the host (see evm block.go/vm.go) — the SM Apply and the seq-marker advance commit all-or- nothing, closing the crash-between-Apply-and-marker -> duplicate-Put -> halt window. Regression: native_haltwindow_test.go (re-accept after a committed window is a clean no-op; crash-before-commit re-applies cleanly; the marker must ride the Apply batch). FIX 3 (conservation blast-radius): native_state.go adds seamReserve[a], the seam's OWN pot, decoupled from the depositor pot (settleVault) and the maker pot (makerLockedVault). lockIntentInput funds seamReserve; creditSettlementOutput checks+debits seamReserve. A settlement credit can never raid a depositor's claim; a withdraw can never strand a backed settlement. Invariant realHolding == settleVault + makerLockedVault + seamReserve. Regression: native_conservation_test.go (both subsystems, same asset). FIX 4 (reentrancy defense-in-depth): SettleSwap now takes the same single custody guard slot as deposit/withdraw/modifyLiquidity (enterCustodyKV), so the whole 0x9999 money surface is single-in-flight and an ERC-20 transfer callback cannot re-enter. Regression: native_callindex_test.go (callIndex determinism + intent-id collision-freedom across two swaps + a reverted sub-frame + a STATICCALL; SettleSwap non-reentrant + guard released for sequential calls). 305 prior + 10 new dex tests pass (CGO_ENABLED=0). Live-EVM-call-tree and live-crash-injection tests are CI-only (CGO + luxfi/accel LUXCPP). |
||
|
|
636478f0aa | fix: gofmt -s across repo (CI format check) | ||
|
|
aadb570cbd |
precompile/fhe: close Mul DoS — re-derive gas at real 12M mainnet limit + activation gate
The pre-2026 adversarial test sized the FHE Mul gas (750_000) against a
30M block gas limit, but the real Lux primary-network C-Chain genesis
gasLimit is 12_000_000 (genesis/configs/mainnet/cchain.json line 16).
At 750k gas/Mul + 12M block limit, 16 Muls fit per tx; at ~78s per Mul
on commodity ARM (Apple M1 Max, measured 2026-06-01), one tx stalls a
validator for >20 minutes against a 2s block-target. Sustained: chain
halt at <0.4 LUX cost.
Fix is two-axis and bench-driven:
1. Re-derive every FHE Gas* constant against measured wall-clock and a
1ms-per-(12_000-gas) calibration. New GasMul = 936_000_000 (78s ×
12k); GasAdd = 180_000_000 (15s × 12k); etc. These exceed the 12M
block-gas-limit, which is the protection: zero Muls fit per block,
so no DoS surface remains.
2. Add an activation gate in fhe.module.Configure that refuses fheConfig
when the chain's effective gasLimit < GasMul × MinSafeGasMulRatio.
The gate uses a new feature-detection interface
contract.FeeConfigReporter (mirrors the StrictPQReporter pattern):
ChainConfigs that implement it engage the gate; those that don't
(cross-chain integrators) remain permissive. The error type is
contract.ErrFHEUnsafeGasLimit.
Tests:
- adversarial_test.go reworked into three explicit fixtures:
TestFHEGasCost_Mul_DoSResistance_{30M_Reference,12M_Mainnet,
20M_PostFeeManager}. Each asserts max ops/block × per-op-ms <= budget.
- module_test.go: 7 activation-gate cases (refuses at 12M/20M/30M;
permits at >= GasMul; permissive without FeeConfigReporter; nil chain
config; wrong config type).
- dos_audit.go + dos_audit_test.go: in-tree cross-precompile DoS audit
table walked by tests at both 12M and 20M gasLimit. ML-DSA, SLH-DSA,
Magnetar, Pulsar, BLS12-381 pairing, ZK Groth16, FROST, CGGMP21, VRF,
HQC, P3Q all measured safe; FHE rows priced out (the activation gate
is what surfaces them explicitly).
- bench_test.go: BenchmarkFHE{Mul,Add}_Uint8 — the canonical
measurement-derivation hooks. Re-run on each arch to regenerate
WallClockMs* constants.
- contract/feeconfig_test.go: 8 cases for RequireGasLimit (no reporter;
nil chainConfig; reporter above/at/below threshold; reporter
abstains; time-aware feeConfigManagerConfig flip; nil block context).
The e2e/fhe_private_auction test was pinning the OLD gas constants
verbatim; updated to assert the bench-derived calibration so a future
constant drift surfaces as test failure rather than silent regression.
NO CAVEAT. Either the gas is sufficient (gasLimit >= GasMul ×
MinSafeGasMulRatio) OR the precompile refuses to activate. The FHE
precompile is currently in the "refuses to activate" regime on real
mainnet C-Chain (gasLimit=12M < 936M required). Recovery is operator-
side: raise gasLimit via feeConfigManagerConfig OR land a faster TFHE
evaluator (see memory/vulkan_tfhe_pbs_fast_path.md — 20.4x landed,
need 78x to close the gap).
|
||
|
|
f69b524211 |
precompile: decomplect strict-PQ gate across every classical Run() (#2)
* precompile: pulsar threshold ML-DSA at LP-4200 0x012204 Adds the Pulsar precompile slot for threshold ML-DSA verification. Pulsar's wire format is byte-equal to FIPS 204 ML-DSA (Class N1 manifesto), so the verification op dispatches to luxfi/crypto/mldsa under a Pulsar-specific domain-separation context. - Address: 0x0000000000000000000000000000000000012204 (LP-4200 unified PQCrypto block; companion to ML-KEM 0x012201, ML-DSA 0x012202, SLH-DSA 0x012203). - Three modes: Pulsar-44 / Pulsar-65 / Pulsar-87 mirroring ML-DSA Categories 2 / 3 / 5. Production target is Pulsar-65. - Gas: per-mode base + 10/byte (matches ML-DSA precompile tier). - Context: lux-evm-precompile-pulsar-v1 to keep on-chain transcripts unambiguous vs. single-party ML-DSA at 0x012202. Tests: 5 passing (address pin, gas table, round-trip across 3 modes, tampered-signature rejection, invalid-mode rejection). GOWORK=off go test -count=1 ./pulsar/... is green. Imports github.com/luxfi/pulsar-m (pre-rename); will be retargeted to github.com/luxfi/pulsar when the cross-repo move lands. * precompile/LLM.md: mark Pulsar 0x012204 as IMPLEMENTED Pulsar precompile now lives in precompile/pulsar/ and dispatches to luxfi/crypto/mldsa under a Pulsar-specific context. Update the LP-4200 canonical note to reflect deployment status alongside the other three PQCrypto slots. * precompile/zk: rip classical pairing-based opcodes (PQ-only) Shor breaks Groth16/PLONK/fflonk/Halo2/KZG/IPA/Bulletproofs. The 0x0900 EVM precompile now exposes only the PQ-safe ops: hash-based nullifiers (0x21) and Merkle commitment inclusion (0x22). PQ STARK verification is dispatched via the Z-Chain envelope path (consensus/protocol/zchain/verify.go), not from this precompile. * precompile/kzg4844: retire BLS12-381 KZG blob-opening precompile KZG point-evaluation verifies via the BLS12-381 pairing e(C - [y]G1, G2) == e(W, [tau - z]G2). The discrete-log assumption that anchors BLS12-381's security is broken by Shor's algorithm, so EIP-4844-style blob commitments give zero soundness against a future quantum adversary. The Lux EVM is PQ-only. Strip the implementation (gokzg4844, accel GPU MSM, all CPU opcodes). The module still registers the address 0xB002 so the precompile registry numbering stays stable, but every call to the precompile returns the new contract.ErrClassicalForbiddenInPQ sentinel. The minimum 21000 gas is deducted on every call to prevent the retired address being used as a zero-cost probe. Blob availability on the Lux EVM is now provided by hash-based commitments (Blake3 / Poseidon2) and STARK-validated state roots. * precompile/zk: strip Go-level classical pairing SNARK verifiers The Run() path of the 0x0900 ZK precompile only exposes hash-based nullifier checks and Merkle commitment inclusion, both PQ-safe. The remaining Go-level methods on ZKVerifier (VerifyGroth16, VerifyPlonk, VerifyFflonk, VerifyKZG, VerifyRangeProof, VerifyRollupBatch and their helpers) all dispatched through a BN254 pairing check or a BLS12-381 KZG opening, both broken by Shor's algorithm. Strip those methods, their bn256 / kzg4844 / accel-MSM dependencies, and the rollup-batch types (RollupBatch, RollupConfig, StateTransition, KZGSetup, BN254P) that only existed to feed the pairing verifier. Update verifier_test.go to cover only the PQ-safe surface that remains: NewZKVerifier, RegisterVerifyingKey (Go-side bookkeeping), nullifier check/spend, confidential pool create/add-commitment, hash-based Merkle inclusion. PQ STARK verification continues to be handled by the P3Q backend on the Z-chain envelope path (consensus/protocol/zchain/verify.go); it was never wired through this Go struct. * precompile/babyjubjub: retire BN254-companion twisted Edwards curve Baby Jubjub is the ZK-friendly twisted Edwards curve aligned to BN254's scalar field (used by circom / iden3 / Polygon zkEVM / Semaphore / RLN / EdDSA-on-BN254). Its security reduces to discrete-log on a curve of BN254 scalar order; Shor breaks that in poly time. Every consumer of Baby Jubjub is a classical pairing-based SNARK gadget with no PQ analogue, so the Lux EVM retires the precompile rather than offer a quantum-broken primitive. Strip the gnark-crypto twisted-edwards implementation. The module still registers address 0x0500..0007 so the precompile registry numbering stays stable, but every call returns contract.ErrClassicalForbiddenInPQ with 21000 minimum gas deducted. * precompile/pedersen: retire BN254 Pedersen commitment precompile The implementation built homomorphic Pedersen commitments over BN254 G1 (C = v*G + r*H, plus a 32-slot vector commitment). Both binding and hiding reduce to the discrete-log assumption on BN254, which Shor's algorithm breaks. The Lux EVM is PQ-only and retires the precompile rather than offer a quantum-broken commitment. Strip the gnark-crypto BN254 implementation and the 34 deterministic generators. The module still registers address 0x0500..0006 so the precompile registry numbering stays stable, but every call returns contract.ErrClassicalForbiddenInPQ with the 21000 minimum gas deducted. PQ-safe commitment schemes on the Lux EVM use hash-based primitives (Blake3 0x3202, Poseidon2 0x3201) and STARK gadgets on Z-chain. * Revert "precompile/pedersen: retire BN254 Pedersen commitment precompile" This reverts commit |
||
|
|
e79a887d0e |
fix: P0-P2 hardening — consensus safety, OOM prevention, error unification
P0 consensus-critical:
- FHE: deterministic keygen from SHA-256("LUX_FHE_KEYGEN_v1"), StateDB
ciphertext storage (replaces ephemeral in-memory ctStore), remove
crypto/rand fallback
- time.Now() eliminated from 20 state-modifying paths across bridge (12),
dex (7), attestation (5) — all now use blockTimestamp parameter
- WebSocket: 1024-client semaphore, 64KB read limit, 60s deadline, pong
handler, origin check on chain/dag/evm indexer subscribers
P1 correctness:
- I-Chain indexer: identity.Health → identity.getLatestBlock
- Graph pagination: min(limit, 1000) cap across 25 resolver files
- Error sentinels: 31 packages unified to contract.ErrInvalidInput
P2 standardization:
- 14 packages migrated from manual gas checks to contract.DeductGas()
- Gas naming: {Name}Gas → Gas{Name} in secp256r1, ed25519, math, quasar
- Registry address format comment clarified (PCII vs legacy ranges)
37/37 precompile tests pass.
|
||
|
|
0e44530458 |
chore: remove AI slop, fix stubs, delete dead code
Cleanup: - Delete docs/ (Next.js site in Go repo) - Delete 11 package-level README.md files (AI-generated filler) - Remove orphaned SelectorDonate and runDonate stub from dex - Remove dead applyChainAdjustment (every branch returned input unchanged) - Remove unused GetChainId (hardcoded to 96369, never called) - Remove stale TODOs from fhe/module.go and contract/utils.go Fixes: - ai/verifyTEESignature: enforce min 64-byte signature length instead of accepting any non-empty bytes (was a security hole) - ai/ai_mining_test.go: use 64-byte test signature to match new check All 28 packages pass. |
||
|
|
dde5838640 | chore: sync dependencies and format code | ||
|
|
7ec8994ab0 |
chore: sync with latest dependencies and add tests
- Update frost, mldsa, mlkem contracts - Add contract tests - Add threshold accel support |
||
|
|
acd2aaf533 |
feat: rename module to github.com/luxfi/precompile
BREAKING CHANGE: Module renamed from precompiles to precompile (singular) - Remove lattice/v6 indirect dependency - Exclude lattice/v6 versions to prevent transitive imports - Only lattice/v7 is used now |
||
|
|
941c515a08 |
fix(contract): simplify PrecompileEnvironment interface
The minimal interface only requires ReadOnly() which is sufficient for stateless precompiles like PQ crypto. This removes the need for complex adapters in geth. |
||
|
|
2148cab0ca |
fix(contract): break import cycle with geth/core/vm
- Define PrecompileEnvironment locally instead of importing from geth/core/vm - Define ErrOutOfGas locally instead of using vm.ErrOutOfGas - Update crypto dependency to v1.17.26 This fixes the import cycle: precompiles/contract -> geth/core/vm -> precompiles |
||
|
|
53c9e7d62c |
chore: update PQ crypto precompiles to use luxfi/crypto wrappers
- mldsa: use luxfi/crypto/mldsa wrapper - pqcrypto: use luxfi/crypto for all PQ operations - corona: update imports - cggmp21: update imports - contract: update interfaces and utils |
||
|
|
16ff682fd7 | feat: consolidate all precompiles from standard repo |