0x9999 is AlwaysOn from genesis and writes NO code marker (github.com/luxfi/ precompile/dex: DEXFill is THE native-CLOB settlement signal). The old step-3 eth_getCode==0x01 proxy was a false invariant — mainnet carries a legacy 0x01 with zero fills, a fresh-genesis net has empty code with real fills. Step 3 is now informational; step 4 (a DEXFill was emitted) is the hard settlement gate. Proven: testnet (96368) TestDEXBootSpec GREEN — 65 DEXFills, 3 validators converge on block 360 hash 0x7d5a66b1…84eb.
287 lines
9.5 KiB
Go
287 lines
9.5 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package dex is the deterministic DEX boot verification GATE for the Lux
|
|
// network. It is the read-path half of the boot/reset/upgrade harness: given one
|
|
// or more C-Chain JSON-RPC endpoints (LUX_RPC, comma-separated — one per
|
|
// validator), it asserts the locked-in spec is live:
|
|
//
|
|
// 1. chainId matches the expected EVM chain id (LUX_CHAIN_ID),
|
|
// 2. the C-Chain has booted past genesis (block height > 0) and is producing
|
|
// blocks,
|
|
// 3. the 0x9999 DEX settlement precompile is ACTIVE and has settled at least
|
|
// once (eth_getCode == 0x01 — the vault marker written on first settlement),
|
|
// 4. a DEXFill has been emitted by 0x9999 (the DEX has actually traded), and
|
|
// 5. every endpoint converges on an identical block hash at a common height
|
|
// (all validators agree — the determinism proof).
|
|
//
|
|
// It is dependency-free (stdlib JSON-RPC) so it runs against ANY target: a fresh
|
|
// disposable localnet booted by boot.sh (after `trader prove` writes a fill), an
|
|
// in-cluster throwaway namespace, or — for spec verification — the live
|
|
// v1.31.2 nets. The destructive deploy/open/fill leg is the `trader prove`
|
|
// binary, invoked by boot.sh; this file never writes to the chain.
|
|
//
|
|
// Skipped (not failed) when LUX_RPC is unset, so it is CI-safe.
|
|
package dex
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// dexPoolManager is the 0x9999 V4 settlement precompile, identical on every
|
|
// Lux EVM chain (github.com/luxfi/precompile/dex DEXPoolManagerAddress).
|
|
dexPoolManager = "0x0000000000000000000000000000000000009999"
|
|
|
|
// dexActiveMarker is eth_getCode(0x9999) once the precompile has settled at
|
|
// least once — the one-byte vault marker. Empty ("0x") before first fill.
|
|
dexActiveMarker = "0x01"
|
|
|
|
// dexFillTopic = keccak256("DEXFill(bytes32,address,uint256,uint256)") — the
|
|
// canonical settlement signal emitted by 0x9999.
|
|
dexFillTopic = "0x6f744d074efc4fa8512636853f8a4c67842f230d4279421c7599dc5c5daf9874"
|
|
)
|
|
|
|
// endpoints returns the LUX_RPC validator endpoints, or skips the test.
|
|
func endpoints(t *testing.T) []string {
|
|
t.Helper()
|
|
raw := os.Getenv("LUX_RPC")
|
|
if strings.TrimSpace(raw) == "" {
|
|
t.Skip("LUX_RPC unset — set it to one or more comma-separated C-Chain RPC " +
|
|
"endpoints (one per validator) to run the DEX boot gate")
|
|
}
|
|
var out []string
|
|
for _, u := range strings.Split(raw, ",") {
|
|
if u = strings.TrimSpace(u); u != "" {
|
|
out = append(out, u)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// rpc is a minimal eth JSON-RPC client over one endpoint.
|
|
type rpc struct {
|
|
url string
|
|
c *http.Client
|
|
}
|
|
|
|
func dial(url string) *rpc { return &rpc{url: url, c: &http.Client{Timeout: 20 * time.Second}} }
|
|
|
|
func (r *rpc) call(method string, params ...any) (json.RawMessage, error) {
|
|
if params == nil {
|
|
params = []any{}
|
|
}
|
|
body, _ := json.Marshal(map[string]any{
|
|
"jsonrpc": "2.0", "id": 1, "method": method, "params": params,
|
|
})
|
|
resp, err := r.c.Post(r.url, "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", method, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
var out struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error *struct {
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
return nil, fmt.Errorf("%s: decode: %w", method, err)
|
|
}
|
|
if out.Error != nil {
|
|
return nil, fmt.Errorf("%s: rpc error: %s", method, out.Error.Message)
|
|
}
|
|
return out.Result, nil
|
|
}
|
|
|
|
// hexString decodes a JSON-RPC quoted string result (e.g. "0x1f").
|
|
func (r *rpc) hexString(method string, params ...any) (string, error) {
|
|
raw, err := r.call(method, params...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(raw, &s); err != nil {
|
|
return "", fmt.Errorf("%s: not a string: %w", method, err)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// hexUint decodes a 0x-quantity result.
|
|
func (r *rpc) hexUint(method string, params ...any) (uint64, error) {
|
|
s, err := r.hexString(method, params...)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
n, ok := new(big.Int).SetString(strings.TrimPrefix(s, "0x"), 16)
|
|
if !ok {
|
|
return 0, fmt.Errorf("%s: bad hex %q", method, s)
|
|
}
|
|
return n.Uint64(), nil
|
|
}
|
|
|
|
func (r *rpc) chainID() (uint64, error) { return r.hexUint("eth_chainId") }
|
|
func (r *rpc) blockNumber() (uint64, error) { return r.hexUint("eth_blockNumber") }
|
|
|
|
func (r *rpc) code(addr string) (string, error) {
|
|
return r.hexString("eth_getCode", addr, "latest")
|
|
}
|
|
|
|
func (r *rpc) blockHash(height uint64) (string, error) {
|
|
raw, err := r.call("eth_getBlockByNumber", fmt.Sprintf("0x%x", height), false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var blk struct {
|
|
Hash string `json:"hash"`
|
|
}
|
|
if err := json.Unmarshal(raw, &blk); err != nil {
|
|
return "", fmt.Errorf("getBlockByNumber: %w", err)
|
|
}
|
|
return blk.Hash, nil
|
|
}
|
|
|
|
// dexFills counts DEXFill logs emitted by 0x9999 over [from, latest].
|
|
func (r *rpc) dexFills(from uint64) (int, error) {
|
|
raw, err := r.call("eth_getLogs", map[string]any{
|
|
"address": dexPoolManager,
|
|
"topics": []any{dexFillTopic},
|
|
"fromBlock": fmt.Sprintf("0x%x", from),
|
|
"toBlock": "latest",
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var logs []json.RawMessage
|
|
if err := json.Unmarshal(raw, &logs); err != nil {
|
|
return 0, fmt.Errorf("getLogs: %w", err)
|
|
}
|
|
return len(logs), nil
|
|
}
|
|
|
|
// TestDEXBootSpec is the boot gate. Green == a known-good, taggable spec version.
|
|
func TestDEXBootSpec(t *testing.T) {
|
|
eps := endpoints(t)
|
|
primary := dial(eps[0])
|
|
|
|
// 1) chainId.
|
|
cid, err := primary.chainID()
|
|
if err != nil {
|
|
t.Fatalf("eth_chainId on %s: %v", eps[0], err)
|
|
}
|
|
if want := os.Getenv("LUX_CHAIN_ID"); want != "" {
|
|
wc, ok := new(big.Int).SetString(want, 10)
|
|
if !ok {
|
|
t.Fatalf("LUX_CHAIN_ID=%q is not decimal", want)
|
|
}
|
|
if wc.Uint64() != cid {
|
|
t.Fatalf("chainId = %d, want %s", cid, want)
|
|
}
|
|
}
|
|
t.Logf("chainId = %d ✓", cid)
|
|
|
|
// 2) booted past genesis + producing blocks.
|
|
h0, err := primary.blockNumber()
|
|
if err != nil {
|
|
t.Fatalf("eth_blockNumber: %v", err)
|
|
}
|
|
if h0 == 0 {
|
|
t.Fatalf("C-Chain at block 0 — not booted past genesis (no blocks built)")
|
|
}
|
|
t.Logf("C-Chain height = %d (booted past genesis) ✓", h0)
|
|
|
|
// 3) 0x9999 is AlwaysOn from genesis. Per github.com/luxfi/precompile/dex the
|
|
// settlement precompile is active from genesis and writes NO code marker —
|
|
// eth_getCode is informational only (a legacy net may carry a stray 0x01, a
|
|
// fresh-genesis net is empty; neither is the settlement signal). The canonical
|
|
// "has settled / has traded" proof is the DEXFill log asserted in (4):
|
|
// DEXFill is THE indexable native-CLOB settlement signal the precompile emits
|
|
// on every filled swap (dex/events.go, dex/swap_sync.go).
|
|
codeHex, _ := primary.code(dexPoolManager)
|
|
t.Logf("0x9999 AlwaysOn from genesis; eth_getCode=%q (informational, not the settlement signal) ✓", codeHex)
|
|
|
|
// 4) a DEXFill was emitted (the DEX actually traded) — the canonical
|
|
// settlement proof. Query window is [LUX_FROM_BLOCK, latest]; defaults to
|
|
// the whole chain. A public/load-balanced node may cap the getLogs range —
|
|
// if the query ERRORS we surface a NOTE (use a direct validator endpoint);
|
|
// but zero fills over a served range is a hard FAIL: the money path is
|
|
// unproven on a net that has never settled a trade.
|
|
from := uint64(0)
|
|
if f := os.Getenv("LUX_FROM_BLOCK"); f != "" {
|
|
if v, ok := new(big.Int).SetString(f, 10); ok {
|
|
from = v.Uint64()
|
|
}
|
|
}
|
|
n, err := primary.dexFills(from)
|
|
switch {
|
|
case err != nil:
|
|
if os.Getenv("LUX_REQUIRE_FILL") == "1" {
|
|
t.Fatalf("eth_getLogs(DEXFill) from %d: %v", from, err)
|
|
}
|
|
t.Logf("NOTE: DEXFill log query over [%d,latest] errored (%v) — likely a "+
|
|
"public getLogs range cap. Point LUX_RPC at a validator endpoint (no cap).", from, err)
|
|
case n == 0:
|
|
t.Fatalf("zero DEXFill logs over [%d,latest] — 0x9999 has not settled a "+
|
|
"trade on this net (the DEX has not traded); DEXFill is the canonical "+
|
|
"native-CLOB settlement signal, a coherent DEX net must have ≥1", from)
|
|
default:
|
|
t.Logf("DEXFill logs over [%d,latest] = %d (DEX has traded) ✓", from, n)
|
|
}
|
|
|
|
// 5) convergence: every validator endpoint agrees on the block hash at a
|
|
// common settled height. This is the determinism proof for a multi-node
|
|
// boot. Single endpoint (live ingress) trivially converges.
|
|
if len(eps) > 1 {
|
|
assertConvergence(t, eps)
|
|
} else {
|
|
t.Logf("single endpoint — convergence check is for multi-validator targets")
|
|
}
|
|
|
|
t.Logf("DEX is trading ✓ — spec on %s is a known-good, taggable boot", eps[0])
|
|
}
|
|
|
|
// assertConvergence picks a settled height present on all endpoints and asserts
|
|
// they report an identical block hash there.
|
|
func assertConvergence(t *testing.T, eps []string) {
|
|
t.Helper()
|
|
minH := ^uint64(0)
|
|
for _, ep := range eps {
|
|
h, err := dial(ep).blockNumber()
|
|
if err != nil {
|
|
t.Fatalf("blockNumber on %s: %v", ep, err)
|
|
}
|
|
if h < minH {
|
|
minH = h
|
|
}
|
|
}
|
|
if minH < 3 {
|
|
t.Fatalf("min height %d too low to pick a settled common block", minH)
|
|
}
|
|
target := minH - 2 // a height every node has accepted
|
|
|
|
var want string
|
|
for i, ep := range eps {
|
|
hash, err := dial(ep).blockHash(target)
|
|
if err != nil {
|
|
t.Fatalf("blockHash(%d) on %s: %v", target, ep, err)
|
|
}
|
|
if i == 0 {
|
|
want = hash
|
|
continue
|
|
}
|
|
if hash != want {
|
|
t.Fatalf("DIVERGENCE at block %d: %s reports %s, %s reports %s",
|
|
target, eps[0], want, ep, hash)
|
|
}
|
|
}
|
|
t.Logf("all %d validators converge on block %d hash %s ✓", len(eps), target, want)
|
|
}
|