mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
Consensus-correctness fix for the native DEX settlement VM. Root cause of the launch-blocking forks: order size/price were IEEE-754 float64 on the ZAP wire, so every validator re-derived the settlement integer via uint64(float)/int64(float*1e8), whose out-of-range result is implementation-defined per the Go spec and DIFFERS BY CPU ARCHITECTURE (arm64 saturates to 2^64-1; amd64 wraps to 2^63). Two honest validators on different archs then derived different reserves/balances/fills from the SAME ordered block. F1/F3/F5 — exact-integer size/price end to end: - pkg/zapwire: Place/Submit/Fill carry price as uint64 fixed-point (×1e8 = PriceInt) and size as uint64 atomic base units. No float64 on the wire. Removed PutFloat64/ Float64. Decode is exact bytes->uint64 (deterministic on every arch). - pkg/dchain/execute: decode the exact integers, set the authoritative PriceUnits/ SizeUnits lane; the float Price/Size become derived projections only (never a source of a consensus value). Reject price==0/size==0/price>maxPriceUnits. - pkg/dex/persist: the DEXOrder/DEXTrade row stores the EXACT PriceInt (in DEXPrice.Integer) and EXACT base units (Quantity/Remaining); RowToOrder restores the integer lane verbatim (identity — no float round-trip) and Filled=Qty-Remaining. Removed floatToFixedQty/fixedQtyToFloat/floatToFixedPrice (the non-idempotent round(size*1e8)/Q64.64 encoding that clamped at ~1.845e11 and forked on restart). - pkg/dex matcher: cross and level-key on the exact integer PriceUnits. - pkg/dchain/settle: orderLock/floorsToZeroLock take exact integers; removed sizeToUnits/priceToInt float conversions and the dead restoreIntegerLane. F2 — commit the custody ledger into the execution root: - state.ComposeRoot/ExecutionRoot gain a ledgerRoot term (Merkle over balance:/ locked:/orderasset: rows). Validators can no longer finalize agreeing on the book while holding different money. F4 — release the ceil-lock/floor-charge residual on full fill: - decrementMakerReserves now closes a fully-filled maker's reserve and returns the ceil(size*price)-floor residual to the owner (a filled order can never be cancelled), through the same unlock path a cancel uses. No more permanently frozen quote. PROVEN: the two-phase cross-arch harness (arm64 build / amd64 Rosetta verify) now derives byte-identical execRoots AND byte-identical custody ledgers for the identical block (uint64(1e20) still differs at the language level — 0xffff.. vs 0x8000.. — but the exact-integer wire freezes the value in the bytes so no per-validator float->int conversion remains). Red guards flipped to assert the fix; conservation/byzantine/batch-parity/KAT/e2e all green (go test ./... -count=1), go vet clean. OUT OF SCOPE (flagged, NOT fixed here): redteam_unbacked_deposit_test documents a SEPARATE critical finding (F9) — dex_deposit credits an arbitrary self-signed amount with no C-side backing proof, exposed on the public RPC. That needs an access-control/ custody-model decision, not a mechanical fix, and is left for a follow-up. Co-authored-by: Hanzo Dev <[email protected]>
94 lines
4.2 KiB
Go
94 lines
4.2 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
//
|
|
// redteam_restart_refutation_test.go — RED TEAM verification of the "restart forks
|
|
// execRoot" hypothesis at the REAL VM consensus boundary (not the persist-layer
|
|
// helper functions in isolation).
|
|
//
|
|
// The hypothesis (from the persist.go floatToFixedQty non-idempotence): a resting
|
|
// order with a fractional wire size whose round(size*1e8) drifts by 1 on the
|
|
// fixedQtyToFloat reload makes a RESTARTED validator derive a different bookRoot /
|
|
// execRoot than a non-restarted one, forking the set.
|
|
//
|
|
// This test REFUTES that at the integration level. Block.execute rebuilds every
|
|
// touched market's book FROM THE COMMITTED DB ROWS via rebuildBookFromDB on EVERY
|
|
// block (block.go: "the SAME function used at startup ... and during execution"),
|
|
// on every node, restart or not. So the in-RAM wire float only reaches bookRoot in
|
|
// the order's PLACE block (which all nodes process from identical tx bytes); every
|
|
// subsequent block re-derives the same reloaded float on all nodes. A restarted
|
|
// validator therefore VERIFIES a continuing proposer's post-place block cleanly —
|
|
// no root divergence, no fork.
|
|
package dchain
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/dex/pkg/zapwire"
|
|
)
|
|
|
|
func TestRedteam_RestartDoesNotForkExecRoot(t *testing.T) {
|
|
db := memdb.New()
|
|
vm1, _ := newTestVM(t, db) // the CONTINUING (never-restarted) proposer
|
|
defer vm1.Shutdown(context.Background())
|
|
|
|
const seller = "seller"
|
|
pool := [32]byte{0xf0, 0x2c} // "fork?"
|
|
|
|
// The exact wire size whose 1e8-fixed image is non-idempotent under reload
|
|
// (Q=4159461352541877 -> reload -> Q'=4159461352541878). If ANYTHING forks on
|
|
// restart, this size triggers it.
|
|
wireSize := math.Float64frombits(0x4183d577ac340ec1) // ~41,594,613.525419
|
|
|
|
// Fund + open market + rest a SELL of the fractional size (locks base=uint64(size)).
|
|
addBlock(t, vm1,
|
|
depositTx(t, seller, assetLUX, 50_000_000),
|
|
openMarketTx(t, pool, assetLUX, assetLUSD),
|
|
)
|
|
addBlock(t, vm1, placePoolTx(t, pool, zapwire.SideSell, 2.5, wireSize, seller))
|
|
rootAtPlace := vm1.lastRoot // committed accepted root through the place block (height Hp)
|
|
|
|
// RESTART: a fresh VM re-Initializes over the SAME committed db. It rebuilds
|
|
// every book from the durable rows (RowToOrder -> reloaded float S').
|
|
vm2, _ := newTestVM(t, db)
|
|
defer vm2.Shutdown(context.Background())
|
|
|
|
// A restart must preserve the accepted execution root exactly.
|
|
if vm2.lastRoot != rootAtPlace {
|
|
t.Fatalf("RESTART CHANGED ACCEPTED ROOT: continuing=%x restarted=%x", rootAtPlace[:8], vm2.lastRoot[:8])
|
|
}
|
|
|
|
// The CONTINUING proposer (vm1) builds height Hp+1: a benign second place in the
|
|
// same market (touches the market => bookRoot includes the resting fractional
|
|
// SELL, re-derived from the committed row). Build WITHOUT accepting so the shared
|
|
// db stays at Hp for the restarted verifier.
|
|
ctx := context.Background()
|
|
vm1.mempool.Add(placePoolTx(t, pool, zapwire.SideBuy, 1.0, 1.0, seller))
|
|
built, err := vm1.BuildBlock(ctx)
|
|
if err != nil {
|
|
t.Fatalf("continuing proposer BuildBlock: %v", err)
|
|
}
|
|
proposerRoot := built.(*Block).execRoot
|
|
|
|
// The RESTARTED validator (vm2) independently re-derives and verifies the SAME
|
|
// block bytes. Verify recomputes the execRoot from vm2's restarted view and
|
|
// checks it against the proposer's claim (block.go:188 result.root != execRoot).
|
|
reparsed, err := vm2.ParseBlock(ctx, built.Bytes())
|
|
if err != nil {
|
|
t.Fatalf("restarted validator ParseBlock: %v", err)
|
|
}
|
|
if err := reparsed.Verify(ctx); err != nil {
|
|
t.Fatalf("CONSENSUS FORK CONFIRMED: restarted validator REJECTED the continuing proposer's "+
|
|
"post-place block (root divergence): %v", err)
|
|
}
|
|
if got := reparsed.(*Block).execRoot; got != proposerRoot {
|
|
t.Fatalf("root divergence continuing=%x restarted=%x", proposerRoot[:8], got[:8])
|
|
}
|
|
t.Logf("REFUTED: restarted validator re-derived the identical execRoot %x for the post-place "+
|
|
"block and Verify passed — no fork. execute() rebuilds the book from committed DB rows every "+
|
|
"block, so the wire-float only reaches the root in the place block (processed identically by all).",
|
|
proposerRoot[:8])
|
|
}
|