476 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 9ababce758 A precompile's config key names the precompile
Forty-five of the fifty-six suite modules keyed off <name>Config and eleven did
not, in three other spellings: Verify, Threshold, Encapsulate. The LP-4200
unified PQCrypto block carried four of those conventions across its eight
addresses — two KEMs named differently (mlkemConfig beside hqcEncapsulate) and
two threshold schemes named differently (pulsarVerify beside coronaThreshold) —
so the block that exists to be one thing did not read as one thing.

The deviations also named an OPERATION where a precompile belongs. HQC
decapsulates as well as encapsulates and the Verify precompiles do more than
verify, so hqcEncapsulate was the name of one entry point standing in for the
whole module. A config key selects a precompile; it is not a method selector.

All fifty-six are <name>Config now. There is no compatibility shim: an
upgrade.json naming an old key is naming something that does not exist, which
luxd already refuses loudly, and every chain config in the estate moves with this.

Co-authored-by: Hanzo Dev <[email protected]>
v0.19.22
2026-09-10 19:27:25 -07:00
zeekayandHanzo Dev 748ea99f91 go 1.26.8
One toolchain across the estate. The tree carried 1.26.0, .1, .3, .4 and .5
at once, so which compiler built a binary depended on which repo it came
from.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 00:00:05 -07:00
zeekayandHanzo Dev b5a79f7e49 chain letters are lowercase
The primary-network chains answer to either case; one spelling beats two.
/v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 15:24:21 -07:00
zeekayandHanzo Dev 1e5aebe404 ci: a runner label names the platform, not the org
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.

One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:25:46 -07:00
zeekayandHanzo Dev 6f9051df3a feat(ai): permit sovereign mining without TEE
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:41:14 -07:00
zeekayandHanzo Dev 32a931e1c7 State a version once
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.

Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:41:14 -07:00
zeekayandHanzo Dev 84587ddb0f Resolve the market before a swap can name a halt
SettleSwap took the caller's PoolKey and never asked whether that pool
existed, so key.ID() fed the halt slot, the volume shard and the fill
event alike. A fabricated key names a slot nobody set, and the swap
settles while the real market is halted.

Resolving once above the phase switch and passing the resolved poolID
down means no phase -- including one added later -- can key on anything
the registry has not admitted. checkHalt no longer accepts a key, so it
cannot derive one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 02:41:14 -07:00
zeekay 950f864c1d Mined work settles on the C-Chain, at the C-Chain's id
The cross-chain mining tests minted against chain id 420420, which is not a Lux
network. The chain id is part of every work id — it is what stops the same
attested work being minted twice on two chains — so a number invented for a
test is one that can collide with a real one, and the collision shows up only
as work that mints on both.

It is the C-Chain's id now, 96369, because that is where the mint happens. The
A-Chain produces the work and is not an EVM, so it has no chain id to lend.

The package comment also listed three networks by name. This repository is
Lux's, and what the precompile does does not depend on who else runs it.
2026-09-07 18:04:12 -07:00
zeekayandHanzo Dev 47a8082a1b ai: the interface stops advertising a selector nobody may call
markSpent is closed in the implementation, so the published ABI documenting it
as "callable by authorized contracts" now describes something that cannot
happen — and that promise was the original defect: the interface asserted an
authorization the Go code never implemented. The selector is retained so a
caller gets an explicit refusal instead of "unknown selector", and the comment
says plainly that spending is not a separate step: the atomic mint paths mark
work spent as the last action of admitting a claim.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-05 15:20:24 -07:00
zeekayandHanzo Dev d722de8465 ai: close markSpent, and bound what one attested device can claim
markSpent was an unauthenticated public selector on the double-spend guard.
workId = BLAKE3(deviceId‖nonce‖chainId) is computable offline, so any address
could mark any claim spent before its owner presented it — denying the reward,
and, where a missing claim reads as a missing attestation, forcing a slash. The
Solidity interface has always documented "only callable by authorized
contracts"; no check existed and ErrUnauthorized sat declared and unused. It now
fails closed: the atomic paths mark spent internally as the last step of
admitting a claim, which is the only safe ordering, so nothing legitimate is
lost and there is no authority to guess at.

The mint paths already require a TEE quote and bind it to the signing key
(ErrMissingAttestation / ErrDeviceKeyMismatch), which establishes that a real
device signed the claim. It does not establish that the claim is true: an
attested device still writes its own computeMinutes and dataSize, and the
reward schedule multiplies them. Unbounded, one sovereign-privacy data claim
settles 1e18 * (2^64-1) * 1.5 / 1e4 = 2.767e19 AI. Both magnitudes are now
bounded per claim — a ceiling on how wrong one claim can be, not a substitute
for the binding above.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-05 15:07:14 -07:00
zeekayandHanzo Dev a12bcbed4c e2e/clob: require the dex that is published, not the one beside the checkout
The e2e module reached github.com/luxfi/dex through a replace to ../../../dex,
which resolves on one machine and fails on every other; the require it masked
still said v1.5.4. It now requires v1.14.37, the tag the checkout beside it
sits nearest to, and the replace is gone. The replace to ../.. stays: that one
points inside this repository, which is how a nested module builds against
the tree it lives in.

Co-authored-by: Hanzo Dev <[email protected]>
v0.19.21
2026-09-04 09:36:06 -07:00
zeekay 6700d99adc Pin past the versions the proxy can no longer serve
The 2026-07-15 retag moved these tags, so proxy.golang.org answers not-found, Go
falls through to direct, and the bytes GitHub serves now disagree with
sum.golang.org. The build stopped at a checksum before reaching a compiler.
Those versions can never verify again; moving past them is the repair.
2026-09-01 15:55:09 -07:00
zeekay f95f9f3cf6 Keep the forward pins only where the module builds
The previous commit carried go.mod changes from modules that do not build,
which is not a state worth publishing. Restore those and keep the pins for
the modules that resolve and build clean.
2026-09-01 13:26:20 -07:00
zeekay 8fe308bbbe Pin past the versions the proxy can no longer serve
The bulk retag moved published tags. go.sum and sum.golang.org hold the
original bytes, proxy.golang.org answers not-found, and the direct fetch
disagrees, so builds stopped at a checksum before reaching a compiler.
Pin forward to versions the proxy still serves.
2026-09-01 13:11:10 -07:00
zeekay 9ae0bb824e Pin past the versions the proxy can no longer serve
The bulk retag moved published tags. go.sum and sum.golang.org hold the
original bytes, proxy.golang.org answers not-found, and the direct fetch
disagrees, so builds stopped at a checksum before reaching a compiler.
Pin forward to versions the proxy still serves.
2026-09-01 11:10:20 -07:00
zeekay a777ace666 Pin past the versions the proxy can no longer serve
The 2026-07-15 retag moved these tags: proxy.golang.org answers not-found, Go
falls through to direct, and the bytes GitHub serves now disagree with
sum.golang.org, so the build stopped at a checksum before reaching a compiler.
Those versions can never verify again; moving past them is the repair.
2026-09-01 10:30:08 -07:00
zeekay 30327bb7ac docs: chain routes are /v1/chain
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181):

  /v1/bc/<alias>/...   -> /v1/chain/<alias>/...
  /ext/bc/<alias>/...  -> /v1/chain/<alias>/...

Observations keep their original addresses: a dated note or a recorded status
code is a fact about when it was written, not an instruction to follow.
2026-08-30 17:17:26 -07:00
zeekay 9d7d771465 Cite specs that exist, at their current numbers
LP-4835 now specifies 0x012220, so the STARK-FRI slot is allocated
rather than a placeholder, and 0x012205 next door verifies the P3Q
threshold-signature family. The 0x999x constants were labelled with LP
numbers that were never issued; only LP-9999 exists, and its address
table specifies the family.
v0.19.19 v0.19.18
2026-08-28 20:29:37 -07:00
zeekay 8ee37084f2 Merge remote-tracking branch 'github/main' 2026-08-28 20:21:04 -07:00
zeekay 5ab320e270 dex, fhe: finish the refactor the merge half-applied
collectPosition's calldata is a fixed 156 bytes now — claimID, asset,
positionID, and the 60-byte claim — so the two tests that built the old
128-byte layout were refusing on length before reaching what they meant to
exercise. The reentrancy test carries a real claim; the amount-domain test
becomes a width sweep, because there is no longer an amount word to put out of
range: the amount is the uint64 inside the claim, which a caller cannot widen.
ErrLPCollectBadAmount goes with it, having become a sentinel nothing returns.

The swap arm's write witness moves 13 -> 10. One custody pot and no per-order
escrow is less state, and the witness exists to make that a decision rather
than a drift.

fhe's derived gas comments still stated the products of the old wall-clock
figures — 180M and 936M against constants that now compute 300M and 2.88G.
2026-08-28 20:19:27 -07:00
zeekay daa64e3332 fix(deps): re-record the hashes of the tags that moved
github.com/luxfi/[email protected] and luxfi/[email protected].2 were force-moved after
this repo recorded them, so `go mod` stopped at SECURITY ERROR: checksum
mismatch before reaching a compiler.

The replacement bytes are ours — the go.mod the proxy serves for [email protected] is
byte-identical to the one at our own git tag (commit 243d2d3a, 2026-07-15).
go.sum held the tag's earlier content, so this writes down what the tag now
is rather than trusting something new. Four lines.

Verified: `go mod tidy` exits 0 and no checksum error remains anywhere in the
graph. NOT verified: a full build. It now stops on `pkg-config: luxgpu not
found` and a missing lux/gpu/hqc.h, which is this machine lacking the native
GPU libraries — the same gap that stops luxfi/crypto here, present before this
change and unrelated to it.

