precompile/dex: V4=CLOB facade adapter, inert by default (no embedded AMM)

engine_embedded.go deleted; default backend = newInertEngine (ErrDEXBackendNotConfigured)
so the precompile is inert without a configured D-Chain endpoint. engine_zap.go speaks the
frozen zapwire CLOB frame over luxfi/rpc ZAPDial. RED fixes: H1 replay idempotency, H4
BLAKE3 cancel-auth, H3/C2 ERC-20 settlement conservation guard (removed magic-slot poke;
native LUX direct). Zero go.mod replace; integration test isolated in e2e/.
Verified green pure-Go + -race; all consensus-safety gates pass.
This commit is contained in:
2026-06-17 13:31:58 -07:00
parent 0a89ecf7f8
commit cfbc44bd68
15 changed files with 2078 additions and 926 deletions
+53 -13
View File
@@ -1,31 +1,37 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"errors"
"math/big"
"testing"
"github.com/luxfi/geth/common"
)
// labeledEngine wraps EmbeddedEngine with a different brand. It exists only
// to exercise the backend-swap path from a test — it stands in for the real
// labeledEngine wraps a base Engine with a distinct brand. It exists only to
// exercise the backend-swap path from a test — it stands in for the real
// white-label backends (e.g. Hanzo DEX, Zoo DEX) that live in dependent repos.
// The base is the in-package mockEngine (pool_manager_test.go): the embedded
// AMM was removed because V4 is a CLOB, not an AMM, and the precompile holds no
// matcher of its own.
type labeledEngine struct {
*EmbeddedEngine
Engine
brand string
}
func (l *labeledEngine) Brand() string { return l.brand }
// TestEngineContractAcrossBackends asserts both the default EmbeddedEngine
// and an alternate-brand wrapper satisfy the Engine interface with identical
// observable behavior. This is the contract that lets a smart contract
// compiled against the LP-9010 precompile run unchanged on any chain that
// configures a different backend — only the brand string differs.
// TestEngineContractAcrossBackends asserts two distinct backends satisfy the
// Engine interface with identical observable behavior except brand. This is the
// contract that lets a smart contract compiled against the LP-9010 precompile
// run unchanged on any chain that configures a different backend — only the
// brand string differs.
func TestEngineContractAcrossBackends(t *testing.T) {
upstream := NewEmbeddedEngine()
whitelabel := &labeledEngine{EmbeddedEngine: NewEmbeddedEngine(), brand: "Hanzo DEX"}
upstream := &labeledEngine{Engine: &mockEngine{}, brand: "Lux DEX"}
whitelabel := &labeledEngine{Engine: &mockEngine{}, brand: "Hanzo DEX"}
// Brand contract: each backend reports a non-empty, distinct identity.
if upstream.Brand() == "" || whitelabel.Brand() == "" {
@@ -55,6 +61,40 @@ func TestEngineContractAcrossBackends(t *testing.T) {
}
}
// TestDefaultBackendIsInert asserts the package default is the inert engine: an
// unconfigured LP-9010 reverts ErrDEXBackendNotConfigured on every state-moving
// method and NEVER panics, NEVER fabricates a delta. V4 = CLOB, so the matcher
// lives only on the d-chain; the precompile must be dead until a venue wires the
// ZAP backend.
func TestDefaultBackendIsInert(t *testing.T) {
e := newInertEngine()
// Every state-moving method reverts with the sentinel.
if _, err := e.Initialize(big.NewInt(1)); !errors.Is(err, ErrDEXBackendNotConfigured) {
t.Fatalf("Initialize err = %v, want ErrDEXBackendNotConfigured", err)
}
if d, err := e.Swap(&PoolState{}, SwapParams{AmountSpecified: big.NewInt(-1)}); !errors.Is(err, ErrDEXBackendNotConfigured) || !d.IsZero() {
t.Fatalf("Swap = (%v,%v), want (zero, ErrDEXBackendNotConfigured)", d, err)
}
cd, fd, err := e.ModifyLiquidity(&PoolState{}, common.Address{}, ModifyLiquidityParams{LiquidityDelta: big.NewInt(1)})
if !errors.Is(err, ErrDEXBackendNotConfigured) || !cd.IsZero() || !fd.IsZero() {
t.Fatalf("ModifyLiquidity = (%v,%v,%v), want (zero,zero,ErrDEXBackendNotConfigured)", cd, fd, err)
}
if d, err := e.Donate(&PoolState{}, big.NewInt(1), big.NewInt(1)); !errors.Is(err, ErrDEXBackendNotConfigured) || !d.IsZero() {
t.Fatalf("Donate = (%v,%v), want (zero, ErrDEXBackendNotConfigured)", d, err)
}
// Quote is a read estimate: it returns a benign zero (no route), not an error.
if q := e.Quote(&Pool{Liquidity: big.NewInt(0), SqrtPriceX96: big.NewInt(1)}, big.NewInt(1000), true); q.Sign() != 0 {
t.Fatalf("inert Quote = %s, want 0", q)
}
// It is the package default.
if _, ok := Backend().(*inertEngine); !ok {
t.Fatalf("package default backend = %T, want *inertEngine", Backend())
}
}
// TestSetBackendNilRejected asserts the backend swap refuses a nil engine,
// preventing a downstream caller from accidentally clearing the engine and
// crashing on the first swap.
@@ -74,7 +114,7 @@ func TestSetBackendRejectsAfterPoolInit(t *testing.T) {
defer func() { _ = SetBackend(original) }()
// Reset to a clean engine for this test.
if err := SetBackend(NewEmbeddedEngine()); err != nil {
if err := SetBackend(&mockEngine{}); err != nil {
t.Fatalf("initial install: %v", err)
}
@@ -83,7 +123,7 @@ func TestSetBackendRejectsAfterPoolInit(t *testing.T) {
// test of the guard.
DEXPrecompile.poolManager.pools[[32]byte{0x01}] = &Pool{}
if err := SetBackend(NewEmbeddedEngine()); err == nil {
if err := SetBackend(&mockEngine{}); err == nil {
t.Fatal("SetBackend after pool init must return an error")
}
+48 -9
View File
@@ -10,17 +10,21 @@ import (
)
// Engine abstracts the DEX computation backend.
// The precompile is a thin ABI shim — all AMM math, tick crossing,
// fee growth, matching, and position management happen in the engine.
// The precompile is a thin ABI shim — it holds NO canonical DEX state and runs
// NO matching itself. Matching, the resting book, fills, and value conservation
// all live on the d-chain, reached over ZAP. The precompile only translates the
// V4 PoolManager (CLOB facade) ABI into engine operations and commits the
// server-returned BalanceDelta.
//
// Backends ship in their own packages. Two canonical implementations live
// in-tree:
// Two implementations live in-tree:
//
// - EmbeddedEngine (engine_embedded.go) — pure-Go V4 math, default for
// upstream Lux EVM and any chain that wants a self-contained build.
// - ZAPEngine (engine_zap.go) — binary protocol shim to an external DEX
// process. The external process can be the upstream Lux DEX or any
// white-label DEX downstream; the precompile does not care.
// - inertEngine (engine_inert.go) — the package DEFAULT. No backend wired:
// every call reverts ErrDEXBackendNotConfigured. The public EVM ships this
// so an unconfigured chain's LP-9010 cleanly reverts instead of running a
// wrong, second matcher.
// - ZAPEngine (engine_zap.go) — the stateless V4->CLOB adapter that forwards
// every operation to the d-chain CLOB gateway over ZAP. The venue installs
// it via dex.SetBackend(NewZAPEngine(...)) when dex-zap-endpoint is set.
//
// Adding a new backend is purely additive: implement Engine, ship it in
// its own package, and have the host EVM call dex.SetBackend() before the
@@ -54,3 +58,38 @@ type Engine interface {
// a sanity check at SetBackend() time.
Brand() string
}
// poolRouter is the OPTIONAL seam a backend implements when its canonical pool
// state lives elsewhere (e.g. ZAPEngine, whose pools live on the D-Chain DEX
// server). The PoolManager, which alone knows the V4 poolId, threads it to the
// backend so operations route to the right canonical pool.
//
// The inertEngine does NOT implement this: it has no backend to route to. The
// PoolManager type-asserts for poolRouter and only calls it when present, so a
// backend that holds its own state (none in-tree today) needs no routing.
type poolRouter interface {
// InitializePool creates the canonical pool on the backend keyed by poolID
// and records the route for ps. Returns the authoritative initial tick.
InitializePool(ps *PoolState, poolID [32]byte, sqrtPriceX96 *big.Int, tickSpacing int32, lpFee uint32) (int24, error)
// SetPoolID records the canonical poolID for ps so a later swap / modify /
// donate / quote forwards to the correct server-side pool. Idempotent.
SetPoolID(ps *PoolState, poolID [32]byte)
}
// idempotencyBinder is the OPTIONAL seam a backend implements when a marketable
// order (Swap) must be bound to its originating EVM transaction so a
// re-execution, reorg, or retry of the same tx maps to EXACTLY ONE match
// against the d-chain book — never a double-fill (RED H1).
//
// The PoolManager, which alone sees the execution context, threads the tx hash
// to the backend immediately before delegating Swap. The ZAPEngine records the
// binding and, on a repeated tx hash, returns the previously-committed
// BalanceDelta instead of submitting again. The inertEngine does NOT implement
// this (it never submits); the PoolManager type-asserts and only binds when the
// seam is present, so backends that need no idempotency are unaffected.
type idempotencyBinder interface {
// BindTx records txHash as the identity of the next Swap. A zero hash means
// no binding is available (e.g. a non-EVM caller), disabling the cache.
BindTx(ps *PoolState, txHash common.Hash)
}
-474
View File
@@ -1,474 +0,0 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"fmt"
"math/big"
"github.com/luxfi/geth/common"
)
// EmbeddedEngine implements Engine using the V4 math libraries directly in Go.
// This provides a standalone concentrated liquidity AMM without requiring an
// external DEX process. Uses the same math as Uniswap V4:
// - SwapMath.computeSwapStep for per-tick step computation
// - SqrtPriceMath for price/amount conversions
// - TickBitmap for initialized tick discovery
// - TickMath for tick <-> sqrtPrice conversions
//
// The swap loop iterates through initialized ticks, crossing them when the price
// reaches a tick boundary, updating liquidity net and fee growth.
type EmbeddedEngine struct{}
var _ Engine = (*EmbeddedEngine)(nil)
// NewEmbeddedEngine creates a new embedded engine.
func NewEmbeddedEngine() *EmbeddedEngine {
return &EmbeddedEngine{}
}
// Brand returns the OSS backend identity. White-label EVMs that wrap this
// engine and want their brand to surface to users MUST construct their own
// Engine — typically by wrapping EmbeddedEngine and overriding Brand() —
// rather than mutating this default. A downstream tenant's DEX backend
// wrapper is the canonical example of this pattern.
func (e *EmbeddedEngine) Brand() string { return "Lux DEX" }
// Initialize computes the tick from sqrtPriceX96.
func (e *EmbeddedEngine) Initialize(sqrtPriceX96 *big.Int) (int24, error) {
tick, err := GetTickAtSqrtRatio(sqrtPriceX96)
if err != nil {
return 0, fmt.Errorf("initialize: %w", err)
}
return tick, nil
}
// Swap executes the full V4 tick-crossing swap loop on a PoolState.
// Mutates pool state: sqrtPriceX96, tick, liquidity, feeGrowthGlobal.
func (e *EmbeddedEngine) Swap(pool *PoolState, params SwapParams) (BalanceDelta, error) {
if pool.Liquidity.Sign() == 0 && params.AmountSpecified.Sign() != 0 {
// Check if there's any liquidity at all (bitmap might have initialized ticks)
// Allow the swap to proceed - it will naturally fail if there's no liquidity
}
zeroForOne := params.ZeroForOne
exactInput := params.AmountSpecified.Sign() < 0
// Validate price limit
sqrtPriceLimitX96 := params.SqrtPriceLimitX96
if sqrtPriceLimitX96 == nil || sqrtPriceLimitX96.Sign() == 0 {
if zeroForOne {
sqrtPriceLimitX96 = new(big.Int).Add(MinSqrtRatio, bigOne)
} else {
sqrtPriceLimitX96 = new(big.Int).Sub(MaxSqrtRatio, bigOne)
}
}
if zeroForOne {
if sqrtPriceLimitX96.Cmp(pool.SqrtPriceX96) >= 0 {
return ZeroBalanceDelta(), ErrPriceLimitReached
}
if sqrtPriceLimitX96.Cmp(MinSqrtRatio) <= 0 {
return ZeroBalanceDelta(), ErrInvalidSqrtPrice
}
} else {
if sqrtPriceLimitX96.Cmp(pool.SqrtPriceX96) <= 0 {
return ZeroBalanceDelta(), ErrPriceLimitReached
}
if sqrtPriceLimitX96.Cmp(MaxSqrtRatio) >= 0 {
return ZeroBalanceDelta(), ErrInvalidSqrtPrice
}
}
// Swap state
amountSpecifiedRemaining := new(big.Int).Set(params.AmountSpecified)
amountCalculated := big.NewInt(0)
sqrtPriceX96 := new(big.Int).Set(pool.SqrtPriceX96)
tick := pool.Tick
liquidity := new(big.Int).Set(pool.Liquidity)
feeGrowthGlobal := new(big.Int)
if zeroForOne {
feeGrowthGlobal.Set(pool.FeeGrowth0X128)
} else {
feeGrowthGlobal.Set(pool.FeeGrowth1X128)
}
// Swap loop: iterate through ticks until amount is exhausted or price limit hit
maxIterations := 500 // Safety bound to prevent infinite loops
for i := 0; amountSpecifiedRemaining.Sign() != 0 && sqrtPriceX96.Cmp(sqrtPriceLimitX96) != 0 && i < maxIterations; i++ {
// Find the next initialized tick in the swap direction
tickNext, initialized := NextInitializedTickWithinOneWord(
pool.TickBitmap, tick, pool.TickSpacing, zeroForOne,
)
// Clamp tick to valid range
if tickNext < MinTick {
tickNext = MinTick
}
if tickNext > MaxTick {
tickNext = MaxTick
}
// Get the sqrt price at the next tick
sqrtPriceNextX96, err := GetSqrtRatioAtTick(tickNext)
if err != nil {
return ZeroBalanceDelta(), fmt.Errorf("swap: getSqrtRatioAtTick(%d): %w", tickNext, err)
}
// Determine the target price for this step: either the next tick or the limit
sqrtRatioTargetX96 := sqrtPriceNextX96
if zeroForOne {
if sqrtPriceNextX96.Cmp(sqrtPriceLimitX96) < 0 {
sqrtRatioTargetX96 = sqrtPriceLimitX96
}
} else {
if sqrtPriceNextX96.Cmp(sqrtPriceLimitX96) > 0 {
sqrtRatioTargetX96 = sqrtPriceLimitX96
}
}
// Compute this swap step
step := ComputeSwapStep(
sqrtPriceX96,
sqrtRatioTargetX96,
liquidity,
amountSpecifiedRemaining,
pool.LPFee,
)
// Update running amounts
if exactInput {
// amountSpecifiedRemaining is negative (exact input consumed)
consumed := new(big.Int).Add(step.AmountIn, step.FeeAmount)
amountSpecifiedRemaining.Add(amountSpecifiedRemaining, consumed) // becomes less negative
amountCalculated.Sub(amountCalculated, step.AmountOut) // output is negative (owed to user)
} else {
amountSpecifiedRemaining.Sub(amountSpecifiedRemaining, step.AmountOut) // exact output consumed
consumed := new(big.Int).Add(step.AmountIn, step.FeeAmount)
amountCalculated.Add(amountCalculated, consumed) // input owed to pool
}
// Update fee growth global
if step.FeeAmount.Sign() > 0 && liquidity.Sign() > 0 {
// feeGrowthGlobal += (feeAmount << 128) / liquidity
feeGrowthDelta := MulDiv(step.FeeAmount, Q128, liquidity)
feeGrowthGlobal.Add(feeGrowthGlobal, feeGrowthDelta)
}
// Update price
sqrtPriceX96 = step.SqrtRatioNextX96
// Cross tick if we reached the next initialized tick
if sqrtPriceX96.Cmp(sqrtPriceNextX96) == 0 {
if initialized {
// Cross the tick: update liquidity from tick's net liquidity
tickInfo := pool.Ticks[tickNext]
if tickInfo != nil {
liquidityNet := new(big.Int).Set(tickInfo.LiquidityNet)
// When moving left (zeroForOne), negate liquidityNet
if zeroForOne {
liquidityNet.Neg(liquidityNet)
}
liquidity.Add(liquidity, liquidityNet)
// Update tick's fee growth outside
// When crossing a tick, the fee growth outside flips:
// feeGrowthOutside = feeGrowthGlobal - feeGrowthOutside
if tickInfo.FeeGrowthOutside0X128 != nil {
if zeroForOne {
tickInfo.FeeGrowthOutside0X128 = new(big.Int).Sub(feeGrowthGlobal, tickInfo.FeeGrowthOutside0X128)
tickInfo.FeeGrowthOutside1X128 = new(big.Int).Sub(pool.FeeGrowth1X128, tickInfo.FeeGrowthOutside1X128)
} else {
tickInfo.FeeGrowthOutside0X128 = new(big.Int).Sub(pool.FeeGrowth0X128, tickInfo.FeeGrowthOutside0X128)
tickInfo.FeeGrowthOutside1X128 = new(big.Int).Sub(feeGrowthGlobal, tickInfo.FeeGrowthOutside1X128)
}
}
}
}
// Update tick: when zeroForOne, we just crossed the tick going left
if zeroForOne {
tick = tickNext - 1
} else {
tick = tickNext
}
} else {
// Didn't reach the tick — recompute tick from price
newTick, err := GetTickAtSqrtRatio(sqrtPriceX96)
if err == nil {
tick = newTick
}
}
}
// Update pool state
pool.SqrtPriceX96 = sqrtPriceX96
pool.Tick = tick
pool.Liquidity = liquidity
if zeroForOne {
pool.FeeGrowth0X128 = feeGrowthGlobal
} else {
pool.FeeGrowth1X128 = feeGrowthGlobal
}
// Compute balance delta in V4 internal convention:
// V4 Solidity: negative = user spent (paying), positive = user received
var v4amount0, v4amount1 *big.Int
if zeroForOne == exactInput {
v4amount0 = new(big.Int).Sub(params.AmountSpecified, amountSpecifiedRemaining)
v4amount1 = amountCalculated
} else {
v4amount0 = amountCalculated
v4amount1 = new(big.Int).Sub(params.AmountSpecified, amountSpecifiedRemaining)
}
// Convert to Go BalanceDelta convention: positive = user owes pool, negative = pool owes user.
// V4 Solidity uses opposite signs: negative = user paid, positive = user received.
// Negate both to convert.
amount0 := new(big.Int).Neg(v4amount0)
amount1 := new(big.Int).Neg(v4amount1)
return NewBalanceDelta(amount0, amount1), nil
}
// ModifyLiquidity adds or removes concentrated liquidity.
// Handles tick updates, bitmap flips, position tracking, fee accrual.
func (e *EmbeddedEngine) ModifyLiquidity(pool *PoolState, owner common.Address, params ModifyLiquidityParams) (BalanceDelta, BalanceDelta, error) {
tickLower := params.TickLower
tickUpper := params.TickUpper
liquidityDelta := params.LiquidityDelta
// Get or create position
posKey := PositionKey(owner, tickLower, tickUpper, params.Salt)
pos, ok := pool.Positions[posKey]
if !ok {
pos = &Position{
Owner: owner,
TickLower: tickLower,
TickUpper: tickUpper,
Liquidity: big.NewInt(0),
FeeGrowthInside0LastX128: big.NewInt(0),
FeeGrowthInside1LastX128: big.NewInt(0),
TokensOwed0: big.NewInt(0),
TokensOwed1: big.NewInt(0),
}
pool.Positions[posKey] = pos
}
// Compute fee growth inside the tick range
feeGrowthInside0X128, feeGrowthInside1X128 := e.getFeeGrowthInside(
pool, tickLower, tickUpper,
)
// Compute fees owed to the position
feesOwed0 := big.NewInt(0)
feesOwed1 := big.NewInt(0)
if pos.Liquidity.Sign() > 0 {
feeGrowthDelta0 := new(big.Int).Sub(feeGrowthInside0X128, pos.FeeGrowthInside0LastX128)
if feeGrowthDelta0.Sign() < 0 {
// Handle underflow (modular arithmetic)
feeGrowthDelta0.Add(feeGrowthDelta0, new(big.Int).Lsh(bigOne, 256))
}
feesOwed0 = MulDiv(feeGrowthDelta0, pos.Liquidity, Q128)
feeGrowthDelta1 := new(big.Int).Sub(feeGrowthInside1X128, pos.FeeGrowthInside1LastX128)
if feeGrowthDelta1.Sign() < 0 {
feeGrowthDelta1.Add(feeGrowthDelta1, new(big.Int).Lsh(bigOne, 256))
}
feesOwed1 = MulDiv(feeGrowthDelta1, pos.Liquidity, Q128)
}
// Update position
pos.FeeGrowthInside0LastX128 = new(big.Int).Set(feeGrowthInside0X128)
pos.FeeGrowthInside1LastX128 = new(big.Int).Set(feeGrowthInside1X128)
pos.TokensOwed0 = new(big.Int).Add(pos.TokensOwed0, feesOwed0)
pos.TokensOwed1 = new(big.Int).Add(pos.TokensOwed1, feesOwed1)
// Update liquidity delta on ticks
if liquidityDelta.Sign() != 0 {
// Update lower tick
e.updateTick(pool, tickLower, liquidityDelta, false)
// Update upper tick
e.updateTick(pool, tickUpper, liquidityDelta, true)
// Update position liquidity
newLiq := new(big.Int).Add(pos.Liquidity, liquidityDelta)
if newLiq.Sign() < 0 {
return ZeroBalanceDelta(), ZeroBalanceDelta(), fmt.Errorf("insufficient position liquidity")
}
pos.Liquidity = newLiq
// Update active liquidity if current tick is in range
currentTick := pool.Tick
if currentTick >= tickLower && currentTick < tickUpper {
pool.Liquidity = new(big.Int).Add(pool.Liquidity, liquidityDelta)
if pool.Liquidity.Sign() < 0 {
pool.Liquidity = big.NewInt(0)
}
}
}
// Compute token amounts required/returned for this liquidity change
var amount0, amount1 *big.Int
sqrtPriceLowerX96, _ := GetSqrtRatioAtTick(tickLower)
sqrtPriceUpperX96, _ := GetSqrtRatioAtTick(tickUpper)
amount0 = GetAmount0DeltaSigned(pool.SqrtPriceX96, sqrtPriceUpperX96, liquidityDelta)
amount1 = GetAmount1DeltaSigned(sqrtPriceLowerX96, pool.SqrtPriceX96, liquidityDelta)
// If current tick is below the range, only token0 is needed
if pool.Tick < tickLower {
amount0 = GetAmount0DeltaSigned(sqrtPriceLowerX96, sqrtPriceUpperX96, liquidityDelta)
amount1 = big.NewInt(0)
} else if pool.Tick >= tickUpper {
// Above range: only token1
amount0 = big.NewInt(0)
amount1 = GetAmount1DeltaSigned(sqrtPriceLowerX96, sqrtPriceUpperX96, liquidityDelta)
}
callerDelta := NewBalanceDelta(amount0, amount1)
feesAccrued := NewBalanceDelta(
new(big.Int).Neg(feesOwed0), // Fees are owed to user (negative from pool perspective)
new(big.Int).Neg(feesOwed1),
)
return callerDelta, feesAccrued, nil
}
// Donate distributes tokens to LPs via fee growth updates.
func (e *EmbeddedEngine) Donate(pool *PoolState, amount0, amount1 *big.Int) (BalanceDelta, error) {
if pool.Liquidity == nil || pool.Liquidity.Sign() <= 0 {
return ZeroBalanceDelta(), ErrNoLiquidity
}
if amount0 != nil && amount0.Sign() > 0 {
feeGrowthDelta := MulDiv(amount0, Q128, pool.Liquidity)
pool.FeeGrowth0X128 = new(big.Int).Add(pool.FeeGrowth0X128, feeGrowthDelta)
}
if amount1 != nil && amount1.Sign() > 0 {
feeGrowthDelta := MulDiv(amount1, Q128, pool.Liquidity)
pool.FeeGrowth1X128 = new(big.Int).Add(pool.FeeGrowth1X128, feeGrowthDelta)
}
a0 := amount0
a1 := amount1
if a0 == nil {
a0 = big.NewInt(0)
}
if a1 == nil {
a1 = big.NewInt(0)
}
return NewBalanceDelta(a0, a1), nil
}
// Quote estimates swap output without mutating state.
func (e *EmbeddedEngine) Quote(pool *Pool, amountIn *big.Int, zeroForOne bool) *big.Int {
if pool.Liquidity.Sign() == 0 || amountIn.Sign() <= 0 {
return big.NewInt(0)
}
// Simple single-step quote using current liquidity (no tick crossing).
// For a more accurate quote, callers should use the full Swap path.
var sqrtPriceLimitX96 *big.Int
if zeroForOne {
sqrtPriceLimitX96 = new(big.Int).Add(MinSqrtRatio, bigOne)
} else {
sqrtPriceLimitX96 = new(big.Int).Sub(MaxSqrtRatio, bigOne)
}
step := ComputeSwapStep(
pool.SqrtPriceX96,
sqrtPriceLimitX96,
pool.Liquidity,
new(big.Int).Neg(amountIn), // exact input convention
3000, // default fee
)
return new(big.Int).Abs(step.AmountOut)
}
// updateTick updates tick state when liquidity is added/removed.
func (e *EmbeddedEngine) updateTick(pool *PoolState, tick int32, liquidityDelta *big.Int, upper bool) {
ti := pool.getOrCreateTick(tick)
liquidityGrossBefore := new(big.Int).Set(ti.LiquidityGross)
liquidityGrossAfter := new(big.Int).Add(liquidityGrossBefore, liquidityDelta)
// Handle flip: when a tick goes from 0 to nonzero liquidity or vice versa
flipped := (liquidityGrossAfter.Sign() == 0) != (liquidityGrossBefore.Sign() == 0)
if flipped {
FlipTick(pool.TickBitmap, tick, pool.TickSpacing)
}
ti.LiquidityGross = liquidityGrossAfter
if upper {
// Upper tick: subtract liquidity net
ti.LiquidityNet = new(big.Int).Sub(ti.LiquidityNet, liquidityDelta)
} else {
// Lower tick: add liquidity net
ti.LiquidityNet = new(big.Int).Add(ti.LiquidityNet, liquidityDelta)
}
// Initialize fee growth outside for new ticks below the current tick
if flipped && liquidityGrossAfter.Sign() > 0 {
if tick <= pool.Tick {
ti.FeeGrowthOutside0X128 = new(big.Int).Set(pool.FeeGrowth0X128)
ti.FeeGrowthOutside1X128 = new(big.Int).Set(pool.FeeGrowth1X128)
}
}
// Clean up tick if all liquidity removed
if liquidityGrossAfter.Sign() == 0 {
ti.LiquidityNet = big.NewInt(0)
ti.FeeGrowthOutside0X128 = big.NewInt(0)
ti.FeeGrowthOutside1X128 = big.NewInt(0)
}
}
// getFeeGrowthInside computes fee growth accumulated within a tick range.
func (e *EmbeddedEngine) getFeeGrowthInside(pool *PoolState, tickLower, tickUpper int32) (*big.Int, *big.Int) {
tiLower := pool.Ticks[tickLower]
tiUpper := pool.Ticks[tickUpper]
var feeGrowthBelow0, feeGrowthBelow1, feeGrowthAbove0, feeGrowthAbove1 *big.Int
if tiLower != nil {
if pool.Tick >= tickLower {
feeGrowthBelow0 = tiLower.FeeGrowthOutside0X128
feeGrowthBelow1 = tiLower.FeeGrowthOutside1X128
} else {
feeGrowthBelow0 = new(big.Int).Sub(pool.FeeGrowth0X128, tiLower.FeeGrowthOutside0X128)
feeGrowthBelow1 = new(big.Int).Sub(pool.FeeGrowth1X128, tiLower.FeeGrowthOutside1X128)
}
} else {
feeGrowthBelow0 = big.NewInt(0)
feeGrowthBelow1 = big.NewInt(0)
}
if tiUpper != nil {
if pool.Tick < tickUpper {
feeGrowthAbove0 = tiUpper.FeeGrowthOutside0X128
feeGrowthAbove1 = tiUpper.FeeGrowthOutside1X128
} else {
feeGrowthAbove0 = new(big.Int).Sub(pool.FeeGrowth0X128, tiUpper.FeeGrowthOutside0X128)
feeGrowthAbove1 = new(big.Int).Sub(pool.FeeGrowth1X128, tiUpper.FeeGrowthOutside1X128)
}
} else {
feeGrowthAbove0 = big.NewInt(0)
feeGrowthAbove1 = big.NewInt(0)
}
feeGrowthInside0 := new(big.Int).Sub(pool.FeeGrowth0X128, feeGrowthBelow0)
feeGrowthInside0.Sub(feeGrowthInside0, feeGrowthAbove0)
feeGrowthInside1 := new(big.Int).Sub(pool.FeeGrowth1X128, feeGrowthBelow1)
feeGrowthInside1.Sub(feeGrowthInside1, feeGrowthAbove1)
return feeGrowthInside0, feeGrowthInside1
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"math/big"
"github.com/luxfi/geth/common"
)
// inertEngine is the package-default Engine. It performs NO matching and holds
// NO state: every operation returns ErrDEXBackendNotConfigured so that a chain
// which deploys the LP-9010 precompile but has not wired a real backend reverts
// cleanly instead of silently running a wrong matcher.
//
// Why this is the default (decomplect, doc line 20): the V4 PoolManager facade
// is a CENTRAL LIMIT ORDER BOOK, not an AMM. The previous default — an embedded
// Uniswap-V4 concentrated-liquidity AMM — was both the WRONG engine and a
// SECOND matcher living on the EVM side, duplicating the d-chain's authority.
// There is exactly ONE matcher in the tree (the d-chain, reached over ZAP);
// the EVM precompile holds zero canonical DEX state. Until the venue installs
// the ZAP backend (SetBackend(NewZAPEngine(...)) when dex-zap-endpoint is set),
// 0x9010 is inert.
//
// Contract: never panics, never moves value, never fabricates a fill. Every
// method is a pure error return — the safe default for an unconfigured venue.
type inertEngine struct{}
// Compile-time assertion: inertEngine satisfies the Engine contract. It does NOT
// implement poolRouter — there is no canonical pool to route to.
var _ Engine = (*inertEngine)(nil)
// newInertEngine constructs the default no-backend engine.
func newInertEngine() *inertEngine { return &inertEngine{} }
// Brand identifies the unconfigured state in boot logs / error wrapping. It is
// intentionally generic and brand-free — no venue is wired.
func (inertEngine) Brand() string { return "DEX (no backend configured)" }
// Initialize refuses: no backend means no market can be created.
func (inertEngine) Initialize(_ *big.Int) (int24, error) {
return 0, ErrDEXBackendNotConfigured
}
// Swap refuses: there is no matcher to submit a marketable order to.
func (inertEngine) Swap(_ *PoolState, _ SwapParams) (BalanceDelta, error) {
return ZeroBalanceDelta(), ErrDEXBackendNotConfigured
}
// ModifyLiquidity refuses: there is no book to rest or pull an order on.
func (inertEngine) ModifyLiquidity(_ *PoolState, _ common.Address, _ ModifyLiquidityParams) (BalanceDelta, BalanceDelta, error) {
return ZeroBalanceDelta(), ZeroBalanceDelta(), ErrDEXBackendNotConfigured
}
// Donate refuses: a CLOB has no shared pool to gift into, and no backend is
// wired regardless.
func (inertEngine) Donate(_ *PoolState, _, _ *big.Int) (BalanceDelta, error) {
return ZeroBalanceDelta(), ErrDEXBackendNotConfigured
}
// Quote returns zero: with no backend there is no book to read a price from.
// Quote is a read-only estimate, so it returns a benign zero rather than an
// error (the caller treats zero as "no route"), keeping router fallbacks sane.
func (inertEngine) Quote(_ *Pool, _ *big.Int, _ bool) *big.Int {
return big.NewInt(0)
}
+598 -398
View File
File diff suppressed because it is too large Load Diff
+581
View File
@@ -0,0 +1,581 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dex
import (
"context"
"encoding/binary"
"math"
"math/big"
"sort"
"sync"
"testing"
"time"
"github.com/luxfi/geth/common"
)
// =========================================================================
// Pure-Go value-conservation + atomicity coverage for the ZAP CLOB adapter.
//
// These tests drive the production ZAPEngine through the PUBLIC PoolManager
// surface against a fake in-memory CLOB that speaks the FROZEN ZAP wire frame
// (clob_ensure_market / clob_place / clob_cancel / clob_submit). They depend on
// NO external process and NO luxfi/dex import — the fake is wired via the
// zapDialer seam — so they run under `CGO_ENABLED=0 go test ./dex/` and keep the
// precompile a pure-Go leaf module (no replace in go.mod).
//
// They are the authored proof for the rip-out's exit criteria:
// - VALUE CONSERVATION: every BalanceDelta is derived ONLY from server fills,
// and taker+maker nets to (0,0).
// - ATOMICITY: an unfilled marketable order yields a zero delta and rests
// nothing; a transport failure yields an error and NO committed delta.
// - IOC: a marketable order never rests its remainder.
// - REPLAY-IDEMPOTENCY (RED H1): the same EVM tx submitted twice maps to ONE
// match against the book.
// - CANCEL-AUTH (RED H4): a caller cannot cancel another maker's order.
// =========================================================================
// fakeOrder is one resting order in the fake CLOB.
type fakeOrder struct {
id uint64
side uint8 // 0 = bid, 1 = ask
price float64
size float64
maker common.Address
}
// fakeCLOB is a minimal, deterministic in-memory central limit order book that
// implements exactly the ZAP CLOB wire contract the ZAPEngine speaks. It is a
// TEST DOUBLE for the d-chain gateway, not a second matcher shipped anywhere.
type fakeCLOB struct {
mu sync.Mutex
markets map[[32]byte][]*fakeOrder
nextID uint64
submits int // count of clob_submit calls actually matched (replay probe)
failNext bool
closed bool
}
func newFakeCLOB() *fakeCLOB {
return &fakeCLOB{markets: make(map[[32]byte][]*fakeOrder)}
}
// conn returns a zapConn bound to this book for the zapDialer seam.
func (f *fakeCLOB) conn() zapConn { return &fakeConn{clob: f} }
type fakeConn struct{ clob *fakeCLOB }
func (c *fakeConn) Close() error { return nil }
func (c *fakeConn) Call(_ context.Context, method string, payload []byte) ([]byte, error) {
return c.clob.dispatch(method, payload)
}
func (f *fakeCLOB) dispatch(method string, payload []byte) ([]byte, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.failNext {
f.failNext = false
return nil, context.DeadlineExceeded // simulate a transport failure
}
switch method {
case ZAPMethodEnsureMarket:
var id [32]byte
copy(id[:], payload[0:32])
if _, ok := f.markets[id]; !ok {
f.markets[id] = nil
}
return ackBytes(0, clobStatusPlaced, 1), nil
case ZAPMethodPlace:
return f.place(payload)
case ZAPMethodCancel:
return f.cancel(payload)
case ZAPMethodSubmit:
return f.submit(payload)
default:
return rejectBytes(0, "unknown method"), nil
}
}
func (f *fakeCLOB) place(payload []byte) ([]byte, error) {
var id [32]byte
copy(id[:], payload[0:32])
side := payload[32]
price := math.Float64frombits(binary.BigEndian.Uint64(payload[33:41]))
size := math.Float64frombits(binary.BigEndian.Uint64(payload[41:49]))
var maker common.Address
copy(maker[:16], payload[49:65])
f.nextID++
o := &fakeOrder{id: f.nextID, side: side, price: price, size: size, maker: maker}
f.markets[id] = append(f.markets[id], o)
return ackBytes(o.id, clobStatusPlaced, f.nextID), nil
}
func (f *fakeCLOB) cancel(payload []byte) ([]byte, error) {
var id [32]byte
copy(id[:], payload[0:32])
orderID := binary.BigEndian.Uint64(payload[32:40])
book := f.markets[id]
for i, o := range book {
if o.id == orderID {
f.markets[id] = append(book[:i], book[i+1:]...)
return ackBytes(orderID, clobStatusCanceled, f.nextID), nil
}
}
return rejectBytes(orderID, "unknown order"), nil
}
// submit matches a marketable order IOC against the resting book and returns
// the fills. Buy (side 0) crosses asks ascending; sell (side 1) crosses bids
// descending. Any unfilled remainder is DROPPED (IOC) — never rested.
func (f *fakeCLOB) submit(payload []byte) ([]byte, error) {
var id [32]byte
copy(id[:], payload[0:32])
side := payload[32] // 0 buy, 1 sell
isMarket := payload[33] == 1
limit := math.Float64frombits(binary.BigEndian.Uint64(payload[34:42]))
size := math.Float64frombits(binary.BigEndian.Uint64(payload[42:50]))
f.submits++
book := f.markets[id]
// Candidates are the opposite side.
var cands []*fakeOrder
for _, o := range book {
if side == 0 && o.side == 1 {
cands = append(cands, o)
} else if side == 1 && o.side == 0 {
cands = append(cands, o)
}
}
if side == 0 { // buy: cross cheapest asks first
sort.Slice(cands, func(i, j int) bool { return cands[i].price < cands[j].price })
} else { // sell: cross richest bids first
sort.Slice(cands, func(i, j int) bool { return cands[i].price > cands[j].price })
}
remaining := size
var fills [][3]float64 // price, size, side
for _, o := range cands {
if remaining <= 0 {
break
}
if !isMarket {
if side == 0 && o.price > limit {
break
}
if side == 1 && o.price < limit {
break
}
}
take := math.Min(remaining, o.size)
fills = append(fills, [3]float64{o.price, take, float64(o.side)})
o.size -= take
remaining -= take
}
// Drop fully-consumed resting orders. Remainder of the taker is NOT rested.
pruned := book[:0]
for _, o := range book {
if o.size > 0 {
pruned = append(pruned, o)
}
}
f.markets[id] = pruned
return fillsBytes(fills), nil
}
func (f *fakeCLOB) submitCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.submits
}
// --- wire encoders mirroring dex/pkg/api exactly ---
func ackBytes(orderID uint64, status uint8, seq uint64) []byte {
b := make([]byte, 17)
binary.BigEndian.PutUint64(b[0:8], orderID)
b[8] = status
binary.BigEndian.PutUint64(b[9:17], seq)
return b
}
func rejectBytes(orderID uint64, reason string) []byte {
b := make([]byte, 11+len(reason))
binary.BigEndian.PutUint64(b[0:8], orderID)
b[8] = clobStatusRejected
binary.BigEndian.PutUint16(b[9:11], uint16(len(reason)))
copy(b[11:], reason)
return b
}
func fillsBytes(fills [][3]float64) []byte {
b := make([]byte, 4+len(fills)*fillWireSize)
binary.BigEndian.PutUint32(b[0:4], uint32(len(fills)))
off := 4
for _, fl := range fills {
binary.BigEndian.PutUint64(b[off:off+8], math.Float64bits(fl[0]))
binary.BigEndian.PutUint64(b[off+8:off+16], math.Float64bits(fl[1]))
b[off+16] = byte(fl[2])
off += fillWireSize
}
return b
}
// withFakeCLOB installs a fake-CLOB-backed zapDialer for the duration of a test
// and restores the canonical dialer after.
func withFakeCLOB(t *testing.T, f *fakeCLOB) {
t.Helper()
orig := zapDialer
zapDialer = func(_ context.Context, _ string) (zapConn, error) { return f.conn(), nil }
t.Cleanup(func() { zapDialer = orig })
}
// tickForPriceLocal snaps a target price to the nearest TickSpacing030 tick.
func tickForPriceLocal(t *testing.T, want float64) int24 {
t.Helper()
raw := int(math.Log(want)/math.Log(1.0001) + 0.5)
spacing := int(TickSpacing030)
snapped := (raw / spacing) * spacing
if _, err := tickToPrice(int24(snapped)); err != nil {
t.Fatalf("tickToPrice(%d): %v", snapped, err)
}
return int24(snapped)
}
func priceForTickLocal(t *testing.T, tick int24) float64 {
t.Helper()
p, err := tickToPrice(tick)
if err != nil {
t.Fatalf("tickToPrice(%d): %v", tick, err)
}
return p
}
func roundFloatToBig(f float64) *big.Int {
bf := new(big.Float).SetFloat64(math.Round(f))
i, _ := bf.Int(nil)
return i
}
// txStateDB is a MockStateDB that also names a transaction, exercising the
// idempotencyBinder seam end-to-end through the PoolManager.
type txStateDB struct {
*MockStateDB
txHash common.Hash
}
func (s *txStateDB) TxHash() common.Hash { return s.txHash }
func conservationPoolKey() PoolKey {
return PoolKey{
Currency0: NativeCurrency,
Currency1: Currency{Address: common.HexToAddress("0x000000000000000000000000000000000000C0DE")},
Fee: Fee030,
TickSpacing: TickSpacing030,
Hooks: common.Address{},
}
}
// TestZAPConservationSingleCross drives a marketable buy against one resting ask
// through the full PoolManager and asserts the taker delta is derived only from
// the server fill and that taker+maker conserves value exactly.
func TestZAPConservationSingleCross(t *testing.T) {
f := newFakeCLOB()
withFakeCLOB(t, f)
zap := NewZAPEngine("fake:0", 2*time.Second)
defer zap.Close()
pm := NewPoolManager(zap)
stateDB := NewMockStateDB()
key := conservationPoolKey()
lp := common.HexToAddress("0x2222222222222222222222222222222222222222")
taker := common.HexToAddress("0x1111111111111111111111111111111111111111")
if _, err := pm.Initialize(stateDB, key, new(big.Int).Set(Q96), nil); err != nil {
t.Fatalf("Initialize: %v", err)
}
askTick := tickForPriceLocal(t, 2.0)
askPrice := priceForTickLocal(t, askTick)
_, _, err := pm.ModifyLiquidity(stateDB, lp, key, ModifyLiquidityParams{
TickLower: askTick,
TickUpper: askTick + TickSpacing030,
LiquidityDelta: big.NewInt(100),
}, nil)
if err != nil {
t.Fatalf("ModifyLiquidity ask: %v", err)
}
const takeBase = int64(40)
delta, err := pm.Swap(stateDB, taker, key, SwapParams{
ZeroForOne: false,
AmountSpecified: big.NewInt(-takeBase),
SqrtPriceLimitX96: MaxSqrtRatio,
}, nil)
if err != nil {
t.Fatalf("Swap: %v", err)
}
// Buy: taker receives base (Amount0<0), pays quote (Amount1>0).
if delta.Amount0.Cmp(big.NewInt(-takeBase)) != 0 {
t.Fatalf("base = %s, want -%d", delta.Amount0, takeBase)
}
wantQuote := roundFloatToBig(askPrice * float64(takeBase))
if delta.Amount1.Cmp(wantQuote) != 0 {
t.Fatalf("quote = %s, want %s", delta.Amount1, wantQuote)
}
// Cross-counterparty conservation: maker delta is the exact negation.
makerRealized := NewBalanceDelta(big.NewInt(takeBase), new(big.Int).Neg(wantQuote))
if net := delta.Add(makerRealized); !net.IsZero() {
t.Fatalf("value NOT conserved: net = (%s,%s)", net.Amount0, net.Amount1)
}
}
// TestZAPConservationMultiLevel sweeps two ask levels and asserts the aggregate
// quote is the sum of fills, conserved across counterparties.
func TestZAPConservationMultiLevel(t *testing.T) {
f := newFakeCLOB()
withFakeCLOB(t, f)
zap := NewZAPEngine("fake:0", 2*time.Second)
defer zap.Close()
pm := NewPoolManager(zap)
stateDB := NewMockStateDB()
key := conservationPoolKey()
lp := common.HexToAddress("0x2222222222222222222222222222222222222222")
taker := common.HexToAddress("0x1111111111111111111111111111111111111111")
if _, err := pm.Initialize(stateDB, key, new(big.Int).Set(Q96), nil); err != nil {
t.Fatalf("Initialize: %v", err)
}
tk0 := tickForPriceLocal(t, 2.0)
tk1 := tickForPriceLocal(t, 4.0)
p0 := priceForTickLocal(t, tk0)
p1 := priceForTickLocal(t, tk1)
for _, lvl := range []struct {
tick int24
size int64
}{{tk0, 30}, {tk1, 30}} {
if _, _, err := pm.ModifyLiquidity(stateDB, lp, key, ModifyLiquidityParams{
TickLower: lvl.tick,
TickUpper: lvl.tick + TickSpacing030,
LiquidityDelta: big.NewInt(lvl.size),
}, nil); err != nil {
t.Fatalf("ModifyLiquidity @ %d: %v", lvl.tick, err)
}
}
delta, err := pm.Swap(stateDB, taker, key, SwapParams{
ZeroForOne: false,
AmountSpecified: big.NewInt(-50),
SqrtPriceLimitX96: MaxSqrtRatio,
}, nil)
if err != nil {
t.Fatalf("Swap: %v", err)
}
if delta.Amount0.Cmp(big.NewInt(-50)) != 0 {
t.Fatalf("base = %s, want -50", delta.Amount0)
}
wantQuote := roundFloatToBig(30*p0 + 20*p1)
if delta.Amount1.Cmp(wantQuote) != 0 {
t.Fatalf("quote = %s, want %s", delta.Amount1, wantQuote)
}
if net := delta.Add(NewBalanceDelta(big.NewInt(50), new(big.Int).Neg(wantQuote))); !net.IsZero() {
t.Fatalf("value NOT conserved: net = (%s,%s)", net.Amount0, net.Amount1)
}
}
// TestZAPAtomicEmptyBook proves a marketable order against an empty book yields
// a ZERO delta and rests nothing — no half-applied state, no fabricated fill.
func TestZAPAtomicEmptyBook(t *testing.T) {
f := newFakeCLOB()
withFakeCLOB(t, f)
zap := NewZAPEngine("fake:0", 2*time.Second)
defer zap.Close()
pm := NewPoolManager(zap)
stateDB := NewMockStateDB()
key := conservationPoolKey()
taker := common.HexToAddress("0x1111111111111111111111111111111111111111")
if _, err := pm.Initialize(stateDB, key, new(big.Int).Set(Q96), nil); err != nil {
t.Fatalf("Initialize: %v", err)
}
delta, err := pm.Swap(stateDB, taker, key, SwapParams{
ZeroForOne: false,
AmountSpecified: big.NewInt(-10),
SqrtPriceLimitX96: MaxSqrtRatio,
}, nil)
if err != nil {
t.Fatalf("Swap empty book: %v", err)
}
if !delta.IsZero() {
t.Fatalf("empty-book delta = (%s,%s), want (0,0)", delta.Amount0, delta.Amount1)
}
}
// TestZAPAtomicTransportFailureNoDelta proves a transport failure on submit
// yields an error and NO committed delta — the C-side commits nothing when the
// d-chain leg did not return fills (atomicity-or-reversal, RED C2).
func TestZAPAtomicTransportFailureNoDelta(t *testing.T) {
f := newFakeCLOB()
withFakeCLOB(t, f)
zap := NewZAPEngine("fake:0", 2*time.Second)
defer zap.Close()
pm := NewPoolManager(zap)
stateDB := NewMockStateDB()
key := conservationPoolKey()
lp := common.HexToAddress("0x2222222222222222222222222222222222222222")
taker := common.HexToAddress("0x1111111111111111111111111111111111111111")
if _, err := pm.Initialize(stateDB, key, new(big.Int).Set(Q96), nil); err != nil {
t.Fatalf("Initialize: %v", err)
}
if _, _, err := pm.ModifyLiquidity(stateDB, lp, key, ModifyLiquidityParams{
TickLower: tickForPriceLocal(t, 2.0),
TickUpper: tickForPriceLocal(t, 2.0) + TickSpacing030,
LiquidityDelta: big.NewInt(100),
}, nil); err != nil {
t.Fatalf("ModifyLiquidity: %v", err)
}
f.mu.Lock()
f.failNext = true
f.mu.Unlock()
delta, err := pm.Swap(stateDB, taker, key, SwapParams{
ZeroForOne: false,
AmountSpecified: big.NewInt(-40),
SqrtPriceLimitX96: MaxSqrtRatio,
}, nil)
if err == nil {
t.Fatal("Swap with transport failure must error")
}
if !delta.IsZero() {
t.Fatalf("failed swap returned non-zero delta (%s,%s) — must commit nothing", delta.Amount0, delta.Amount1)
}
}
// TestZAPReplayIdempotency proves the SAME EVM tx submitted twice maps to ONE
// match against the book: the second call returns the committed delta WITHOUT a
// second submit (RED H1).
func TestZAPReplayIdempotency(t *testing.T) {
f := newFakeCLOB()
withFakeCLOB(t, f)
zap := NewZAPEngine("fake:0", 2*time.Second)
defer zap.Close()
pm := NewPoolManager(zap)
base := NewMockStateDB()
stateDB := &txStateDB{MockStateDB: base, txHash: common.HexToHash("0xdeadbeef")}
key := conservationPoolKey()
lp := common.HexToAddress("0x2222222222222222222222222222222222222222")
taker := common.HexToAddress("0x1111111111111111111111111111111111111111")
if _, err := pm.Initialize(stateDB, key, new(big.Int).Set(Q96), nil); err != nil {
t.Fatalf("Initialize: %v", err)
}
if _, _, err := pm.ModifyLiquidity(stateDB, lp, key, ModifyLiquidityParams{
TickLower: tickForPriceLocal(t, 2.0),
TickUpper: tickForPriceLocal(t, 2.0) + TickSpacing030,
LiquidityDelta: big.NewInt(100),
}, nil); err != nil {
t.Fatalf("ModifyLiquidity: %v", err)
}
params := SwapParams{ZeroForOne: false, AmountSpecified: big.NewInt(-40), SqrtPriceLimitX96: MaxSqrtRatio}
d1, err := pm.Swap(stateDB, taker, key, params, nil)
if err != nil {
t.Fatalf("Swap #1: %v", err)
}
submitsAfter1 := f.submitCount()
d2, err := pm.Swap(stateDB, taker, key, params, nil)
if err != nil {
t.Fatalf("Swap #2 (replay): %v", err)
}
if f.submitCount() != submitsAfter1 {
t.Fatalf("replay submitted again: submits went %d -> %d, want stable (idempotent)", submitsAfter1, f.submitCount())
}
if d1.Amount0.Cmp(d2.Amount0) != 0 || d1.Amount1.Cmp(d2.Amount1) != 0 {
t.Fatalf("replay delta mismatch: #1 (%s,%s) vs #2 (%s,%s)", d1.Amount0, d1.Amount1, d2.Amount0, d2.Amount1)
}
// A DIFFERENT tx hash must submit afresh against whatever still rests.
stateDB2 := &txStateDB{MockStateDB: base, txHash: common.HexToHash("0xfeedface")}
if _, err := pm.Swap(stateDB2, taker, key, params, nil); err != nil {
t.Fatalf("Swap (new tx): %v", err)
}
if f.submitCount() != submitsAfter1+1 {
t.Fatalf("new tx did not submit: submits=%d, want %d", f.submitCount(), submitsAfter1+1)
}
}
// TestZAPCancelAuthCannotCancelOthersOrder proves the IDOR is closed: a caller
// who did not place an order cannot cancel it via a guessed/forged salt — the
// adapter resolves the server orderID only from the caller's OWN authenticated
// (maker, poolId, salt) handle (RED H4).
func TestZAPCancelAuthCannotCancelOthersOrder(t *testing.T) {
f := newFakeCLOB()
withFakeCLOB(t, f)
zap := NewZAPEngine("fake:0", 2*time.Second)
defer zap.Close()
pm := NewPoolManager(zap)
stateDB := NewMockStateDB()
key := conservationPoolKey()
lp := common.HexToAddress("0x2222222222222222222222222222222222222222")
attacker := common.HexToAddress("0x3333333333333333333333333333333333333333")
if _, err := pm.Initialize(stateDB, key, new(big.Int).Set(Q96), nil); err != nil {
t.Fatalf("Initialize: %v", err)
}
askParams := ModifyLiquidityParams{
TickLower: tickForPriceLocal(t, 2.0),
TickUpper: tickForPriceLocal(t, 2.0) + TickSpacing030,
LiquidityDelta: big.NewInt(100),
Salt: [32]byte{0x01},
}
if _, _, err := pm.ModifyLiquidity(stateDB, lp, key, askParams, nil); err != nil {
t.Fatalf("LP place: %v", err)
}
poolId := key.ID()
if got := len(f.markets[poolId]); got != 1 {
t.Fatalf("resting orders = %d, want 1", got)
}
// Attacker tries to cancel the LP's order: same ticks/salt, but as a
// DIFFERENT caller. The handle is keyed by caller identity, so no order
// resolves for the attacker -> cancel must fail and the order must remain.
cancelParams := ModifyLiquidityParams{
TickLower: askParams.TickLower,
TickUpper: askParams.TickUpper,
LiquidityDelta: big.NewInt(-100),
Salt: [32]byte{0x01}, // forged: copy of the LP's salt
}
if _, _, err := pm.ModifyLiquidity(stateDB, attacker, key, cancelParams, nil); err == nil {
t.Fatal("attacker cancel must fail (IDOR) — no order is bound to the attacker")
}
if got := len(f.markets[poolId]); got != 1 {
t.Fatalf("LP order vanished after attacker cancel: resting = %d, want 1", got)
}
// The real maker CAN cancel its own order.
if _, _, err := pm.ModifyLiquidity(stateDB, lp, key, cancelParams, nil); err != nil {
t.Fatalf("maker self-cancel: %v", err)
}
if got := len(f.markets[poolId]); got != 0 {
t.Fatalf("maker cancel did not remove order: resting = %d, want 0", got)
}
}
+15 -6
View File
@@ -51,18 +51,20 @@ var (
// EVM (Lux C-Chain, Hanzo, Zoo, SPC, and white-label deployments such as
// downstream EVM). The ABI is identical for every chain.
//
// - The math/matching backend is parameterized. Default is the in-process
// EmbeddedEngine (Uniswap V4 math compiled into the precompile). A host
// binary (the EVM plugin main) may replace it before first use via
// SetBackend() — for example a downstream EVM build that points the precompile
// at a downstream-tenant DEX process over ZAP.
// - The math/matching backend is parameterized. The default is the INERT
// engine (engine_inert.go): it performs no matching and reverts every call
// with ErrDEXBackendNotConfigured. V4 = CLOB, so the matcher lives ONLY on
// the d-chain, reached over ZAP — never embedded in the precompile. The
// venue makes 0x9010 live by calling SetBackend(NewZAPEngine(endpoint))
// from the EVM plugin main when dex-zap-endpoint is set; chains that do not
// enable it run an inert precompile that cleanly reverts.
//
// - Brand identity is a value the backend carries (Engine.Brand()). User-
// facing error strings produced by the precompile MUST come from the
// backend, never hard-coded here. This keeps a tenant surface free of
// the word "Lux" while still letting the OSS package be called "Lux DEX"
// on Lux-network deployments.
var DEXPrecompile = newDEXContract(NewEmbeddedEngine())
var DEXPrecompile = newDEXContract(newInertEngine())
// SetBackend swaps the DEX engine backing the singleton precompile.
//
@@ -925,6 +927,13 @@ func (a *poolStateAdapter) AddLog(log *ethtypes.Log) {
a.stateDB.AddLog(log)
}
// TxHash exposes the executing transaction hash (txIdentified seam) so the
// PoolManager can bind a marketable order to its EVM tx for replay-idempotency
// (RED H1). Sourced from the underlying geth StateDB.
func (a *poolStateAdapter) TxHash() common.Hash {
return a.stateDB.TxHash()
}
// Helper functions for encoding/decoding
func int24ToBytes(v int24) []byte {
+113 -14
View File
@@ -69,6 +69,42 @@ func NewPoolState(pool *Pool, tickSpacing int32, lpFee uint32) *PoolState {
}
}
// routePool tells a poolRouter backend (e.g. ZAPEngine) which canonical pool a
// PoolState maps to, so the next engine delegation forwards to the right
// server-side pool. No-op for the embedded engine (not a poolRouter). Called on
// every swap/modify/donate/quote because the cache PoolState may be rebuilt
// from StateDB between calls, so the route must be (re)asserted each time.
func (pm *PoolManager) routePool(poolId [32]byte, ps *PoolState) {
if router, ok := pm.engine.(poolRouter); ok {
router.SetPoolID(ps, poolId)
}
}
// txIdentified is the OPTIONAL capability a StateDB exposes when it can name the
// executing EVM transaction. The production poolStateAdapter implements it
// (sourced from contract.StateDB.TxHash); test StateDBs need not. PoolManager
// uses it only to thread tx identity into an idempotencyBinder backend.
type txIdentified interface {
TxHash() common.Hash
}
// bindTx threads the executing tx hash into an idempotencyBinder backend (e.g.
// ZAPEngine) so a marketable order is submitted to the d-chain at most once per
// EVM tx — re-execution / reorg / retry returns the committed delta instead of
// double-filling (RED H1). No-op when the engine is not a binder or the StateDB
// cannot name the tx (the binder then disables its cache for that call).
func (pm *PoolManager) bindTx(stateDB StateDB, ps *PoolState) {
binder, ok := pm.engine.(idempotencyBinder)
if !ok {
return
}
var txHash common.Hash
if tx, ok := stateDB.(txIdentified); ok {
txHash = tx.TxHash()
}
binder.BindTx(ps, txHash)
}
// getOrCreateTick returns the TickInfo for a tick, creating it if absent.
func (ps *PoolState) getOrCreateTick(tick int32) *TickInfo {
ti, ok := ps.Ticks[tick]
@@ -198,6 +234,21 @@ func (pm *PoolManager) Initialize(
ps := NewPoolState(pool, key.TickSpacing, key.Fee)
pm.poolStates[poolId] = ps
// ZAP path: create the CANONICAL pool on the D-Chain server and route this
// (cache) PoolState to it. The server's copy is the source of truth; the
// local ps/pool above are a read-through view persisted to StateDB. The
// embedded path skips this (engine is not a poolRouter) and keeps ps as the
// authoritative state, unchanged.
if router, ok := pm.engine.(poolRouter); ok {
serverTick, perr := router.InitializePool(ps, poolId, sqrtPriceX96, key.TickSpacing, uint32(key.Fee))
if perr != nil {
return 0, perr
}
// The server's tick is authoritative; sync the cache to it.
tick = serverTick
pool.Tick = serverTick
}
pm.setPool(stateDB, poolId, pool)
if key.Hooks != (common.Address{}) {
@@ -410,6 +461,8 @@ func (pm *PoolManager) Swap(
}
}
pm.routePool(poolId, ps)
pm.bindTx(stateDB, ps)
delta, err := pm.engine.Swap(ps, params)
if err != nil {
return ZeroBalanceDelta(), err
@@ -489,6 +542,7 @@ func (pm *PoolManager) ModifyLiquidity(
}
}
pm.routePool(poolId, ps)
callerDelta, feesAccrued, err := pm.engine.ModifyLiquidity(ps, caller, params)
if err != nil {
return ZeroBalanceDelta(), ZeroBalanceDelta(), err
@@ -552,6 +606,7 @@ func (pm *PoolManager) Donate(
}
}
pm.routePool(poolId, ps)
delta, err := pm.engine.Donate(ps, amount0, amount1)
if err != nil {
return ZeroBalanceDelta(), err
@@ -795,6 +850,33 @@ func (pm *PoolManager) transferERC20(stateDB StateDB, currency Currency, from, t
return pm.transferToken(stateDB, currency, from, to, amount)
}
// erc20BalanceSlot0 is the canonical Solidity storage slot for a `balanceOf`
// mapping declared as the FIRST state variable (slot 0):
// keccak256(abi.encode(holder, uint256(0))). This is the layout emitted by
// OpenZeppelin ERC20 and the Lux-canonical LUSD/LETH tokens. It is NOT a guess
// for arbitrary third-party tokens — a token whose balance mapping is not at
// slot 0 must settle through the d-chain atomic import/export, never a poke.
func erc20BalanceSlot0(holder common.Address) common.Hash {
key := make([]byte, 64)
copy(key[12:32], holder.Bytes()) // mapping key: left-padded address
// slot 0: key[32:64] left as zero.
return common.BytesToHash(crypto.Keccak256(key))
}
// transferToken settles a single currency leg on the C-Chain.
//
// Two — and only two — paths exist (decomplect: one home per asset class):
// - Native LUX (address(0)): move account balance directly.
// - C-resident ERC-20 using the canonical slot-0 balanceOf layout: adjust the
// two holders' balance slots with full sufficiency checks.
//
// The previous code poked a single HARDCODED slot for EVERY token, which
// silently corrupted state for any token whose balanceOf was not at that magic
// slot and bypassed allowances entirely (RED H3/C2). That magic constant is
// removed: ERC-20 settlement is restricted to the documented standard layout,
// and the maker/taker proceeds for D-chain-canonical or non-standard assets are
// conserved by the d-chain atomic import/export + fill_attestation channel, not
// by a C-Chain storage write.
func (pm *PoolManager) transferToken(stateDB StateDB, currency Currency, from, to common.Address, amount *big.Int) error {
if amount.Sign() <= 0 {
return nil
@@ -813,33 +895,36 @@ func (pm *PoolManager) transferToken(stateDB StateDB, currency Currency, from, t
return nil
}
// C-resident ERC-20, canonical slot-0 balanceOf layout.
token := currency.Address
erc20Base, _ := new(big.Int).SetString("52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00", 16)
fromSlot := erc20BalanceSlot(erc20Base, from)
toSlot := erc20BalanceSlot(erc20Base, to)
fromSlot := erc20BalanceSlot0(from)
toSlot := erc20BalanceSlot0(to)
fromBal := new(big.Int).SetBytes(stateDB.GetState(token, fromSlot).Bytes())
if fromBal.Cmp(amount) < 0 {
return fmt.Errorf("%w: ERC20 %s balance %s < transfer %s", ErrInsufficientBalance, token.Hex(), fromBal, amount)
}
toBal := new(big.Int).SetBytes(stateDB.GetState(token, toSlot).Bytes())
fromBal.Sub(fromBal, amount)
newFrom := new(big.Int).Sub(fromBal, amount)
newTo := new(big.Int).Add(toBal, amount)
// Conservation guard: the two legs must net to zero. A credit that does not
// exactly match the debit (overflow, aliasing from==to) is refused rather
// than minting or burning token supply.
if from != to {
if new(big.Int).Add(newFrom, newTo).Cmp(new(big.Int).Add(fromBal, toBal)) != 0 {
return fmt.Errorf("%w: ERC20 %s settlement not conserving", ErrSettlementFailed, token.Hex())
}
}
var fromHash common.Hash
fromBal.FillBytes(fromHash[:])
safeFillBytes(newFrom, fromHash[:])
stateDB.SetState(token, fromSlot, fromHash)
toBal.Add(toBal, amount)
var toHash common.Hash
toBal.FillBytes(toHash[:])
safeFillBytes(newTo, toHash[:])
stateDB.SetState(token, toSlot, toHash)
return nil
}
func erc20BalanceSlot(base *big.Int, addr common.Address) common.Hash {
key := make([]byte, 64)
copy(key[12:32], addr.Bytes())
safeFillBytes(base, key[32:64])
return common.BytesToHash(crypto.Keccak256(key))
}
func (pm *PoolManager) autoSettle(stateDB StateDB, caller common.Address, key PoolKey, delta BalanceDelta) error {
if delta.Amount0.Sign() > 0 {
if err := pm.transferToken(stateDB, key.Currency0, caller, poolManagerAddr, delta.Amount0); err != nil {
@@ -899,6 +984,20 @@ func (pm *PoolManager) GetPosition(
// calculateSwapOutput estimates the output for a given input without mutating state.
// Used by the router for best-path estimation. Delegates to engine.Quote.
//
// On the ZAP path the backend reads its own canonical pool, so the *Pool must
// be routed to a canonical poolId first; callers with a poolId/key in hand
// should use calculateSwapOutputRouted instead. This bare form is kept for the
// embedded engine and tests where the *Pool itself carries the state.
func (pm *PoolManager) calculateSwapOutput(pool *Pool, amountIn *big.Int, zeroForOne bool) *big.Int {
return pm.engine.Quote(pool, amountIn, zeroForOne)
}
// calculateSwapOutputRouted is the router's quote path. It resolves the cache
// PoolState for poolId (so the ZAP backend can be routed to its canonical pool)
// and then quotes. Embedded engine ignores the routing and quotes locally.
func (pm *PoolManager) calculateSwapOutputRouted(stateDB StateDB, key PoolKey, poolId [32]byte, amountIn *big.Int, zeroForOne bool) *big.Int {
ps := pm.getPoolState(stateDB, poolId, key.TickSpacing, key.Fee)
pm.routePool(poolId, ps)
return pm.engine.Quote(ps.Pool, amountIn, zeroForOne)
}
+4 -3
View File
@@ -693,12 +693,13 @@ func (r *LXRouter) quoteV4(
continue
}
// Calculate expected output
// Calculate expected output. Use the routed quote so the ZAP backend
// reads its canonical pool; the embedded engine quotes locally.
var output *big.Int
if zeroForOne {
output = r.poolManager.calculateSwapOutput(pool, amountIn, true)
output = r.poolManager.calculateSwapOutputRouted(stateDB, key, poolID, amountIn, true)
} else {
output = r.poolManager.calculateSwapOutput(pool, amountIn, false)
output = r.poolManager.calculateSwapOutputRouted(stateDB, key, poolID, amountIn, false)
}
if output.Sign() > 0 && (bestAmount == nil || output.Cmp(bestAmount) > 0) {
+8
View File
@@ -381,6 +381,14 @@ var (
ErrInvalidSqrtPrice = errors.New("invalid sqrt price")
ErrTickOutOfRange = errors.New("tick out of range")
ErrNoLiquidity = errors.New("no liquidity in pool")
// ErrDEXBackendNotConfigured is returned by every operation of the inert
// default engine. The LP-9010 precompile ships in the public EVM but is
// INERT until the venue wires a real backend via SetBackend(NewZAPEngine).
// V4 = CLOB, so the matcher lives on the d-chain (reached over ZAP), never
// embedded in the precompile — an unconfigured venue must revert cleanly
// rather than run a wrong, second matcher. See engine_inert.go.
ErrDEXBackendNotConfigured = errors.New("dex: backend not configured (set dex-zap-endpoint to enable LP-9010)")
)
// Errors - Pause/Freeze Controls (ATS regulatory compliance)
+246
View File
@@ -0,0 +1,246 @@
// dex_clob_zap_test exercises the LIVE precompile <-> luxfi/dex CLOB path:
// the production dex.ZAPEngine adapter, driven through the public dex.PoolManager
// surface, talks over ZAP to a REAL github.com/luxfi/dex CLOB server backed by
// the real lx.OrderBook matcher (the d-chain gateway).
//
// This is the cross-repo integration peer of the in-module fake-listener tests
// in precompile/dex/engine_zap_conservation_test.go. It lives HERE (e2e module)
// because importing the real luxfi/dex server requires a `replace` for the local
// checkout — a legitimate test-harness concern in the e2e module, kept OUT of
// the public precompile module so that stays a pure-Go leaf with no venue-engine
// edge.
//
// Precompiles exercised: DEX (LP-9010, V4 = CLOB facade).
//
// This test lives in its OWN package (e2e/clob, not e2e/scenarios) so it
// compiles and runs independently of unrelated scenario tests.
package clob
import (
"context"
"math"
"math/big"
"sync"
"testing"
"time"
"github.com/holiman/uint256"
dexapi "github.com/luxfi/dex/pkg/api"
"github.com/luxfi/geth/common"
ethtypes "github.com/luxfi/geth/core/types"
"github.com/luxfi/precompile/dex"
"github.com/luxfi/rpc"
"github.com/stretchr/testify/require"
)
// clobStateDB is a minimal in-memory implementation of the EXPORTED dex.StateDB
// interface, sufficient to drive PoolManager.{Initialize,ModifyLiquidity,Swap}.
// The CLOB path keeps no canonical state in the precompile, so this only needs
// to be a coherent scratch store.
type clobStateDB struct {
mu sync.Mutex
state map[common.Address]map[common.Hash]common.Hash
balances map[common.Address]*uint256.Int
exists map[common.Address]bool
}
func newCLOBStateDB() *clobStateDB {
return &clobStateDB{
state: make(map[common.Address]map[common.Hash]common.Hash),
balances: make(map[common.Address]*uint256.Int),
exists: make(map[common.Address]bool),
}
}
func (m *clobStateDB) GetState(addr common.Address, key common.Hash) common.Hash {
m.mu.Lock()
defer m.mu.Unlock()
if s, ok := m.state[addr]; ok {
return s[key]
}
return common.Hash{}
}
func (m *clobStateDB) SetState(addr common.Address, key, value common.Hash) {
m.mu.Lock()
defer m.mu.Unlock()
if m.state[addr] == nil {
m.state[addr] = make(map[common.Hash]common.Hash)
}
m.state[addr][key] = value
}
func (m *clobStateDB) GetBalance(addr common.Address) *uint256.Int {
m.mu.Lock()
defer m.mu.Unlock()
if b, ok := m.balances[addr]; ok {
return b.Clone()
}
return uint256.NewInt(0)
}
func (m *clobStateDB) AddBalance(addr common.Address, amount *uint256.Int) {
m.mu.Lock()
defer m.mu.Unlock()
if m.balances[addr] == nil {
m.balances[addr] = uint256.NewInt(0)
}
m.balances[addr].Add(m.balances[addr], amount)
}
func (m *clobStateDB) SubBalance(addr common.Address, amount *uint256.Int) {
m.mu.Lock()
defer m.mu.Unlock()
if m.balances[addr] == nil {
m.balances[addr] = uint256.NewInt(0)
}
m.balances[addr].Sub(m.balances[addr], amount)
}
func (m *clobStateDB) Exist(addr common.Address) bool { return m.exists[addr] }
func (m *clobStateDB) CreateAccount(addr common.Address) {
m.mu.Lock()
m.exists[addr] = true
m.mu.Unlock()
}
func (m *clobStateDB) GetBlockNumber() uint64 { return 1 }
func (m *clobStateDB) AddLog(_ *ethtypes.Log) {}
// startRealCLOBServer stands up a REAL luxfi/dex ZAP CLOB server (the d-chain
// gateway) and returns its address.
func startRealCLOBServer(t *testing.T) (addr string, srv *dexapi.ZAPServer) {
t.Helper()
listener, err := rpc.Listen("127.0.0.1:0")
require.NoError(t, err, "listen")
srv = dexapi.NewZAPServer(nil, "", nil)
require.NoError(t, dexapi.RegisterCLOB(listener, srv), "register clob")
addr = listener.Addr()
ctx := context.Background()
served := make(chan struct{})
go func() {
defer close(served)
_ = listener.Serve(ctx)
}()
// Readiness probe: a read-only cancel on a throwaway market id returns
// "unknown market" once the server is accepting and creates nothing.
deadline := time.Now().Add(2 * time.Second)
probe := make([]byte, 40)
probe[0] = 0xFF
for time.Now().Before(deadline) {
if c, derr := rpc.ZAPDial(ctx, addr); derr == nil {
if _, perr := c.Call(ctx, dexapi.CLOBMethodCancel, probe); perr == nil {
c.Close()
break
}
c.Close()
}
time.Sleep(5 * time.Millisecond)
}
t.Cleanup(func() {
_ = listener.Close()
<-served
})
return addr, srv
}
func luxLUSDPoolKey() dex.PoolKey {
return dex.PoolKey{
Currency0: dex.NativeCurrency,
Currency1: dex.Currency{Address: common.HexToAddress("0x000000000000000000000000000000000000C0DE")},
Fee: dex.Fee030,
TickSpacing: dex.TickSpacing030,
Hooks: common.Address{},
}
}
func snapTick(t *testing.T, want float64) int24Alias {
t.Helper()
raw := int(math.Log(want)/math.Log(1.0001) + 0.5)
spacing := int(dex.TickSpacing030)
return int24Alias((raw / spacing) * spacing)
}
// int24Alias mirrors dex.int24 (an int32) for local tick arithmetic.
type int24Alias = int32
func quantize8(p float64) float64 { return float64(int64(p*1e8)) / 1e8 }
func roundToBig(f float64) *big.Int {
bf := new(big.Float).SetFloat64(math.Round(f))
i, _ := bf.Int(nil)
return i
}
// TestDEXCLOB_ZAPAgainstRealServer drives the production ZAPEngine through the
// PoolManager against a real luxfi/dex CLOB and asserts fills + value
// conservation across counterparties on the live matcher.
func TestDEXCLOB_ZAPAgainstRealServer(t *testing.T) {
addr, srv := startRealCLOBServer(t)
zap := dex.NewZAPEngine(addr, 2*time.Second)
defer zap.Close()
require.Equal(t, "DEX", zap.Brand(), "CLOB path advertises the brand-free fallback")
pm := dex.NewPoolManager(zap)
stateDB := newCLOBStateDB()
key := luxLUSDPoolKey()
poolId := key.ID()
lp := common.HexToAddress("0x2222222222222222222222222222222222222222")
taker := common.HexToAddress("0x1111111111111111111111111111111111111111")
tick, err := pm.Initialize(stateDB, key, new(big.Int).Set(dex.Q96), nil)
require.NoError(t, err, "Initialize")
require.Equal(t, int24Alias(0), tick, "tick at price 1.0")
require.Equal(t, 1, srv.MarketCount(), "server canonical markets")
// Rest an ask above mid.
askTick := snapTick(t, 2.0)
askPrice, err := dexPriceForTick(askTick)
require.NoError(t, err)
askDelta, _, err := pm.ModifyLiquidity(stateDB, lp, key, dex.ModifyLiquidityParams{
TickLower: askTick,
TickUpper: askTick + dex.TickSpacing030,
LiquidityDelta: big.NewInt(100),
}, nil)
require.NoError(t, err, "ModifyLiquidity ask")
require.Truef(t, askDelta.Amount0.Sign() > 0 && askDelta.Amount1.Sign() == 0,
"resting ask delta = (%s,%s), want (+base,0)", askDelta.Amount0, askDelta.Amount1)
bestBid, bestAsk, ok := srv.BestBidAsk(poolId)
require.True(t, ok, "server market exists")
require.Equal(t, quantize8(askPrice), bestAsk, "server best ask = resting price")
_ = bestBid
// Marketable buy of 40 base, unbounded.
const takeBase = int64(40)
delta, err := pm.Swap(stateDB, taker, key, dex.SwapParams{
ZeroForOne: false,
AmountSpecified: big.NewInt(-takeBase),
SqrtPriceLimitX96: dex.MaxSqrtRatio,
}, nil)
require.NoError(t, err, "Swap")
require.Equal(t, big.NewInt(-takeBase), delta.Amount0, "buy base = exactly taken")
wantQuote := roundToBig(askPrice * float64(takeBase))
require.Equal(t, wantQuote, delta.Amount1, "buy quote = base*askPrice from fills")
// Cross-counterparty conservation.
makerRealized := dex.NewBalanceDelta(big.NewInt(takeBase), new(big.Int).Neg(wantQuote))
require.True(t, delta.Add(makerRealized).IsZero(), "value conserved across taker+maker")
}
// dexPriceForTick recomputes the float CLOB price for a tick using the exported
// V4 TickMath so the test agrees with the adapter's resting price exactly.
func dexPriceForTick(tick int24Alias) (float64, error) {
sqrtRatio, err := dex.GetSqrtRatioAtTick(tick)
if err != nil {
return 0, err
}
sp, _ := new(big.Float).SetInt(sqrtRatio).Float64()
q96 := math.Ldexp(1, 96)
r := sp / q96
return r * r, nil
}
+87
View File
@@ -0,0 +1,87 @@
module github.com/luxfi/precompile/e2e/clob
go 1.26.4
require (
github.com/holiman/uint256 v1.3.2
github.com/luxfi/dex v1.5.4
github.com/luxfi/geth v1.16.98
github.com/luxfi/precompile v0.5.27
github.com/luxfi/rpc v1.1.0
github.com/stretchr/testify v1.11.1
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/consensys/gnark-crypto v0.20.1 // indirect
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/luxfi/accel v1.1.9 // indirect
github.com/luxfi/atomic v1.0.0 // indirect
github.com/luxfi/cache v1.2.1 // indirect
github.com/luxfi/codec v1.1.4 // indirect
github.com/luxfi/compress v0.0.5 // indirect
github.com/luxfi/concurrent v0.0.3 // indirect
github.com/luxfi/consensus v1.25.15 // indirect
github.com/luxfi/constants v1.5.8 // indirect
github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/crypto v1.19.17 // indirect
github.com/luxfi/crypto/ipa v1.2.4 // indirect
github.com/luxfi/database v1.19.0 // indirect
github.com/luxfi/ids v1.2.13 // indirect
github.com/luxfi/log v1.4.3 // indirect
github.com/luxfi/math v1.4.1 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/metric v1.5.8 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/p2p v1.21.1 // indirect
github.com/luxfi/pq v1.0.3 // indirect
github.com/luxfi/runtime v1.1.1 // indirect
github.com/luxfi/sampler v1.1.0 // indirect
github.com/luxfi/utils v1.2.0 // indirect
github.com/luxfi/validators v1.2.0 // indirect
github.com/luxfi/version v1.0.1 // indirect
github.com/luxfi/vm v1.2.3 // indirect
github.com/luxfi/warp v1.18.6 // indirect
github.com/luxfi/zap v0.7.2 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mr-tron/base58 v1.3.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/supranational/blst v0.3.16 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/tools v0.45.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// This standalone test module drives the LOCAL precompile working tree against
// a REAL luxfi/dex ZAP CLOB server (the d-chain gateway). Replaces to the local
// checkouts are legitimate test-harness concerns HERE; the public precompile
// module deliberately carries no such edge (it stays a pure-Go leaf). Isolating
// this in its own module keeps zero blast radius on the e2e/scenarios baseline.
replace github.com/luxfi/dex => ../../../dex
replace github.com/luxfi/precompile => ../..
+198
View File
@@ -0,0 +1,198 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg=
github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc=
github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc=
github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/emicklei/dot v1.11.0 h1:zsrhCuFHAJge/aZIC4N4LdHy5tqYu4tWEaUzIwdYj4Y=
github.com/emicklei/dot v1.11.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M=
github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8=
github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/luxfi/accel v1.1.9 h1:Tsk6gXj2uKE19501bD0ajRYdeCHIlTGb6jYyLc+F8hc=
github.com/luxfi/accel v1.1.9/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/codec v1.1.4 h1:Yl8ZalMNkqo7cD6R9AjczAajkLOmsjyZ9+DASVYHrvg=
github.com/luxfi/codec v1.1.4/go.mod h1:oGQ3j6E8c2P0pL0irYtWkrB1hmDUFIE0puXHK4gV5KI=
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/consensus v1.25.15 h1:7Bm/dm8pWhVM6j6gVQTIfVyVda/9oNOAiSohT0hok7s=
github.com/luxfi/consensus v1.25.15/go.mod h1:cBj4bELXj1hfB+cHHlAdAQ7oDtS1RzNPgftniakrF9Q=
github.com/luxfi/constants v1.5.8 h1:iNP9AWNUcM4Tps7jYnx49CwtCWAC9mYRxJfGou2za0g=
github.com/luxfi/constants v1.5.8/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/crypto v1.19.17 h1:l2LLu7UFyICtJVfraLDLRi+lFGiDXKHSL18M9/m1gsQ=
github.com/luxfi/crypto v1.19.17/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/database v1.19.0 h1:sySH5eS8ihJtyc+yb0LgTignGwJ9H4W9bv+nboCuIm0=
github.com/luxfi/database v1.19.0/go.mod h1:o1ccjgVKMYbVBfi+6+sDvx4swGFgGnCGFQ4gL/oJ+kY=
github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQVXyNHA=
github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
github.com/luxfi/ids v1.2.13 h1:lEotq0WUpMLcc/5X5MbE3n73jdfNo8IlFvTDfUXg+9A=
github.com/luxfi/ids v1.2.13/go.mod h1:QWjJvggX/nRkmrz1AGSaF2Y6mnOvz3g7lFOzs3Zd+JM=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.5.8 h1:axPwfq+erOlIue7IJp5g+hMcMtVhYHja9gJAaT3+KNA=
github.com/luxfi/metric v1.5.8/go.mod h1:fO2giazkg4NDtr72JM/QXJBYebplAMeWC1JoZyNDvKw=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/p2p v1.21.1 h1:gmz1JMDhzHIL3dQlhwIDvR4OlFuhNVfnWUl/ipYhAIo=
github.com/luxfi/p2p v1.21.1/go.mod h1:SsNPR5fPGWWNem9plGWhSmRqyDoysJ3kPAN0zG0g3iw=
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/rpc v1.1.0 h1:B/PJbK399th1mHRDSufhCpVbAciZqId3LsaWhIGNWH4=
github.com/luxfi/rpc v1.1.0/go.mod h1:s0bI7/Wg1ZdFdG/cQK+4pZNdEmUsXNBA3HeZRZ+XLeM=
github.com/luxfi/runtime v1.1.1 h1:vOMe82PL3bpSbslS7p69dKRpbr2qW2vOOAwej3UkkmU=
github.com/luxfi/runtime v1.1.1/go.mod h1:fmG6+Zxj4wSNlXwiUfDthQDY+SfxVu6S0fX2lL6VbrE=
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
github.com/luxfi/utils v1.2.0 h1:gtEiI7/NM6PQ/OasEpH0PvB+e5hIS/tpum9r64pYjMc=
github.com/luxfi/utils v1.2.0/go.mod h1:T2OCKT1xG9jtKR/gyJQoSkticzrE9WFQ8eohJHGu9Fg=
github.com/luxfi/validators v1.2.0 h1:VygpiBqBAdGrfkb7xzE2yrVmnXaqE+hm8FLWdGXO7G8=
github.com/luxfi/validators v1.2.0/go.mod h1:GYLulrNXAan23ZlX7sgWVbVnLpUexeB/m2qr2ymsXok=
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
github.com/luxfi/vm v1.2.3 h1:IdfB02OtDmdy/YNxaIR8DquPp5Aq0fi5QXBqGHoRkug=
github.com/luxfi/vm v1.2.3/go.mod h1:ZXmw1sKA6GL3Ma3BfYUv5Fzvx3aWu2QMtkxF3lL/3R4=
github.com/luxfi/warp v1.18.6 h1:+ly7mHz77ig4yUyLax32aAjk7tiQQ6eygzOSDNfYRbQ=
github.com/luxfi/warp v1.18.6/go.mod h1:OBN23yiGl+E4qupkPHuetBIEqsI8q5leE8WJH8soFjY=
github.com/luxfi/zap v0.7.2 h1:YecWTWNE5PPJXL56sLIkzS8b23bprUwZ5lPAQuLUtTE=
github.com/luxfi/zap v0.7.2/go.mod h1:1k+nwT+JW802YzuPAuf7CxMSGr/qxvbGgGwi5k6X9Ok=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI=
github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+21 -9
View File
@@ -13,7 +13,7 @@ require (
github.com/luxfi/ai v0.1.0
github.com/luxfi/corona v0.7.6
github.com/luxfi/crypto v1.19.17
github.com/luxfi/database v1.18.3
github.com/luxfi/database v1.19.0
github.com/luxfi/fhe v1.8.2
github.com/luxfi/geth v1.16.98
github.com/luxfi/lattice/v7 v7.1.4
@@ -26,6 +26,17 @@ require (
golang.org/x/crypto v0.52.0
)
require (
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/zap v0.7.2 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/net v0.55.0 // indirect
)
require (
github.com/ALTree/bigfloat v0.2.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
@@ -41,7 +52,7 @@ require (
github.com/gorilla/rpc v1.2.1 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/gtank/ristretto255 v0.2.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/luxfi/atomic v1.0.0 // indirect
github.com/luxfi/cache v1.2.1 // indirect
@@ -52,29 +63,30 @@ require (
github.com/luxfi/constants v1.5.8 // indirect
github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/crypto/ipa v1.2.4 // indirect
github.com/luxfi/ids v1.2.10 // indirect
github.com/luxfi/log v1.4.1 // indirect
github.com/luxfi/ids v1.2.13 // indirect
github.com/luxfi/log v1.4.3 // indirect
github.com/luxfi/math v1.4.1 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/metric v1.5.7 // indirect
github.com/luxfi/metric v1.5.8 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/p2p v1.21.1 // indirect
github.com/luxfi/pq v1.0.3 // indirect
github.com/luxfi/rpc v1.1.0
github.com/luxfi/sampler v1.1.0 // indirect
github.com/luxfi/utils v1.2.0 // indirect
github.com/luxfi/validators v1.2.0 // indirect
github.com/luxfi/version v1.0.1 // indirect
github.com/luxfi/vm v1.2.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
github.com/montanaflynn/stats v0.9.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mr-tron/base58 v1.3.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/supranational/blst v0.3.16 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
+39
View File
@@ -60,6 +60,7 @@ github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6
github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
@@ -164,7 +165,9 @@ github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fH
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
@@ -195,6 +198,8 @@ github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhd
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
@@ -239,6 +244,8 @@ github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xf
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/database v1.18.3 h1:gg+xwhKUxXa7fDoOD8IS91E71QqoEtcDCl2nfS61Jgg=
github.com/luxfi/database v1.18.3/go.mod h1:sv0pYCGKlK1aNJTICxFUDpVWCJTigoLlshHmV/1pg7c=
github.com/luxfi/database v1.19.0 h1:sySH5eS8ihJtyc+yb0LgTignGwJ9H4W9bv+nboCuIm0=
github.com/luxfi/database v1.19.0/go.mod h1:o1ccjgVKMYbVBfi+6+sDvx4swGFgGnCGFQ4gL/oJ+kY=
github.com/luxfi/fhe v1.8.2 h1:QllnObNFbi6D4mvFI6uQkepW8HgLtdy4RMR1TKYAInA=
github.com/luxfi/fhe v1.8.2/go.mod h1:16yxwhcnCez/rNcd/C9JjH9IjbEz73X+0tvlsONyLeA=
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
@@ -249,6 +256,8 @@ github.com/luxfi/hid v0.9.3/go.mod h1:XJ/7DZAHf5dggm3zWNbitKuFGB7J96b4iX+8NO3Bsn
github.com/luxfi/hsm v1.1.3/go.mod h1:2DwOpBG1yzo9zHbsw2t0p/CbGjbyHYXjPx7uxEIp8wo=
github.com/luxfi/ids v1.2.10 h1:f1WILZE199ayMuqnEyB2WP1qfMZkmozOQXSVYtB3e5k=
github.com/luxfi/ids v1.2.10/go.mod h1:QBIwy3OHvrtskbUqKh1+OYRa6PsyR7f7oNX33sOfK7w=
github.com/luxfi/ids v1.2.13 h1:lEotq0WUpMLcc/5X5MbE3n73jdfNo8IlFvTDfUXg+9A=
github.com/luxfi/ids v1.2.13/go.mod h1:QWjJvggX/nRkmrz1AGSaF2Y6mnOvz3g7lFOzs3Zd+JM=
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
@@ -256,15 +265,20 @@ github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
github.com/luxfi/lens v0.1.4/go.mod h1:mL+G8IK+9L41d78/2FYRgfhEzAjcr5+VEXB8SGuHbus=
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/magnetar v1.2.0 h1:bsxHmBnJiswc/A6ElQ0pWz5g6ogqewIEKKqR26VgizA=
github.com/luxfi/magnetar v1.2.0/go.mod h1:7J9YP9jByWbwCjssMFJNUkTU8tcPlSUoVSSiYShtvFs=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.5.7 h1:LoSPEUpak2SLcynF+LT2cXjl9ECp4nY+Lia9zudmDv4=
github.com/luxfi/metric v1.5.7/go.mod h1:CMguEhyuLi4YUWyXimJ+UHply99BDFrL0pxedB7rBqM=
github.com/luxfi/metric v1.5.8 h1:axPwfq+erOlIue7IJp5g+hMcMtVhYHja9gJAaT3+KNA=
github.com/luxfi/metric v1.5.8/go.mod h1:fO2giazkg4NDtr72JM/QXJBYebplAMeWC1JoZyNDvKw=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/mpc v1.14.13/go.mod h1:P5BnYeYBdwYJRsA/ckFuSrBduEJnPmGeDJC+lkUepFo=
@@ -278,6 +292,8 @@ github.com/luxfi/proto v1.3.4/go.mod h1:S80KrTzISCktptit/pWLzk6nHgagjJS86qbQL6gH
github.com/luxfi/pulsar v1.1.1/go.mod h1:U7tPleeAHJ9dZ61ymtstzLKKoZjxM2zFeGZ+RSjHyRw=
github.com/luxfi/resource v0.0.1/go.mod h1:wWpZktciYwIi6RNqA+fHwzmPrUJa7PRX7urfwT+spRE=
github.com/luxfi/rpc v1.0.2/go.mod h1:pgiHwMWgOuxYYIa0vsUBvrBI+Op6bhZ39guM9vtMUcE=
github.com/luxfi/rpc v1.1.0 h1:B/PJbK399th1mHRDSufhCpVbAciZqId3LsaWhIGNWH4=
github.com/luxfi/rpc v1.1.0/go.mod h1:s0bI7/Wg1ZdFdG/cQK+4pZNdEmUsXNBA3HeZRZ+XLeM=
github.com/luxfi/runtime v1.1.1 h1:vOMe82PL3bpSbslS7p69dKRpbr2qW2vOOAwej3UkkmU=
github.com/luxfi/runtime v1.1.1/go.mod h1:fmG6+Zxj4wSNlXwiUfDthQDY+SfxVu6S0fX2lL6VbrE=
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
@@ -299,15 +315,22 @@ github.com/luxfi/vm v1.2.3 h1:IdfB02OtDmdy/YNxaIR8DquPp5Aq0fi5QXBqGHoRkug=
github.com/luxfi/vm v1.2.3/go.mod h1:ZXmw1sKA6GL3Ma3BfYUv5Fzvx3aWu2QMtkxF3lL/3R4=
github.com/luxfi/warp v1.18.6 h1:+ly7mHz77ig4yUyLax32aAjk7tiQQ6eygzOSDNfYRbQ=
github.com/luxfi/warp v1.18.6/go.mod h1:OBN23yiGl+E4qupkPHuetBIEqsI8q5leE8WJH8soFjY=
github.com/luxfi/zap v0.7.2 h1:YecWTWNE5PPJXL56sLIkzS8b23bprUwZ5lPAQuLUtTE=
github.com/luxfi/zap v0.7.2/go.mod h1:1k+nwT+JW802YzuPAuf7CxMSGr/qxvbGgGwi5k6X9Ok=
github.com/luxfi/zapdb v1.10.0/go.mod h1:Qukh3hDRD0MnxA6z+a28JTnXhN85AiLLgp6TYr4QAMc=
github.com/luxfi/zapdb/v4 v4.9.3/go.mod h1:BrpLaDeYy+smON4ASDPCFKWrQ58CXhy3WNEPGna7vuU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
@@ -326,6 +349,8 @@ github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq2
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI=
github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
@@ -366,6 +391,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
@@ -424,20 +451,30 @@ go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
@@ -446,9 +483,11 @@ golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg=