fix(dexvm): exact-integer ZAP wire — match dex v1.14.0, kill cross-arch fork

The dex venue (v1.14.0) froze its ZAP wire to EXACT INTEGERS: order/fill
price is uint64 fixed-point ×1e8 (the PriceInt grid), size is uint64 atomic
base units — no IEEE-754 float64. This eliminated every value-bearing
float→int conversion from the settlement path, whose out-of-range result is
implementation-defined per the Go spec and differs by CPU arch (arm64
saturates, amd64 wraps) — the cross-arch consensus fork (F1-F5).

The stateless atomic ZAP proxy still encoded/decoded the OLD float wire, so
it could no longer interoperate with the new venue. This propagates the
frozen frame byte-identically:

- relay.go: Fill{Price,Size} float64 -> uint64; DecodeFills reads big-endian
  uint64s and rejects zero price/size. New PriceScale const == zapwire.PriceScale.
- carried_fills.go: block-carried fills encode/decode as exact uint64s.
- atomic.go: drop the float→int rounding machinery (quantToCredit/quantToCharge/
  nearestIntWithin/settlementRoundEpsilon/maxSettlementUnit/float64FromBits/
  isFinitePositive). Add quoteUnits (floor(size×price/PriceScale) in big.Int,
  mirroring dex v4BalanceDelta / the matcher's quoteUnitsFor) and settleUint64
  (refuses an aggregate that exceeds uint64 rather than wrapping).
- vm.go settleFromFills: aggregate base=Σsize, quote=Σ per-fill floor as exact
  integers; slippage limit compared as fixed-point uint64; no float. Refund/
  proceeds are the venue's own integers — escrow-truncation mint is structurally
  impossible; spent>locked still refused.
- vm.go encodeCLOBPlace: price = tx.Price×PriceScale, size passthrough (no float).
- feegate.go: swapNativeFeeToLUX takes a fixed-point Fill.Price and floors via
  quoteUnits.
- txs/tx.go: RelayOrderTx.PriceLimit is fixed-point ×PriceScale (was float64-bits).

Dep: github.com/luxfi/dex v1.5.20 (indirect) -> v1.14.0 (direct, test imports
the pure-Go zapwire leaf as the parity oracle).

Tests: all conservation/atomicity/redteam tests translated to the integer
semantics WITHOUT weakening any invariant (the no-mint / overflow-refusal /
mixed-side / slippage / determinism proofs are preserved, re-anchored to the
venue's exact per-fill floor). New frame_parity_test.go asserts the proxy's
Place/Submit/Fill frames are BYTE-IDENTICAL to dex v1.14.0 zapwire.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-02 10:40:05 -07:00
co-authored by Hanzo Dev
parent 3893cdfe3b
commit 1676121436
20 changed files with 541 additions and 473 deletions
+34 -91
View File
@@ -7,7 +7,7 @@ import (
"encoding/binary"
"fmt"
"hash"
"math"
"math/big"
"sort"
"github.com/luxfi/database"
@@ -413,102 +413,45 @@ func decodeExportedOutput(v []byte) (rail txs.Rail, owner ids.ShortID, asset ids
return rail, owner, asset, amount, spent, true
}
// float64FromBits reads a big-endian IEEE-754 float64 (the ZAP fill wire codec,
// byte-identical with dex/pkg/api and the precompile).
func float64FromBits(b []byte) float64 {
return math.Float64frombits(binary.BigEndian.Uint64(b))
}
// isFinitePositive reports whether f is a finite, strictly positive number — the
// only values a real fill can carry. Rejects NaN/Inf/<=0.
func isFinitePositive(f float64) bool {
return !math.IsNaN(f) && !math.IsInf(f, 0) && f > 0
}
// ---------------------------------------------------------------------------
// Conservation-safe float->integer rounding for settlement.
// Exact-integer settlement arithmetic.
//
// Fills cross the ZAP wire as float64 (price, size); on-chain value moves in
// integer asset units (uint64). Converting a fractional notional to an integer
// MUST round in the direction that protects the conservation invariant — the
// proxy NEVER mints — so the rounding is asymmetric BY PURPOSE:
//
// - quantToCredit (FLOOR): a quantity the taker RECEIVES (proceeds) or that
// REDUCES what we owe back. Round DOWN so the proxy never credits a unit it
// did not truly realize. Worst case the taker is under-credited by <1 unit;
// that sub-unit is conserved on the d-chain side (the maker's leg), never
// fabricated out of the proxy.
// - quantToCharge (CEIL): a quantity the taker SPENDS against locked escrow
// (so refund = locked - spent). Round UP so spent is never UNDERstated.
// Understating spent is exactly the escrow-truncation mint (RED): it inflates
// the refund, letting the taker walk away with proceeds PLUS more refund than
// the unspent remainder. Ceiling spent caps refund at the true unspent value.
//
// Both take an EXACT float aggregate (summed over fills once, not per-fill
// truncated — per-fill truncation accumulates a directional leak) and apply the
// rounding ONCE at the asset boundary. A small relative epsilon absorbs IEEE-754
// representation error so an aggregate that is mathematically integral (e.g.
// 1.5*3 + 2.5*3 == 12, but may evaluate to 12 ± 1e-13) snaps to that integer
// instead of spuriously rounding to 11 or 13. epsilon is deterministic (a fixed
// constant over deterministic float inputs) so every validator rounds identically.
// Fills cross the ZAP wire as EXACT integers (price is uint64 fixed-point
// ×PriceScale on the matcher's PriceInt grid; size is uint64 atomic base units),
// and on-chain value moves in the same integer asset units. There is therefore NO
// fractional notional to round and — critically — NO value-bearing float→int
// conversion anywhere on the settlement path (that conversion's out-of-range
// result is implementation-defined per the Go spec and differs by CPU
// architecture; carrying and settling exact integers is what killed the cross-arch
// consensus fork). The proxy derives base and quote from the fills with the SAME
// per-fill floor the venue's matcher (quoteUnitsFor) and the precompile's
// fillsToDelta apply, so the proxy moves EXACTLY what the venue moved — byte-for-
// byte on every validator — and the conservation invariant (the proxy never mints)
// is a direct integer identity, not a rounding argument.
// settlementRoundEpsilon is the relative tolerance for snapping a float aggregate
// to a neighboring integer before directional rounding. ~1e-9 dwarfs the ~1e-13
// double-rounding error of summing realistic fill streams yet is far below one
// asset unit, so it never moves a genuinely fractional notional across an integer.
const settlementRoundEpsilon = 1e-9
// priceScaleBig is PriceScale as a *big.Int (the fixed-point divisor for quote).
var priceScaleBig = new(big.Int).SetUint64(PriceScale)
// nearestIntWithin returns (n, true) when f is within a relative epsilon of the
// integer n, else (0, false). Used to snap a mathematically-integral aggregate
// back to its integer before floor/ceil.
func nearestIntWithin(f float64) (float64, bool) {
r := math.Round(f)
tol := settlementRoundEpsilon * math.Max(1, math.Abs(f))
if math.Abs(f-r) <= tol {
return r, true
}
return 0, false
// quoteUnits returns one fill's quote in exact integers: floor(size × price /
// PriceScale), where size is atomic base units and price is fixed-point ×PriceScale.
// The multiply is done in big.Int (size × price overflows uint64) and floored by
// integer division, mirroring dex v1.14.0's v4BalanceDelta / the matcher's
// quoteUnitsFor. Returned as *big.Int so the caller accumulates without overflow.
func quoteUnits(size, price uint64) *big.Int {
q := new(big.Int).Mul(new(big.Int).SetUint64(size), new(big.Int).SetUint64(price))
return q.Quo(q, priceScaleBig)
}
// quantToCredit converts a proceeds aggregate to integer asset units, rounding
// DOWN (never credit more than realized). Non-finite or negative input, or a
// result exceeding uint64, is refused — the proxy must not settle a value it
// cannot represent exactly.
func quantToCredit(f float64) (uint64, error) {
if math.IsNaN(f) || math.IsInf(f, 0) || f < 0 {
return 0, fmt.Errorf("settle: non-finite proceeds %v", f)
// settleUint64 narrows an exact aggregate (base or quote, summed in big.Int) to a
// uint64 asset amount, refusing a value the proxy cannot represent exactly rather
// than wrapping it. label names the leg for the error. Negative is impossible
// (every summand is non-negative) but is refused defensively.
func settleUint64(v *big.Int, label string) (uint64, error) {
if v.Sign() < 0 {
return 0, fmt.Errorf("settle: negative %s %s", label, v.String())
}
if r, ok := nearestIntWithin(f); ok {
f = r
} else {
f = math.Floor(f)
if !v.IsUint64() {
return 0, fmt.Errorf("settle: %s %s exceeds uint64", label, v.String())
}
if f > maxSettlementUnit {
return 0, fmt.Errorf("settle: proceeds %v exceeds uint64", f)
}
return uint64(f), nil
return v.Uint64(), nil
}
// quantToCharge converts a spent aggregate to integer asset units, rounding UP
// (never charge less than consumed, so the refund is never inflated). Same
// finiteness / range guards as quantToCredit.
func quantToCharge(f float64) (uint64, error) {
if math.IsNaN(f) || math.IsInf(f, 0) || f < 0 {
return 0, fmt.Errorf("settle: non-finite notional %v", f)
}
if r, ok := nearestIntWithin(f); ok {
f = r
} else {
f = math.Ceil(f)
}
if f > maxSettlementUnit {
return 0, fmt.Errorf("settle: notional %v exceeds uint64", f)
}
return uint64(f), nil
}
// maxSettlementUnit is the largest float64 that converts to uint64 without
// overflow on rounding. 2^64 is not exactly representable; the largest float64
// strictly below it is 2^64 - 2048. Using <= against this constant keeps the
// uint64() conversion in-range and deterministic.
const maxSettlementUnit = float64(1<<64 - 2048)
+5 -5
View File
@@ -96,7 +96,7 @@ func settleByzantineFills(t *testing.T, fills []Fill, lockedQuote uint64) (taker
func TestByzantineVenue_OverNotionalAttestedFillCannotMint(t *testing.T) {
const lockedQuote uint64 = 1000
// BUY: receives base = 600, spends quote = 600*2 = 1200 > 1000 locked (mint attempt).
fills := []Fill{{Price: 2, Size: 600, Side: 0}}
fills := []Fill{{Price: fp(2), Size: 600, Side: 0}}
_, baseAsset, quoteAsset, legs := settleByzantineFills(t, fills, lockedQuote)
@@ -127,7 +127,7 @@ func TestByzantineVenue_OverNotionalAttestedFillCannotMint(t *testing.T) {
func TestByzantineVenue_WithinCollateralAttestedFillSettles(t *testing.T) {
const lockedQuote uint64 = 1000
// BUY: receives base = 400, spends quote = 400*2 = 800 <= 1000 locked.
fills := []Fill{{Price: 2, Size: 400, Side: 0}}
fills := []Fill{{Price: fp(2), Size: 400, Side: 0}}
_, baseAsset, quoteAsset, legs := settleByzantineFills(t, fills, lockedQuote)
@@ -153,7 +153,7 @@ func TestByzantineVenue_WithinCollateralAttestedFillSettles(t *testing.T) {
func TestByzantineVenue_MixedSideAttestedFillRefused(t *testing.T) {
const lockedQuote uint64 = 1000
// A lying venue returns [BUY 100, SELL 300]: the mixed-side over-credit attempt.
fills := []Fill{{Price: 2, Size: 100, Side: 0}, {Price: 2, Size: 300, Side: 1}}
fills := []Fill{{Price: fp(2), Size: 100, Side: 0}, {Price: fp(2), Size: 300, Side: 1}}
_, baseAsset, quoteAsset, legs := settleByzantineFills(t, fills, lockedQuote)
@@ -181,8 +181,8 @@ func TestByzantineVenue_MixedSideAttestedFillRefused(t *testing.T) {
func TestByzantineSettlement_DeterministicRegardlessOfFillOrder(t *testing.T) {
const lockedQuote uint64 = 1000
// Two same-side (BUY) fills; total spent = 3*100 + 2*150 = 600 <= 1000 locked.
fillsAB := []Fill{{Price: 3, Size: 100, Side: 0}, {Price: 2, Size: 150, Side: 0}}
fillsBA := []Fill{{Price: 2, Size: 150, Side: 0}, {Price: 3, Size: 100, Side: 0}}
fillsAB := []Fill{{Price: fp(3), Size: 100, Side: 0}, {Price: fp(2), Size: 150, Side: 0}}
fillsBA := []Fill{{Price: fp(2), Size: 150, Side: 0}, {Price: fp(3), Size: 100, Side: 0}}
_, baseA, quoteA, legsAB := settleByzantineFills(t, fillsAB, lockedQuote)
_, baseB, quoteB, legsBA := settleByzantineFills(t, fillsBA, lockedQuote)
+14 -14
View File
@@ -6,7 +6,6 @@ package dexvm
import (
"encoding/binary"
"fmt"
"math"
)
// carried_fills.go — the CARRIED-FILLS primitive of the stateless atomic ZAP
@@ -51,8 +50,9 @@ import (
// bump.
//
// FROZEN-FRAME NOTE. This is the BLOCK format, NOT the ZAP fill frame. The 17-byte
// FillWireSize ZAP frame (relay.go / dex/pkg/zapwire / precompile) is UNCHANGED;
// the per-fill encoding here reuses the same price[8]+size[8]+side[1] layout for
// FillWireSize ZAP frame (relay.go / dex/pkg/zapwire / precompile) carries a uint64
// FIXED-POINT ×PriceScale price and a uint64 ATOMIC-BASE-UNIT size; the per-fill
// encoding here reuses the same exact-integer price[8]+size[8]+side[1] layout for
// continuity but is a distinct, block-level structure.
// carriedFill binds one settling relay's confirmed fills to its position in the
@@ -109,8 +109,8 @@ func encodeCarriedFills(entries []carriedFill, sig []byte) []byte {
binary.BigEndian.PutUint32(buf[off:], uint32(len(e.fills)))
off += 4
for _, f := range e.fills {
binary.BigEndian.PutUint64(buf[off:off+8], math.Float64bits(f.Price))
binary.BigEndian.PutUint64(buf[off+8:off+16], math.Float64bits(f.Size))
binary.BigEndian.PutUint64(buf[off:off+8], f.Price)
binary.BigEndian.PutUint64(buf[off+8:off+16], f.Size)
buf[off+16] = f.Side
off += FillWireSize
}
@@ -125,9 +125,9 @@ func encodeCarriedFills(entries []carriedFill, sig []byte) []byte {
// encodeCarriedFills. It returns the entries, the reserved signature, the number
// of bytes consumed, and any error. Every field is range-checked so a hostile or
// truncated block cannot inject a malformed fill into settlement or over-allocate:
// - the per-fill price/size must be finite and strictly positive and side ∈
// {0,1} (the SAME boundary invariant DecodeFills enforces on the ZAP wire),
// so no impossible Fill ever reaches settleFromFills;
// - the per-fill price/size must be strictly positive and side ∈ {0,1} (the
// SAME boundary invariant DecodeFills enforces on the ZAP wire), so no
// impossible Fill ever reaches settleFromFills;
// - counts are bounded (maxCarriedFillEntries / maxFillsPerEntry);
// - the section must be exactly consumed by the declared lengths.
//
@@ -161,15 +161,15 @@ func decodeCarriedFills(data []byte) (entries []carriedFill, sig []byte, consume
}
fills := make([]Fill, 0, fc)
for j := 0; j < fc; j++ {
p := float64FromBits(data[off : off+8])
s := float64FromBits(data[off+8 : off+16])
p := binary.BigEndian.Uint64(data[off : off+8])
s := binary.BigEndian.Uint64(data[off+8 : off+16])
side := data[off+16]
off += FillWireSize
if !isFinitePositive(p) {
return nil, nil, 0, fmt.Errorf("carried fills: entry %d fill %d invalid price %v", i, j, p)
if p == 0 {
return nil, nil, 0, fmt.Errorf("carried fills: entry %d fill %d invalid price %d", i, j, p)
}
if !isFinitePositive(s) {
return nil, nil, 0, fmt.Errorf("carried fills: entry %d fill %d invalid size %v", i, j, s)
if s == 0 {
return nil, nil, 0, fmt.Errorf("carried fills: entry %d fill %d invalid size %d", i, j, s)
}
if side > 1 {
return nil, nil, 0, fmt.Errorf("carried fills: entry %d fill %d invalid side %d", i, j, side)
+8 -8
View File
@@ -26,9 +26,9 @@ import (
// representative entry set (multiple txIndexes, multiple fills, a zero-fill entry).
func TestCarriedFills_CodecRoundTrip(t *testing.T) {
in := []carriedFill{
{txIndex: 1, fills: []Fill{{Price: 2, Size: 50, Side: 0}, {Price: 2, Size: 50, Side: 0}}},
{txIndex: 1, fills: []Fill{{Price: fp(2), Size: 50, Side: 0}, {Price: fp(2), Size: 50, Side: 0}}},
{txIndex: 3, fills: nil}, // explicit zero-fill (a failed/empty submit)
{txIndex: 7, fills: []Fill{{Price: 1.5, Size: 4, Side: 1}}},
{txIndex: 7, fills: []Fill{{Price: fp(1.5), Size: 4, Side: 1}}},
}
enc := encodeCarriedFills(in, nil)
out, sig, consumed, err := decodeCarriedFills(enc)
@@ -72,7 +72,7 @@ func TestCarriedFills_CodecRoundTrip(t *testing.T) {
// further wire-format change).
func TestCarriedFills_ReservedSignatureRoundTrips(t *testing.T) {
sigIn := []byte("reserved-d-chain-fill-attestation-bytes")
enc := encodeCarriedFills([]carriedFill{{txIndex: 0, fills: []Fill{{Price: 1, Size: 1, Side: 0}}}}, sigIn)
enc := encodeCarriedFills([]carriedFill{{txIndex: 0, fills: []Fill{{Price: fp(1), Size: 1, Side: 0}}}}, sigIn)
_, sigOut, consumed, err := decodeCarriedFills(enc)
if err != nil {
t.Fatalf("decode: %v", err)
@@ -90,7 +90,7 @@ func TestCarriedFills_ReservedSignatureRoundTrips(t *testing.T) {
// on the ZAP wire) — so no impossible Fill ever reaches settlement and no
// over-allocation is possible.
func TestCarriedFills_RejectsMalformed(t *testing.T) {
good := encodeCarriedFills([]carriedFill{{txIndex: 0, fills: []Fill{{Price: 1, Size: 1, Side: 0}}}}, nil)
good := encodeCarriedFills([]carriedFill{{txIndex: 0, fills: []Fill{{Price: fp(1), Size: 1, Side: 0}}}}, nil)
t.Run("truncated_header", func(t *testing.T) {
if _, _, _, err := decodeCarriedFills([]byte{0, 0}); err == nil {
@@ -145,7 +145,7 @@ func TestBlock_CarriedFillsRoundTripAndIDBinding(t *testing.T) {
timestamp: time.Unix(1_700_000_000, 0),
txs: [][]byte{[]byte("tx-a"), []byte("tx-bb")},
carriedFills: []carriedFill{
{txIndex: 1, fills: []Fill{{Price: 2, Size: 50, Side: 0}}},
{txIndex: 1, fills: []Fill{{Price: fp(2), Size: 50, Side: 0}}},
},
}
wire := b.Bytes()
@@ -165,7 +165,7 @@ func TestBlock_CarriedFillsRoundTripAndIDBinding(t *testing.T) {
// ID binding: change ONE carried fill and the block id (hash of bytes) must
// differ — the fills are committed, not malleable.
b2 := *b
b2.carriedFills = []carriedFill{{txIndex: 1, fills: []Fill{{Price: 2, Size: 999, Side: 0}}}}
b2.carriedFills = []carriedFill{{txIndex: 1, fills: []Fill{{Price: fp(2), Size: 999, Side: 0}}}}
p1, _ := parseBlock(cvm, b.Bytes())
p2, _ := parseBlock(cvm, b2.Bytes())
if p1.id == p2.id {
@@ -185,7 +185,7 @@ func TestVertex_CarriedFillsRoundTripAndIDBinding(t *testing.T) {
parents: []ids.ID{{0x01}},
rawTxs: [][]byte{[]byte("raw-tx-1")},
carriedFills: []carriedFill{
{txIndex: 0, fills: []Fill{{Price: 3, Size: 9, Side: 1}}},
{txIndex: 0, fills: []Fill{{Price: fp(3), Size: 9, Side: 1}}},
},
vm: cvm,
}
@@ -212,7 +212,7 @@ func TestVertex_CarriedFillsRoundTripAndIDBinding(t *testing.T) {
timestamp: v.timestamp,
parents: v.parents,
rawTxs: v.rawTxs,
carriedFills: []carriedFill{{txIndex: 0, fills: []Fill{{Price: 3, Size: 9999, Side: 1}}}},
carriedFills: []carriedFill{{txIndex: 0, fills: []Fill{{Price: fp(3), Size: 9999, Side: 1}}}},
}
if tampered.computeID() == v.id {
t.Fatalf("vertex id does NOT commit to carried fills: different fills produced the same id %x", v.id[:8])
+32 -44
View File
@@ -28,7 +28,7 @@ package dexvm
import (
"context"
"math"
"math/big"
"testing"
"time"
@@ -96,31 +96,20 @@ func settledRound(t *testing.T, fills []Fill, collateral uint64, takerSide uint8
// requires be refunded.
//
// It mirrors settleFromFills exactly: the spent quantity is aggregated over the
// fills as an exact float and rounded UP (ceiling) ONCE — the conservation-safe
// direction for a charge, so the refund (locked - spent) is never inflated. (The
// prior per-fill uint64 truncation was the escrow-extraction bug.)
// fills as EXACT integers — a BUY's quote consumed is Σ floor(size × price /
// PriceScale) (the venue matcher's own per-fill quote), a SELL's base consumed is
// Σ size. There is no float and no rounding seam: the proxy spends precisely what
// the venue matched, so refund = locked - spent is exact and never inflated.
func fillNotional(fills []Fill, takerSide uint8) uint64 {
var n float64
total := new(big.Int)
for _, f := range fills {
if takerSide == 0 { // BUY locked quote; quote consumed = price*size
n += f.Price * f.Size
} else { // SELL locked base; base consumed = size
n += f.Size
if takerSide == 0 { // BUY locked quote; quote consumed = Σ floor(size*price/PriceScale)
total.Add(total, quoteUnits(f.Size, f.Price))
} else { // SELL locked base; base consumed = Σ size
total.Add(total, new(big.Int).SetUint64(f.Size))
}
}
return ceilToUnit(n)
}
// ceilToUnit rounds a non-negative float aggregate UP to integer asset units,
// snapping a mathematically-integral aggregate (e.g. 1.5*3+2.5*3 == 12, but may
// evaluate to 12 ± 1e-13) to that integer first. It mirrors atomic.go's
// quantToCharge so the tests reason in the SAME arithmetic the proxy uses.
func ceilToUnit(f float64) uint64 {
r := math.Round(f)
if math.Abs(f-r) <= 1e-9*math.Max(1, math.Abs(f)) {
return uint64(r)
}
return uint64(math.Ceil(f))
return total.Uint64()
}
// TestConservationPartialFillBuy is the decisive partial-settlement proof. The
@@ -132,7 +121,7 @@ func ceilToUnit(f float64) uint64 {
func TestConservationPartialFillBuy(t *testing.T) {
const lockedQuote uint64 = 1000
// One fill: 200 base @ price 2 => 400 quote spent, 600 quote should refund.
fills := []Fill{{Price: 2, Size: 200}}
fills := []Fill{{Price: fp(2), Size: 200}}
const takerSide uint8 = 0 // BUY
valueIn, valueOut := settledRound(t, fills, lockedQuote, takerSide)
@@ -192,7 +181,7 @@ func TestConservationZeroFill(t *testing.T) {
// must be proceeds(1500 quote) + refund(700 base) = 2200.
func TestConservationPartialFillSell(t *testing.T) {
const lockedBase uint64 = 1000
fills := []Fill{{Price: 5, Size: 300}} // 300 base sold, 1500 quote received
fills := []Fill{{Price: fp(5), Size: 300}} // 300 base sold, 1500 quote received
const takerSide uint8 = 1 // SELL
valueIn, valueOut := settledRound(t, fills, lockedBase, takerSide)
@@ -298,41 +287,40 @@ func TestFailedRelayCommitsNothing(t *testing.T) {
}
}
// TestConservationRoundingBoundary probes the float64->uint64 seam in the
// spent/refund split. settleFromFills aggregates the quote notional over the
// fills as an exact float and rounds the SPENT side UP (ceiling) ONCE — the
// conservation-safe direction so the refund (locked - spent) is never inflated.
// Two fractional fills (price 1.5 * size 3 = 4.5; price 2.5 * size 3 = 7.5) sum
// to an aggregate quote notional of exactly 12.0, so spent = ceil(12.0) = 12
// (NOT the old per-fill floor sum 4+7=11, which understated spend and over-
// refunded by 1). Proceeds base rounds DOWN: floor(sum(size)) = floor(6.0) = 6.
// The taker locked 1000 quote; refund = 1000-12 = 988, and value_out =
// base-proceeds(6) + quote-refund(988) = 994.
// TestConservationRoundingBoundary probes the exact-integer quote seam in the
// spent/refund split. settleFromFills aggregates the quote notional over the fills
// as EXACT integers, flooring EACH fill's quote = floor(size × price / PriceScale)
// exactly as the venue matcher did — so the proxy spends precisely what the venue
// moved, with no float and no directional rounding. Two fractional-priced fills
// (price 1.5 × size 3 → floor(4.5)=4; price 2.5 × size 3 → floor(7.5)=7) sum to
// spent = 4+7 = 11 (the venue's own per-fill quote, NOT the old float-aggregate
// ceil of 12). Proceeds base = Σ size = 6 (whole base units). The taker locked 1000
// quote; refund = 1000-11 = 989, and value_out = base-proceeds(6) +
// quote-refund(989) = 995. The refund and proceeds legs agree on the SAME integer
// notional the venue used, so no sub-unit value leaks in either direction.
func TestConservationRoundingBoundary(t *testing.T) {
const lockedQuote uint64 = 1000
fills := []Fill{{Price: 1.5, Size: 3}, {Price: 2.5, Size: 3}} // BUY
fills := []Fill{{Price: fp(1.5), Size: 3}, {Price: fp(2.5), Size: 3}} // BUY
const takerSide uint8 = 0
valueIn, valueOut := settledRound(t, fills, lockedQuote, takerSide)
// SPENT quote = aggregate notional, rounded UP (the exact arithmetic
// settleFromFills uses via quantToCharge). Mirrors fillNotional.
// SPENT quote = Σ floor(size×price/PriceScale), the venue's own per-fill quote.
spent := fillNotional(fills, takerSide)
// PROCEEDS base = aggregate size, rounded DOWN (quantToCredit).
var baseAgg float64
// PROCEEDS base = Σ size (whole atomic base units).
var proceedsBase uint64
for _, f := range fills {
baseAgg += f.Size
proceedsBase += f.Size
}
proceedsBase := uint64(math.Floor(baseAgg))
wantRefund := lockedQuote - spent
wantOut := proceedsBase + wantRefund
t.Logf("CONSERVATION LEDGER (rounding): value_in=%d spent(ceil)=%d proceeds_base(floor)=%d refund=%d measured_value_out=%d want=%d",
t.Logf("CONSERVATION LEDGER (integer quote): value_in=%d spent(Σfloor)=%d proceeds_base=%d refund=%d measured_value_out=%d want=%d",
valueIn, spent, proceedsBase, wantRefund, valueOut, wantOut)
if spent != 12 || proceedsBase != 6 {
t.Fatalf("arithmetic anchor drifted: spent=%d (want 12) proceeds_base=%d (want 6)", spent, proceedsBase)
if spent != 11 || proceedsBase != 6 {
t.Fatalf("arithmetic anchor drifted: spent=%d (want 11) proceeds_base=%d (want 6)", spent, proceedsBase)
}
if valueOut != wantOut {
t.Fatalf("CONSERVATION VIOLATED at rounding boundary: value_out=%d, want %d. "+
+16 -7
View File
@@ -17,7 +17,6 @@ package dexvm
import (
"context"
"encoding/binary"
"math"
"testing"
"time"
@@ -66,20 +65,29 @@ func (f *fakeMatcherConn) Call(_ context.Context, method string, payload []byte)
func (f *fakeMatcherConn) Close() error { return nil }
// encodeFillsWire encodes fills in the FROZEN clob_submit response format:
// count[4] then count×(price[8]+size[8]+side[1]).
// count[4] then count×(price[8]+size[8]+side[1]), price a uint64 fixed-point
// ×PriceScale value and size a uint64 atomic-base-unit count — the exact-integer
// inverse of DecodeFills.
func encodeFillsWire(fills []Fill) []byte {
resp := make([]byte, 4+len(fills)*FillWireSize)
binary.BigEndian.PutUint32(resp[0:4], uint32(len(fills)))
off := 4
for _, fl := range fills {
binary.BigEndian.PutUint64(resp[off:off+8], math.Float64bits(fl.Price))
binary.BigEndian.PutUint64(resp[off+8:off+16], math.Float64bits(fl.Size))
binary.BigEndian.PutUint64(resp[off:off+8], fl.Price)
binary.BigEndian.PutUint64(resp[off+8:off+16], fl.Size)
resp[off+16] = fl.Side
off += FillWireSize
}
return resp
}
// fp scales a human quote-per-base price onto the wire's fixed-point ×PriceScale
// grid (the SAME conversion dex's wirePriceUnits applies), so a test keeps readable
// human prices while the Fill/wire carries the exact-integer PriceInt value. quote =
// size × fp(price) / PriceScale, so for whole human prices the settled quote equals
// the old human price × size exactly.
func fp(human float64) uint64 { return uint64(human * float64(PriceScale)) }
// conservationHarness wires a proxy VM with REAL two-chain shared memory and a
// fake matcher, returning the proxy VM, the C-Chain's shared-memory handle, and
// the chain ids.
@@ -208,13 +216,14 @@ func mustParseImport(t *testing.T, b []byte) *txs.ImportTx {
}
// clobSubmitPayload builds the FROZEN 66-byte clob_submit request frame:
// poolId[32] | side[1] | isMarket[1] | limit[8] | size[8] | user[16].
// poolId[32] | side[1] | isMarket[1] | limit[8] | size[8] | user[16]. size is a
// uint64 atomic-base-unit count written directly (no float on the wire).
func clobSubmitPayload(poolID ids.ID, size uint64) []byte {
p := make([]byte, 66)
copy(p[0:32], poolID[:])
p[32] = 0 // buy
p[33] = 1 // market
putZAPFloat(p[42:50], float64(size))
binary.BigEndian.PutUint64(p[42:50], size)
return p
}
@@ -266,7 +275,7 @@ func indexedByTaker(t *testing.T, h *conservationHarness) [][]byte {
func TestEndToEndAtomicValueConservation(t *testing.T) {
// Matcher fills: 600 base @ price 1 + ... summing to a known notional. Use a
// single fill of 1000 base @ price 1 so base=1000, quote=1000 (integer-exact).
fills := []Fill{{Price: 1, Size: 1000, Side: 0}}
fills := []Fill{{Price: fp(1), Size: 1000, Side: 0}}
h := newConservationHarness(t, fills)
taker := ids.GenerateTestShortID()
+15 -14
View File
@@ -99,21 +99,22 @@ func (cvm *ChainVM) FeePolicy() fee.Policy { return cvm.feePolicy }
// swapNativeFeeToLUX converts a fee TENDERED in a non-LUX native asset into its
// canonical-LUX settlement amount, swapping native -> LUX at a CONFIRMED matcher
// fill price (the proxy's own swap path — eat-your-own-dogfood). luxPerNative is
// the Fill.Price of a confirmed native->LUX fill (LUX quote per native base); it
// is a consensus-agreed receipt, so every validator computes the identical LUX
// amount from identical inputs — deterministic settlement.
// the Fill.Price of a confirmed native->LUX fill: a uint64 FIXED-POINT ×PriceScale
// value (LUX quote per native base). It is a consensus-agreed receipt, so every
// validator computes the identical LUX amount from identical inputs — deterministic
// settlement with NO float on the value path.
//
// The LUX output is the asset the fee sink RECEIVES, so it rounds DOWN via
// quantToCredit — the SAME asymmetric proceeds rounding settleFromFills uses for
// every credited leg (never credit a LUX unit the fill did not realize; the
// proxy never mints LUX). Rounding DOWN is also the floor-SAFE direction: a
// caller's floor check can never be passed by rounding error, only failed by it.
// A non-finite or non-positive price is a malformed fill and is refused.
func swapNativeFeeToLUX(nativeFee uint64, luxPerNative float64) (uint64, error) {
if !isFinitePositive(luxPerNative) {
return 0, fmt.Errorf("dexvm: fee swap: invalid LUX/native price %v", luxPerNative)
// The LUX output is the asset the fee sink RECEIVES, so it is the exact quote the
// fill realized: floor(nativeFee × luxPerNative / PriceScale) via quoteUnits — the
// SAME per-fill floor settleFromFills uses for every credited leg (never credit a
// LUX unit the fill did not realize; the proxy never mints LUX). Flooring is the
// floor-SAFE direction: a caller's floor check can never be passed by rounding
// error, only failed by it. A zero price is a malformed fill and is refused.
func swapNativeFeeToLUX(nativeFee uint64, luxPerNative uint64) (uint64, error) {
if luxPerNative == 0 {
return 0, fmt.Errorf("dexvm: fee swap: invalid LUX/native price %d", luxPerNative)
}
return quantToCredit(float64(nativeFee) * luxPerNative)
return settleUint64(quoteUnits(nativeFee, luxPerNative), "fee-swap LUX")
}
// settleFeeInLUX sources the D-Chain tx fee in canonical LUX from a fee tendered
@@ -129,7 +130,7 @@ func swapNativeFeeToLUX(nativeFee uint64, luxPerNative float64) (uint64, error)
// confirmed fill). The floor therefore holds whether the fee is paid in LUX
// directly or swapped from a native token — it cannot be bypassed by tendering
// native.
func (cvm *ChainVM) settleFeeInLUX(nativeFee uint64, luxPerNative float64) (uint64, error) {
func (cvm *ChainVM) settleFeeInLUX(nativeFee uint64, luxPerNative uint64) (uint64, error) {
if cvm.feePolicy == nil {
return 0, fmt.Errorf("dexvm: fee policy not initialized")
}
+15 -18
View File
@@ -7,7 +7,6 @@ import (
"context"
"encoding/json"
"errors"
"math"
"testing"
"github.com/luxfi/chains/dexvm/txs"
@@ -141,13 +140,13 @@ func TestSwapNativeFeeToLUX_Deterministic(t *testing.T) {
cvm := newChainVMForNetwork(t, luxL2NetworkID)
// 1:1 parity — a native fee at the floor settles to exactly the floor in LUX.
if got, err := swapNativeFeeToLUX(fee.MinTxFeeFloor, 1.0); err != nil || got != fee.MinTxFeeFloor {
t.Fatalf("swapNativeFeeToLUX(floor, 1.0) = (%d, %v), want (%d, nil)", got, err, fee.MinTxFeeFloor)
if got, err := swapNativeFeeToLUX(fee.MinTxFeeFloor, fp(1.0)); err != nil || got != fee.MinTxFeeFloor {
t.Fatalf("swapNativeFeeToLUX(floor, fp(1.0)) = (%d, %v), want (%d, nil)", got, err, fee.MinTxFeeFloor)
}
// Determinism: the same native fee + fill price always yields the same LUX.
const nativeFee = uint64(20_000_000)
const price = 0.1 // LUX per native unit
price := fp(0.1) // LUX per native unit, fixed-point ×PriceScale
first, err := swapNativeFeeToLUX(nativeFee, price)
if err != nil {
t.Fatalf("swap: %v", err)
@@ -158,31 +157,29 @@ func TestSwapNativeFeeToLUX_Deterministic(t *testing.T) {
t.Fatalf("non-deterministic swap on iter %d: got (%d,%v), want (%d,nil)", i, got, err, first)
}
}
if first != 2_000_000 { // 20_000_000 * 0.1 = 2_000_000 LUX
t.Fatalf("swap(20e6, 0.1) = %d, want 2_000_000", first)
if first != 2_000_000 { // floor(20_000_000 * 0.1) = 2_000_000 LUX
t.Fatalf("swap(20e6, fp(0.1)) = %d, want 2_000_000", first)
}
// settleFeeInLUX enforces the LUX floor on the swapped output. 2_000_000 >= floor.
if got, err := cvm.settleFeeInLUX(nativeFee, price); err != nil || got != 2_000_000 {
t.Fatalf("settleFeeInLUX(20e6, 0.1) = (%d, %v), want (2_000_000, nil)", got, err)
t.Fatalf("settleFeeInLUX(20e6, fp(0.1)) = (%d, %v), want (2_000_000, nil)", got, err)
}
// An under-floor swap output is refused (native fee too small for the price):
// 1_000_000 native * 0.1 = 100_000 LUX < 1_000_000 floor.
if _, err := cvm.settleFeeInLUX(1_000_000, 0.1); !errors.Is(err, fee.ErrInsufficientFee) {
// floor(1_000_000 * 0.1) = 100_000 LUX < 1_000_000 floor.
if _, err := cvm.settleFeeInLUX(1_000_000, fp(0.1)); !errors.Is(err, fee.ErrInsufficientFee) {
t.Fatalf("settleFeeInLUX(under-floor output) = %v, want ErrInsufficientFee", err)
}
// Proceeds round DOWN (never credit LUX the fill did not realize, floor-safe):
// 3 native * 0.5 = 1.5 LUX -> 1 LUX.
if got, err := swapNativeFeeToLUX(3, 0.5); err != nil || got != 1 {
t.Fatalf("swapNativeFeeToLUX(3, 0.5) = (%d, %v), want (1, nil) [floor-safe round down]", got, err)
// Proceeds floor DOWN (never credit LUX the fill did not realize, floor-safe):
// floor(3 * 0.5) = floor(1.5) = 1 LUX.
if got, err := swapNativeFeeToLUX(3, fp(0.5)); err != nil || got != 1 {
t.Fatalf("swapNativeFeeToLUX(3, fp(0.5)) = (%d, %v), want (1, nil) [floor-safe round down]", got, err)
}
// Malformed fill prices are refused (no silent zero/garbage settlement).
for _, bad := range []float64{0, -1, math.NaN(), math.Inf(1), math.Inf(-1)} {
if _, err := swapNativeFeeToLUX(fee.MinTxFeeFloor, bad); err == nil {
t.Fatalf("swapNativeFeeToLUX with invalid price %v = nil err, want refusal", bad)
}
// A zero fill price is refused (no silent zero/garbage settlement).
if _, err := swapNativeFeeToLUX(fee.MinTxFeeFloor, 0); err == nil {
t.Fatalf("swapNativeFeeToLUX with zero price = nil err, want refusal")
}
}
+150
View File
@@ -0,0 +1,150 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dexvm
import (
"bytes"
"testing"
"github.com/luxfi/dex/pkg/zapwire"
"github.com/luxfi/ids"
"github.com/luxfi/chains/dexvm/txs"
)
// frame_parity_test.go is the CROSS-MODULE oracle: it pins the proxy's exact-integer
// ZAP frames BYTE-IDENTICAL to dex v1.14.0's frozen zapwire codec (the venue's own
// source of truth). An interop mismatch is the whole failure mode of the wire change
// — the proxy must produce/consume the SAME bytes the d-chain gateway speaks — so we
// assert it against the canonical package directly, not a re-declared copy.
//
// zapwire is a PURE-GO stdlib leaf (no cgo/GPU matcher), so the proxy can import it
// in a test even under CGO_ENABLED=0. The production proxy re-declares the frozen
// constants (relay.go / vm.go) to stay a clean leaf over luxfi/rpc; this test is the
// guard that the re-declaration never drifts from the canonical frame.
// TestFrameParity_Constants pins the wire constants to zapwire's.
func TestFrameParity_Constants(t *testing.T) {
if PriceScale != zapwire.PriceScale {
t.Fatalf("PriceScale %d != zapwire.PriceScale %d — the fixed-point grid diverged", PriceScale, zapwire.PriceScale)
}
if FillWireSize != zapwire.FillWireSize {
t.Fatalf("FillWireSize %d != zapwire.FillWireSize %d — the fill frame diverged", FillWireSize, zapwire.FillWireSize)
}
if ZAPMethodSubmit == "" || ZAPMethodPlace == "" {
t.Fatalf("empty ZAP method names")
}
}
// TestFrameParity_PlaceFrame proves encodeCLOBPlace emits the EXACT bytes
// zapwire.EncodePlace does for the same order, and that the venue's DecodePlace reads
// back the exact-integer fixed-point price and base-unit size.
func TestFrameParity_PlaceFrame(t *testing.T) {
var pool [32]byte
for i := range pool {
pool[i] = byte(i + 1)
}
const humanPrice = uint64(10)
const size = uint64(5)
from := ids.ShortID{0xAB, 0xCD, 0xEF, 0x01, 0x02}
tx := txs.NewPlaceOrderTx(from, 0, pool, zapwire.SideSell, humanPrice, size)
maker := tx.Sender()
got := encodeCLOBPlace(tx)
// The venue reads the price field as the fixed-point ×PriceScale PriceInt grid
// value, so the proxy scales the whole human price onto it: humanPrice*PriceScale.
want := zapwire.EncodePlace(pool, zapwire.SideSell, humanPrice*PriceScale, size, string(maker[:16]))
if !bytes.Equal(got, want) {
t.Fatalf("place frame != zapwire.EncodePlace\n got=%x\nwant=%x", got, want)
}
// The venue's own decoder reads back exact integers (no float on the wire).
dPool, dSide, dPrice, dSize, _, err := zapwire.DecodePlace(got)
if err != nil {
t.Fatalf("zapwire.DecodePlace(proxy frame): %v", err)
}
if dPool != pool || dSide != zapwire.SideSell {
t.Fatalf("decoded pool/side mismatch: side=%d", dSide)
}
if dPrice != humanPrice*PriceScale {
t.Fatalf("decoded price=%d, want %d (fixed-point ×PriceScale)", dPrice, humanPrice*PriceScale)
}
if dSize != size {
t.Fatalf("decoded size=%d, want %d (atomic base units)", dSize, size)
}
}
// TestFrameParity_SubmitFrame proves the 66-byte submit frame the proxy's test
// harness builds (clobSubmitPayload) is byte-identical to zapwire.EncodeSubmit for a
// market order — the exact-integer frame the 0x9999 EVM surface and the native venue
// both speak.
func TestFrameParity_SubmitFrame(t *testing.T) {
var pool [32]byte
for i := range pool {
pool[i] = byte(0x40 + i)
}
const size = uint64(1234)
got := clobSubmitPayload(ids.ID(pool), size)
// clobSubmitPayload builds a market BUY with limit 0 and empty user, size in base
// units — the same fields, byte-for-byte, as the venue's EncodeSubmit.
want := zapwire.EncodeSubmit(pool, zapwire.SideBuy, true, 0, size, "")
if !bytes.Equal(got, want) {
t.Fatalf("submit frame != zapwire.EncodeSubmit\n got=%x\nwant=%x", got, want)
}
_, dSide, dMarket, dLimit, dSize, _, err := zapwire.DecodeSubmit(got)
if err != nil {
t.Fatalf("zapwire.DecodeSubmit(proxy frame): %v", err)
}
if dSide != zapwire.SideBuy || !dMarket || dLimit != 0 || dSize != size {
t.Fatalf("decoded submit mismatch: side=%d market=%v limit=%d size=%d", dSide, dMarket, dLimit, dSize)
}
}
// TestFrameParity_FillWire proves the fill response wire round-trips byte-identically
// between the proxy's DecodeFills and the venue's zapwire.EncodeFills/DecodeFills:
// price is fixed-point ×PriceScale, size is atomic base units, both EXACT uint64s.
func TestFrameParity_FillWire(t *testing.T) {
// A stream with a fractional-flooring price to exercise the fixed-point grid.
proxyFills := []Fill{
{Price: fp(2), Size: 100, Side: zapwire.SideBuy},
{Price: fp(1.5), Size: 4, Side: zapwire.SideSell},
}
// (a) proxy-encoded wire decodes identically under the venue's codec.
wire := encodeFillsWire(proxyFills)
venueFills, err := zapwire.DecodeFills(wire)
if err != nil {
t.Fatalf("zapwire.DecodeFills(proxy wire): %v", err)
}
if len(venueFills) != len(proxyFills) {
t.Fatalf("fill count %d != %d", len(venueFills), len(proxyFills))
}
for i, f := range proxyFills {
if venueFills[i].Price != f.Price || venueFills[i].Size != f.Size || venueFills[i].TakerSide != f.Side {
t.Fatalf("fill %d mismatch: venue=%+v proxy=%+v", i, venueFills[i], f)
}
}
// (b) venue-encoded wire decodes identically under the proxy's DecodeFills — the
// real direction: the venue returns fills, the proxy consumes them.
venueWire := zapwire.EncodeFills([]zapwire.Fill{
{Price: fp(2), Size: 100, TakerSide: zapwire.SideBuy},
{Price: fp(1.5), Size: 4, TakerSide: zapwire.SideSell},
})
if !bytes.Equal(venueWire, wire) {
t.Fatalf("fill wire not byte-identical\n proxy=%x\n venue=%x", wire, venueWire)
}
back, err := DecodeFills(venueWire)
if err != nil {
t.Fatalf("proxy DecodeFills(venue wire): %v", err)
}
for i, f := range proxyFills {
if back[i].Price != f.Price || back[i].Size != f.Size || back[i].Side != f.Side {
t.Fatalf("proxy round-trip fill %d mismatch: %+v vs %+v", i, back[i], f)
}
}
}
+114 -144
View File
@@ -18,7 +18,7 @@ import (
"bytes"
"context"
"encoding/binary"
"math"
"math/big"
"sort"
"sync"
"testing"
@@ -172,7 +172,7 @@ func makeBlock(cvm *ChainVM, parentID ids.ID, height uint64, ts time.Time, txByt
// This test exercises the validator path (makeBlock, no build), which never relays,
// so its reject strands nothing.
func TestRED_VerifyThenReject_StrandsDChainFill(t *testing.T) {
fills := []Fill{{Price: 1, Size: 1000, Side: 0}} // taker BUY, receives 1000 base
fills := []Fill{{Price: fp(1), Size: 1000, Side: 0}} // taker BUY, receives 1000 base
cvm, matcher, cChainSM, proxyChain, _ := newCountingHarness(t, fills)
ctx := context.Background()
@@ -294,24 +294,22 @@ func TestRED_ReVerify_DoublesPlaceOrder(t *testing.T) {
}
}
// TestRED_FractionalFill_NeverMints is the fractional-fill conservation proof. A
// CLOB fill crosses the wire as float64, but on-chain value is integer asset
// units, so a sub-integer proceeds amount cannot be credited without minting a
// fractional unit. The CONSERVATION-SAFE rule (the law: the proxy NEVER mints)
// is to round a RECEIVED quantity DOWN (quantToCredit) and a SPENT quantity UP
// (quantToCharge). Worst case the taker is under-credited by <1 unit; that
// sub-unit is conserved on the d-chain side (the maker's leg), never fabricated
// out of the proxy — the opposite of a mint.
// TestRED_FractionalFill_NeverMints is the fractional-QUOTE conservation proof.
// The wire now carries EXACT integers (size in whole base units, price fixed-point
// ×PriceScale), so a fill's quote = floor(size × price / PriceScale) is itself an
// exact integer — the SAME per-fill floor the venue matcher moved. There is no
// float and no directional rounding seam; the proxy spends precisely the venue's
// quote and the escrow-truncation mint is structurally impossible.
//
// This pins the resolved behavior with an ESCROW present so both legs are
// auditable: a taker BUY locks 10 quote and fills 4.5 base @ price 2 (a fill
// stream with a fractional base AND a fractional notional). The quote spent
// ceils to 9, base proceeds floor to 4; value_out (4 base + 1 quote refund) must
// never exceed value_in, and the locked-quote ledger must balance exactly.
// This pins the behavior with an ESCROW present so both legs are auditable: a taker
// BUY locks 10 quote and fills 3 base @ price 2.5 (fixed-point fp(2.5)). The quote =
// floor(3 × 2.5) = floor(7.5) = 7 (the venue's own integer quote), base proceeds =
// 3; value_out (3 base + 3 quote refund) must never exceed value_in, and the
// locked-quote ledger must balance exactly (spent 7 + refund 3 == locked 10).
func TestRED_FractionalFill_NeverMints(t *testing.T) {
// BUY: receives base = sum(size) = 4.5 -> floor 4. spends quote =
// sum(price*size) = 9.0 -> ceil 9. locked 10 quote -> refund 10-9 = 1.
fills := []Fill{{Price: 2, Size: 4.5, Side: 0}}
// BUY: receives base = Σ size = 3. spends quote = floor(3 × fp(2.5)/PriceScale) =
// floor(7.5) = 7. locked 10 quote -> refund 10-7 = 3.
fills := []Fill{{Price: fp(2.5), Size: 3, Side: 0}}
cvm, _, _, _, _ := newCountingHarness(t, fills)
taker := ids.GenerateTestShortID()
@@ -346,57 +344,41 @@ func TestRED_FractionalFill_NeverMints(t *testing.T) {
}
}
}
t.Logf("fractional fill: base proceeds=%d (floor 4.5) quote refund=%d (locked 10 - ceil(9.0))", base, refund)
t.Logf("fractional-quote fill: base proceeds=%d (Σ size = 3) quote refund=%d (locked 10 - floor(7.5)=7)", base, refund)
// Proceeds round DOWN: never credit more base than the integer floor.
if base > 4 {
t.Fatalf("MINT: credited %d base for a 4.5 fill — proceeds must floor to 4, never round up.", base)
// Proceeds are the exact base traded: never credit more base than Σ size.
if base > 3 {
t.Fatalf("MINT: credited %d base for a 3-base fill — proceeds must equal Σ size = 3, never more.", base)
}
// Spent quote ceils to 9, so refund is exactly 1 — never inflated (that would
// be the escrow-truncation mint), never negative.
if refund != lockedQuote-9 {
t.Fatalf("CONSERVATION VIOLATED: quote refund=%d, want %d (locked 10 - spent ceil(9.0)=9).", refund, lockedQuote-9)
// Spent quote floors to 7 (the venue's exact quote), so refund is exactly 3 —
// never inflated (that would be the escrow-truncation mint), never negative.
if refund != lockedQuote-7 {
t.Fatalf("CONSERVATION VIOLATED: quote refund=%d, want %d (locked 10 - spent floor(7.5)=7).", refund, lockedQuote-7)
}
// No-mint over the locked-quote ledger: spent(9) + refund(1) == locked(10).
if spent := lockedQuote - refund; spent != 9 {
// No-mint over the locked-quote ledger: spent(7) + refund(3) == locked(10).
if spent := lockedQuote - refund; spent != 7 {
t.Fatalf("NO-MINT VIOLATED: spent+refund != locked (spent=%d refund=%d locked=%d).", spent, refund, lockedQuote)
}
}
// TestRED_OverflowFill_SaturatesUint64 is the float64->uint64 SATURATION proof —
// the headline of the finding: a fill amount that exceeds what a uint64 can hold
// must be REFUSED at the asset boundary, never silently truncated/saturated into
// a valid-looking on-chain amount.
// TestRED_OverflowFill_SaturatesUint64 is the exact-integer OVERFLOW-REFUSAL proof
// — the headline of the finding: an aggregate fill amount that exceeds what a uint64
// can hold must be REFUSED at the asset boundary, never silently wrapped/truncated
// into a valid-looking on-chain amount.
//
// Go's float64->uint64 conversion does NOT wrap or error on overflow: uint64(19e18)
// SATURATES to 18446744073709551615 (uint64 max). uint64 max is ~1.845e19 = only
// ~18.44 tokens at 18 decimals, so a single 19-token fill, cast naively, would be
// summed into the exported amount as a plausible ~18.44-token value — value minted
// out of thin air, and (because the export amount is a fixed 8-byte uint64 with no
// widening downstream — atomic.go encodeExportedOutput PutUint64(v[53:61])) there
// is nothing downstream to catch it.
// The old float64->uint64 cast SATURATED on overflow (uint64(19e18) -> uint64 max),
// minting a plausible ~18.44-token value. The exact-integer wire removes the float
// entirely: base = Σ size and quote = Σ floor(size × price / PriceScale) are summed
// in big.Int, and settleUint64 REFUSES any aggregate that does not fit uint64 (via
// big.Int.IsUint64) BEFORE it becomes an export amount. So an oversized aggregate
// makes settleFromFills ERROR and export NOTHING — refusal, never a wrapped mint.
//
// The closed behavior: settlement aggregates fills as exact float64 ONCE, then
// quantToCredit/quantToCharge reject any aggregate > maxSettlementUnit (the largest
// float64 strictly below 2^64) BEFORE the uint64() cast. So an oversized fill makes
// settleFromFills ERROR and export NOTHING — refusal, never a saturated mint.
//
// Two legs are exercised, since the finding's PoC inflates BOTH the proceeds cast
// (uint64(size)) and the spent/notional cast (uint64(price*size)):
// - PROCEEDS overflow: a taker BUY whose base size sums to 19e18 (> uint64 max).
// - NOTIONAL overflow: a taker BUY whose quote notional (price*size) overflows,
// which would understate spent => inflate the escrow refund (the mint vector).
// Two legs are exercised, since the finding's PoC inflates BOTH the proceeds
// aggregate (Σ size) and the spent/notional aggregate (Σ quote):
// - PROCEEDS overflow: a taker BUY whose base size SUM exceeds uint64 max.
// - NOTIONAL overflow: a taker BUY whose quote notional overflows uint64 while the
// base stays in range, which would understate spent => inflate the escrow refund.
func TestRED_OverflowFill_SaturatesUint64(t *testing.T) {
// Ground truth the guard must beat: a naive cast saturates instead of erroring.
// overSize is a runtime float64 (NOT an untyped constant) so the uint64() cast
// is the runtime saturating conversion under test — a constant conversion of an
// out-of-range value is a compile error, which is not the behavior being probed.
var overSize float64 = 19e18 // 19 tokens @ 18 decimals; > uint64 max (~1.845e19)
if uint64(overSize) != ^uint64(0) {
t.Fatalf("precondition: expected uint64(%v) to saturate to uint64 max, got %d",
overSize, uint64(overSize))
}
auditCredited := func(ar *atomicRequests) uint64 {
var total uint64
for _, reqs := range ar.reqs {
@@ -409,36 +391,39 @@ func TestRED_OverflowFill_SaturatesUint64(t *testing.T) {
return total
}
// --- Leg 1: PROCEEDS overflow (uint64(size) saturation) ---------------------
// taker BUY receives base = sum(size) = 19e18 -> overflows; settle must refuse.
// --- Leg 1: PROCEEDS overflow (Σ size > uint64 max) -------------------------
// taker BUY receives base = Σ size = uint64max + 2 -> overflows; settle must refuse.
t.Run("proceeds", func(t *testing.T) {
fills := []Fill{{Price: 1, Size: overSize, Side: 0}}
fills := []Fill{
{Price: fp(1), Size: ^uint64(0), Side: 0},
{Price: fp(1), Size: 2, Side: 0},
}
cvm, _, _, _, _ := newCountingHarness(t, fills)
taker := ids.GenerateTestShortID()
// No escrow: isolate the proceeds cast (refund leg absent).
// No escrow: isolate the proceeds aggregate (refund leg absent).
ar := newAtomicRequests()
err := cvm.inner.settleFromFills(taker, ids.GenerateTestID(), fills, ids.GenerateTestID(), 0, false, ids.GenerateTestID(), 0, ar)
credited := auditCredited(ar)
t.Logf("proceeds overflow: settle err=%v exported=%d (naive cast would saturate to %d)", err, credited, ^uint64(0))
t.Logf("proceeds overflow: settle err=%v exported=%d (Σ size = 2^64+1 must not wrap)", err, credited)
if err == nil {
t.Fatalf("SATURATION MINT: settle accepted a 19e18 base fill (> uint64 max); it must be "+
"refused at the asset boundary, not cast (which saturates to %d).", ^uint64(0))
t.Fatalf("OVERFLOW MINT: settle accepted a base aggregate > uint64 max; it must be "+
"refused at the asset boundary, not wrapped into a uint64 export amount.")
}
if credited != 0 {
t.Fatalf("SATURATION MINT: refused settle still exported %d — refusal must export nothing.", credited)
t.Fatalf("OVERFLOW MINT: refused settle still exported %d — refusal must export nothing.", credited)
}
})
// --- Leg 2: NOTIONAL overflow (uint64(price*size) saturation) ---------------
// taker BUY spends quote = sum(price*size); make the NOTIONAL overflow while
// the base size stays in range, so the only overflow is the spent/refund leg.
// A saturated-but-floored spent would UNDERstate spend => inflate refund =
// locked - spent => quote minted out of the proxy's own escrow.
// --- Leg 2: NOTIONAL overflow (Σ quote > uint64 max) ------------------------
// taker BUY spends quote = Σ floor(size × price / PriceScale); make the NOTIONAL
// overflow while the base size stays in range, so the only overflow is the
// spent/refund leg. A wrapped/understated spent would inflate refund = locked -
// spent => quote minted out of the proxy's own escrow.
t.Run("notional_refund", func(t *testing.T) {
// size 1e10 (in range), price 1e10 -> notional 1e20 > uint64 max.
fills := []Fill{{Price: 1e10, Size: 1e10, Side: 0}}
// size 2e10 (base in range); price fp(1e10) -> quote = 2e10 × 1e10 = 2e20 > uint64 max.
fills := []Fill{{Price: fp(1e10), Size: 20_000_000_000, Side: 0}}
cvm, _, _, _, _ := newCountingHarness(t, fills)
taker := ids.GenerateTestShortID()
@@ -455,11 +440,11 @@ func TestRED_OverflowFill_SaturatesUint64(t *testing.T) {
t.Logf("notional overflow: settle err=%v exported=%d (a saturated/floored spent would inflate the refund)", err, credited)
if err == nil {
t.Fatalf("SATURATION MINT: settle accepted a 1e20 quote notional (> uint64 max); the spent " +
"cast would saturate and inflate refund = locked - spent => minted quote.")
t.Fatalf("OVERFLOW MINT: settle accepted a 2e20 quote notional (> uint64 max); a wrapped " +
"spent would inflate refund = locked - spent => minted quote.")
}
if credited != 0 {
t.Fatalf("SATURATION MINT: refused settle still exported %d — refusal must export nothing.", credited)
t.Fatalf("OVERFLOW MINT: refused settle still exported %d — refusal must export nothing.", credited)
}
// The escrow must NOT have been consumed by a refused settle (so a later,
// well-formed relay can still legitimately refund it).
@@ -468,58 +453,40 @@ func TestRED_OverflowFill_SaturatesUint64(t *testing.T) {
t.Fatalf("escrow lookup: %v", eerr)
}
if !haveEscrow {
t.Fatalf("SATURATION MINT: refused settle consumed the escrow — a refused settle must leave it intact.")
t.Fatalf("OVERFLOW MINT: refused settle consumed the escrow — a refused settle must leave it intact.")
}
})
}
// TestRED_SettlementBoundary_ExactUint64Edge pins the float->uint64 boundary
// CONSTANT (maxSettlementUnit) exactly, so a future edit cannot widen it past the
// representable range and silently reintroduce the saturation mint. maxSettlementUnit
// is the largest float64 strictly below 2^64 (== 2^64 - 2048; 2^64 itself is the
// next representable float64 and is ONE above uint64 max):
// TestRED_SettlementBoundary_ExactUint64Edge pins the exact-integer settlement
// boundary (settleUint64) precisely, so a future edit cannot silently widen it past
// the representable range and reintroduce the overflow mint. The seam is now a pure
// big.Int -> uint64 narrowing:
//
// - the largest passing aggregate (maxSettlementUnit) converts IN-RANGE for both
// a credit (floor) and a charge (ceil);
// - the next float64 up (2^64) is refused by both — it is exactly one above
// uint64 max, the value uint64(.) would wrap/saturate on.
// - the largest passing aggregate (uint64 max) narrows IN-RANGE;
// - one above uint64 max (2^64) is REFUSED — the value a wrapped conversion would
// have minted;
// - a negative aggregate (impossible over non-negative summands, but defensive) is
// REFUSED rather than reinterpreted as a huge uint64.
func TestRED_SettlementBoundary_ExactUint64Edge(t *testing.T) {
const u64max = ^uint64(0)
// maxSettlementUnit must be strictly below 2^64 and cast in-range.
if maxSettlementUnit >= float64(u64max) {
// (float64(u64max) rounds to 2^64; the constant must be below it.)
t.Fatalf("maxSettlementUnit %.0f must be strictly below 2^64", maxSettlementUnit)
}
if got, err := quantToCredit(maxSettlementUnit); err != nil {
t.Fatalf("quantToCredit(maxSettlementUnit) must pass, got err=%v", err)
} else if uint64(got) > u64max { // tautology on uint64, but documents in-range intent
t.Fatalf("quantToCredit(maxSettlementUnit)=%d out of uint64 range", got)
}
if _, err := quantToCharge(maxSettlementUnit); err != nil {
t.Fatalf("quantToCharge(maxSettlementUnit) must pass, got err=%v", err)
// The largest representable amount passes and round-trips exactly.
maxV := new(big.Int).SetUint64(u64max)
if got, err := settleUint64(maxV, "base"); err != nil || got != u64max {
t.Fatalf("settleUint64(uint64max) = (%d, %v), want (%d, nil)", got, err, u64max)
}
// The next representable float64 above the boundary is exactly 2^64 — one above
// uint64 max — and MUST be refused by both directions.
over := math.Nextafter(maxSettlementUnit, math.Inf(1))
if over != float64(u64max)+1 && over != 18446744073709551616.0 {
t.Logf("note: nextafter(maxSettlementUnit)=%.0f (expected 2^64=18446744073709551616)", over)
}
if _, err := quantToCredit(over); err == nil {
t.Fatalf("OVERFLOW: quantToCredit(%.0f) accepted a value above uint64 max — must refuse.", over)
}
if _, err := quantToCharge(over); err == nil {
t.Fatalf("OVERFLOW: quantToCharge(%.0f) accepted a value above uint64 max — must refuse.", over)
// Exactly one above uint64 max (2^64) MUST be refused — it is the value a wrapped
// conversion would have minted.
over := new(big.Int).Add(maxV, big.NewInt(1))
if _, err := settleUint64(over, "base"); err == nil {
t.Fatalf("OVERFLOW: settleUint64(2^64) accepted a value above uint64 max — must refuse.")
}
// +Inf (e.g. a per-fill product that overflowed to +Inf in the aggregate loop)
// is refused too — the aggregate never reaches the cast.
if _, err := quantToCredit(math.Inf(1)); err == nil {
t.Fatalf("OVERFLOW: quantToCredit(+Inf) accepted — must refuse.")
}
if _, err := quantToCharge(math.Inf(1)); err == nil {
t.Fatalf("OVERFLOW: quantToCharge(+Inf) accepted — must refuse.")
// A negative aggregate is refused (never reinterpreted as a large uint64).
if _, err := settleUint64(big.NewInt(-1), "quote"); err == nil {
t.Fatalf("OVERFLOW: settleUint64(-1) accepted a negative aggregate — must refuse.")
}
}
@@ -538,8 +505,8 @@ func TestRED_MixedSideFills_OverCredits(t *testing.T) {
// First fill BUY 10 base, second claims SELL 1000 base — a stream no honest
// single submit can produce.
fills := []Fill{
{Price: 1, Size: 10, Side: 0},
{Price: 1, Size: 1000, Side: 1},
{Price: fp(1), Size: 10, Side: 0},
{Price: fp(1), Size: 1000, Side: 1},
}
cvm, _, _, _, _ := newCountingHarness(t, fills)
@@ -565,7 +532,6 @@ func TestRED_MixedSideFills_OverCredits(t *testing.T) {
if credited != 0 {
t.Fatalf("OVER-CREDIT/MINT: refused settle still exported %d base — refusal must accumulate nothing.", credited)
}
_ = math.Float64bits
}
// TestRED_DecodeFills_RejectsInvalidSideByte is the WIRE-BOUNDARY proof for the
@@ -582,8 +548,8 @@ func TestRED_DecodeFills_RejectsInvalidSideByte(t *testing.T) {
// Two structurally-valid fills; poison the SECOND fill's side byte to 7 — a
// value no honest matcher emits.
wire := encodeFillsWire([]Fill{
{Price: 1, Size: 10, Side: 0},
{Price: 1, Size: 5, Side: 0},
{Price: fp(1), Size: 10, Side: 0},
{Price: fp(1), Size: 5, Side: 0},
})
// Overwrite the second fill's side byte (offset: 4 count + 1*FillWireSize + 16).
wire[4+FillWireSize+16] = 7
@@ -611,7 +577,7 @@ func TestRED_DecodeFills_RejectsInvalidSideByte(t *testing.T) {
// (db.Abort on restart), so re-Verify re-submits the SAME order to the d-chain:
// a SECOND irreversible match. The maker is filled twice; the taker double-pays.
func TestRED_CrashBeforeAccept_DoubleSubmits(t *testing.T) {
fills := []Fill{{Price: 1, Size: 1000, Side: 0}}
fills := []Fill{{Price: fp(1), Size: 1000, Side: 0}}
cvm, matcher, cChainSM, proxyChain, _ := newCountingHarness(t, fills)
ctx := context.Background()
@@ -659,22 +625,25 @@ func TestRED_CrashBeforeAccept_DoubleSubmits(t *testing.T) {
}
}
// TestRED_EscrowTruncation_OverRefunds is the value-EXTRACTION proof. The settle
// computes spent quote as sum(uint64(price*size)) — truncating each fill's notional
// DOWN. For a taker BUY, refund = locked - spent. Understated spent => INFLATED
// refund. The taker receives the (real) base proceeds AND a refund larger than
// their true unspent collateral: net value extracted from the proxy escrow.
// TestRED_EscrowTruncation_OverRefunds is the value-EXTRACTION proof, now
// structurally CLOSED by the exact-integer wire. The old settle summed spent as
// Σ uint64(price*size) with a PER-FILL float truncation, so many small fills each
// floored to 0 recorded spent=0 and refunded the whole lock (mint). The wire now
// carries the venue's EXACT integer quote and settleFromFills spends exactly
// Σ floor(size × price / PriceScale) — the SAME per-fill quote the venue moved — so
// spent can never be understated relative to what the taker actually owes the venue.
//
// Concretely: lock 100 quote. Fill 100 base across fills priced 0.99 each, so the
// TRUE quote cost is 99 (taker should get ~1 quote refund + 100 base). But each
// uint64(0.99*1)=0 => recorded spent=0 => refund=100 quote, PLUS 100 base. The
// taker walks away with 100 quote + 100 base from a 100-quote lock that bought
// 100 base. ~99 quote minted.
// Concretely: lock 100 quote. Fill 100 base across 100 fills each of size 4 @ price
// 0.25 (fixed-point fp(0.25)); each fill's quote = floor(4 × 0.25) = floor(1.0) = 1,
// so spent = 100 (the venue's exact quote), refund = 0, and value_out = 100 base +
// 0 quote refund — no over-refund. The refund is exactly locked - spent; any refund
// beyond that would be the escrow mint this test forbids.
func TestRED_EscrowTruncation_OverRefunds(t *testing.T) {
// 100 fills of size 1 @ price 0.99 (taker BUY). True spent ~= 99 quote.
// 100 fills of size 4 @ price 0.25 (taker BUY). Per-fill quote = floor(4×0.25)=1,
// so total spent = 100 quote — the exact value the venue moved.
fills := make([]Fill, 100)
for i := range fills {
fills[i] = Fill{Price: 0.99, Size: 1, Side: 0}
fills[i] = Fill{Price: fp(0.25), Size: 4, Side: 0}
}
cvm, _, _, _, _ := newCountingHarness(t, fills)
@@ -694,14 +663,15 @@ func TestRED_EscrowTruncation_OverRefunds(t *testing.T) {
}
// Sum the exported value by asset: base (proceeds) vs the locked asset (refund).
// Export wire is rail(1)|owner(20)|asset(32)|amount(8), so asset is [21:53].
var refund, base uint64
for _, reqs := range ar.reqs {
for _, e := range reqs.PutRequests {
if len(e.Value) < 60 {
if len(e.Value) != exportedOutputSize {
continue
}
var a ids.ID
copy(a[:], e.Value[20:52])
copy(a[:], e.Value[21:53])
amt := binary.BigEndian.Uint64(e.Value[53:61])
if a == lockedAsset {
refund += amt
@@ -710,8 +680,8 @@ func TestRED_EscrowTruncation_OverRefunds(t *testing.T) {
}
}
}
trueBase := uint64(100) // sum(size)
trueSpent := uint64(99) // 100 * 0.99
trueBase := uint64(400) // Σ size = 100 × 4
trueSpent := uint64(100) // Σ floor(4 × 0.25) = 100 × 1
trueRefund := locked - trueSpent
t.Logf("locked=%d base proceeds=%d (true %d) refund=%d (true %d)",
locked, base, trueBase, refund, trueRefund)
@@ -720,9 +690,9 @@ func TestRED_EscrowTruncation_OverRefunds(t *testing.T) {
// base proceeds (the bought asset), so any refund > trueRefund is net theft.
if refund > trueRefund {
t.Fatalf("VALUE EXTRACTION: settle refunded %d of the locked asset but only %d "+
"was truly unspent (spent quote truncated via uint64(price*size)). The taker "+
"was truly unspent (spent = Σ floor(size×price/PriceScale) = %d). The taker "+
"keeps %d base proceeds too. ~%d quote minted out of the proxy escrow.",
refund, trueRefund, base, refund-trueRefund)
refund, trueRefund, trueSpent, base, refund-trueRefund)
}
}
@@ -762,8 +732,8 @@ func exportKeys(ar *atomicRequests) [][]byte {
// deterministic-seed fix.
func TestRED_SettlementExportKeyDeterminism(t *testing.T) {
fills := []Fill{
{Price: 2, Size: 50, Side: 0}, // taker BUY: receives base, pays quote
{Price: 2, Size: 50, Side: 0},
{Price: fp(2), Size: 50, Side: 0}, // taker BUY: receives base, pays quote
{Price: fp(2), Size: 50, Side: 0},
}
// The consensus-agreed settlement coordinate every validator sees identically.
blockHash := deriveBlockHash(7, time.Unix(1_700_000_000, 0))
@@ -829,7 +799,7 @@ func TestRED_SettlementExportKeyDeterminism(t *testing.T) {
// time.Now() (or any per-call nondeterminism) back into the settlement export's
// TxID, deriveUTXOID flips and this fails.
func TestRED_SettlementExportKeyIndependentOfWallClock(t *testing.T) {
fills := []Fill{{Price: 1, Size: 1000, Side: 0}}
fills := []Fill{{Price: fp(1), Size: 1000, Side: 0}}
blockHash := deriveBlockHash(42, time.Unix(123, 0))
const txIndex = uint32(0)
taker := ids.GenerateTestShortID()
+5 -5
View File
@@ -35,7 +35,7 @@ func TestRED_FillAttestation_VerifyRejectsForgedFills(t *testing.T) {
t.Fatalf("keygen: %v", err)
}
blockHash := ids.GenerateTestID()
entries := []carriedFill{{txIndex: 0, fills: []Fill{{Price: 2, Size: 100, Side: 0}}}}
entries := []carriedFill{{txIndex: 0, fills: []Fill{{Price: fp(2), Size: 100, Side: 0}}}}
sig := signFillAttestation(priv, blockHash, entries)
if len(sig) == 0 {
@@ -60,7 +60,7 @@ func TestRED_FillAttestation_VerifyRejectsForgedFills(t *testing.T) {
// TAMPERED ENTRIES (the fabrication): the proposer keeps the venue's signature but
// swaps in fabricated fills. The message changes => the signature no longer verifies.
forged := []carriedFill{{txIndex: 0, fills: []Fill{{Price: 2, Size: 1_000_000, Side: 0}}}}
forged := []carriedFill{{txIndex: 0, fills: []Fill{{Price: fp(2), Size: 1_000_000, Side: 0}}}}
if err := verifyFillAttestation(pub, sig, blockHash, forged); err != ErrFillAttestationInvalid {
t.Fatalf("a fabricated fill set under the genuine signature must be ErrFillAttestationInvalid, got: %v", err)
}
@@ -99,7 +99,7 @@ func TestRED_FillAttestation_EndToEnd_ValidAttestationSettles(t *testing.T) {
}
pubFromSeed := priv.Public().(ed25519.PublicKey)
fills := []Fill{{Price: 2, Size: 150, Side: 0}}
fills := []Fill{{Price: fp(2), Size: 150, Side: 0}}
cvm, matcher, cChainSM, proxyChain, _ := newCountingHarness(t, fills)
// Enforcement ON with the matching key pair (proposer signs with seed; validators
// verify with the derived pubkey).
@@ -154,7 +154,7 @@ func TestRED_FillAttestation_FabricatedFillsRefunded(t *testing.T) {
}
// A proposer carries a FABRICATED fill (200 base) but cannot attest it (no seed).
fills := []Fill{{Price: 2, Size: 200, Side: 0}}
fills := []Fill{{Price: fp(2), Size: 200, Side: 0}}
cvm, _, cChainSM, proxyChain, _ := newCountingHarness(t, fills)
cvm.inner.Config.FillAttestationPubKey = pub // enforcement ON
// NO FillAttestationSeed => the proposer's block carries an empty signature.
@@ -205,7 +205,7 @@ func TestRED_FillAttestation_FabricatedFillsRefunded(t *testing.T) {
// settle exactly as the interim model — so the new gate is strictly opt-in and does not
// regress existing deployments.
func TestRED_FillAttestation_OffByDefaultSettlesInterim(t *testing.T) {
fills := []Fill{{Price: 2, Size: 100, Side: 0}}
fills := []Fill{{Price: fp(2), Size: 100, Side: 0}}
cvm, _, cChainSM, proxyChain, _ := newCountingHarness(t, fills)
// Default config: no FillAttestationPubKey => enforcement off.
ctx := context.Background()
+2 -2
View File
@@ -135,7 +135,7 @@ func TestRED_GenesisAttestation_PubKeyReachesConfig(t *testing.T) {
}
// And the gate is now LIVE: verify rejects a missing signature (enforcement on).
bh := ids.GenerateTestID()
entries := []carriedFill{{txIndex: 0, fills: []Fill{{Price: 2, Size: 100, Side: 0}}}}
entries := []carriedFill{{txIndex: 0, fills: []Fill{{Price: fp(2), Size: 100, Side: 0}}}}
if verr := verifyFillAttestation(cvm.inner.Config.FillAttestationPubKey, nil, bh, entries); verr != ErrFillAttestationRequired {
t.Fatalf("with a genesis-pinned pubkey the gate must REQUIRE a signature, got: %v", verr)
}
@@ -154,7 +154,7 @@ func TestRED_GenesisAttestation_FabricatedBlockFullRefund(t *testing.T) {
}
// A proposer carries a FABRICATED fill (200 base) but cannot attest it (no seed in any
// config). Enforcement is on ONLY because the genesis blob pinned the pubkey.
fills := []Fill{{Price: 2, Size: 200, Side: 0}}
fills := []Fill{{Price: fp(2), Size: 200, Side: 0}}
cvm, cChainSM, proxyChain, _, initErr := newGenesisHarness(t, fills, genesisWithPubKey(pub), nil)
if initErr != nil {
t.Fatalf("Initialize: %v", initErr)
+3 -3
View File
@@ -156,8 +156,8 @@ func TestRED_PerValidatorRelay_SplitsConsensus(t *testing.T) {
// The book moves: first relay fills 1000 base @1; a later relay fills only 600.
matcher := &movingBookMatcher{perCall: [][]Fill{
{{Price: 1, Size: 1000, Side: 0}},
{{Price: 1, Size: 600, Side: 0}},
{{Price: fp(1), Size: 1000, Side: 0}},
{{Price: fp(1), Size: 600, Side: 0}},
}}
// Fixed ids so the block is byte-identical across both validators — the ONLY
@@ -223,7 +223,7 @@ func TestRED_PerValidatorRelay_SplitsConsensus(t *testing.T) {
// the split test above covers accept — this isolates the Verify leg.)
func TestRED_VerifyDoesNotRelay(t *testing.T) {
ctx := context.Background()
matcher := &movingBookMatcher{perCall: [][]Fill{{{Price: 1, Size: 1000, Side: 0}}}}
matcher := &movingBookMatcher{perCall: [][]Fill{{{Price: fp(1), Size: 1000, Side: 0}}}}
taker := fixedShortID("verify-purity-taker")
asset := idFromByte(0xb2)
+2 -2
View File
@@ -124,8 +124,8 @@ func TestRED_StateRoot_DivergentRelayOutcomeForksRoot(t *testing.T) {
// Node A settles a full 1000-base fill; node B settles a 600-base partial (the
// remaining 400 quote refunds). Same block bytes, DIFFERENT carried fills.
resA := runBlock([]Fill{{Price: 1, Size: 1000, Side: 0}})
resB := runBlock([]Fill{{Price: 1, Size: 600, Side: 0}})
resA := runBlock([]Fill{{Price: fp(1), Size: 1000, Side: 0}})
resB := runBlock([]Fill{{Price: fp(1), Size: 600, Side: 0}})
// Block hashes are identical, so the OLD blockHash-only root — a pure function
// of (blockHash, height) — necessarily collided on these divergent settlements.
+12 -12
View File
@@ -26,7 +26,6 @@ package dexvm
import (
"context"
"encoding/binary"
"math"
"testing"
"time"
@@ -70,7 +69,7 @@ func legsTo(ar *atomicRequests, owner ids.ShortID) map[ids.ID]uint64 {
// assetID(currency_out); only an export under the real id can ever satisfy it.
func TestRED_SwapProceeds_ExportedUnderRealOutputAsset(t *testing.T) {
// Taker BUY: 100 base @ price 2 => receives 100 base, spends 200 quote.
fills := []Fill{{Price: 2, Size: 100, Side: 0}}
fills := []Fill{{Price: fp(2), Size: 100, Side: 0}}
cvm, _, _, _, _ := newCountingHarness(t, fills)
taker := ids.GenerateTestShortID()
@@ -127,7 +126,7 @@ func TestRED_SwapProceeds_ExportedUnderRealOutputAsset(t *testing.T) {
// the swap output. This is the proof that swaps actually complete.
func TestRED_SwapProceeds_EndToEnd_TakerCreditedRealOutput(t *testing.T) {
// Taker BUY: book fills 150 base @ price 2 => 150 base received, 300 quote spent.
fills := []Fill{{Price: 2, Size: 150, Side: 0}}
fills := []Fill{{Price: fp(2), Size: 150, Side: 0}}
cvm, matcher, cChainSM, proxyChain, _ := newCountingHarness(t, fills)
ctx := context.Background()
@@ -217,9 +216,10 @@ type atomicShared interface {
var _ = txs.RailSwap
// floatBits packs a float64 price into the uint64 wire form the relay carries (the
// same math.Float64bits the precompile's priceLimitToCLOB produces).
func floatBits(p float64) uint64 { return math.Float64bits(p) }
// limitUnits packs a human quote-per-base price limit into the uint64 fixed-point
// ×PriceScale wire form the relay carries (the SAME grid as Fill.Price, the value
// the precompile's priceLimitToCLOB now produces).
func limitUnits(p float64) uint64 { return fp(p) }
// TestRED_SwapSlippageLimit_RejectsWorseThanLimit is the MEDIUM (bounded sandwich/MEV)
// proof. A taker BUY carries a worst-acceptable price limit (an UPPER bound: never pay
@@ -235,14 +235,14 @@ func TestRED_SwapSlippageLimit_RejectsWorseThanLimit(t *testing.T) {
const limit = 2.0 // BUY ceiling: never pay more than 2 quote per base.
// (a) WORSE fill: price 3 > limit 2 (a sandwich pushed the price up). Refused.
worse := []Fill{{Price: 3, Size: 100, Side: 0}}
worse := []Fill{{Price: fp(3), Size: 100, Side: 0}}
cvm, _, _, _, _ := newCountingHarness(t, worse)
ref := ids.GenerateTestID()
if err := cvm.inner.state.PutEscrow(ref, taker, quoteAsset, lockedQuote); err != nil {
t.Fatalf("PutEscrow: %v", err)
}
ar := newAtomicRequests()
err := cvm.inner.settleFromFills(taker, ref, worse, baseAsset, floatBits(limit), true, ids.GenerateTestID(), 0, ar)
err := cvm.inner.settleFromFills(taker, ref, worse, baseAsset, limitUnits(limit), true, ids.GenerateTestID(), 0, ar)
if err == nil {
t.Fatalf("MEV GUARD MISSING: a fill at price 3 (above the taker's limit 2) was settled — a "+
"sandwich could fill the taker beyond their slippage floor.")
@@ -257,14 +257,14 @@ func TestRED_SwapSlippageLimit_RejectsWorseThanLimit(t *testing.T) {
t.Logf("MEV GUARD: fill@3 rejected against BUY limit 2 (escrow intact): %v", err)
// (b) AT-LIMIT fill: price 2 == limit 2. Settles normally (100 base proceeds).
atLimit := []Fill{{Price: 2, Size: 100, Side: 0}}
atLimit := []Fill{{Price: fp(2), Size: 100, Side: 0}}
cvm2, _, _, _, _ := newCountingHarness(t, atLimit)
ref2 := ids.GenerateTestID()
if err := cvm2.inner.state.PutEscrow(ref2, taker, quoteAsset, lockedQuote); err != nil {
t.Fatalf("PutEscrow: %v", err)
}
ar2 := newAtomicRequests()
if err := cvm2.inner.settleFromFills(taker, ref2, atLimit, baseAsset, floatBits(limit), true, ids.GenerateTestID(), 0, ar2); err != nil {
if err := cvm2.inner.settleFromFills(taker, ref2, atLimit, baseAsset, limitUnits(limit), true, ids.GenerateTestID(), 0, ar2); err != nil {
t.Fatalf("an at-limit fill (price == limit) must settle, got: %v", err)
}
if legsTo(ar2, taker)[baseAsset] != 100 {
@@ -274,14 +274,14 @@ func TestRED_SwapSlippageLimit_RejectsWorseThanLimit(t *testing.T) {
// (c) SELL floor: a SELL carries a LOWER bound (never receive less than `limit` quote
// per base). A fill BELOW the floor is refused.
sellFloor := []Fill{{Price: 4, Size: 100, Side: 1}}
sellFloor := []Fill{{Price: fp(4), Size: 100, Side: 1}}
cvm3, _, _, _, _ := newCountingHarness(t, sellFloor)
ref3 := ids.GenerateTestID()
if err := cvm3.inner.state.PutEscrow(ref3, taker, baseAsset /*locked base for a sell*/, lockedQuote); err != nil {
t.Fatalf("PutEscrow: %v", err)
}
ar3 := newAtomicRequests()
if err := cvm3.inner.settleFromFills(taker, ref3, sellFloor, quoteAsset, floatBits(5.0), false, ids.GenerateTestID(), 0, ar3); err == nil {
if err := cvm3.inner.settleFromFills(taker, ref3, sellFloor, quoteAsset, limitUnits(5.0), false, ids.GenerateTestID(), 0, ar3); err == nil {
t.Fatalf("SELL floor MISSING: a fill at price 4 (below the floor 5) was settled.")
}
t.Logf("MEV GUARD: SELL fill@4 rejected against floor 5")
+32 -20
View File
@@ -81,9 +81,17 @@ const (
)
// FillWireSize is one fill in a clob_submit response: price[8]+size[8]+side[1].
// FROZEN — must equal dex/pkg/api.FillWireSize and the precompile's fillWireSize.
// FROZEN — must equal dex/pkg/zapwire.FillWireSize and the precompile's fillWireSize.
// price is a uint64 FIXED-POINT ×PriceScale value; size is uint64 ATOMIC BASE UNITS.
const FillWireSize = 17
// PriceScale is the fixed-point scale of the wire price field (price × 1e8) — the
// matcher's PriceInt grid. FROZEN, == dex/pkg/zapwire.PriceScale. quote owed =
// size × price / PriceScale, computed in EXACT integers so no value-bearing
// float→int conversion (whose out-of-range result is architecture-dependent) can
// fork two honest validators on different CPUs.
const PriceScale = 100000000
// ErrRelayNotConfigured is returned when an order relay is attempted but no
// d-chain ZAP endpoint is configured. The proxy is inert on the relay leg until
// the venue operator points dex-zap-endpoint at a d-chain gateway.
@@ -187,24 +195,28 @@ func (r *RelayClient) Close() error {
}
// Fill is one execution returned by the d-chain matcher: price + size + the
// taker side this submit took with. The proxy derives the settlement delta
// ONLY from these server-returned fills — never from a client-supplied amount.
// taker side this submit took with. Price is a uint64 FIXED-POINT ×PriceScale
// value (the matcher's PriceInt grid); Size is uint64 ATOMIC BASE UNITS. The
// proxy derives the settlement delta ONLY from these server-returned EXACT
// integers — never from a client-supplied amount, and never through a float
// (whose out-of-range int conversion is architecture-dependent and would fork).
type Fill struct {
Price float64
Size float64
Price uint64
Size uint64
Side uint8
}
// DecodeFills parses a clob_submit response: count[4] then count×(price[8] +
// size[8] + side[1]). Every field is range-checked so a backend that lies (or a
// MITM on the socket) cannot inject a structurally invalid fill into settlement:
// price/size must be finite and strictly positive, and side MUST be a valid CLOB
// side (0=BUY, 1=SELL). A side byte outside {0,1} is malformed wire exactly like
// a NaN price — rejecting it here keeps a Fill value always well-formed, so no
// downstream consumer ever sees an impossible side. (Cross-fill side CONSISTENCY
// — "a single submit takes exactly one side" — is a settlement-policy invariant
// enforced in settleFromFills, not a per-fill wire property; the two checks own
// different invariants and do not overlap.)
// size[8] + side[1]), every multi-byte field a big-endian uint64. Each field is
// range-checked so a backend that lies (or a MITM on the socket) cannot inject a
// structurally invalid fill into settlement: price/size must be strictly positive
// (a zero-price or zero-size execution is not a real fill), and side MUST be a
// valid CLOB side (0=BUY, 1=SELL). A side byte outside {0,1} is malformed wire
// exactly like a zero price — rejecting it here keeps a Fill value always
// well-formed, so no downstream consumer ever sees an impossible side. (Cross-fill
// side CONSISTENCY — "a single submit takes exactly one side" — is a
// settlement-policy invariant enforced in settleFromFills, not a per-fill wire
// property; the two checks own different invariants and do not overlap.)
func DecodeFills(data []byte) ([]Fill, error) {
if len(data) < 4 {
return nil, fmt.Errorf("fills response too short: %d", len(data))
@@ -216,15 +228,15 @@ func DecodeFills(data []byte) ([]Fill, error) {
fills := make([]Fill, 0, n)
off := 4
for i := 0; i < n; i++ {
p := float64FromBits(data[off : off+8])
s := float64FromBits(data[off+8 : off+16])
p := binary.BigEndian.Uint64(data[off : off+8])
s := binary.BigEndian.Uint64(data[off+8 : off+16])
side := data[off+16]
off += FillWireSize
if !isFinitePositive(p) {
return nil, fmt.Errorf("fill %d: invalid price %v", i, p)
if p == 0 {
return nil, fmt.Errorf("fill %d: invalid price %d", i, p)
}
if !isFinitePositive(s) {
return nil, fmt.Errorf("fill %d: invalid size %v", i, s)
if s == 0 {
return nil, fmt.Errorf("fill %d: invalid size %d", i, s)
}
if side > 1 {
return nil, fmt.Errorf("fill %d: invalid side %d (must be 0=BUY or 1=SELL)", i, side)
+3 -2
View File
@@ -356,8 +356,9 @@ type RelayOrderTx struct {
// signature-bound (JSON-covered by SigningBytes); a wrong value can only break the
// taker's OWN liveness — ImportSettlement equality-rejects a mismatched asset (no
// theft, no mint), the same bounded surface as the carried-fills proposer-trust model.
// PriceLimit is the worst-acceptable CLOB price (quote-per-base, float64-bits) for a
// settling clob_submit, derived from the V4 SqrtPriceLimitX96. settleFromFills refuses
// PriceLimit is the worst-acceptable CLOB price (quote-per-base, uint64 FIXED-POINT
// ×PriceScale, the SAME grid as Fill.Price) for a settling clob_submit, derived from
// the V4 SqrtPriceLimitX96. settleFromFills refuses
// any carried fill WORSE than this (BUY: price above the limit; SELL: price below it),
// enforcing the taker's slippage floor against a sandwich/MEV move. Zero = no limit.
// LimitIsUpper records the direction the limit bounds (true: reject price ABOVE the
+74 -80
View File
@@ -11,7 +11,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"net/http"
"sync"
"time"
@@ -939,11 +939,11 @@ func (vm *VM) settleCarried(result *BlockResult, ar *atomicRequests) {
// RECEIVES the opposite asset. The settle therefore exports TWO legs:
//
// PROCEEDS leg — the received (opposite) asset:
// - taker BUY (side 0): receives base = sum(size).
// - taker SELL (side 1): receives quote = sum(price*size).
// - taker BUY (side 0): receives base = Σ size.
// - taker SELL (side 1): receives quote = Σ floor(size × price / PriceScale).
// REFUND leg — the unfilled remainder of the LOCKED asset:
// - taker BUY locked quote; spent = sum(price*size); refund = locked - spent.
// - taker SELL locked base; spent = sum(size); refund = locked - spent.
// - taker BUY locked quote; spent = Σ floor(size×price/PriceScale); refund = locked - spent.
// - taker SELL locked base; spent = Σ size; refund = locked - spent.
//
// Crediting only the proceeds leg (the prior behavior) DESTROYED the unfilled
// remainder on every partial/zero fill. The refund leg closes that leak: the
@@ -953,25 +953,18 @@ func (vm *VM) settleCarried(result *BlockResult, ar *atomicRequests) {
// refunded twice. With no escrow (a relay not backed by a proxy-side import),
// there is no locked collateral to refund — proceeds only.
//
// FRACTIONAL-NOTIONAL ROUNDING (RED escrow-truncation mint): fills cross the ZAP
// wire as float64, so base = sum(size) and quote = sum(price*size) are generally
// FRACTIONAL while on-chain value moves in integer asset units. The float->uint
// conversion is ASYMMETRIC by purpose, to preserve "the proxy never mints"
// (atomic.go quantToCredit/quantToCharge):
// - PROCEEDS (the opposite asset the taker RECEIVES) round DOWN — never credit
// a unit that was not realized.
// - SPENT (the locked asset consumed, which REDUCES the refund) round UP —
// never UNDERstate spend. Understating spend is exactly this bug: a BUY's
// spent = floor(notional) inflates refund = locked - spent, so the taker
// keeps the base proceeds AND a refund larger than the true unspent quote =>
// quote minted out of escrow.
//
// Totals are summed over the fills ONCE as exact floats, then rounded ONCE at the
// asset boundary. The prior per-fill uint64(price*size) truncated DOWN on every
// fill and summed the error: 100 fills of notional 0.99 recorded spent=0 and
// refunded the entire lock. Aggregate ceiling gives spent=ceil(99.0)=99, so the
// refund is capped at the true unspent value — no extraction. spent > locked is
// still refused (minting against the proxy's own escrow).
// EXACT-INTEGER NOTIONAL (no float, no mint): fills cross the ZAP wire as EXACT
// integers — size is atomic base units, price is fixed-point ×PriceScale — so
// base = Σ size and quote = Σ floor(size × price / PriceScale) are themselves exact
// integers, computed with the SAME per-fill floor the venue matcher applied. The
// proxy therefore moves EXACTLY what the venue matched: there is no fractional
// notional to round, no asymmetric floor/ceil, and — decisively — no value-bearing
// float→int conversion (whose out-of-range result is architecture-dependent and
// was the cross-arch fork). The escrow-truncation mint is structurally impossible:
// spent is the venue's own quote integer, refund = locked - spent, and spent >
// locked is still refused (minting against the proxy's own escrow). The aggregates
// are summed in big.Int and refused if they exceed uint64 — a value the proxy
// cannot represent exactly is never settled.
//
// DETERMINISM (RED split): the export this settle constructs is reconstructed
// independently on every validator, so its shared-memory UTXO keys MUST be a
@@ -1030,68 +1023,69 @@ func (vm *VM) settleFromFills(relaySender ids.ShortID, collateralRef ids.ID, fil
return fmt.Errorf("settle: invalid taker side %d", takerSide)
}
// Aggregate the fill totals ONCE as exact floats (base = sum(size), quote =
// sum(price*size)). Rounding to integer asset units happens later, in the
// direction proper to each leg — NOT per-fill, which would truncate DOWN on
// every fill and sum a directional leak (the escrow-truncation mint).
// Aggregate the fill totals as EXACT integers: base = Σ size (atomic base
// units), quote = Σ floor(size × price / PriceScale) — the SAME per-fill floor
// the venue's matcher (quoteUnitsFor) and the precompile's fillsToDelta apply,
// so the proxy settles EXACTLY the value the venue moved. There is no fractional
// notional and no float→int conversion on this path (the cross-arch fork the
// exact-integer wire eliminates); big.Int accumulation cannot overflow and the
// narrowing to uint64 refuses any aggregate the proxy cannot represent exactly.
//
// SINGLE-SIDE GUARD (RED mixed-side over-credit): the proceeds/spent split
// below applies ONE direction (takerSide) to the WHOLE aggregate, so a fill
// stream that mixes sides would be credited as if every fill took takerSide —
// minting the opposite-side volume (a lying/MITM backend returns [BUY 10,
// SELL 1000] and the proxy credits base = 1010). A submit cannot legitimately
// fill both sides; refuse the stream rather than over-credit.
// SINGLE-SIDE GUARD (RED mixed-side over-credit): the proceeds/spent split below
// applies ONE direction (takerSide) to the WHOLE aggregate, so a fill stream that
// mixes sides would be credited as if every fill took takerSide — minting the
// opposite-side volume (a lying/MITM backend returns [BUY 10, SELL 1000] and the
// proxy credits base = 1010). A submit cannot legitimately fill both sides; refuse
// the stream rather than over-credit.
//
// SLIPPAGE-LIMIT GUARD (MEDIUM — bounded sandwich/MEV): the taker carried a
// worst-acceptable CLOB price (priceLimit, derived from their V4 SqrtPriceLimitX96).
// A fill WORSE than that limit is refused before any value moves — so an adversary
// who sandwiches the order and moves the price beyond the taker's floor produces
// fills the proxy will not settle; the escrow stays intact and is reclaimable. The
// limit composes per-fill (it is a price bound, not a min-total-out), so each fill is
// checked independently. limitIsUpper governs the direction: an exact-input BUY caps
// the price ABOVE the limit (never pay more than `limit` quote per base); a SELL
// floors it BELOW (never receive less than `limit`). priceLimit==0 means no limit
// (the V4 sentinel min/max sqrt price, preserved as "unbounded").
var limit float64
if priceLimit != 0 {
limit = math.Float64frombits(priceLimit)
}
var baseFloat, quoteFloat float64
// worst-acceptable CLOB price (priceLimit, a uint64 fixed-point ×PriceScale value
// derived from their V4 SqrtPriceLimitX96, the SAME grid as Fill.Price). A fill
// WORSE than that limit is refused before any value moves — so an adversary who
// sandwiches the order and moves the price beyond the taker's floor produces fills
// the proxy will not settle; the escrow stays intact and is reclaimable. The limit
// composes per-fill (it is a price bound, not a min-total-out), so each fill is
// checked independently by EXACT integer comparison. limitIsUpper governs the
// direction: an exact-input BUY caps the price ABOVE the limit (never pay more than
// `limit` quote per base); a SELL floors it BELOW (never receive less than `limit`).
// priceLimit==0 means no limit (the V4 sentinel min/max sqrt price, "unbounded").
base := new(big.Int)
quote := new(big.Int)
for _, f := range fills {
if f.Side != takerSide {
return fmt.Errorf("settle: mixed-side fills (fill side %d != taker side %d)", f.Side, takerSide)
}
if limit > 0 {
if limitIsUpper && f.Price > limit {
return fmt.Errorf("settle: fill price %g exceeds slippage limit %g (sandwich/MEV guard)", f.Price, limit)
if priceLimit != 0 {
if limitIsUpper && f.Price > priceLimit {
return fmt.Errorf("settle: fill price %d exceeds slippage limit %d (sandwich/MEV guard)", f.Price, priceLimit)
}
if !limitIsUpper && f.Price < limit {
return fmt.Errorf("settle: fill price %g below slippage limit %g (sandwich/MEV guard)", f.Price, limit)
if !limitIsUpper && f.Price < priceLimit {
return fmt.Errorf("settle: fill price %d below slippage limit %d (sandwich/MEV guard)", f.Price, priceLimit)
}
}
baseFloat += f.Size
quoteFloat += f.Price * f.Size
base.Add(base, new(big.Int).SetUint64(f.Size))
quote.Add(quote, quoteUnits(f.Size, f.Price))
}
// Directional rounding by purpose (atomic.go): a RECEIVED quantity rounds DOWN
// (never over-credit); a SPENT quantity rounds UP (never inflate the refund).
// BUY receives base, spends quote. SELL receives quote, spends base.
baseUnits, err := settleUint64(base, "base")
if err != nil {
return err
}
quoteTotal, err := settleUint64(quote, "quote")
if err != nil {
return err
}
// BUY receives base, spends quote. SELL receives quote, spends base. Both legs
// are the venue's own exact integers — the proxy moves precisely what the venue
// matched (no rounding, no mint); spent > locked is still refused below.
var proceeds uint64 // the opposite asset the taker receives
var spent uint64 // the locked asset consumed (reduces the refund)
if takerSide == 0 { // BUY
if proceeds, err = quantToCredit(baseFloat); err != nil {
return err
}
if spent, err = quantToCharge(quoteFloat); err != nil {
return err
}
if takerSide == 0 { // BUY
proceeds = baseUnits
spent = quoteTotal
} else { // SELL
if proceeds, err = quantToCredit(quoteFloat); err != nil {
return err
}
if spent, err = quantToCharge(baseFloat); err != nil {
return err
}
proceeds = quoteTotal
spent = baseUnits
}
outs := make([]txs.AtomicOutput, 0, 2)
@@ -1499,24 +1493,24 @@ func (vm *VM) cChainID() ids.ID {
}
// encodeCLOBPlace builds a byte-identical clob_place frame from a thin place
// envelope, mirroring the d-chain gateway's expected layout (poolId[32] |
// side[1] | price[8] | size[8] | maker[16]).
// envelope, mirroring the d-chain gateway's frozen exact-integer layout
// (poolId[32] | side[1] | price[8:u64 fixed ×PriceScale] | size[8:u64 base units] |
// maker[16]). tx.Price is a whole quote-per-base price scaled onto the matcher's
// PriceInt grid (× PriceScale); tx.Size is already atomic base units. No float
// touches the wire — the venue reads these exact integers directly (dex/pkg/zapwire
// EncodePlace / DecodePlace), so no architecture-dependent float→int conversion
// can fork the derived book state.
func encodeCLOBPlace(tx *txs.PlaceOrderTx) []byte {
payload := make([]byte, 65)
copy(payload[0:32], tx.PoolID[:])
payload[32] = tx.Side
putZAPFloat(payload[33:41], float64(tx.Price))
putZAPFloat(payload[41:49], float64(tx.Size))
binary.BigEndian.PutUint64(payload[33:41], tx.Price*PriceScale)
binary.BigEndian.PutUint64(payload[41:49], tx.Size)
maker := tx.Sender()
copy(payload[49:65], maker[:16])
return payload
}
// putZAPFloat writes a ZAP float wire field (big-endian IEEE-754 float64 bits).
func putZAPFloat(b []byte, f float64) {
binary.BigEndian.PutUint64(b, math.Float64bits(f))
}
// idHash is the canonical 32-byte hash used for derived ids.
func idHash(b []byte) [32]byte { return sha256.Sum256(b) }
+2 -2
View File
@@ -13,6 +13,7 @@ require (
github.com/luxfi/constants v1.5.8
github.com/luxfi/crypto v1.19.26
github.com/luxfi/database v1.20.4
github.com/luxfi/dex v1.14.0
github.com/luxfi/evm v1.99.51
github.com/luxfi/geth v1.17.12
github.com/luxfi/ids v1.2.15
@@ -64,7 +65,7 @@ require (
github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.6 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect
github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
@@ -141,7 +142,6 @@ require (
github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/corona v0.10.3 // indirect
github.com/luxfi/crypto/ipa v1.2.4 // indirect
github.com/luxfi/dex v1.5.20 // indirect
github.com/luxfi/dkg v0.3.5 // indirect
github.com/luxfi/fhe v1.8.2 // indirect
github.com/luxfi/filesystem v0.0.1 // indirect
+3
View File
@@ -65,6 +65,7 @@ github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v5
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.6 h1:IzlsEr9olcSRKB/n7c4351F3xHKxS2lma+1UFGCYd4E=
github.com/btcsuite/btcd/btcec/v2 v2.3.6/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ=
github.com/btcsuite/btcd/btcec/v2 v2.5.0/go.mod h1:+K/MYXcLBtHEQjRbjHuJChuybk4LCgjdjgRwil+e+Kk=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
@@ -321,6 +322,8 @@ github.com/luxfi/database v1.20.4 h1:WOt2GIGJxf8AFpg49odMz8DZ8RFSLDrozGhZtmorN70
github.com/luxfi/database v1.20.4/go.mod h1:S/LvmfzNYWVNslcEcZwDrntqUO2ksaL8ql1nRmLUA/Q=
github.com/luxfi/dex v1.5.20 h1:Vu3D0iS1QcAPs1DtPjjtQpd+8bclkf5nisVYqaoEbkA=
github.com/luxfi/dex v1.5.20/go.mod h1:95k6Tl5HoyBRVd6IFzGlPOZE6SGqOyjnh4dSrJcplzM=
github.com/luxfi/dex v1.14.0 h1:4PtorVbRmbD73S6YWPvgp5GRCb9jeuaedl7mo1CbzCI=
github.com/luxfi/dex v1.14.0/go.mod h1:wYWQmwospvdKBWQ6OJMXb8I+kfgEtE46R1rD8H7vHCQ=
github.com/luxfi/dkg v0.3.5 h1:s2L2mMQaz+n9m0b0ghvoV5VZNxiwb2z4WrGugvK0udY=
github.com/luxfi/dkg v0.3.5/go.mod h1:M+WH7GFRN+YUD851Rlnumdp0Md98kplNN8pVx65U8I8=
github.com/luxfi/evm v1.99.51 h1:Ai/++j0hK95YOrBGH/4ZWznO5C6yL9aRqwnaZ83BrbQ=