A moved tag is the thing to stop doing. Go treats a version as immutable and
every consumer holds you to it; the fix for a bad release is the next patch,
never the same number twice.
2026-08-28 20:15:03 -07:00
zeekay 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.
2026-08-28 20:13:25 -07:00
zeekay 0b34639af4 the corporate name is Lux Industries Inc 2026-08-28 15:42:33 -07:00
zeekay 643fad2f68 fhe: measure the comparison defect instead of asserting around it
Every handler is now driven one byte short of its minimum body, on one gas, on
zero gas, and with no state db, and must refuse each without writing state.
That required the selector table to carry each handler's EXACT minimum: a body
longer than the minimum still gets accepted when truncated by one, so the guard
it means to exercise goes unproven.

TestOps_ComparisonsAreHomomorphic no longer asserts lt, le, ge or ne by value.
Over twelve fresh encryptions of the same plaintexts under the same key,
lt(3,5) answered wrong 2 times out of 12, and ne — built as (a<b) OR (a>b) —
inherited exactly the same 2/12, which locates the defect in the primitive
rather than the composition: the noise budget does not survive the comparison
circuit at these parameters, so the answer depends on the randomness drawn at
encryption time. The four are still asserted to be reachable and to answer a
single bit, because a wrong bit and a missing operation are different failures.
Restoring the value assertions is the acceptance test for a parameter fix.
2026-08-28 15:40:00 -07:00
zeekay d4474142f1 dex, e2e, LLM.md: pin the hook and market surfaces, and point e2e at current deps
The D-Chain, hook and market surfaces had behaviour no test observed. e2e/clob
moves to the module versions this repo builds against. LLM.md records the two
orthogonal calldata bounds — Cursor for how many bytes, Unsigned/Signed for
which values — and why hand-writing either is the bug they exist to close.
2026-08-28 15:39:28 -07:00
zeekay e41cd6d4ab crypto precompiles: unvalidated points, a price that could not be paid, and free calls
Across the curve and hash precompiles: decode refuses a point that is not on
its curve or not canonically encoded, rather than handing an off-curve point to
an arithmetic routine; pasta's inversion no longer treats a multiple of p as
invertible, where ModInverse returns nil and leaves the divisor untouched;
compute refuses a quoted price wider than 256 bits instead of returning it
modulo, and enforces readOnly; poseidon's sponge fee grows with its input; hqc
reports an unwired backend as itself rather than as a verification failure; and
sr25519 pins that the cgo and non-cgo verifiers name the same backend and agree,
which is a consensus split when they do not. Every path deducts gas before it
works, so no call is free. KAT vectors accompany each.
2026-08-28 15:39:27 -07:00
zeekay 0ba85e6aaf vrf: an algorithm from one RFC 9381 suite, labelled with another's byte
The package documented ECVRF-EDWARDS25519-SHA512-ELL2 and sent suite byte 0x04,
while the code ran try-and-increment — which is TAI, suite 0x03. The suite byte
is hashed into both the encode-to-curve and challenge digests, so the
combination matched no conforming prover and could not verify a proof from one.
Aligned on TAI throughout, with the RFC's front/back domain separators.

Verification also now runs with validate_key = TRUE (Section 5.4.5). The public
key arrives in calldata, so it is attacker-chosen, and that check is what buys
the collision resistance and unpredictability-under-malicious-key-generation an
on-chain lottery is paying for (Sections 7.1.1, 7.1.3). Point decoding refuses
non-canonical encodings.

Gas follows the work: encode_to_curve hashes the whole alpha string once per
counter, so the fee carries a per-word term, and a call that does no work is
charged rather than free.
2026-08-28 15:39:15 -07:00
zeekay 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.
2026-08-28 15:39:14 -07:00
zeekay 15e5648035 registry, modules: one record of where a precompile lives
registry kept ~400 address constants beside the addresses the precompiles
actually claim in their init(). Two records of one fact, and they had already
drifted: the prose mapped chain nibble 3 to D-Chain while the constants used it
for Q-Chain. The package now derives InFamily from modules.RegisteredModules(),
so a question asked here and an address dispatched by the EVM cannot disagree.
PrecompileAddress, ChainSlot and FamilyPage compute what the table enumerated.

RegisteredModules returned the package slice itself. The host walks it to build
the per-block precompile set, so its order is consensus-visible, and any caller
that sorted or truncated it reordered the registry for every subsequent block.
It returns a copy. The duplicate 0x9000-0x9FFF allowlist range is one entry.
2026-08-28 15:39:13 -07:00
zeekay 96ba923c2f bridge: a governance authorisation that never expired, and two ways to spend an unauthorised call
The registrar digest bound only the selector and the arguments, so a signature
set authorising "register chain 5" stayed valid forever: anyone who saw the
original transaction could re-apply it after an unregister, or re-apply a
superseded SetGateway. The digest now carries a governance nonce that advances
after each successful write. Selector 0x06 reads it. The nonce lives outside
the govSlot namespace so re-seeding cannot revive retired authorisations.

