mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
orderuser->full 16-byte user is the only settlement identity; 8-byte UserID is a matcher/STP handle only, never a money key. Missing orderuser fails closed (ErrMissingSettlementUser), no matcher-handle fallback. Ownership invariant harness alongside conservation (catches value-conserving theft). txHash-ref custody idempotency closes vault-drain/strand. Reject zero-lock orders; persist orderuser before book insert; restart asserts coverage.
117 lines
5.1 KiB
Go
117 lines
5.1 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dchain
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/luxfi/dex/pkg/lx"
|
|
"github.com/luxfi/dex/pkg/zapwire"
|
|
)
|
|
|
|
// TestUserIDFoldMatchesLx pins the DECOMPLECTED identity split that the custody
|
|
// model rests on:
|
|
//
|
|
// - The matcher's order-book ROW (lx.DEXOrder.UserID) stays the 8-byte STP
|
|
// handle (the frozen GPU ABI). lx.OrderToRow folds the user to the leading 8
|
|
// bytes; that is UNCHANGED and is sufficient for self-trade prevention and
|
|
// price-time rebuild.
|
|
// - The d-chain SETTLEMENT identity (userKey16) is the FULL 16-byte wire user
|
|
// (zapwire.UserSize) — the value balance:/locked: key by. It is byte-identical
|
|
// to the wire's null-padded user[16] field, so the ledger credits the SAME
|
|
// account the orders/deposits name.
|
|
//
|
|
// The two are deliberately DIFFERENT widths: the row models matching, d-chain
|
|
// state models settlement identity. This test pins both so neither silently
|
|
// regresses (a widened row would break the GPU ABI; a narrowed ledger identity
|
|
// would reopen the cross-user drain).
|
|
func TestUserIDFoldMatchesLx(t *testing.T) {
|
|
for _, u := range []string{"", "a", "maker-a", "taker-x-very-long-name-overflow"} {
|
|
// The matcher ROW handle is the 8-byte fold (frozen GPU ABI / STP).
|
|
row := lx.OrderToRow(&lx.Order{ID: 1, User: u, UserID: u, Size: 1, Price: 1, Side: lx.Buy})
|
|
var want8 [8]byte
|
|
copy(want8[:], u)
|
|
if got, want := row.UserID, binary.BigEndian.Uint64(want8[:]); got != want {
|
|
t.Errorf("ROW handle: lx OrderToRow.UserID(%q) = %#x, want 8-byte fold %#x", u, got, want)
|
|
}
|
|
|
|
// The d-chain SETTLEMENT identity is the FULL 16-byte wire user.
|
|
var wantWire [zapwire.UserSize]byte
|
|
copy(wantWire[:], u)
|
|
if got := userKey16(u); got != wantWire {
|
|
t.Errorf("LEDGER identity: userKey16(%q) = %x, want full wire user[16] %x", u, got, wantWire)
|
|
}
|
|
if len(userKey16(u)) != zapwire.UserSize {
|
|
t.Errorf("LEDGER identity width = %d, want %d (zapwire.UserSize)", len(userKey16(u)), zapwire.UserSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestLedgerUserKeyNoCollision is the PERMANENT REGRESSION for CRITICAL-1
|
|
// (cross-user native drain). It tests the REAL production derivation — userKey16
|
|
// folded into balanceLedgerKey — not a test double. Two addresses that SHARE their
|
|
// leading 8 bytes but differ in bytes 8..15 MUST map to DISTINCT ledger accounts,
|
|
// so the second can never read/withdraw the first's balance.
|
|
//
|
|
// FAIL-on-old -> PASS-on-fix: the deleted 8-byte fold (userID8) produced an
|
|
// IDENTICAL uint64 for both addresses, so their balance keys collided — the drain.
|
|
// The 16-byte userKey16 keeps every byte, so the keys differ. The test asserts the
|
|
// keys differ AND demonstrates the old 8-byte fold would have collided.
|
|
func TestLedgerUserKeyNoCollision(t *testing.T) {
|
|
// Two 16-byte users sharing the leading 8 bytes, differing in bytes 8..15 —
|
|
// exactly the red PoC's "colliding address" shape.
|
|
victim := string([]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08})
|
|
attacker := string([]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8})
|
|
|
|
var asset [32]byte // native (ids.Empty) — the asset the PoC drains
|
|
for i := range asset {
|
|
asset[i] = 0
|
|
}
|
|
|
|
vKey := userKey16(victim)
|
|
aKey := userKey16(attacker)
|
|
if vKey == aKey {
|
|
t.Fatalf("userKey16 collided for leading-8-shared users: %x == %x (CRITICAL-1 regressed)", vKey, aKey)
|
|
}
|
|
|
|
vLedger := balanceLedgerKey(prefixBalance, vKey, asset)
|
|
aLedger := balanceLedgerKey(prefixBalance, aKey, asset)
|
|
if bytes.Equal(vLedger, aLedger) {
|
|
t.Fatalf("balance ledger keys collided for distinct users:\n victim = %x\n attacker = %x\n(CRITICAL-1: attacker would read victim's balance)", vLedger, aLedger)
|
|
}
|
|
|
|
// Demonstrate the DELETED 8-byte fold WOULD have collided (the bug we removed):
|
|
// folding either user to its leading 8 bytes yields the SAME uint64, so the
|
|
// pre-fix balance:<user:8> keys were identical.
|
|
old8 := func(u string) uint64 {
|
|
var b [8]byte
|
|
copy(b[:], u)
|
|
return binary.BigEndian.Uint64(b[:])
|
|
}
|
|
if old8(victim) != old8(attacker) {
|
|
t.Fatalf("test setup invalid: the two users do not share leading 8 bytes (old fold %#x != %#x)", old8(victim), old8(attacker))
|
|
}
|
|
}
|
|
|
|
// TestQuoteUnitsCeilCoversFloor proves the lock ceil is always >= the matcher's
|
|
// floor quote for the same (base, price): lock >= spend, so a settle never
|
|
// overspends the lock (the affordability invariant that makes the custody ledger
|
|
// conserving without matcher surgery).
|
|
func TestQuoteUnitsCeilCoversFloor(t *testing.T) {
|
|
for _, base := range []uint64{1, 3, 7, 100, 999} {
|
|
for _, price := range []float64{0.5, 1, 1.5, 5, 101.25, 3.333333} {
|
|
pi := priceToInt(price)
|
|
lock := quoteUnitsCeil(new(big.Int).SetUint64(base), pi)
|
|
// matcher floor: floor(base*priceInt/PriceMultiplier)
|
|
floor := base * uint64(pi) / uint64(lx.PriceMultiplier)
|
|
if lock < floor {
|
|
t.Errorf("ceil lock %d < floor spend %d for base=%d price=%v", lock, floor, base, price)
|
|
}
|
|
}
|
|
}
|
|
}
|