mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
main
19
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. |
||
|
|
0b433110ac |
dex: the funding rail carries value, and nothing else
Move once, trade many. Value crosses the C<->D boundary twice in its life; in
between, D owns it and every order, match and trade is a local balance movement.
So the crossing has exactly one job — move ownership exactly once — and the
object shrinks to what that job needs:
beneficiary(20) | asset(32) | amount(8) = 60 bytes
Out of the wire: the 49-byte market/side/limit/size tail, the `spent` execution
witness, and the lane byte selecting a trading pot. Out of the precompile: the
per-order escrow with its principal cap and deadline, the taker-authenticated
price floor, the deadline reclaim and its selector, the shared-memory discovery
trait, and the .v2 domain rotation that existed only because the tail had
changed the object's width.
ONE CUSTODY POT. seamReserve and committedPositions were the swap lane and the
LP lane of one quantity — "value C holds because D owns it" — and the lane byte
was the only thing keeping them apart. Splitting them is what created the
cross-lane drain the byte then had to defend against. One number per asset, one
credit path, nothing to cross.
WHAT REPLACES THE PER-ORDER CAP AND THE PRICE FLOOR: D refuses an over-debit on
export, C refuses a credit custody cannot back, a claim is consumable exactly
once, and Block.Verify proves the carried bytes are the recorded object. Those
are properties of the value. The cap and the floor were properties of an order,
which is why the order had to ride the object for them to be checkable at all.
The claim id is one derivation for both directions and both repos:
SHA-256(domain | source | dest | sourceTx | index). It replaces DeriveOrderID,
DerivePositionCommitID and deriveSeamOutputID, which were three spellings of the
same fact.
Co-authored-by: Hanzo Dev <[email protected]>
|
||
|
|
2391e31aaa |
dex: the seam's unit of work is an order, not an intent
Order -> Match -> Execution -> Trade is the vocabulary; "intent" was the old
name for what an Order is. Renames the identifiers across the 0x9999 settle
precompile so the code reads in the settled terms.
Values that live on a chain keep their bytes, because renaming one of those is
a migration and not a rename:
- "lux.dex.native.intent.v1" and "lux.dex.native.intent.pending.v2" are hash
INPUTS; a byte changed there moves every derived order id and the pending
trait both repos must agree on.
- settleStateNamespace + "intent" is an on-disk key prefix; renaming it
orphans the submitted-order replay set.
- reclaimIntent(bytes32), IntentSubmitted(...) and IntentReclaimed(...) are
the deployed ABI; the signature is what hashes, so a rename would move a
selector and two topics.
Each of those now carries one line saying why the literal does not track the
identifier that names it, so the inconsistency is not "fixed" later.
TestOrderID_CrossRepoGolden still reads 5b0e8d38... and
TestSeamPendingTrait_MatchesDChainGolden still reads 5f10f9ad..., which is the
evidence no derivation input moved.
Co-authored-by: Hanzo Dev <[email protected]>
|
||
|
|
56e332bff2 |
dex: the C->D object carries the taker's order, not just their money
The 69-byte C->D object was a VALUE: this much of this asset belongs to this
owner. A value says what moves; it never said what to do with it. So D consumed
the intent, credited the taker's account, and stopped. Nothing placed an order,
nothing crossed, no proceeds existed, the settlement export never fired, Phase B
had no object, and the settle reverted. eth_getLogs on 0x9999 returns zero events
on every chain since genesis because the seam was unreachable by construction,
not because it was misconfigured.
A swap intent now appends the operation the taker authorized:
market(32) | side(1) | limitPrice(8) | size(8)
onto the unchanged value head, giving a 118-byte intent object. Composed, not
braided: the head is byte-for-byte what every settlement and LP consumer already
reads, so their golden vectors stay valid, and width alone discriminates the two
readings. 69 is a value object and instructs nothing; 118 is an instruction. A
69-byte object is refused as an intent rather than imported with an invented
market and side, which would be an order the taker never signed.
The limit is required. A cross-chain taker cannot re-price mid-flight and their
order runs in a block they do not control, so an unbounded market order across
the seam is precisely the sandwich the spent witness exists to refuse — and at a
zero limit that check is vacuous. Refused at submission, before any principal
moves, rather than sent out naked. Size is derived from the taker's own limit
(exact integer division on the PriceInt grid), so the lock can never exceed what
was locked.
The discovery trait rotates to .v2. That is the flag day, and it is what keeps
the two generations from ever disagreeing: an old D node enumerates .v1, finds no
v2 object, emits no import, and goes on validating everything else; a new D node
enumerates .v2 and never touches a stale op-less object. Neither can execute the
other's intent, so the changeover costs seam liveness alone.
isMarket and limitIsUpper are gone from the wire and from IntentRequest — every
seam order is a limit order, and a BUY's limit is its ceiling. One declaration of
each fact instead of two to reconcile.
344 tests pass, 0 fail.
Co-authored-by: Hanzo Dev <[email protected]>
|
||
|
|
fd1cc0904e |
dex: carry the cross-chain object in the transaction, not shared memory
A settled swap could never be synced past. Both C-side consume paths (ImportSettlement, ImportPositionCollect) read the D->C object from shared memory DURING EVM execution, while the consuming Remove landed at block Accept. A node re-executing that block — bootstrapping, state-syncing, re-tracing — found the object gone, computed a reverted receipt where the network had computed a successful one, and died on the receipt root. The seam settled live and wedged every node that tried to follow. Move the object's 69 bytes into the transaction (Phase-B hookData, and the collectPosition calldata) so execution is a pure function of the block and replays byte-identically forever. Both rails now go through ONE binding primitive, bindImportObject, which also DECLARES the consumption as a log committing to the object's hash. The host authenticates that declaration against shared memory at Block.Verify, so a forged object costs the producer a block instead of forking a receipt root. The ship rule did not weaken, it moved: a C balance is still credited only by consuming a real D->C object. What changed is where that is proven. Also: - amount is no longer restated on the wire; it IS the object's amount, so there is one declaration of the fact instead of two to cross-check. - the retired DS01 tag is refused BY NAME. The fallback for an unknown tag is Phase A, so a stale settle would otherwise have been silently re-read as an intent and LOCKED the sender's input — the one mixed-fleet outcome worse than a revert. - the zero-amount refusal moved into the single binding point, so an absent object zero-padded to canonical width reports the same clear error on both rails rather than surfacing as a cross-rail violation. 336 tests pass, 0 fail. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
af68fed375 |
dex: decomplect to canonical settle-only 0x9999 (remove embedded matcher)
ONE money path: D-Chain matches, C-Chain 0x9999 SETTLES, C never matches. - 0x9999 swap() routes ONLY to SettleSwap (native C<->D atomic seam): untagged/ DI01 => Phase A intent (lock input, write C->D object, return intent id); DS01 => Phase B settlement (consume a real D->C atomic object once, credit RECORDED amount). No-receipt-no-settle: absent a real object, Phase B reverts ErrNativeNoSettlement (TestDecomplect_NoReceiptNoSettle). - Deleted the embedded matcher: swap_sync*.go, swap_custody.go (maker order book), buildRouter/evmAMMPool in swap_amm_pool.go, and the swapDeposit/withdraw/place/ cancel selectors. A live in-trie matcher forks consensus; deterministic atomic-object consumption is fork-safe inline. - 0x9010 already non-dispatched. 0x9012 (LXRouter) value selectors exactInput*/ exactOutput* now revert ErrPrecompileMoved (PRECOMPILE_MOVED); read-only quote/ route views kept. - Cleanups (dead code removal, all unreachable via any dispatched selector): deleted the legacy engine PoolManager.Swap + LXRouter.Exact*/executeV4Swap*; deleted the vestigial makerLockedVault ledger (0 callers, always 0); deleted the unwired Block-STM scaffolding (AssertWriteSetWithin/AccessCommitment/Predict*). - Activation rides the EXISTING canonical gate 1766708400 (Dec 25 2025 16:20 PST); no new timestamp. Build CGO_ENABLED=0 (pure-Go luxfi/dex path); go.sum zap re-tag checksum refreshed. 327 dex tests PASS / 0 FAIL, go vet clean. RED-reviewed (0 critical, all drain vectors safe, fund-strand cleared, negative invariant falsified-then-restored). Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
844f3d3587 |
dex: decomplect to canonical settle-only 0x9999 (remove embedded matcher)
ONE money path: D-Chain matches, C-Chain 0x9999 SETTLES, C never matches. - 0x9999 swap() routes ONLY to SettleSwap (native C<->D atomic seam): untagged/ DI01 => Phase A intent (lock input, write C->D object, return intent id); DS01 => Phase B settlement (consume a real D->C atomic object once, credit RECORDED amount). No-receipt-no-settle: absent a real object, Phase B reverts ErrNativeNoSettlement (TestDecomplect_NoReceiptNoSettle). - Deleted the embedded matcher: swap_sync*.go, swap_custody.go (maker order book), buildRouter/evmAMMPool in swap_amm_pool.go, and the swapDeposit/withdraw/place/ cancel selectors. A live in-trie matcher forks consensus; deterministic atomic-object consumption is fork-safe inline. - 0x9010 already non-dispatched. 0x9012 (LXRouter) value selectors exactInput*/ exactOutput* now revert ErrPrecompileMoved (PRECOMPILE_MOVED); read-only quote/ route views kept. - Cleanups (dead code removal, all unreachable via any dispatched selector): deleted the legacy engine PoolManager.Swap + LXRouter.Exact*/executeV4Swap*; deleted the vestigial makerLockedVault ledger (0 callers, always 0); deleted the unwired Block-STM scaffolding (AssertWriteSetWithin/AccessCommitment/Predict*). - Activation rides the EXISTING canonical gate 1766708400 (Dec 25 2025 16:20 PST); no new timestamp. Build CGO_ENABLED=0 (pure-Go luxfi/dex path); go.sum zap re-tag checksum refreshed. 327 dex tests PASS / 0 FAIL, go vet clean. RED-reviewed (0 critical, all drain vectors safe, fund-strand cleared, negative invariant falsified-then-restored). Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
37527d0dc3 |
refactor(dex): decomplect 0x9999 value path to one live synchronous router + wire prod asset resolver
The 0x9999 native DEX is ALWAYS-ON and UNCONDITIONAL (live since the Dec 25 2025 activation). Remove the value-activation gate entirely (value_gate_boot.go, valueSwapsEnabled/EnableValueSwaps/ErrValueSwapsNotEnabled, the EnableValueSwapsViaQuorumGate/RequireQuorumFinalityForValueDEX composition) and make the synchronous on-chain smart-order-router the sole untagged-swap path. The gate is replaced by the swap's INTRINSIC fail-closed controls, enforced on every call: real-asset registry admission (the installed AssetResolver), the live-code EXTCODESIZE verifier, the per-swap min-out / price floor, the halt switches, and the non-reentrant custody mutex. A node that wired no resolver fails closed (ErrNoAssetResolver) — the structural replacement for the consensus-mode gate. - swap_sync.go / swap_custody.go (deposit+place) / settle_module.go dispatch / settle_stm_stateful.go predictor: drop the gate; untagged swap routes to runSyncSwap unconditionally. - settle_market.go: gate Initialize through the SAME resolver+verifier admission (admitMarketAssets) so a fabricated-asset initialize is refused (no phantom MarketRecord), at parity with the swap path. - swap_sync.go: fix the stale 'left-16-bytes' account-fold comment to the full 32-byte injective AccountFromBytes left-pad. - tests: harness installs a default resolver for its own market (hermetic, no cross-test global leak); add TestDecomplect_* proving a real registered asset fills end-to-end and an unregistered/synthetic asset fails closed. value_gate_boot.go deleted for real. Full dex suite green (-count=1). |
||
|
|
0401061006 |
refactor(dex): decomplect 0x9999 value path to one live synchronous router + wire prod asset resolver
The 0x9999 native DEX is ALWAYS-ON and UNCONDITIONAL (live since the Dec 25 2025 activation). Remove the value-activation gate entirely (value_gate_boot.go, valueSwapsEnabled/EnableValueSwaps/ErrValueSwapsNotEnabled, the EnableValueSwapsViaQuorumGate/RequireQuorumFinalityForValueDEX composition) and make the synchronous on-chain smart-order-router the sole untagged-swap path. The gate is replaced by the swap's INTRINSIC fail-closed controls, enforced on every call: real-asset registry admission (the installed AssetResolver), the live-code EXTCODESIZE verifier, the per-swap min-out / price floor, the halt switches, and the non-reentrant custody mutex. A node that wired no resolver fails closed (ErrNoAssetResolver) — the structural replacement for the consensus-mode gate. - swap_sync.go / swap_custody.go (deposit+place) / settle_module.go dispatch / settle_stm_stateful.go predictor: drop the gate; untagged swap routes to runSyncSwap unconditionally. - settle_market.go: gate Initialize through the SAME resolver+verifier admission (admitMarketAssets) so a fabricated-asset initialize is refused (no phantom MarketRecord), at parity with the swap path. - swap_sync.go: fix the stale 'left-16-bytes' account-fold comment to the full 32-byte injective AccountFromBytes left-pad. - tests: harness installs a default resolver for its own market (hermetic, no cross-test global leak); add TestDecomplect_* proving a real registered asset fills end-to-end and an unregistered/synthetic asset fails closed. value_gate_boot.go deleted for real. Full dex suite green (-count=1). |
||
|
|
4ccb06e588 |
dex(0x9999): restore A2 value-activation gate + wire L2 sync access-set soundness
A2 (do-not-ship): restore the fail-closed value-activation gate that the working
tree had deleted (value_gate_boot.go gone, dispatch conjunct stripped, custody
gates removed, regression test inverted). Root cause of the deletion: HEAD d60a0db
double-declared the gate symbols in BOTH swap_sync.go and value_gate_boot.go (does
not compile); the dedup correctly removed them from swap_sync.go but over-corrected
by deleting value_gate_boot.go. Fix (DRY, one home): gate symbols live ONLY in
value_gate_boot.go. Restored:
- the fail-closed gate check in runSyncSwap (value FILL)
- the && ValueSwapsEnabled() dispatch conjunct in settle_module.go
- the value gate on runSwapDeposit + runSwapPlace (value-IN / new trading)
- EXEMPTED runSwapWithdraw + runSwapCancel (EXIT paths — fail-secure, matches the
canonical 'halt blocks new trading, ALWAYS allows cancel'; HEAD over-gated these,
a latent fund-lock now fixed)
- un-inverted Test9999SyncSwap_ValueGateFailsClosed (gate-off -> no sync fill)
B7/B8: PredictSyncSwapWriteSet models runSyncSwap's ACTUAL writes (swapCounterKey +
dexcore book/index/ledger rows via the dexcore key schema + native legs + custody
guard); route-dependent maker rows enumerated from the LIVE order index (sound
superset, mirrors PredictModifyLiquidityWriteSet). PredictWriteSetForCall now routes
a value-enabled untagged swap to the sync predictor. Parity gate proven (observed
subset declared) with fail-without on swapCounterKey (the async predictor's blind spot).
L2: the live sync dispatch is wrapped in AssertWriteSetWithin(PredictSyncSwapWriteSet)
— an undeclared 0x9999 write fails closed (ErrAccessSetUndeclaredWrite) in Verify,
never a fork. The sync predictor's keys are not amount-derived, so a fee-on-transfer
ERC-20 does not false-reject (B6: sync path fee-on-transfer-safe by construction).
writeObservingStateDB.GetCodeSize forwards the EXTCODESIZE capability so a
poolStateAdapter wrapping the observer stays code-capable (else the C1 verifier
fails closed under L2).
Test isolation: newSettleHarness installs a default market resolver; sync-path C1/
registry/BLS tests enable the value gate explicitly. Full dex suite green 417/0.
|
||
|
|
9b7e500726 |
dex(0x9999): restore A2 value-activation gate + wire L2 sync access-set soundness
A2 (do-not-ship): restore the fail-closed value-activation gate that the working
tree had deleted (value_gate_boot.go gone, dispatch conjunct stripped, custody
gates removed, regression test inverted). Root cause of the deletion: HEAD 292d954
double-declared the gate symbols in BOTH swap_sync.go and value_gate_boot.go (does
not compile); the dedup correctly removed them from swap_sync.go but over-corrected
by deleting value_gate_boot.go. Fix (DRY, one home): gate symbols live ONLY in
value_gate_boot.go. Restored:
- the fail-closed gate check in runSyncSwap (value FILL)
- the && ValueSwapsEnabled() dispatch conjunct in settle_module.go
- the value gate on runSwapDeposit + runSwapPlace (value-IN / new trading)
- EXEMPTED runSwapWithdraw + runSwapCancel (EXIT paths — fail-secure, matches the
canonical 'halt blocks new trading, ALWAYS allows cancel'; HEAD over-gated these,
a latent fund-lock now fixed)
- un-inverted Test9999SyncSwap_ValueGateFailsClosed (gate-off -> no sync fill)
B7/B8: PredictSyncSwapWriteSet models runSyncSwap's ACTUAL writes (swapCounterKey +
dexcore book/index/ledger rows via the dexcore key schema + native legs + custody
guard); route-dependent maker rows enumerated from the LIVE order index (sound
superset, mirrors PredictModifyLiquidityWriteSet). PredictWriteSetForCall now routes
a value-enabled untagged swap to the sync predictor. Parity gate proven (observed
subset declared) with fail-without on swapCounterKey (the async predictor's blind spot).
L2: the live sync dispatch is wrapped in AssertWriteSetWithin(PredictSyncSwapWriteSet)
— an undeclared 0x9999 write fails closed (ErrAccessSetUndeclaredWrite) in Verify,
never a fork. The sync predictor's keys are not amount-derived, so a fee-on-transfer
ERC-20 does not false-reject (B6: sync path fee-on-transfer-safe by construction).
writeObservingStateDB.GetCodeSize forwards the EXTCODESIZE capability so a
poolStateAdapter wrapping the observer stays code-capable (else the C1 verifier
fails closed under L2).
Test isolation: newSettleHarness installs a default market resolver; sync-path C1/
registry/BLS tests enable the value gate explicitly. Full dex suite green 417/0.
|
||
|
|
c7295c45a2 |
dex/0x9999: taker-authenticated MEV floor + 69-byte seam wire lockstep (swap-seam r2)
THE WIRE (happy-path break + the floor's witness). The dexvm side widened the C<->D atomic
object to 69 bytes (rail|owner|asset|amount|spent); this side was still 61, so EVERY D->C swap
settlement object failed to decode (ErrNativeSettleMalformed) and no taker was ever credited.
Extend exportedOutputSize9999 to 69 and decode the trailing spent witness (the matched input
on a proceeds leg, 0 elsewhere), byte-identical with chains/dexvm/atomic.go. encodeAtomicObject
writes spent=0 (a C->D intent carries no matched input); encodeAtomicObjectSpent is the full
encoder the parity/redteam surface uses. native_seam_parity_test pins a golden vector
reproduced byte-for-byte by the dexvm twin.
THE MEV FLOOR (taker-authenticated, the keeper-sandwich fix). The slippage limit was
keeper-asserted: settleFromFills enforced RelayOrderTx.PriceLimit, but swapIntentRecord DROPPED
the taker's PriceLimit at SubmitSwapIntent, so a malicious keeper could relay priceLimit=0 and
sandwich the taker. Now:
- swapIntentRecord persists PriceLimit + LimitIsUpper (from the taker's V4 SqrtPriceLimitX96
via priceLimitToCLOB) in its own durable slot, recorded at submit.
- ImportSettlement enforces enforceProceedsPriceFloor on the PROCEEDS leg: the realized price
(out/spent for a SELL floor, spent/out for a BUY ceiling) is checked against the taker's OWN
RECORDED limit, drawn from the venue-attested spent/out witness the keeper cannot forge. A
fill worse than the recorded limit is refused (ErrSettlePriceLimit); the intent stays Open
and reclaimable. So a keeper that zeroes the relay limit STILL cannot get a bad-price
proceeds credit — the binding authority is the taker's intent, not the keeper. Fail-secure:
spent==0 under a real limit is rejected (price unprovable); priceLimit==0 is unbounded.
- The check is cross-multiplied (no float division), in the matcher's float64 CLOB domain.
native_swap_mevfloor_redteam_test: keeper-zeroed limit -> rejected (the required proof), BUY
ceiling rejected, spent==0 fail-secure, no-limit unbounded, and the recorded limit survives the
REAL SubmitSwapIntent path (the plumbing the gap dropped).
LOW/INFO also addressed: corrected the SubmitSwapIntent CEI docstring (the post-lock replay
check is forced by the observed-delta id binding, not a CEI violation; replay-safety is EVM
revert atomicity); documented the deliberate swap(nonce) vs LP(txID+callIndex) id asymmetry in
DerivePositionCommitID so it is not 'aligned' into a consensus break; fixed the stale 60-byte
wire comment.
CGO_ENABLED=0 pure-Go; full dex suite green.
|
||
|
|
5e7328fd41 |
dex/0x9999: taker-authenticated MEV floor + 69-byte seam wire lockstep (swap-seam r2)
THE WIRE (happy-path break + the floor's witness). The dexvm side widened the C<->D atomic
object to 69 bytes (rail|owner|asset|amount|spent); this side was still 61, so EVERY D->C swap
settlement object failed to decode (ErrNativeSettleMalformed) and no taker was ever credited.
Extend exportedOutputSize9999 to 69 and decode the trailing spent witness (the matched input
on a proceeds leg, 0 elsewhere), byte-identical with chains/dexvm/atomic.go. encodeAtomicObject
writes spent=0 (a C->D intent carries no matched input); encodeAtomicObjectSpent is the full
encoder the parity/redteam surface uses. native_seam_parity_test pins a golden vector
reproduced byte-for-byte by the dexvm twin.
THE MEV FLOOR (taker-authenticated, the keeper-sandwich fix). The slippage limit was
keeper-asserted: settleFromFills enforced RelayOrderTx.PriceLimit, but swapIntentRecord DROPPED
the taker's PriceLimit at SubmitSwapIntent, so a malicious keeper could relay priceLimit=0 and
sandwich the taker. Now:
- swapIntentRecord persists PriceLimit + LimitIsUpper (from the taker's V4 SqrtPriceLimitX96
via priceLimitToCLOB) in its own durable slot, recorded at submit.
- ImportSettlement enforces enforceProceedsPriceFloor on the PROCEEDS leg: the realized price
(out/spent for a SELL floor, spent/out for a BUY ceiling) is checked against the taker's OWN
RECORDED limit, drawn from the venue-attested spent/out witness the keeper cannot forge. A
fill worse than the recorded limit is refused (ErrSettlePriceLimit); the intent stays Open
and reclaimable. So a keeper that zeroes the relay limit STILL cannot get a bad-price
proceeds credit — the binding authority is the taker's intent, not the keeper. Fail-secure:
spent==0 under a real limit is rejected (price unprovable); priceLimit==0 is unbounded.
- The check is cross-multiplied (no float division), in the matcher's float64 CLOB domain.
native_swap_mevfloor_redteam_test: keeper-zeroed limit -> rejected (the required proof), BUY
ceiling rejected, spent==0 fail-secure, no-limit unbounded, and the recorded limit survives the
REAL SubmitSwapIntent path (the plumbing the gap dropped).
LOW/INFO also addressed: corrected the SubmitSwapIntent CEI docstring (the post-lock replay
check is forced by the observed-delta id binding, not a CEI violation; replay-safety is EVM
revert atomicity); documented the deliberate swap(nonce) vs LP(txID+callIndex) id asymmetry in
DerivePositionCommitID so it is not 'aligned' into a consensus break; fixed the stale 60-byte
wire comment.
CGO_ENABLED=0 pure-Go; full dex suite green.
|
||
|
|
d30d0fcaf5 |
Thinking Chains: precompile (#7)
* dex/0x9999: per-intent swap escrow record — per-taker cap (MEDIUM) + deadline reclaim (HIGH) The swap rail lacked the LP rail's per-taker bound and any reclaim path: a swap's locked tokenIn was stranded forever if D never settled (Deadline hardcoded 0, no reclaim selector, SubmitCancel dead), and ImportSettlement bounded a credit only by the shared seamReserve — no per-taker cap, so an over-export for one taker could draw on others' pooled tokenIn. ONE new per-intent escrow record (native_state.go swapIntentRecord: owner, assetIn, remaining principal, deadline, status) drives BOTH fixes, created at SubmitSwapIntent — the swap-rail analog of the LP RestingOrder: MEDIUM (per-taker cap): ImportSettlement binds each settlement to the taker's OWN named Open intent (owner==recorded owner, not past deadline). The SAME-ASSET refund leg (recAsset==intent.AssetIn) is capped by + decrements the remaining locked principal (ErrSettleExceedsIntent) — the exact same-asset analog of ErrLPCollectExceedsPosition (the LP collects the asset it committed). The proceeds leg (opposite asset) is NOT input-unit-capped (output units differ from input at any price != 1 — an input-unit cap would wrongly refuse honest fills); its no-mint guard is seamReserve backing + one-time consumption + the intent binding. HIGH (reclaim liveness): a real non-zero Deadline is plumbed from an explicit DI01 intent-body word (V4 SwapParams tuple UNCHANGED) into the record. New reclaimIntent selector refunds the remaining locked principal from seamReserve to the locker once the deadline passes (one-time, deadline-gated; reclaim+late-settle are mutually exclusive via the shared remaining counter + terminal Reclaimed status; a compensating C->D Remove prevents D double-funding). So locked swap input can always exit, like deposit/withdraw. INFO: corrected the swap-rail-safety docstrings (settle9999.go, native_dchain_client.go, native_events.go) to state per-rail what C enforces vs relies on D for; fixed the false "reclaims via the cancel path" comment. Deleted the dead SubmitCancel/SubmitCollect client methods (the cancel/collect keeper events are emitted at their lifecycle sites; the swap reclaim is reclaimIntent). Regression: native_swap_intent_redteam_test.go (8 tests) — RefundBoundToOwnIntent Principal (MEDIUM cap), ProceedsNotCappedByInputPrincipal (dimension correctness), ReclaimRefundsStrandedPrincipal + ReclaimGuards + ReclaimAndSettleAreMutually Exclusive (HIGH), SettleRefusedPastDeadline, DeadlinePlumbedFromIntentBody, SettleMustNameAnOpenIntent. Full dex suite passes. Gated on red re-verify; NOT promoted to testnet/mainnet. * aivmbridge: C->A inference bridge precompile (0x0300..04). Pattern A submit-intent / Pattern B verify committed receipt (keccak-merkle). LP-5301. * aivmbridge: build-tag crossmodule parity test (CI green) The cross-module byte-equality test is the only file importing chains/aivm (resolvable only via go.work). Tag it `crossmodule` so default go test ./... (CI, no workspace) skips it; run with `go test -tags crossmodule` + workspace. Production aivmbridge stays decoupled (wire-format only). --------- Co-authored-by: zeekay <[email protected]> |
||
|
|
741b9a56df |
Thinking Chains: precompile (#7)
* dex/0x9999: per-intent swap escrow record — per-taker cap (MEDIUM) + deadline reclaim (HIGH) The swap rail lacked the LP rail's per-taker bound and any reclaim path: a swap's locked tokenIn was stranded forever if D never settled (Deadline hardcoded 0, no reclaim selector, SubmitCancel dead), and ImportSettlement bounded a credit only by the shared seamReserve — no per-taker cap, so an over-export for one taker could draw on others' pooled tokenIn. ONE new per-intent escrow record (native_state.go swapIntentRecord: owner, assetIn, remaining principal, deadline, status) drives BOTH fixes, created at SubmitSwapIntent — the swap-rail analog of the LP RestingOrder: MEDIUM (per-taker cap): ImportSettlement binds each settlement to the taker's OWN named Open intent (owner==recorded owner, not past deadline). The SAME-ASSET refund leg (recAsset==intent.AssetIn) is capped by + decrements the remaining locked principal (ErrSettleExceedsIntent) — the exact same-asset analog of ErrLPCollectExceedsPosition (the LP collects the asset it committed). The proceeds leg (opposite asset) is NOT input-unit-capped (output units differ from input at any price != 1 — an input-unit cap would wrongly refuse honest fills); its no-mint guard is seamReserve backing + one-time consumption + the intent binding. HIGH (reclaim liveness): a real non-zero Deadline is plumbed from an explicit DI01 intent-body word (V4 SwapParams tuple UNCHANGED) into the record. New reclaimIntent selector refunds the remaining locked principal from seamReserve to the locker once the deadline passes (one-time, deadline-gated; reclaim+late-settle are mutually exclusive via the shared remaining counter + terminal Reclaimed status; a compensating C->D Remove prevents D double-funding). So locked swap input can always exit, like deposit/withdraw. INFO: corrected the swap-rail-safety docstrings (settle9999.go, native_dchain_client.go, native_events.go) to state per-rail what C enforces vs relies on D for; fixed the false "reclaims via the cancel path" comment. Deleted the dead SubmitCancel/SubmitCollect client methods (the cancel/collect keeper events are emitted at their lifecycle sites; the swap reclaim is reclaimIntent). Regression: native_swap_intent_redteam_test.go (8 tests) — RefundBoundToOwnIntent Principal (MEDIUM cap), ProceedsNotCappedByInputPrincipal (dimension correctness), ReclaimRefundsStrandedPrincipal + ReclaimGuards + ReclaimAndSettleAreMutually Exclusive (HIGH), SettleRefusedPastDeadline, DeadlinePlumbedFromIntentBody, SettleMustNameAnOpenIntent. Full dex suite passes. Gated on red re-verify; NOT promoted to testnet/mainnet. * aivmbridge: C->A inference bridge precompile (0x0300..04). Pattern A submit-intent / Pattern B verify committed receipt (keccak-merkle). LP-5301. * aivmbridge: build-tag crossmodule parity test (CI green) The cross-module byte-equality test is the only file importing chains/aivm (resolvable only via go.work). Tag it `crossmodule` so default go test ./... (CI, no workspace) skips it; run with `go test -tags crossmodule` + workspace. Production aivmbridge stays decoupled (wire-format only). --------- Co-authored-by: zeekay <[email protected]> |
||
|
|
d2cf9b449a |
dex: close Red H1 cross-rail D->C consumption + per-object/owner collect bound (FIX 1-5)
Rail-bind the C<->D atomic object to close H1 in BOTH directions, plus the LP-collect per-object/per-owner bound, lifecycle, seam liveness seed, and the AssetAddr-trust hardening. Targeted fixes on the existing seam + LP rail; no redesign. Wire goes 60->61 bytes (rail tag leads); unreleased branch, markets:0, no live objects exist on the old wire. FIX 1 (HIGH, H1 both directions): the D->C object now carries a rail tag (native_wire.go: rail(1)|owner(20)|asset(32)|amount(8)). ImportSettlement consumes ONLY railSwap (debits seamReserve); ImportPositionCollect consumes ONLY railLP (debits committedPositions). SubmitSwapIntent stamps railSwap; SubmitPositionCommit stamps railLP; dexvm executeWithdraw stamps railLP, fill exports default railSwap. executeImport binds the credited rail to the consumed UTXO's recorded rail (symmetric to asset/owner). Plus a per-object gate: collectPosition names a positionID; ImportPositionCollect binds the recorded owner to THAT record (Open/Closing) and bounds the credit by its LockedAmt. H1-A closed: TestRED_LP_CollectObjectCannotBeSwapSettled (railLP -> swap settle reverts ErrSettleWrongRail; seamReserve untouched). H1-B closed: TestRED_LP_SwapObjectCannotBeCollected (railSwap -> collect reverts ErrLPCollectWrongRail; committedPositions untouched). Per-object gate: TestRED_LP_CollectMustNameAnOpenPosition; dexvm TestNativeRail_ImportRejectsRailMismatch (+ matching-rail positive control). FIX 2 (MED): collect is bounded by the recorded owner's OWN committed reserve (the named record's LockedAmt + loadLockedReserve), decremented on collect. TestRED_LP_CollectBoundToOwnPositionPrincipal: an over-export for X cannot draw on Y's committed principal in the shared pot. FIX 3 (LOW): collect drives the record Open->Closing->Closed by remaining LockedAmt (Closed at 0); re-commit while Closing is forbidden (ErrLPCommitWhileClosing). ownerHasOpenPosition removed (dead). TestRED_LP_LifecycleClosedOnFullCollect. FIX 4 (MED, liveness): seedSeamReserve(address,uint256) operator-gated selector funds the swap rail's counterparty backing (production path, not test-only), so a real market's first matched swap settles; creditPositionFee(bytes32,address, uint256) is the symmetric per-owner LP fee credit. Both value-backed (observed-delta native / transferFrom) and protocolFeeController-gated. TestFIX4_FirstFillRevertsWithoutSeed / OperatorSeedBacksFirstFill / SeedIsOperatorGatedAndValueBacked / FeeCreditIsOperatorGated. FIX 5 (LOW): creditSettlementOutput / creditPositionCollect derive the transfer token from the recorded asset (assetAddress(recAsset)); claim.AssetAddr is ignored for the transfer. Corrected the custodyGuardKey9999 comment. TestRED_FIX5_CreditTokenDerivedFromRecordedAsset. Ship rule (fixed): a C credit comes ONLY from consuming a D->C object OF THE MATCHING RAIL; a unit is CSpendable XOR DCommitted across BOTH rails. All prior gates kept (AccountID/AssetID full width, observed-delta, one-time, nonReentrant, CEI, 4-pot conservation realHolding == settleVault+makerLockedVault+seamReserve+ committedPositions). Pure Go, luxfi only. CGO_ENABLED=0 GOWORK=off go build ./dex/... + go test ./dex/ green. |
||
|
|
7432a1e09f |
dex: close Red H1 cross-rail D->C consumption + per-object/owner collect bound (FIX 1-5)
Rail-bind the C<->D atomic object to close H1 in BOTH directions, plus the LP-collect per-object/per-owner bound, lifecycle, seam liveness seed, and the AssetAddr-trust hardening. Targeted fixes on the existing seam + LP rail; no redesign. Wire goes 60->61 bytes (rail tag leads); unreleased branch, markets:0, no live objects exist on the old wire. FIX 1 (HIGH, H1 both directions): the D->C object now carries a rail tag (native_wire.go: rail(1)|owner(20)|asset(32)|amount(8)). ImportSettlement consumes ONLY railSwap (debits seamReserve); ImportPositionCollect consumes ONLY railLP (debits committedPositions). SubmitSwapIntent stamps railSwap; SubmitPositionCommit stamps railLP; dexvm executeWithdraw stamps railLP, fill exports default railSwap. executeImport binds the credited rail to the consumed UTXO's recorded rail (symmetric to asset/owner). Plus a per-object gate: collectPosition names a positionID; ImportPositionCollect binds the recorded owner to THAT record (Open/Closing) and bounds the credit by its LockedAmt. H1-A closed: TestRED_LP_CollectObjectCannotBeSwapSettled (railLP -> swap settle reverts ErrSettleWrongRail; seamReserve untouched). H1-B closed: TestRED_LP_SwapObjectCannotBeCollected (railSwap -> collect reverts ErrLPCollectWrongRail; committedPositions untouched). Per-object gate: TestRED_LP_CollectMustNameAnOpenPosition; dexvm TestNativeRail_ImportRejectsRailMismatch (+ matching-rail positive control). FIX 2 (MED): collect is bounded by the recorded owner's OWN committed reserve (the named record's LockedAmt + loadLockedReserve), decremented on collect. TestRED_LP_CollectBoundToOwnPositionPrincipal: an over-export for X cannot draw on Y's committed principal in the shared pot. FIX 3 (LOW): collect drives the record Open->Closing->Closed by remaining LockedAmt (Closed at 0); re-commit while Closing is forbidden (ErrLPCommitWhileClosing). ownerHasOpenPosition removed (dead). TestRED_LP_LifecycleClosedOnFullCollect. FIX 4 (MED, liveness): seedSeamReserve(address,uint256) operator-gated selector funds the swap rail's counterparty backing (production path, not test-only), so a real market's first matched swap settles; creditPositionFee(bytes32,address, uint256) is the symmetric per-owner LP fee credit. Both value-backed (observed-delta native / transferFrom) and protocolFeeController-gated. TestFIX4_FirstFillRevertsWithoutSeed / OperatorSeedBacksFirstFill / SeedIsOperatorGatedAndValueBacked / FeeCreditIsOperatorGated. FIX 5 (LOW): creditSettlementOutput / creditPositionCollect derive the transfer token from the recorded asset (assetAddress(recAsset)); claim.AssetAddr is ignored for the transfer. Corrected the custodyGuardKey9999 comment. TestRED_FIX5_CreditTokenDerivedFromRecordedAsset. Ship rule (fixed): a C credit comes ONLY from consuming a D->C object OF THE MATCHING RAIL; a unit is CSpendable XOR DCommitted across BOTH rails. All prior gates kept (AccountID/AssetID full width, observed-delta, one-time, nonReentrant, CEI, 4-pot conservation realHolding == settleVault+makerLockedVault+seamReserve+ committedPositions). Pure Go, luxfi only. CGO_ENABLED=0 GOWORK=off go build ./dex/... + go test ./dex/ green. |
||
|
|
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). |