Two more, both reachable without any signature being valid. Every submitted
signature is ECRECOVERed before any of them is known to belong to an operator,
and the flat write fee bought up to 255 of them; signatures are now billed at
the EVM's own ECRECOVER price. And loadGovernance allocated from a count it
read out of storage, which genesis alloc can pre-populate at any address, so a
malformed genesis could ask for four billion addresses at boot.
2026-08-28 15:38:57 -07:00
zeekay a95305827c attestation: a flat fee over a caller-chosen blob, and a sign dropped on encode
RequiredGas charged one number per selector regardless of payload size, but
every verifying selector JSON-decodes the whole payload and hands the receipt
to ai.VerifyTEE, which parses and verifies the X.509 chain inside it. A
megabyte cost the same as a hundred bytes. It now takes the input rather than
the selector and adds a per-byte term, saturating instead of wrapping.

ABIEncode built its word with Bytes() and a computed pad, which encodes -1 as
1 and panics on a value wider than 32 bytes because the pad length goes
negative. U256Bytes covers the whole int256/uint256 range.
2026-08-28 15:38:56 -07:00
zeekay 3e36e41ef1 One version per package, on the current release
Every dependency on a @hanzo/@luxfi/@zooai package names the same version across
the repo: a catalog entry where pnpm supports one, an exact pin where it does
not, and a range only where a peer genuinely means one.

pnpm itself is pinned too — the estate ran eighteen different versions, five of
them too old to read a catalog.
2026-08-28 15:33:26 -07:00
zeekay 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.
2026-08-28 15:08:29 -07:00
zeekay fa8eb6aaea dex: record which surfaces ask whether a market was ever registered
initialize is where a market becomes real: it resolves both assets through the
installed resolver and requires live on-chain code at each address. The LP
commit, the quoter and both stateview reads re-read that record and refuse an
inactive one. The swap money path does not read it at all, so a Phase-A intent
against a key initialize never saw is accepted -- it locks the caller's asset
into the seam and exports a cross-chain object carrying a market id nothing
admitted. The lock is the caller's own and remains reclaimable, so this is
permissionless state growth rather than theft, but the admission performed at
initialize governs nothing the value path does.

Closing it is the same one-line status check the LP commit already makes.
Landing it changes which calls a chain accepts, and it fails eighteen existing
tests -- every one of which swaps against a key it never registered -- so it is a
coordinated upgrade and a decision above this change, not a patch to slip in.

What lands here is the measurement: one test for the predicate, one for the
surfaces that ask it, and one that pins the swap path answering yes today. That
last one fails the moment the check is added, and says so in its own failure
message, so the gap cannot be closed silently or forgotten quietly.
2026-08-28 14:21:08 -07:00
zeekay a79ed1beee dex: an exact-output quote answers the exact-output question
quoteExactOutput and quoteExactOutputSingle discarded their direction into `_`
and returned the exact-input projection, so a caller asking what 1000 token1
costs got what 1000 token0 buys -- wrong by the square of the price, with the
fee taken off the wrong side. At price 4 it answered 4000 where the answer is
250. The two questions are inverses of one price-and-fee map: the exact-output
side divides where the other multiplies and grosses the fee onto the input
rather than deducting it. Every step rounds up, so the quoted input covers the
output asked for and no smaller input does -- swept over five fees, five prices,
both directions and six amounts, with the one-unit-lower input asserted short.
The exact-input answer is unchanged, which is asserted directly.

A token move refuses an address carrying no contract code. The EVM answers a
CALL into such an address with success and no return data, and that is also how
a non-compliant ERC-20 reports success, so the return alone cannot separate
them; SafeERC20 separates them by requiring code, and now so does this. It
matters on the push leg: withdraw debits the claim before it moves the token, so
reading that empty return as delivery retires a claim against a transfer that
did not happen. The pull leg was already covered by its delivered-amount check.
One moveToken now issues both, so the requirement and the return reading cannot
drift apart. Mutation: dropping the code check fails two tests. A token that
HAS code and returns nothing still settles, which is asserted separately -- the
refusal is bought by the code check alone.

getOpenOrders charges for the record loop before running it. The order count
comes from the index alone, so the bill is knowable first; the sibling
enumeration on 0x9996 already charged at that point.

A length read out of a storage word is bounded before it sizes an allocation.
int(uint64) is negative above 2^63 and make takes that as a panic, which from a
precompile is a halt rather than a refusal. slotLen is the one bound and the
three readers -- record, order index, staging -- answer absent above it. This
replaces a characterisation test that asserted the panic and asked to be
rewritten once the bound existed.

