mirror of
https://github.com/luxfi/maker.git
synced 2026-10-30 16:20:04 +00:00
The scale prices cross the wire on was declared five times — priceGridMul, gridMul, priceMultiplier, refPriceMultiplier, and again per command — and read back three ways: priceToGrid, gridToFloat, gridToPrice. Same number every time. dex.go said so out loud: "The maker re-declares it so it can size a deposit." The place a value is used had been braided into the value itself. It is one value, so it gets one name: PriceGridMul, exported because the commands need the same scale and were each declaring their own. Conversions become one inverse pair, GridOf and PriceOf, with PriceOfBig for a grid read off the wire as a word — no open-coded /1e8 left anywhere. Co-authored-by: Hanzo Dev <[email protected]>
231 lines
8.1 KiB
Go
231 lines
8.1 KiB
Go
package maker
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"math/big"
|
|
"testing"
|
|
)
|
|
|
|
// dexcustody_test.go proves the maker's CUSTODY frames (DEFECT 2) and lock-sizing
|
|
// math are byte/value-identical to github.com/luxfi/dex/pkg/zapwire and
|
|
// pkg/dchain. We re-state the canonical encoders/lock math verbatim as references
|
|
// (the DEX's own pattern — re-declare the frozen frame, assert equality — rather
|
|
// than importing the whole dex module for a few pure functions) and assert the
|
|
// maker matches them. A frame or lock-math divergence fails the build.
|
|
|
|
// --- reference encoders, verbatim from dex/pkg/zapwire/zapwire.go ---
|
|
|
|
// refEncodeOpenMarket == zapwire.EncodeOpenMarket (zapwire.go:431-437).
|
|
func refEncodeOpenMarket(poolID, baseAsset, quoteAsset [32]byte) []byte {
|
|
const PoolIDSize, AssetIDSize = 32, 32
|
|
out := make([]byte, PoolIDSize+AssetIDSize+AssetIDSize)
|
|
copy(out[0:PoolIDSize], poolID[:])
|
|
copy(out[PoolIDSize:PoolIDSize+AssetIDSize], baseAsset[:])
|
|
copy(out[PoolIDSize+AssetIDSize:], quoteAsset[:])
|
|
return out
|
|
}
|
|
|
|
// refEncodeDeposit == zapwire.EncodeDeposit (zapwire.go:377-384).
|
|
func refEncodeDeposit(user string, asset [32]byte, amount uint64, ref [32]byte) []byte {
|
|
const UserSize, AssetIDSize, RefSize = 16, 32, 32
|
|
const custodyAmountOff = UserSize + AssetIDSize // 48
|
|
const custodyRefOff = UserSize + AssetIDSize + 8 // 56
|
|
out := make([]byte, UserSize+AssetIDSize+8+RefSize)
|
|
pad := make([]byte, UserSize)
|
|
copy(pad, user)
|
|
copy(out[0:UserSize], pad)
|
|
copy(out[UserSize:custodyAmountOff], asset[:])
|
|
binary.BigEndian.PutUint64(out[custodyAmountOff:custodyRefOff], amount)
|
|
copy(out[custodyRefOff:custodyRefOff+RefSize], ref[:])
|
|
return out
|
|
}
|
|
|
|
func TestEncodeOpenMarketMatchesZapwire(t *testing.T) {
|
|
pool := SymbolPoolID("LUX/LUSD")
|
|
got := encodeOpenMarket(pool, AssetLUX, AssetLUSD)
|
|
want := refEncodeOpenMarket(pool, AssetLUX, AssetLUSD)
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("encodeOpenMarket\n got=%x\nwant=%x", got, want)
|
|
}
|
|
if len(got) != 96 {
|
|
t.Errorf("open_market frame = %d bytes, want 96", len(got))
|
|
}
|
|
}
|
|
|
|
func TestEncodeDepositMatchesZapwire(t *testing.T) {
|
|
ref := depositRef("maker", AssetLUSD, 1250)
|
|
got := encodeDeposit("maker", AssetLUSD, 1250, ref)
|
|
want := refEncodeDeposit("maker", AssetLUSD, 1250, ref)
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("encodeDeposit\n got=%x\nwant=%x", got, want)
|
|
}
|
|
if len(got) != 88 {
|
|
t.Errorf("deposit frame = %d bytes, want 88", len(got))
|
|
}
|
|
}
|
|
|
|
// TestDepositRefMatchesVenueContentRef proves the maker's idempotency ref is
|
|
// byte-identical to the venue's contentRef (conservation_test.go:45-58 /
|
|
// cmd/fourpath-live main.go:88-97 with the deposit tag), so a byte-identical
|
|
// re-seed dedups to exactly-once on the venue.
|
|
func TestDepositRefMatchesVenueContentRef(t *testing.T) {
|
|
// refContentRef is verbatim venue contentRef with typ = TxDeposit(1).
|
|
refContentRef := func(typ byte, user string, asset [32]byte, amount uint64) [32]byte {
|
|
var ref [32]byte
|
|
copy(ref[:], asset[:])
|
|
ref[0] ^= typ
|
|
for i := 0; i < len(user) && i+1 < 24; i++ {
|
|
ref[1+i] ^= user[i]
|
|
}
|
|
binary.BigEndian.PutUint64(ref[24:32], binary.BigEndian.Uint64(asset[24:32])^amount)
|
|
return ref
|
|
}
|
|
for _, tc := range []struct {
|
|
user string
|
|
asset [32]byte
|
|
amount uint64
|
|
}{
|
|
{"maker", AssetLUSD, 1250},
|
|
{"maker", AssetLETH, 1},
|
|
{"seeder-x", AssetLUX, 99},
|
|
} {
|
|
got := depositRef(tc.user, tc.asset, tc.amount)
|
|
want := refContentRef(txDeposit, tc.user, tc.asset, tc.amount)
|
|
if got != want {
|
|
t.Errorf("depositRef(%q,%x,%d) = %x, want %x", tc.user, tc.asset, tc.amount, got, want)
|
|
}
|
|
}
|
|
// Idempotency: identical inputs -> identical ref.
|
|
if depositRef("maker", AssetLUSD, 1250) != depositRef("maker", AssetLUSD, 1250) {
|
|
t.Error("depositRef not deterministic")
|
|
}
|
|
// Distinctness: a different amount -> a different ref.
|
|
if depositRef("maker", AssetLUSD, 1250) == depositRef("maker", AssetLUSD, 1251) {
|
|
t.Error("depositRef collided across distinct amounts")
|
|
}
|
|
}
|
|
|
|
// --- reference lock math, verbatim from dex/pkg/dchain settle.go ---
|
|
|
|
|
|
// refOrderLock == pkg/dchain.orderLock composed with sizeToUnits/priceToInt/
|
|
// quoteUnitsCeil (settle.go:55-105). It returns the integer amount a place locks.
|
|
func refOrderLock(side uint8, price, size float64) uint64 {
|
|
if !(size > 0) {
|
|
return 0
|
|
}
|
|
units := uint64(size) // sizeToUnits
|
|
if units == 0 {
|
|
return 0
|
|
}
|
|
if side == sideSell {
|
|
return units
|
|
}
|
|
if !(price > 0) {
|
|
return 0
|
|
}
|
|
priceInt := uint64(price * PriceGridMul) // priceToInt
|
|
if priceInt == 0 {
|
|
return 0
|
|
}
|
|
q := new(big.Int).Mul(new(big.Int).SetUint64(units), new(big.Int).SetUint64(priceInt))
|
|
q.Add(q, big.NewInt(PriceGridMul-1)) // ceil
|
|
q.Quo(q, big.NewInt(PriceGridMul))
|
|
if !q.IsUint64() {
|
|
return 0
|
|
}
|
|
return q.Uint64()
|
|
}
|
|
|
|
func TestLockUnitsMatchesVenueOrderLock(t *testing.T) {
|
|
cases := []struct {
|
|
side uint8
|
|
price float64
|
|
size float64
|
|
}{
|
|
{sideSell, 12.5125, 100}, // ask: locks floor(100)=100 base
|
|
{sideBuy, 12.4875, 100}, // bid: locks ceil(100*12.4875)=1249 quote
|
|
{sideSell, 0.004, 100}, // LETH ask
|
|
{sideBuy, 0.003996, 150}, // LETH bid, fractional price
|
|
{sideSell, 1.0, 1.9}, // truncating sell size -> 1 unit
|
|
{sideBuy, 7.0, 3.0}, // exact: 21 quote
|
|
{sideBuy, 0.0000001, 1}, // sub-tick price floors to 0 lock
|
|
}
|
|
for _, c := range cases {
|
|
got := lockUnits(c.side, c.price, c.size)
|
|
want := refOrderLock(c.side, c.price, c.size)
|
|
if got != want {
|
|
t.Errorf("lockUnits(side=%d, %g, %g) = %d, venue orderLock = %d", c.side, c.price, c.size, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDepositCoversLock is the funding-correctness invariant: the deposit a
|
|
// custody seed issues for one side must be >= the sum of that side's per-order
|
|
// locks, so no place is rejected for insufficient funds. We size with lockUnits
|
|
// (== orderLock), so equality holds exactly.
|
|
func TestDepositCoversLock(t *testing.T) {
|
|
// A representative ask ladder (size, price) for LUX/LUSD.
|
|
asks := []struct{ price, size float64 }{
|
|
{12.5125, 100}, {12.575, 150}, {12.6375, 225},
|
|
}
|
|
var deposit, totalLock uint64
|
|
for _, a := range asks {
|
|
deposit += lockUnits(sideSell, a.price, a.size) // what fundAll deposits
|
|
totalLock += refOrderLock(sideSell, a.price, a.size) // what the venue locks
|
|
}
|
|
if deposit < totalLock {
|
|
t.Fatalf("base deposit %d < total ask lock %d (would under-fund)", deposit, totalLock)
|
|
}
|
|
if deposit != totalLock {
|
|
t.Errorf("base deposit %d != exact lock %d (over/under-funding)", deposit, totalLock)
|
|
}
|
|
}
|
|
|
|
// TestDecodeBalanceRespMatchesZapwire proves the maker reads a deposit response
|
|
// the same way zapwire.DecodeBalanceResp writes it (status[1]+amount[8]).
|
|
func TestDecodeBalanceRespMatchesZapwire(t *testing.T) {
|
|
// refEncodeBalanceResp == zapwire.EncodeBalanceResp (zapwire.go:453-458).
|
|
refEncode := func(status uint8, amount uint64) []byte {
|
|
out := make([]byte, 9)
|
|
out[0] = status
|
|
binary.BigEndian.PutUint64(out[1:9], amount)
|
|
return out
|
|
}
|
|
resp := refEncode(statusPlaced, 1249)
|
|
status, amount, err := decodeBalanceResp(resp)
|
|
if err != nil {
|
|
t.Fatalf("decodeBalanceResp: %v", err)
|
|
}
|
|
if status != statusPlaced || amount != 1249 {
|
|
t.Errorf("decoded (status=%d, amount=%d), want (0, 1249)", status, amount)
|
|
}
|
|
if _, _, err := decodeBalanceResp([]byte{0x00}); err == nil {
|
|
t.Error("expected error on short balance resp")
|
|
}
|
|
}
|
|
|
|
// TestAssetIDsArePinned documents and locks the canonical devnet asset ids
|
|
// (DEFECT 3): native LUX = all-zero; LUSD/LETH = the a32 label form the live
|
|
// settlement rail (cmd/fourpath-live, conservation_test) uses on devnet.
|
|
func TestAssetIDsArePinned(t *testing.T) {
|
|
if AssetLUX != ([32]byte{}) {
|
|
t.Errorf("AssetLUX must be the all-zero native id, got %x", AssetLUX)
|
|
}
|
|
// a32(0x4c555344_000001): ASCII "LUSD" packed into the low bytes.
|
|
var wantLUSD, wantLETH [32]byte
|
|
binary.BigEndian.PutUint64(wantLUSD[24:32], 0x4c555344_000001)
|
|
binary.BigEndian.PutUint64(wantLETH[24:32], 0x4c455448_000001)
|
|
if AssetLUSD != wantLUSD {
|
|
t.Errorf("AssetLUSD = %x, want %x", AssetLUSD, wantLUSD)
|
|
}
|
|
if AssetLETH != wantLETH {
|
|
t.Errorf("AssetLETH = %x, want %x", AssetLETH, wantLETH)
|
|
}
|
|
// The two non-native ids must be distinct (injective), and distinct from native.
|
|
if AssetLUSD == AssetLETH || AssetLUSD == AssetLUX || AssetLETH == AssetLUX {
|
|
t.Error("asset ids are not injective")
|
|
}
|
|
}
|