reclaim no longer stages a remove it cannot deliver. A remove names a key in the
inbound space, where objects a peer exported to this chain live; the intent
object went the other way, so the staged remove could never name it, and against
a missing key it writes a tombstone into a namespace that has nothing to do with
it. What reclaim guarantees on this chain -- the principal exits once, because
the zeroed remaining caps any later settlement -- is unchanged and now stated
where the comment claimed more.
2026-08-28 14:14:21 -07:00
zeekay 73f3159a94 starkfri: refuse a proof that picked its own difficulty
The proof argument carries the p3q header, and this precompile never
looked at it -- only the "P3Q1" tag and the length framing. Everything
about how hard the verification would be, log_blowup and num_queries,
arrived off calldata chosen by whoever submitted the proof, and at the
weakest settings the wire admits that is about one bit.

admit() is now the one question both entry points ask: the envelope
tag, a frame long enough to state parameters at all, and
log_blowup * num_queries >= MinSoundnessBits. The verifier enforces
the same floor, but a consensus rule belongs where the chain can be
read, not only inside a linked staticlib -- and refusing here means
the node crosses no FFI boundary for a proof it was never going to
accept. The gas charge is unchanged; it is a function of calldata
length alone, so the saving is the node's work, not the caller's fee.
Under the starkfri_p3q tag a test reads p3q_min_soundness_bits() out
of the linked library, so the two copies cannot drift.

The fixtures move with it. "P3Q1" followed by arbitrary bytes was
standing in for a well-formed proof, but those bytes ARE the
parameters, so whether such a fixture cleared the floor was an
accident -- and under crypto/rand, not the same accident twice.
sfProof builds a frame that states what it means to state.
2026-08-28 14:04:37 -07:00
zeekay 2b0e35ad84 dex: a deposit may claim only what a deposit delivered
0x9999 holds ONE native balance backing THREE pots -- settleVault (depositor
claims), seamReserve (swap-rail input locks) and committedPositions (LP
commits). A precompile cannot read msg.value, so every native funding path asks
the same question by difference: balance(0x9999) minus what is already
accounted for. The operator seed subtracted all three pots. deposit subtracted
settleVault alone, so it read the seam and LP pots as value the caller had just
carried and minted a claim over them; withdraw then paid that claim out of the
one balance, leaving the intents and positions that locked it unbacked. Its own
comment stated the property the code did not hold.

deliveredNative is now that question, asked in one place and answered the same
way for both paths. A pot added later cannot be forgotten by half the callers,
because there is only one caller of the arithmetic. Mutation: dropping the two
pot terms fails ten tests, four new and six that already pinned the seed side.

The five Run entry points read their selector through contract.Cursor. Three of
them spelled the same four-byte read three different ways, and all five handed
their handlers input[4:] -- a two-index slice, so cap ran on into the EVM memory
the caller filled with MSTORE before the call. Every downstream guard bounds its
own high index correctly today, but the property was held up by fifty
independent hand-written checks; Cursor.Rest caps the slice to its own length,
so a slice expression past the declared input refuses instead of returning
bytes the caller chose. Suite green with the cap in place, which is the evidence
that no live path reads past its input.
2026-08-28 14:00:28 -07:00
zeekay 5ec478c3ed stableswap, math: two halts from calldata, and a pool that leaked a wei per swap
A precompile panic aborts the block rather than the call, so both of these
stopped every validator on any chain with stableswap enabled.

An amplification coefficient of zero divides by zero in all three Newton ops --
computeY's c/(A*n*n) and D/(A*n), and computeD's denominator (A*n-1)*D +
(n+1)*dP, which crosses exactly zero. A 40,000-input sweep hit it 665 times. A
is now read and refused at the boundary, which is also the honest answer: the
invariant is undefined at A=0.

addLiquidity divides by D0, and computeD returns zero for an empty pool.
Balances and totalSupply arrive as independent calldata, so "no assets, LP
tokens outstanding" is reachable. Post-fix sweep: zero panics in 40,000 inputs.

computeY used floor division, so its iterates drifted BELOW the true root in
55% of solves, by up to 2 wei, which getDy's single -1 margin did not cover.
The invariant therefore DECREASED across a swap -- 27 losses in 19,996 random
swaps, a wei at a time out of the pool. Newton converges from above on this
convex function, so ceiling division restores the one-sidedness the proof
depends on. After: zero losses, 19,847 gains.

math returned every result through padTo32, which keeps only the low 32 bytes.
MulDiv(2^255, 4, 1) returned 0. MulDiv(max, max, 1) returned 1. And dex.MulDiv
PANICS on the same input -- two behaviours for one operation, one of them a
plausible-looking wrong number in the primitive that computes fees and share
ratios. Now refuses with ErrOverflow, matching FullMath.sol. Consensus-visible;
it needs a coordinated upgrade.

Coverage: dex 57.1 -> 99.1, stableswap 80.6 -> 98.6, swap 87.6 -> 99.6,
v3 78.1 -> 98.7, math 78.5 -> 97.6. 50 of 52 mutations caught, both survivors
proven unreachable rather than assumed. dex is now order-independent: -count=2
and -shuffle=on both pass, 843 tests run individually and 260 shuffle seeds
clean. The leak was a package-level resolver latch that installed on top of
whatever was live; t.Cleanup had fixed the exit and not the entry.

Also corrected five comments describing the CLOB price limit as IEEE-754
float64 bits. It is exact big.Int on an integer grid, and a reviewer would have
believed consensus compared float bit patterns.
2026-08-28 13:46:40 -07:00
zeekay ca0bf6b303 precompile: seven packages to 96-99%, from lanes killed before they could commit
slhdsa 97.9 · mlkem 97.4 · mldsa 96.8 · xwing 96.2 · bls12381 97.2 ·
kzg4844 97.8 · v3 98.7. All build, vet and gofmt clean, all green under
-count=1, and preserved here rather than lost to a session limit.

v3's lane did report before it died: 78.1% to 98.7%, 70 of 70 mutations caught,
four of which survived a first pass and needed tests written. The two that
matter are rounding: burn rounding principal UP, and fee growth rounding UP,
neither of which any behaviour test could see -- a Q128.128 off-by-one is
2^-128 of a token and floors to zero through every payout path, so it took an
exact-value assertion. It also found that a full drain of a pool with
overlapping positions REVERTS rather than paying out, because deposits round
down once per position while payouts round down once per step over combined
liquidity, and floor(a+b) >= floor(a)+floor(b). Custody holds; not one unbacked
wei leaves. Liveness, not theft.

The other six carry no report -- their lanes were killed mid-run. Build, vet,
gofmt and the suite are all I can vouch for.
2026-08-28 13:27:00 -07:00
zeekayandHanzo Dev 456dfa4ccf the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 13:04:22 -07:00
zeekay b37c13bf34 magnetar, ring, zk: the last hand-written calldata bounds
magnetar forges under a single reverted read, measured: declare 7924 bytes,
put the true final byte in the spare capacity, and the precompile answers
verdict word 1 over 7925 bytes. len=7924 cap=8181, no panic, no error. That
is what a missing bound costs when the input is a window into EVM memory
rather than a copy.

ring does not forge on one reverted read, and the reason is structural rather
than lucky: every length-bearing field is followed by another bound, and the
trailing open field is s[low:], which bounds against len even when the read
before it walked into cap. So the over-read lands as a halt instead. Recorded
as a halt, not a forgery.

magnetar's exact framing survives as End(); deleting only that term accepts a
single trailing byte, which would let one signature ride many calldatas.

zk: plonkVerify reads its nine points and six evaluations through the cursor.
Two floors in verifyPLONK and verifyFflonk survived deletion because the only
thing they protected was the KEYED path — with a key registered, a short proof
must come back malformed rather than merely false, and nothing went down that
path. That distinction now has a test. parseFflonkProof's floor was redundant
against its own reads and is gone.

Removed in ring: a nil check on a slice make() cannot return nil, left over
from the GPU batch path deleted under M-06.
2026-08-28 12:17:00 -07:00
zeekay 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.
2026-08-28 12:10:59 -07:00
zeekay d7bff26fbb Name Zen in the hero card's font stack
The social card asked for Inter first. That is not a face this estate
licenses, ships or ever drew, so a renderer with Inter installed painted
the wrong face and one without it silently fell through to system-ui.
Zen leads now; the same tail follows it.
2026-08-28 11:53:56 -07:00
zeekay 7883fef008 ai, graph, fhe: price the work, enforce readOnly, refuse the impossible
graph 58.0 -> 99.1, graphvm 0 -> 100, ai 90.0 -> 96.2, inference 87.3 -> 98.0,
modelregistry 70.2 -> 98.9, aivmbridge 93.1 -> 99.8, anchor 98.1 -> 99.1,
fhe 33.0 -> 80.1.

graph charged one flat fee for any amount of work. The sharpest form was not
response size but TargetChains: decoded from caller JSON with no cap, one
GraphQL query per entry, so seven bytes of calldata bought an unbounded
fan-out. Response size is only known after the work, so the fee now reserves
the worst case up front and refunds down -- there is no deduction that can come
up short, and a failed run consumes the reservation rather than everything
supplied, so malformed input is not a cheap probe.

ai halted the chain from eight bytes. Two handlers bounded their length
prefixes in uint32, so near 2^32 the sum wrapped, the check passed and the
slice panicked. Reproduced at both 96 and 8 bytes. The correct int-based parser
already existed in the same package; the handlers just did not call it, and the
existing test only probed 0xFFFF, which never wraps.

fhe never enforced readOnly. It was threaded through Run and all 42 handlers
and read by none, so every op but decrypt and sealOutput wrote ciphertext,
metadata and ownership to the trie under STATICCALL. Decided once at dispatch;
an unknown selector counts as a writer.

fhe also allocated before it checked: five bytes of calldata asked for 34 GB
behind a flat 50,000 gas fee. div and rem were registered and priced at 655M
gas with no case in the switch, so both fell through and returned the zero
handle with a nil error -- success for a guaranteed no-op, with the real
implementations written and never wired. select decoded its control as a bare
Ciphertext when every ebool the precompile stores is a BitCiphertext, so it
could not have worked for any input; it surfaced only once failures stopped
being reported as success.

graph decodeArgs accepted truncated calldata as a shorter argument list, so
trimming bytes changed which variables a query expanded with. graphvm chose
resolvers by map iteration order, which is consensus-visible.

Deleted a dead second gas table that disagreed with the real one, an address
constant appearing only at its own declaration, and ten other symbols with no
reader.
2026-08-28 11:53:22 -07:00
zeekay 59c880b1e8 cggmp21: a recovery id above 3 is a different byte, not a second spelling
I shipped a consensus split in 7c364dc. A genuine signature with its recovery
byte set to g+4 -- 170 bytes of calldata, no special construction -- returned 0
on a cgo node and 1 on a node built without cgo.

recoverPublicKey subtracted 27 and passed anything through. libsecp256k1
refuses an id above 3, but the pure-Go fallback routes through btcec, whose
header is 27+id uncompressed and 31+id compressed: v=4 survived the
subtraction, became header 31, and read back as a valid id 0. So the two builds
answered differently about the same transaction.

It shipped because the suite was only ever run under cgo. The tests already in
that commit -- TestCGGMP21_RecoveryByteSweep and
TestCGGMP21_RefuseUnrecoverableSignature -- fail on pristine 7c364dc with
CGO_ENABLED=0. They were right and nobody asked them.

Refused where the byte becomes a value, the same place ring's scalar() refuses
an out-of-range scalar for exactly this reason. The range [0,3] is
libsecp256k1's, so cgo behaviour is unchanged and only the pure-Go aliases go.
Both builds now admit exactly {id, id+27}, and the full cggmp21 suite passes
under CGO_ENABLED=0 for the first time -- as do ring, frost, pulsar, corona,
magnetar and p3q.

Run the crypto suites under both build tags. Every build-dependent defect found
this week was invisible to a single-tag run.
2026-08-28 11:53:03 -07:00
zeekay 33a2acfbd3 dex: pin what margin, perps, teleport and vaults actually do
Four files at 0% coverage, now 99.6 / 100 / 95.1 / 100. Tests only; no source
line changed. 51 of 52 mutations caught, the survivor proven equivalent rather
than assumed: at profit == 0 the fall-through computes a zero fee, a zero net,
a no-op add and returns a fresh zero, which is byte-identical to the early
return no test can separate.

The reachability verdict comes first, because it decides how to read the rest.
Nothing dispatches to any of it. MarginEngine, TeleportBridge, OmnichainRouter,
VaultManager and PerpetualEngine have zero references outside their own files,
the five registered modules reach none of them, and all four are pure in-memory
Go with no StateDB anywhere -- so they cannot take part in a state transition
even if something did call them. Every defect below is latent.

Two of them are the same shape that produced three universal forgeries in zk
this week: a length check returned as a verdict. verifyWarpMessage is
`return len(message) > 0`, so any non-empty byte string authorises a mint, and
it never looks at the teleport id it is handed. Alongside it, bridge signatures
are counted and never examined -- three copies of one byte satisfy a threshold
of three.

Seven public entry points panic on arithmetic: division by a zero leverage,
zero size, zero margin, zero index price, zero total assets, and a
CreateVault that slices a strategy name to ten bytes without checking its
length -- the engine's own LP_YIELD is eight, so creating a vault over it is an
unconditional panic. RouteTransfer takes the write lock and then calls
GetBestRoute, which takes the read lock on the same non-reentrant mutex, so
every routed transfer deadlocks permanently.

The margin constants are all ten times their documented values, which is one
root cause with three consequences: above 20x a long's liquidation price sits
above its entry, the long/short ordering inverts, and fees at 750bps exceed
maintenance at 500bps -- so the full-fee liquidation branch is dead code and
every liquidation is a socialised loss.

Also latent: a daily transfer limit stored and never enforced, admin actions
with no caller argument at all and an operator list nothing reads, a fee floor
that can exceed the amount transferred, an over-close that pays PnL on phantom
size, a withdrawal solvency check that restates what the caller already proved,
and a leverage ceiling bypassed by a uint64 truncation.

Three uncovered blocks, each provably unreachable and named in the report: one
is unreachable precisely because the check guarding it is vacuous.
2026-08-28 11:48:47 -07:00
zeekay 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.
2026-08-28 11:47:04 -07:00
zeekay 68fd104380 dex: the package builds again
Three compile errors left by interrupted work. Two tests named CurrencyIn and
CurrencyOut on SwapExactInputSingleParams, whose fields are TokenIn and
TokenOut; one wrote 2^96 as an untyped literal to big.NewInt, which takes an
int64 and cannot hold it -- built with Lsh instead.

Also lands comment corrections that had fallen behind the code: the CLOB price
limit is an exact uint64 on the PriceInt grid, not float64 bits, and no float
touches the price path any more. Consensus code that reads a float is a
divergence waiting for a different CPU, so the doc saying otherwise was worth
fixing.

The package now builds and vets clean. It does not pass: the suite has an
order dependence that ends in a division by zero inside GetAccountHealth, which
in a precompile is an unrecovered panic. Both are being worked separately.
2026-08-28 11:38:50 -07:00
zeekay 84add56ce2 swap: the claim and refund windows must never both be open
87.6% -> 99.6%, tests only -- no source file changed.

The two boundary conditions are the whole safety property of an atomic swap,
and nothing pinned them. Moving swap.go:333 from >= to >, or swap.go:398 from
< to <=, opens both windows at one instant and the same swap is claimed AND
refunded. TestZZExactlyOneOutcomeAtEveryClock sweeps the clock and requires
exactly one to succeed at every tick, so a boundary drifting in either
direction fails rather than merely changing behaviour.

46 mutations applied, 46 caught, restore verified by md5 and by an empty diff.

Three defects found and documented rather than fixed, since the brief was
coverage:

A native donation griefs the next lock. Delivery is measured as
balanceOf(swapAddr) - reserve[native], so LUX force-sent by selfdestruct or
coinbase is indistinguishable from msg.value: one wei makes every correctly
built in-flight native lock fail ErrDeltaMismatch, cheaply and repeatably. The
donation is not stolen, it is absorbed by whichever lock finally reconciles.
The ERC-20 path is immune because it measures a per-call transferFrom delta.

A zero preimage is a legal secret that the relay cannot read: getPreimage
returns bytes32(0), byte-identical to never claimed, so a watchtower polling
for the cross-leg secret stalls on its own leg. The event and the status word
still carry the truth, so it is observability, not funds.

The timeout bounds are unguarded uint64 arithmetic. Above 2^64 - MaxTimeout
both sums wrap and a lock is accepted with an expiry in the past -- born
expired, refundable in its own block, with the counterparty window gone.
Unreachable on any real chain (it needs a block timestamp within ~30 days of
584 billion years), so it is pinned as behaviour, not asserted as a bug.

Two gas notes, changed nothing: the ERC-20 sub-call takes no gas parameter and
returns none, so an arbitrarily expensive transfer runs under a flat fee -- the
one genuinely unbounded path. And lock writes nine slots, seven of them
zero->nonzero, for 60,000 gas, so it subsidises permanent state growth by
roughly 3x at reference prices. Fee ordering is pinned so a repricing surfaces
as a failure rather than silently.
2026-08-28 11:35:22 -07:00
zeekay a466c21334 crypto precompiles: a halt from calldata, two forgeries, and free calls
ring's lsagVerify halted the chain from calldata. A scalar of 0 or >= N makes
ScalarMult return a nil point and curve.Add dereferences it; geth's dispatch
has no recover, so one transaction stops every validator. Its parser also left
scalars unvalidated, and the two builds disagree about them -- cgo refuses an
out-of-range scalar, pure-Go reduces it silently -- so the same calldata
produced different verdicts depending on how the node was compiled.

frost's Schnorr check was a total forgery. An unliftable x-only key fell
through to the identity, collapsing z*G == R + c*Y to z*G == R, so choosing any
z and publishing R = z*G verifies under every off-curve x. cggmp21 forges the
same way if either coordinate comparison is dropped: one coordinate does not
identify a secp256k1 point, since (P, -P) share x and (P, lambda*P,
lambda^2*P) share y through the GLV endomorphism.

pulsar returned the full supplied gas on four exits -- work done, nothing
billed -- and ring had three arms returning zero. Corona quoted base + n*10000
with n an unbounded uint32 off the wire, up to 4.29e13 gas. Capping the charger
alone would have left the exported EstimateGas disagreeing with the fee, so
quote and charge are now one function.

corona's deserializePoly ignored the count from bytes.Reader.Read, which
returns a short read with a nil error, so a truncated blob was zero-filled and
fabricated coefficients were accepted as signed material. Its exported
ExpectedSignatureSize said 4096 against a real 163840 -- it omitted the A
matrix and bTilde that its own parser reads -- so any caller sizing calldata by
it built a call that could only be rejected.

Per-party gas prices nothing: corona's verifier never referenced t or n (dead
parameters, now gone), and frost and cggmp21 use totalSigners only for a
structural check. All three charge a variable fee for constant work. The cap
contains it; the schedule is consensus-visible and left alone.

Vectors are frozen hex with the generators deleted, because a fresh-key round
trip passes when signer and verifier are uniformly wrong together. Each carries
a genuine signature by the same key over the same message under a sibling
slot's context, which must be refused -- domain separation tested against a
real signature rather than corrupted bytes.

189 mutations, one at a time with restore-and-reverify. Five survivors, each
proven equivalent by a downstream check reaching the identical verdict.
2026-08-28 11:22:18 -07:00