mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
dex: standalone D-Chain VM (pkg/dchain) + persist + venue cmd/dchain
block.ChainVM on luxfi/consensus via luxfi/vm/rpc.Serve: matcher-at-Verify on a versiondb overlay -> zapdb commit at Accept; book rebuilt from order:* rows on restart (byte-identical). pkg/lx/persist.go = DEXOrder<->OrderBook bridge (Q64.64 fixed-point rows, never float). execution_root reuses xvm crypto/merkle RFC-6962. clob_* handlers return consensus-computed fills (txID idempotency). cmd/dchain (//go:build dchain&&cgo) boots via rpc.Serve. VM tests + localnet venue e2e (seed->cross->settle->restart->re-cross) green; race-clean.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build dchain && cgo
|
||||
// +build dchain,cgo
|
||||
|
||||
// Command dchain is the standalone D-Chain DEX VM plugin binary. It is the
|
||||
// venue entrypoint: a ZAP plugin process the Lux node launches (like the EVM
|
||||
// plugin), running the lx.OrderBook matcher inside consensus and persisting the
|
||||
// authoritative order book to its own zapdb. Built under //go:build dchain &&
|
||||
// cgo so it links the cgo/GPU matching accelerator; CI builds it per org+arch.
|
||||
//
|
||||
// The testable VM logic (pkg/dchain) is pure-Go and proven without this binary;
|
||||
// this file only wires the proven VM into the rpc.Serve plugin harness.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/dex/cmd/dchain/runner"
|
||||
"github.com/luxfi/dex/pkg/dchain"
|
||||
"github.com/luxfi/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
versionString := fmt.Sprintf("Lux-DChain/%s [node=%s, rpcchainvm=%d]",
|
||||
dchain.Version, version.Current, version.RPCChainVMProtocol)
|
||||
runner.Run(versionString)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build dchain && cgo
|
||||
// +build dchain,cgo
|
||||
|
||||
// Package runner boots the standalone D-Chain DEX VM as a ZAP plugin process,
|
||||
// mirroring github.com/luxfi/evm/plugin/runner. It is the //go:build dchain &&
|
||||
// cgo VENUE entrypoint: the binary links the cgo/GPU matching engine (the venue
|
||||
// accelerator) alongside the pure-Go consensus VM, so it is built and shipped by
|
||||
// CI for each org+arch, never linked in the pure-Go test path.
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/luxfi/dex/pkg/dchain"
|
||||
log "github.com/luxfi/log"
|
||||
"github.com/luxfi/vm/rpc"
|
||||
)
|
||||
|
||||
// Run boots the d-chain VM over the ZAP plugin transport. rpc.Serve connects to
|
||||
// the node runtime named in the runtime.EngineAddressKey env var, opens this
|
||||
// chain's own badgerdb/zapdb at ChainDataDir/db, hands it to VM.Initialize as
|
||||
// init.DB, and serves block requests — the exact harness lux/evm boots through.
|
||||
// It blocks until the runtime shuts the VM down. versionStr, when non-empty and
|
||||
// the process was invoked with the version flag, is printed and the process
|
||||
// exits 0 (the node probes plugin versions this way).
|
||||
func Run(versionStr string) {
|
||||
if printVersion() && versionStr != "" {
|
||||
fmt.Println(versionStr)
|
||||
os.Exit(0)
|
||||
}
|
||||
if err := rpc.Serve(context.Background(), log.Root(), &dchain.VM{}); err != nil {
|
||||
fmt.Printf("dchain rpc.Serve error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// printVersion reports whether the process was asked to just print its version
|
||||
// and exit. The node passes a single "version" arg when probing a plugin.
|
||||
func printVersion() bool {
|
||||
for _, a := range os.Args[1:] {
|
||||
if a == "version" || a == "--version" || a == "-version" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -5,14 +5,18 @@ go 1.26.4
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/luxfi/accel v1.1.9
|
||||
github.com/luxfi/crypto v1.19.17
|
||||
github.com/luxfi/accel v1.2.4
|
||||
github.com/luxfi/consensus v1.25.17
|
||||
github.com/luxfi/crypto v1.19.21
|
||||
github.com/luxfi/czmq/v4 v4.2.2
|
||||
github.com/luxfi/database v1.19.0
|
||||
github.com/luxfi/geth v1.16.98
|
||||
github.com/luxfi/ids v1.2.15
|
||||
github.com/luxfi/log v1.4.3
|
||||
github.com/luxfi/pq v1.0.3
|
||||
github.com/luxfi/rpc v1.1.0
|
||||
github.com/luxfi/version v1.0.1
|
||||
github.com/luxfi/vm v1.2.3
|
||||
github.com/nats-io/nats.go v1.52.0
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -21,48 +25,105 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
github.com/DataDog/zstd v1.5.7 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cockroachdb/errors v1.12.0 // indirect
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
|
||||
github.com/cockroachdb/pebble v1.1.5 // indirect
|
||||
github.com/cockroachdb/redact v1.1.8 // indirect
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
|
||||
github.com/consensys/gnark-crypto v0.20.1 // indirect
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.9.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect
|
||||
github.com/getsentry/sentry-go v0.44.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/luxfi/age v1.5.0 // indirect
|
||||
github.com/luxfi/api v1.0.12 // indirect
|
||||
github.com/luxfi/atomic v1.0.0 // indirect
|
||||
github.com/luxfi/cache v1.2.1 // indirect
|
||||
github.com/luxfi/codec v1.1.4 // indirect
|
||||
github.com/luxfi/compress v0.0.5 // indirect
|
||||
github.com/luxfi/concurrent v0.0.3 // indirect
|
||||
github.com/luxfi/constants v1.5.8 // indirect
|
||||
github.com/luxfi/container v0.0.4 // indirect
|
||||
github.com/luxfi/crypto/ipa v1.2.4 // indirect
|
||||
github.com/luxfi/ids v1.2.13 // indirect
|
||||
github.com/luxfi/filesystem v0.0.1 // indirect
|
||||
github.com/luxfi/math v1.4.1 // indirect
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/mdns v0.1.1 // indirect
|
||||
github.com/luxfi/metric v1.5.8 // indirect
|
||||
github.com/luxfi/mock v0.1.1 // indirect
|
||||
github.com/luxfi/zap v0.7.0 // indirect
|
||||
github.com/luxfi/p2p v1.21.1 // indirect
|
||||
github.com/luxfi/proto v1.3.4 // indirect
|
||||
github.com/luxfi/resource v0.0.1 // indirect
|
||||
github.com/luxfi/runtime v1.1.1 // indirect
|
||||
github.com/luxfi/sampler v1.1.0 // indirect
|
||||
github.com/luxfi/upgrade v1.0.1 // indirect
|
||||
github.com/luxfi/utils v1.2.0 // indirect
|
||||
github.com/luxfi/validators v1.2.0 // indirect
|
||||
github.com/luxfi/warp v1.18.6 // indirect
|
||||
github.com/luxfi/zap v0.7.2 // indirect
|
||||
github.com/luxfi/zapdb v1.10.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.100 // indirect
|
||||
github.com/mr-tron/base58 v1.3.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/supranational/blst v0.3.16 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
@@ -71,6 +132,7 @@ require (
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
gonum.org/v1/gonum v0.17.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||
github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM=
|
||||
@@ -10,12 +12,19 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
|
||||
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
|
||||
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A=
|
||||
@@ -34,6 +43,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc=
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||
@@ -46,6 +58,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/dot v1.11.0 h1:zsrhCuFHAJge/aZIC4N4LdHy5tqYu4tWEaUzIwdYj4Y=
|
||||
@@ -56,10 +70,18 @@ github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJ
|
||||
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
|
||||
github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8=
|
||||
github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/getsentry/sentry-go v0.44.1 h1:/cPtrA5qB7uMRrhgSn9TYtcEF36auGP3Y6+ThvD/yaI=
|
||||
github.com/getsentry/sentry-go v0.44.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
@@ -67,20 +89,36 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
|
||||
@@ -97,12 +135,17 @@ github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZ
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
|
||||
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
|
||||
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
@@ -115,10 +158,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/luxfi/accel v1.1.9 h1:Tsk6gXj2uKE19501bD0ajRYdeCHIlTGb6jYyLc+F8hc=
|
||||
github.com/luxfi/accel v1.1.9/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
|
||||
github.com/luxfi/age v1.4.0 h1:tU5q65RQSQdaVq64Z/DVUhiPQMoMPQT6pr41LsvG0AQ=
|
||||
github.com/luxfi/age v1.4.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
|
||||
github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
|
||||
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
|
||||
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
|
||||
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
|
||||
github.com/luxfi/age v1.5.0 h1:G69HbSV4R3vKEH9B0CulnRaMdSdf4RalMgP8xKmxHeI=
|
||||
github.com/luxfi/age v1.5.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
|
||||
github.com/luxfi/api v1.0.12 h1:HnLfoowXDZ2oGGB+40CHlmQ0XET5I9I828EVeYixlXc=
|
||||
github.com/luxfi/api v1.0.12/go.mod h1:q3Hh3mmkvp3cnv3aqe2+gCtmmSAL/uFjuzrqGavQLNA=
|
||||
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
|
||||
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
|
||||
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
|
||||
@@ -129,28 +176,34 @@ github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
|
||||
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
|
||||
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
|
||||
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
|
||||
github.com/luxfi/consensus v1.22.84 h1:fKw4n7fY+lCWW5HSx/ciGeRSxv4knODQGWh6lBWAveQ=
|
||||
github.com/luxfi/consensus v1.22.84/go.mod h1:y6y+bVjvpDLkkTBCCKLcch6R4PRW8EA+lTfr+6sB6F8=
|
||||
github.com/luxfi/constants v1.4.7 h1:e/Qs+DQP3pugle3Zncq6fZCxKgqqtbyD/z7Gm4ZjsYg=
|
||||
github.com/luxfi/constants v1.4.7/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
|
||||
github.com/luxfi/consensus v1.25.17 h1:4EvtruOZSiInByIN9miha0SHlxGNlI5S59lhE1xa3LU=
|
||||
github.com/luxfi/consensus v1.25.17/go.mod h1:lvyDsuqWSDxwc0keI84PccZi8sRFu5zn+LAcPt79oIg=
|
||||
github.com/luxfi/constants v1.5.8 h1:iNP9AWNUcM4Tps7jYnx49CwtCWAC9mYRxJfGou2za0g=
|
||||
github.com/luxfi/constants v1.5.8/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
|
||||
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
|
||||
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
|
||||
github.com/luxfi/corona v0.4.1 h1:DGMdZMrENl6vmJf9CgiQMD8c5ORSTMymL0z9KgcZstk=
|
||||
github.com/luxfi/corona v0.4.1/go.mod h1:4aD7+ZqnlZ2aVuU/DBQ5aspIagv5ux45LW2sJ4+siY8=
|
||||
github.com/luxfi/crypto v1.19.17 h1:l2LLu7UFyICtJVfraLDLRi+lFGiDXKHSL18M9/m1gsQ=
|
||||
github.com/luxfi/crypto v1.19.17/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
|
||||
github.com/luxfi/corona v0.7.6 h1:CJP6smygD55dL0HHkKkWryL9H24a+wXvs+L+WchK7Nc=
|
||||
github.com/luxfi/corona v0.7.6/go.mod h1:4aD7+ZqnlZ2aVuU/DBQ5aspIagv5ux45LW2sJ4+siY8=
|
||||
github.com/luxfi/crypto v1.19.21 h1:x7s/Yy1BYMv5sbbWsZk+mUU007Z0HX/Ta4/RkrNHiw0=
|
||||
github.com/luxfi/crypto v1.19.21/go.mod h1:0tfz+EbAjsW1QBWB0cte9kdjB5XhhYFmCr8BkZRux48=
|
||||
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
|
||||
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
|
||||
github.com/luxfi/czmq/v4 v4.2.2 h1:D4QDl99OfGoi8Gk3RI8GZ6pqt+040rolj+h36scSj2M=
|
||||
github.com/luxfi/czmq/v4 v4.2.2/go.mod h1:WFY9ldUbeU900P8bcIRr7Jim23veOhJL5A5zSpTogdU=
|
||||
github.com/luxfi/database v1.19.0 h1:sySH5eS8ihJtyc+yb0LgTignGwJ9H4W9bv+nboCuIm0=
|
||||
github.com/luxfi/database v1.19.0/go.mod h1:o1ccjgVKMYbVBfi+6+sDvx4swGFgGnCGFQ4gL/oJ+kY=
|
||||
github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwEY=
|
||||
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
|
||||
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
|
||||
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
|
||||
github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQVXyNHA=
|
||||
github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
|
||||
github.com/luxfi/ids v1.2.13 h1:lEotq0WUpMLcc/5X5MbE3n73jdfNo8IlFvTDfUXg+9A=
|
||||
github.com/luxfi/ids v1.2.13/go.mod h1:QWjJvggX/nRkmrz1AGSaF2Y6mnOvz3g7lFOzs3Zd+JM=
|
||||
github.com/luxfi/lattice/v7 v7.1.0 h1:mr3HvN6olNTS2LT/xAW/JBhTqfvpsGmsopDMeR7BSJs=
|
||||
github.com/luxfi/lattice/v7 v7.1.0/go.mod h1:IaaUN+3ysnBG4BA8ILRYG0j80+qtYDP4C5lkaDb2pDE=
|
||||
github.com/luxfi/ids v1.2.15 h1:omE+E4+0Poj9DzM11ejSFgteaSQ3KDHi5g54iH6jcxI=
|
||||
github.com/luxfi/ids v1.2.15/go.mod h1:Fj73K5xcblvdE0SxU/ip+jE8VqNdu+80548su5KJ7xI=
|
||||
github.com/luxfi/keychain v1.0.2 h1:uQgmjs37/VBIALEiYrrszTpxvtqr07/YvS9TnmxGafs=
|
||||
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
|
||||
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
|
||||
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
|
||||
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
|
||||
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
|
||||
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
|
||||
@@ -163,32 +216,44 @@ github.com/luxfi/metric v1.5.8 h1:axPwfq+erOlIue7IJp5g+hMcMtVhYHja9gJAaT3+KNA=
|
||||
github.com/luxfi/metric v1.5.8/go.mod h1:fO2giazkg4NDtr72JM/QXJBYebplAMeWC1JoZyNDvKw=
|
||||
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
|
||||
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
|
||||
github.com/luxfi/p2p v1.19.2 h1:uqZq7ofmEDbXlTkv1QThtci01Q+dmDkNmAPeORIyP8E=
|
||||
github.com/luxfi/p2p v1.19.2/go.mod h1:tI9Bt1R0ouvVtJvXG4e20GlGeV4AR230k4mFF9Vglzk=
|
||||
github.com/luxfi/p2p v1.21.1 h1:gmz1JMDhzHIL3dQlhwIDvR4OlFuhNVfnWUl/ipYhAIo=
|
||||
github.com/luxfi/p2p v1.21.1/go.mod h1:SsNPR5fPGWWNem9plGWhSmRqyDoysJ3kPAN0zG0g3iw=
|
||||
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
|
||||
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
|
||||
github.com/luxfi/precompile v0.5.27 h1:lEF5gXY8ny5xXp8Qg8PK0Y2RQlaRNOZeDU+wP6Sj24M=
|
||||
github.com/luxfi/precompile v0.5.27/go.mod h1:boKRJeoa4XEhCoylRnPI/0IGfBgm5bZnFNI9llDktWo=
|
||||
github.com/luxfi/precompile v0.5.37 h1:2v0zTZtU3cP/hlCA1602Bz//mK+9jjUkYCBsc3KU67w=
|
||||
github.com/luxfi/precompile v0.5.37/go.mod h1:z1ZLWPKPdZXqIQZOSdVObM8nTIxrblP6a+fqf3O7OHs=
|
||||
github.com/luxfi/proto v1.3.4 h1:dNrdQ/414Pe9otIekVUmQ9QLqemaMFK43H4N3DIbuzQ=
|
||||
github.com/luxfi/proto v1.3.4/go.mod h1:S80KrTzISCktptit/pWLzk6nHgagjJS86qbQL6gHutE=
|
||||
github.com/luxfi/resource v0.0.1 h1:mTh+ICWSy548GTUSSyx7V/X5dV18oEwxZeQEYGJQhD4=
|
||||
github.com/luxfi/resource v0.0.1/go.mod h1:wWpZktciYwIi6RNqA+fHwzmPrUJa7PRX7urfwT+spRE=
|
||||
github.com/luxfi/rpc v1.1.0 h1:B/PJbK399th1mHRDSufhCpVbAciZqId3LsaWhIGNWH4=
|
||||
github.com/luxfi/rpc v1.1.0/go.mod h1:s0bI7/Wg1ZdFdG/cQK+4pZNdEmUsXNBA3HeZRZ+XLeM=
|
||||
github.com/luxfi/runtime v1.0.1 h1:cii3OsRiVSIl9jzfWFM6++62T1r6ZSGP+/3F0Ezhpqw=
|
||||
github.com/luxfi/runtime v1.0.1/go.mod h1:2hBKjzbEeE4dzrhUKH8dqkRgLEyiXz6GmuVusy3vJMs=
|
||||
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
|
||||
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
|
||||
github.com/luxfi/utils v1.1.4 h1:8OY0jXCkpp6OotN1Y++6DATJkvtEwzq2k0p56imJlAk=
|
||||
github.com/luxfi/utils v1.1.4/go.mod h1:c3yz1RjzrB+cs5GZm+q1T3/2cCKElO9vxm9yRRtgSEM=
|
||||
github.com/luxfi/validators v1.0.0 h1:zE1HJM1lfvC+v1Lalg+MUgkKBGFWnwTfOOmXraiNnpE=
|
||||
github.com/luxfi/validators v1.0.0/go.mod h1:QGppjtI/gqprbplWc10J+4OIK8UYWpv+53mfH2aKhy4=
|
||||
github.com/luxfi/runtime v1.1.1 h1:vOMe82PL3bpSbslS7p69dKRpbr2qW2vOOAwej3UkkmU=
|
||||
github.com/luxfi/runtime v1.1.1/go.mod h1:fmG6+Zxj4wSNlXwiUfDthQDY+SfxVu6S0fX2lL6VbrE=
|
||||
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
|
||||
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
|
||||
github.com/luxfi/timer v1.0.2 h1:g/odi0VQJIsrzdklJUG1thHZ/sGNnbIiVGcU6LctJm0=
|
||||
github.com/luxfi/timer v1.0.2/go.mod h1:SoaZwntYigUE3H6z1GV32YwP8QaSiAT0UiEv7iPugXg=
|
||||
github.com/luxfi/upgrade v1.0.1 h1:7+ygYeUf/MuLeGL7pjIu6ckQimxctCp+Swybhpy64go=
|
||||
github.com/luxfi/upgrade v1.0.1/go.mod h1:Re7g9Y+SYf/LvkHFpN0vbtlVH/Rr5ZpHQdPeVFEo3Jw=
|
||||
github.com/luxfi/utils v1.2.0 h1:gtEiI7/NM6PQ/OasEpH0PvB+e5hIS/tpum9r64pYjMc=
|
||||
github.com/luxfi/utils v1.2.0/go.mod h1:T2OCKT1xG9jtKR/gyJQoSkticzrE9WFQ8eohJHGu9Fg=
|
||||
github.com/luxfi/utxo v0.3.7 h1:JlQ0F0u/QazHcgRK8CRu1mdJOyA+oGAlRMNoAu0/HpU=
|
||||
github.com/luxfi/utxo v0.3.7/go.mod h1:dbJ7RHU8qj5ttobGYK/A2PsZIQpCsHAIay6xKwc8YQ8=
|
||||
github.com/luxfi/validators v1.2.0 h1:VygpiBqBAdGrfkb7xzE2yrVmnXaqE+hm8FLWdGXO7G8=
|
||||
github.com/luxfi/validators v1.2.0/go.mod h1:GYLulrNXAan23ZlX7sgWVbVnLpUexeB/m2qr2ymsXok=
|
||||
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
|
||||
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
|
||||
github.com/luxfi/vm v1.0.40 h1:kA0V/9p1VdC88EjgbljWb8t+fl9qz5bCyF3IxnV20yg=
|
||||
github.com/luxfi/vm v1.0.40/go.mod h1:Y5WUKhT76PR6HnbUykHBWI2f5sjssrm5BL6/o7b6IfQ=
|
||||
github.com/luxfi/warp v1.18.5 h1:yXFCT+lnvzJHs8nVvfUCQ9wNvhtgbDGaXJkJeiwgKf4=
|
||||
github.com/luxfi/warp v1.18.5/go.mod h1:SFyC529HDvbP/TWRAdYQSyJUliMa5JKFRtBrTLEElp4=
|
||||
github.com/luxfi/zap v0.7.0 h1:wiOXfctxiNk0EAiaEPURTXemvO8fyaGViIkoQVWs7Kw=
|
||||
github.com/luxfi/zap v0.7.0/go.mod h1:WOBoeqEh0BCApg1wfxoadOLMXZUC9T1ksOABGoEp8os=
|
||||
github.com/luxfi/zapdb v1.8.0 h1:E9hXMW1MTuS6kmDVs07j62gIq5qI5gZOdyzVfiBc1wg=
|
||||
github.com/luxfi/zapdb v1.8.0/go.mod h1:Qukh3hDRD0MnxA6z+a28JTnXhN85AiLLgp6TYr4QAMc=
|
||||
github.com/luxfi/vm v1.2.3 h1:IdfB02OtDmdy/YNxaIR8DquPp5Aq0fi5QXBqGHoRkug=
|
||||
github.com/luxfi/vm v1.2.3/go.mod h1:ZXmw1sKA6GL3Ma3BfYUv5Fzvx3aWu2QMtkxF3lL/3R4=
|
||||
github.com/luxfi/warp v1.18.6 h1:+ly7mHz77ig4yUyLax32aAjk7tiQQ6eygzOSDNfYRbQ=
|
||||
github.com/luxfi/warp v1.18.6/go.mod h1:OBN23yiGl+E4qupkPHuetBIEqsI8q5leE8WJH8soFjY=
|
||||
github.com/luxfi/zap v0.7.2 h1:YecWTWNE5PPJXL56sLIkzS8b23bprUwZ5lPAQuLUtTE=
|
||||
github.com/luxfi/zap v0.7.2/go.mod h1:1k+nwT+JW802YzuPAuf7CxMSGr/qxvbGgGwi5k6X9Ok=
|
||||
github.com/luxfi/zapcodec v1.0.1 h1:pRxLxCOi6uihQMg8A8riDjNjefU2cXZxfRVZ+obeuL8=
|
||||
github.com/luxfi/zapcodec v1.0.1/go.mod h1:txrRt2JK4O76ssTxlXIwoNVsgzyZVL0ES4mlXqGNogs=
|
||||
github.com/luxfi/zapdb v1.10.0 h1:1lLHEmkyC0BucnA/zjQYsMkUVxuEo2vQkEaQGjYfuuc=
|
||||
github.com/luxfi/zapdb v1.10.0/go.mod h1:Qukh3hDRD0MnxA6z+a28JTnXhN85AiLLgp6TYr4QAMc=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
@@ -220,8 +285,25 @@ github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM=
|
||||
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
@@ -232,8 +314,10 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
@@ -244,6 +328,7 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
@@ -256,6 +341,9 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
|
||||
@@ -276,6 +364,8 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
@@ -294,6 +384,10 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
@@ -302,51 +396,106 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
|
||||
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is BibTeX, Version 0.99d (TeX Live 2025)
|
||||
This is BibTeX, Version 0.99e (TeX Live 2026)
|
||||
Capacity: max_strings=200000, hash_size=200000, hash_prime=170003
|
||||
The top-level auxiliary file: lx-dex-whitepaper.aux
|
||||
The style file: plain.bst
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Fdb version 4
|
||||
["bibtex lx-dex-whitepaper"] 1781738039.60459 "lx-dex-whitepaper.aux" "lx-dex-whitepaper.bbl" "lx-dex-whitepaper" 1781738039.80828 0
|
||||
"./references.bib" 1781513601.69579 5797 9d1155cf49e7b884db6b7a2ee75d8cb4 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/bibtex/bst/base/plain.bst" 1292289607 20613 bd3fbfa9f64872b81ac57a0dd2ed855f ""
|
||||
"lx-dex-whitepaper.aux" 1765511369.99242 5541 ae35c2873d136a9b8cb159693de98c41 "pdflatex"
|
||||
(generated)
|
||||
"lx-dex-whitepaper.bbl"
|
||||
"lx-dex-whitepaper.blg"
|
||||
(rewritten before read)
|
||||
["pdflatex"] 1781738039.62384 "lx-dex-whitepaper.tex" "lx-dex-whitepaper.pdf" "lx-dex-whitepaper" 1781738039.80835 2
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/fonts/tfm/public/cm/cmr10.tfm" 1136768653 1296 45809c5a464d5f32c8f98ba97c1bb47f ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/bigintcalc/bigintcalc.sty" 1576625341 40635 c40361e206be584d448876bba8a64a3b ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/bitset/bitset.sty" 1576016050 33961 6b5c75130e435b2bfdb9f480a09a39f9 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/gettitlestring/gettitlestring.sty" 1576625223 8371 9d55b8bd010bc717624922fb3477d92e ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/iftex/iftex.sty" 1734129479 7984 7dbb9280f03c0a315425f1b4f35d43ee ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/infwarerr/infwarerr.sty" 1575499628 8356 7bbb2c2373aa810be568c29e333da8ed ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/intcalc/intcalc.sty" 1576625065 31769 002a487f55041f8e805cfbf6385ffd97 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/kvdefinekeys/kvdefinekeys.sty" 1576878844 5412 d5a2436094cd7be85769db90f29250a6 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/ltxcmds/ltxcmds.sty" 1701727651 17865 1a9bd36b4f98178fa551aca822290953 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/pdfescape/pdfescape.sty" 1576015897 19007 15924f7228aca6c6d184b115f4baa231 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/pdftexcmds/pdftexcmds.sty" 1593379760 20089 80423eac55aa175305d35b49e04fe23b ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/stringenc/stringenc.sty" 1575152242 21514 b7557edcee22835ef6b03ede1802dad4 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/generic/uniquecounter/uniquecounter.sty" 1576624663 7008 f92eaa0a3872ed622bbf538217cd2ab7 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsfonts/amsfonts.sty" 1359763108 5949 3f3fd50a8cc94c3d4cbf4fc66cd3df1c ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsfonts/amssymb.sty" 1359763108 13829 94730e64147574077f8ecfea9bb69af4 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsbsy.sty" 1780258940 2222 19e175673ef879f3f232f36ac15767a8 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsgen.sty" 1780258940 4173 c36014b45333fb59a0b1c1ba84df10f4 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsmath.sty" 1780258940 88826 df33b75c2b510b1365372c880e3e43b1 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsopn.sty" 1780258940 4474 e8e78ae89fe9e8de1decac472cd058c2 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amstext.sty" 1780258940 2444 7f0b8a693414aa18acf9a58b6c630be5 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/article.cls" 1780258940 20144 e6010938fb17dfbae90a243ad7cf351d ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/inputenc.sty" 1780258940 5048 d19270c31bb8731a546f4e34e34d62fc ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/size11.clo" 1780258940 8464 d6d1b8c8ee5d0bc83b2c1251b4fc6dae ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/etoolbox/etoolbox.sty" 1759437024 46885 8953c67ffba03252c6090aa19568b8ba ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics-cfg/graphics.cfg" 1465944070 1224 978390e9c2234eab29404bc21b268d1e ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics-def/pdftex.def" 1759176675 19626 23e2822b9b2b5005f4c549ca98b9334d ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/graphics.sty" 1780258940 18585 02393ee85dc80955e05e568e02814a7c ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/graphicx.sty" 1780258940 8118 42e441807705be09edaacda384cc0b03 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/keyval.sty" 1780258940 2671 22a90a26d1edef7d1936f15a91d387af ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/trig.sty" 1780258940 4023 5a4fcfe68d7c6df02f94d7bf578e99fd ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/hycolor/hycolor.sty" 1580250785 17914 4c28a13fc3d975e6e81c9bea1d697276 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/hpdftex.def" 1777145858 48140 974cb144aca24f4af37f434c0cfdfcc8 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/hyperref.sty" 1777145858 222324 0031817ce93a9942f5c9df80df47244b ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/nameref.sty" 1769721417 11459 697f11f6c439d25d39d2674b99566af4 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/pd1enc.def" 1777145858 14249 7b55182dcc5724adaf8997085683ebc1 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/puenc.def" 1777145858 117118 2cabdbf2b74662802f142d352392bed5 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/kvoptions/kvoptions.sty" 1655478651 22555 6d8e155cfef6d82c3d5c742fea7c992e ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/kvsetkeys/kvsetkeys.sty" 1665067230 13815 760b0c02f691ea230f5359c4e1de23a7 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/refcount/refcount.sty" 1576624809 9878 9e94e8fa600d95f9c7731bb21dfb67a4 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/rerunfilecheck/rerunfilecheck.sty" 1750533675 9684 a33a14b82ce60d6e77cb9be689d79ee6 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/tex/latex/url/url.sty" 1388531844 12796 8edb7d69a20b857904dd0ea757c14ec9 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-dist/web2c/texmf.cnf" 1770478701 43630 e9fa012910491fe5ae08a72c3c2997a7 ""
|
||||
"/Users/z/Library/TinyTeX/texmf-var/web2c/pdftex/pdflatex.fmt" 1781671207 2251160 82e41dfdd28d0c05974443a16ebbbe61 ""
|
||||
"/Users/z/Library/TinyTeX/texmf.cnf" 1781671163 637 edd806fb06a89761a6034e1968af9017 ""
|
||||
"cite.sty" 0 -1 0 ""
|
||||
"lx-dex-whitepaper.aux" 1765511369.99242 5541 ae35c2873d136a9b8cb159693de98c41 "pdflatex"
|
||||
"lx-dex-whitepaper.bbl" 1781738039.6225 49 0eba253fc001332e731e8aba3f23127b "bibtex lx-dex-whitepaper"
|
||||
"lx-dex-whitepaper.tex" 1781513601.69527 14732 ae531721827179c287ac95df8c6c4eed ""
|
||||
(generated)
|
||||
"lx-dex-whitepaper.aux"
|
||||
"lx-dex-whitepaper.log"
|
||||
"lx-dex-whitepaper.pdf"
|
||||
(rewritten before read)
|
||||
@@ -0,0 +1,93 @@
|
||||
PWD /Users/z/work/lx/dex/paper
|
||||
INPUT /Users/z/Library/TinyTeX/texmf.cnf
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/web2c/texmf.cnf
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-var/web2c/pdftex/pdflatex.fmt
|
||||
INPUT lx-dex-whitepaper.tex
|
||||
OUTPUT lx-dex-whitepaper.log
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/article.cls
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/article.cls
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/size11.clo
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/size11.clo
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/size11.clo
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/fonts/tfm/public/cm/cmr10.tfm
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/inputenc.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/base/inputenc.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsmath.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsmath.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsopn.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amstext.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amstext.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsgen.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsgen.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsbsy.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsbsy.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsmath/amsopn.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsfonts/amssymb.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsfonts/amssymb.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsfonts/amsfonts.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/amsfonts/amsfonts.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/graphicx.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/graphicx.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/keyval.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/keyval.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/graphics.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/graphics.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/trig.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics/trig.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics-def/pdftex.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics-def/pdftex.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/graphics-def/pdftex.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/hyperref.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/hyperref.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/iftex/iftex.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/iftex/iftex.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/kvsetkeys/kvsetkeys.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/kvsetkeys/kvsetkeys.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/kvdefinekeys/kvdefinekeys.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/kvdefinekeys/kvdefinekeys.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/pdfescape/pdfescape.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/pdfescape/pdfescape.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/ltxcmds/ltxcmds.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/ltxcmds/ltxcmds.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/pdftexcmds/pdftexcmds.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/pdftexcmds/pdftexcmds.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/infwarerr/infwarerr.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/infwarerr/infwarerr.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hycolor/hycolor.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hycolor/hycolor.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/nameref.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/nameref.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/refcount/refcount.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/refcount/refcount.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/gettitlestring/gettitlestring.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/gettitlestring/gettitlestring.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/kvoptions/kvoptions.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/kvoptions/kvoptions.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/etoolbox/etoolbox.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/etoolbox/etoolbox.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/stringenc/stringenc.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/stringenc/stringenc.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/pd1enc.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/pd1enc.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/pd1enc.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/intcalc/intcalc.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/intcalc/intcalc.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/puenc.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/puenc.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/puenc.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/url/url.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/url/url.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/bitset/bitset.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/bitset/bitset.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/bigintcalc/bigintcalc.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/bigintcalc/bigintcalc.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/hpdftex.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/hpdftex.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/hyperref/hpdftex.def
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/rerunfilecheck/rerunfilecheck.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/latex/rerunfilecheck/rerunfilecheck.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/uniquecounter/uniquecounter.sty
|
||||
INPUT /Users/z/Library/TinyTeX/texmf-dist/tex/generic/uniquecounter/uniquecounter.sty
|
||||
Binary file not shown.
+95
-45
@@ -42,8 +42,9 @@
|
||||
stringstyle=\color{red}
|
||||
}
|
||||
|
||||
\title{\textbf{LX: A 100M+ Trades/Second Decentralized Exchange} \\
|
||||
\large{Achieving 597ns Latency Through Quantum-Resistant DAG Consensus}}
|
||||
\title{\textbf{LX: A GPU-Native Decentralized Exchange} \\
|
||||
\large{Byte-Exact \texttt{uint256} CLOB Matching at 100--250M Fills/s per Device,
|
||||
Scaling to Fleet-Level Throughput Through Quantum-Resistant Consensus}}
|
||||
|
||||
\author{
|
||||
[email protected] \\
|
||||
@@ -58,25 +59,46 @@ Lux Industries Inc. \\
|
||||
\maketitle
|
||||
|
||||
\begin{abstract}
|
||||
We present \lxdex{}, an ultra-high-performance decentralized exchange achieving over 100 million trades per second with 597 nanosecond order matching latency. Our system combines a novel DAG-based consensus protocol (Lux Consensus) with quantum-resistant cryptography (Corona+BLS hybrid signatures), kernel-bypass networking (DPDK/RDMA), GPU acceleration (MLX/CUDA), and zero-copy architecture to achieve performance exceeding centralized exchanges while maintaining full decentralization. Through integration with the Lux X-Chain for order processing and Q-Chain for quantum finality, we demonstrate 50ms consensus finality with Byzantine fault tolerance. The system utilizes the luxfi/database abstraction layer for consistent storage across multiple backend implementations and achieves linear scaling to 100M+ trades/second on 100Gbps fiber networks.
|
||||
We present \lxdex{}, a GPU-native decentralized exchange whose central-limit
|
||||
order book (CLOB) matching kernel computes byte-exact \texttt{uint256} fills.
|
||||
On a single accelerator the kernel sustains 104--126M fills/s on an NVIDIA GB10
|
||||
(6.9\,ns/match) and approximately 250M fills/s on an Apple M4 Max
|
||||
(4.15\,ns/match); the throughput lever is a Knuth Algorithm-D division that
|
||||
replaces a 512-iteration bit-serial \texttt{uint256} VWAP, an $\sim$8$\times$
|
||||
gain, with output byte-identical to the CPU reference (validated against
|
||||
millions of fuzz and known-answer vectors with zero divergence). Per-book
|
||||
arenas are independent, so aggregate throughput exceeding one billion fills/s
|
||||
is reached by aggregating across nodes (a fleet of roughly 8--10 accelerators),
|
||||
\emph{not} by a single device. Matching is paired with market-sharded Block-STM
|
||||
settlement (2.2M fills/s conflict-detection, with CPU and GPU producing a
|
||||
byte-identical commit order on real silicon) and a post-quantum rollup to the
|
||||
Lux Z-Chain: P3Q (ML-DSA-65, venue-trust) verifies 4{,}714 proofs/s and the
|
||||
trustless STARK/FRI verifier (\texttt{starkfri}) verifies 986 proofs/s on a toy
|
||||
AIR. The system uses the \texttt{luxfi/database} abstraction layer for
|
||||
consistent storage across backends, combining a private single-operator HFT
|
||||
matching chain (the D-Chain) with quantum-resistant consensus and Byzantine
|
||||
fault tolerance.
|
||||
\end{abstract}
|
||||
|
||||
\section{Introduction}
|
||||
|
||||
The cryptocurrency trading ecosystem faces a fundamental performance barrier: current decentralized exchanges (DEXs) process fewer than 1,000 transactions per second, while centralized exchanges (CEXs) handle millions. This 1000x performance gap has relegated DEXs to less than 5\% of total trading volume despite their superior security model.
|
||||
|
||||
\lxdex{} eliminates this performance gap entirely, achieving:
|
||||
\lxdex{} closes this performance gap, achieving:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{100M+ trades/second} throughput on 100Gbps networks
|
||||
\item \textbf{597 nanosecond} order matching latency
|
||||
\item \textbf{50ms} consensus finality through Lux Consensus (formerly FPC)
|
||||
\item \textbf{Quantum resistance} via Corona+BLS hybrid signatures
|
||||
\item \textbf{Zero-copy} networking with DPDK and RDMA
|
||||
\item \textbf{104--126M fills/s} of byte-exact \texttt{uint256} CLOB matching on a single NVIDIA GB10 (6.9\,ns/match), and $\sim$250M fills/s on an Apple M4 Max (4.15\,ns/match)
|
||||
\item \textbf{$>$1B fills/s} aggregate throughput across a node fleet ($\sim$8--10 accelerators), since per-book arenas are independent --- not from a single device
|
||||
\item \textbf{2.2M fills/s} market-sharded Block-STM settlement, with a CPU/GPU byte-identical commit order
|
||||
\item \textbf{Post-quantum rollup} to the Z-Chain: P3Q (ML-DSA-65) at 4{,}714 verifies/s, trustless STARK/FRI (\texttt{starkfri}) at 986 verifies/s
|
||||
\item \textbf{Quantum-resistant consensus} via Corona+BLS hybrid signatures
|
||||
\item \textbf{GPU acceleration} with automatic MLX/CUDA detection
|
||||
\end{itemize}
|
||||
|
||||
Our key innovation is the integration of multiple cutting-edge technologies into a cohesive system that maintains decentralization while exceeding CEX performance.
|
||||
Our key innovation is a byte-exact \texttt{uint256} matching kernel that is
|
||||
correctness-bound rather than precision-truncated: GPU output is identical to
|
||||
the CPU oracle bit for bit, so the throughput gain is free of the silent
|
||||
rounding that disqualifies floating-point matchers from settlement.
|
||||
|
||||
\section{System Architecture}
|
||||
|
||||
@@ -93,39 +115,48 @@ This separation allows parallel processing of orders while maintaining cryptogra
|
||||
|
||||
\subsection{Polyglot Engine Architecture}
|
||||
|
||||
We implement five distinct matching engines, each optimized for specific deployment scenarios:
|
||||
We implement several matching-engine front-ends sharing one byte-exact
|
||||
\texttt{uint256} matching core, each suited to a deployment scenario. The
|
||||
byte-exact CPU reference sustains $\sim$1M fills/s and is the oracle the GPU
|
||||
kernel is validated against; the GPU back-ends (Section~\ref{sec:gpu}) raise
|
||||
single-device throughput to 104--250M fills/s.
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\begin{tabular}{lrrr}
|
||||
\begin{tabular}{lll}
|
||||
\toprule
|
||||
\textbf{Engine} & \textbf{Throughput} & \textbf{Latency} & \textbf{Memory} \\
|
||||
\textbf{Engine} & \textbf{Deployment role} & \textbf{Memory} \\
|
||||
\midrule
|
||||
Pure Go & 90K trades/s & <1ms & Moderate \\
|
||||
Pure C++ & 500K+ trades/s & <100$\microsec$ & Low \\
|
||||
Hybrid Go/C++ & 400K trades/s & <200$\microsec$ & Moderate \\
|
||||
TypeScript & 50K trades/s & <5ms & High \\
|
||||
Rust & 450K trades/s & <150$\microsec$ & Low \\
|
||||
Go (CPU reference) & Default, correctness oracle & Moderate \\
|
||||
C++ & High-frequency CPU path & Low \\
|
||||
Rust & Safety-critical CPU path & Low \\
|
||||
TypeScript & Client/embedded & High \\
|
||||
GPU (CUDA / MLX) & Bulk matching back-end & Low \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Performance characteristics of different engine implementations}
|
||||
\caption{Matching-engine front-ends and their deployment roles. All share the
|
||||
same byte-exact \texttt{uint256} core; the CPU reference is the validation
|
||||
oracle for the GPU kernels.}
|
||||
\label{tab:engines}
|
||||
\end{table}
|
||||
|
||||
\subsection{Order Book Implementation}
|
||||
|
||||
Our order book achieves 597ns latency through multiple optimizations:
|
||||
Our order book attains a per-match cost of 6.9\,ns on an NVIDIA GB10 and
|
||||
4.15\,ns on an Apple M4 Max through several optimizations:
|
||||
|
||||
\subsubsection{Integer Price Representation}
|
||||
\begin{lstlisting}
|
||||
// Traditional (slow): 177ns per operation
|
||||
// Traditional (slow): string-formatted decimal price key
|
||||
priceKey := fmt.Sprintf("%.8f", order.Price)
|
||||
|
||||
// Optimized (fast): 0.597ns per operation
|
||||
// Fixed-point: integer comparison, no formatting/parsing
|
||||
priceInt := int64(order.Price * 1e7) // 7 decimals
|
||||
\end{lstlisting}
|
||||
|
||||
This optimization provides a \textbf{296x speedup} in price operations.
|
||||
Prices and quantities are exact integers (\texttt{uint256} on the matching
|
||||
path), so price-level comparison is a single integer operation with no
|
||||
floating-point rounding and no string formatting.
|
||||
|
||||
\subsubsection{Lock-Free Architecture}
|
||||
\begin{lstlisting}
|
||||
@@ -232,6 +263,7 @@ type BinaryFIXOrder struct {
|
||||
\end{lstlisting}
|
||||
|
||||
\section{GPU Acceleration}
|
||||
\label{sec:gpu}
|
||||
|
||||
\subsection{Universal Backend Detection}
|
||||
|
||||
@@ -255,14 +287,15 @@ func detectBackend() Backend {
|
||||
\centering
|
||||
\begin{tabular}{lrr}
|
||||
\toprule
|
||||
\textbf{Backend} & \textbf{Orders/Batch} & \textbf{Time/Batch} \\
|
||||
\textbf{Backend} & \textbf{Fills/s} & \textbf{Per-match cost} \\
|
||||
\midrule
|
||||
CPU (AVX2) & 1,000 & 1ms \\
|
||||
CUDA (RTX 4090) & 100,000 & 1ms \\
|
||||
MLX (M2 Ultra) & 50,000 & 1ms \\
|
||||
CUDA --- NVIDIA GB10 & 104--126M & 6.9\,ns \\
|
||||
MLX/Metal --- Apple M4 Max & $\sim$250M & 4.15\,ns \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{GPU acceleration provides 50-100x throughput increase}
|
||||
\caption{Measured GPU matching throughput. The throughput lever is a Knuth
|
||||
Algorithm-D division replacing a 512-iteration bit-serial \texttt{uint256}
|
||||
VWAP ($\sim$8$\times$); output is byte-identical to the CPU oracle.}
|
||||
\end{table}
|
||||
|
||||
\section{Storage Layer}
|
||||
@@ -295,13 +328,13 @@ Supported backends:
|
||||
|
||||
\subsection{Benchmark Configuration}
|
||||
|
||||
Testing performed on:
|
||||
Matching-kernel measurements performed on:
|
||||
\begin{itemize}
|
||||
\item Apple M1 Max (10 cores, 64GB RAM)
|
||||
\item AMD EPYC 7763 (64 cores, 512GB RAM)
|
||||
\item NVIDIA A100 GPU (40GB HBM2)
|
||||
\item Mellanox ConnectX-6 (100GbE + RDMA)
|
||||
\item NVIDIA GB10 (Grace Blackwell)
|
||||
\item Apple M4 Max (unified-memory, MLX/Metal backend)
|
||||
\end{itemize}
|
||||
All figures below are measured fills/s of byte-exact \texttt{uint256}
|
||||
matching, validated bit-for-bit against the CPU reference oracle.
|
||||
|
||||
\subsection{Throughput Results}
|
||||
|
||||
@@ -309,16 +342,18 @@ Testing performed on:
|
||||
\centering
|
||||
\begin{tabular}{lrr}
|
||||
\toprule
|
||||
\textbf{Configuration} & \textbf{Throughput} & \textbf{Latency} \\
|
||||
\textbf{Configuration} & \textbf{Throughput} & \textbf{Per-match cost} \\
|
||||
\midrule
|
||||
Single Core (Go) & 90K/s & 894ns \\
|
||||
10 Cores (Go) & 2.1M/s & 597ns \\
|
||||
10 Cores (Hybrid) & 4.2M/s & 421ns \\
|
||||
64 Cores + DPDK & 25M/s & 102ns \\
|
||||
64 Cores + DPDK + GPU & 100M+/s & 89ns \\
|
||||
GPU CLOB --- NVIDIA GB10 & 104--126M fills/s & 6.9\,ns \\
|
||||
GPU CLOB --- Apple M4 Max & $\sim$250M fills/s & 4.15\,ns \\
|
||||
Settlement (Block-STM, market-sharded) & 2.2M fills/s & --- \\
|
||||
Fleet aggregate ($\sim$8--10 GPUs) & $>$1B fills/s & --- \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Throughput scales linearly with resources}
|
||||
\caption{Measured byte-exact \texttt{uint256} matching throughput. A single
|
||||
accelerator is dependency-latency-bound at $\sim$100--250M fills/s; the
|
||||
$>$1B fills/s aggregate is reached across independent per-book arenas on a
|
||||
node fleet, not on one device.}
|
||||
\end{table}
|
||||
|
||||
\subsection{Network Saturation}
|
||||
@@ -329,7 +364,10 @@ At 100Gbps with 60-byte messages:
|
||||
&= 208\text{M messages/sec}
|
||||
\end{align}
|
||||
|
||||
With batching and compression, we achieve 100M+ trades/sec, utilizing 48\% of theoretical bandwidth.
|
||||
A single matching node is dependency-latency-bound well below this ceiling
|
||||
($\sim$100--250M fills/s); network bandwidth is therefore not the binding
|
||||
constraint on a single device, and fleet-level throughput is obtained by
|
||||
sharding independent per-book arenas across nodes.
|
||||
|
||||
\section{Comparative Analysis}
|
||||
|
||||
@@ -343,10 +381,13 @@ Uniswap v3 & 1K/s & 12s & Yes \\
|
||||
Serum (Solana) & 65K/s & 400ms & Yes \\
|
||||
dYdX v4 & 100K/s & 100ms & Partial \\
|
||||
Binance (CEX) & 10M/s & 10ms & No \\
|
||||
\textbf{LX} & \textbf{100M+/s} & \textbf{597ns} & \textbf{Yes} \\
|
||||
\textbf{LX} & \textbf{104--250M fills/s} & \textbf{4--7\,ns/match} & \textbf{Yes} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{LX exceeds CEX performance while maintaining decentralization}
|
||||
\caption{LX single-device byte-exact matching throughput versus reported
|
||||
exchange figures. The LX column is measured GPU CLOB matching (GB10 / M4 Max);
|
||||
fleet aggregation reaches $>$1B fills/s. The latency column is per-match kernel
|
||||
cost, not end-to-end trade confirmation.}
|
||||
\end{table}
|
||||
|
||||
\section{Security Analysis}
|
||||
@@ -403,9 +444,18 @@ The Corona+BLS hybrid scheme provides:
|
||||
|
||||
\section{Conclusion}
|
||||
|
||||
\lxdex{} demonstrates that decentralized exchanges can exceed centralized exchange performance without compromising security or decentralization. Through novel integration of DAG consensus, quantum-resistant cryptography, kernel-bypass networking, and GPU acceleration, we achieve 100M+ trades/second with 597ns latency—a 100,000x improvement over existing DEXs.
|
||||
\lxdex{} demonstrates that a decentralized exchange can match orders at
|
||||
hardware speed without abandoning settlement-grade correctness. The matching
|
||||
kernel sustains 104--126M byte-exact \texttt{uint256} fills/s on an NVIDIA GB10
|
||||
(6.9\,ns/match) and $\sim$250M fills/s on an Apple M4 Max (4.15\,ns/match), with
|
||||
output identical bit-for-bit to the CPU reference. Because per-book arenas are
|
||||
independent, aggregate throughput exceeding one billion fills/s is reached
|
||||
across a node fleet rather than on any single device. Matching composes with
|
||||
market-sharded Block-STM settlement (2.2M fills/s, CPU/GPU byte-identical commit
|
||||
order) and a post-quantum rollup to the Z-Chain.
|
||||
|
||||
Our open-source implementation proves these performance levels are achievable on commodity hardware, democratizing access to high-frequency trading infrastructure. As quantum computing threatens existing cryptographic systems, our quantum-resistant design ensures long-term security for digital asset trading.
|
||||
As quantum computing threatens existing cryptographic systems, our
|
||||
quantum-resistant design ensures long-term security for digital asset trading.
|
||||
|
||||
\section*{Acknowledgments}
|
||||
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/versiondb"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// block.go is the d-chain block: a height-ordered batch of DEX transactions. Its
|
||||
// lifecycle is the heart of the matcher-at-Verify design:
|
||||
//
|
||||
// - Verify re-executes the block's txs against a versiondb OVERLAY on the
|
||||
// committed state, deriving the fills and the resulting book rows,
|
||||
// and checks the proposer's claimed execution root. Every validator
|
||||
// runs this, so a lying proposer (wrong fills, wrong root) is
|
||||
// rejected. The overlay is held on the block until Accept/Reject.
|
||||
// - Accept commits the overlay to durable zapdb atomically (versiondb.Commit)
|
||||
// and advances the in-RAM book + meta watermarks.
|
||||
// - Reject aborts the overlay (versiondb.Abort) and returns the txs to the
|
||||
// mempool.
|
||||
//
|
||||
// The block bytes are self-contained: [parent:32][height:8][ts:8][execRoot:32]
|
||||
// [txlist]. The id is Checksum256 over the bytes.
|
||||
|
||||
// blockHeaderSize is the fixed prefix before the tx list.
|
||||
const blockHeaderSize = 32 + 8 + 8 + 32 // parent + height + timestamp + execRoot
|
||||
|
||||
// Status bytes (mirror the consensus choices.Status small-int convention used by
|
||||
// block.Block.Status() — Processing/Accepted/Rejected).
|
||||
const (
|
||||
statusProcessing uint8 = 0
|
||||
statusAccepted uint8 = 1
|
||||
statusRejected uint8 = 2
|
||||
)
|
||||
|
||||
// Block is one d-chain block implementing block.Block.
|
||||
type Block struct {
|
||||
vm *VM
|
||||
|
||||
parentID ids.ID
|
||||
height uint64
|
||||
timestamp time.Time
|
||||
execRoot [Size]byte
|
||||
txs []*Tx
|
||||
|
||||
bytes []byte
|
||||
id ids.ID
|
||||
status uint8
|
||||
|
||||
// overlay is the versiondb staged during Verify, committed at Accept and
|
||||
// aborted at Reject. nil until Verify runs.
|
||||
overlay *versiondb.Database
|
||||
|
||||
// outcomes are the per-tx consensus results derived by execute (during
|
||||
// Verify or Build). Accept resolves any parked ZAP handler from these. nil
|
||||
// until execute runs against this block.
|
||||
outcomes []txOutcome
|
||||
}
|
||||
|
||||
// newBlock assembles a block from its fields and computes its bytes + id. The
|
||||
// execRoot is the proposer's claim (set by BuildBlock to the locally-computed
|
||||
// root; checked by Verify on every other validator).
|
||||
func newBlock(vm *VM, parentID ids.ID, height uint64, ts time.Time, execRoot [Size]byte, txs []*Tx) *Block {
|
||||
b := &Block{
|
||||
vm: vm,
|
||||
parentID: parentID,
|
||||
height: height,
|
||||
timestamp: ts,
|
||||
execRoot: execRoot,
|
||||
txs: txs,
|
||||
status: statusProcessing,
|
||||
}
|
||||
b.bytes = b.marshal()
|
||||
b.id = ids.Checksum256(b.bytes)
|
||||
return b
|
||||
}
|
||||
|
||||
// marshal encodes the block to its canonical bytes.
|
||||
func (b *Block) marshal() []byte {
|
||||
body := encodeTxList(b.txs)
|
||||
out := make([]byte, blockHeaderSize+len(body))
|
||||
copy(out[0:32], b.parentID[:])
|
||||
binary.BigEndian.PutUint64(out[32:40], b.height)
|
||||
binary.BigEndian.PutUint64(out[40:48], uint64(b.timestamp.UnixNano()))
|
||||
copy(out[48:80], b.execRoot[:])
|
||||
copy(out[80:], body)
|
||||
return out
|
||||
}
|
||||
|
||||
// parseBlock decodes block bytes into a Block bound to vm. It does NOT execute —
|
||||
// execution happens in Verify.
|
||||
func parseBlock(vm *VM, raw []byte) (*Block, error) {
|
||||
if len(raw) < blockHeaderSize {
|
||||
return nil, fmt.Errorf("dchain: block too short: %d", len(raw))
|
||||
}
|
||||
var parent ids.ID
|
||||
copy(parent[:], raw[0:32])
|
||||
height := binary.BigEndian.Uint64(raw[32:40])
|
||||
tsNanos := int64(binary.BigEndian.Uint64(raw[40:48]))
|
||||
var execRoot [Size]byte
|
||||
copy(execRoot[:], raw[48:80])
|
||||
txs, err := decodeTxList(raw[80:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := &Block{
|
||||
vm: vm,
|
||||
parentID: parent,
|
||||
height: height,
|
||||
timestamp: time.Unix(0, tsNanos).UTC(),
|
||||
execRoot: execRoot,
|
||||
txs: txs,
|
||||
status: statusProcessing,
|
||||
}
|
||||
b.bytes = make([]byte, len(raw))
|
||||
copy(b.bytes, raw)
|
||||
b.id = ids.Checksum256(b.bytes)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// block.Block interface.
|
||||
|
||||
func (b *Block) ID() ids.ID { return b.id }
|
||||
func (b *Block) Parent() ids.ID { return b.parentID }
|
||||
func (b *Block) ParentID() ids.ID { return b.parentID }
|
||||
func (b *Block) Height() uint64 { return b.height }
|
||||
func (b *Block) Timestamp() time.Time { return b.timestamp }
|
||||
func (b *Block) Status() uint8 { return b.status }
|
||||
func (b *Block) Bytes() []byte { return b.bytes }
|
||||
|
||||
// Verify re-executes the block against a versiondb overlay on the committed state
|
||||
// and checks the proposer's execution root. The matcher runs here (not at
|
||||
// Accept), so every validator independently derives the fills and can reject a
|
||||
// proposer that lied. The overlay is retained on the block for Accept to commit.
|
||||
//
|
||||
// Execution is deterministic: applyTx supplies block-derived IDs/timestamps,
|
||||
// SubmitMarketable/ConsensusAddOrder never mint. The resulting fills, book rows,
|
||||
// and root are a pure function of (committed state, this block's txs).
|
||||
func (b *Block) Verify(ctx context.Context) error {
|
||||
b.vm.mu.Lock()
|
||||
defer b.vm.mu.Unlock()
|
||||
|
||||
// Parent must be the current last-accepted block: the d-chain is linear.
|
||||
if b.parentID != b.vm.lastAcceptedID {
|
||||
return fmt.Errorf("dchain: block parent %s != last accepted %s", b.parentID, b.vm.lastAcceptedID)
|
||||
}
|
||||
if b.height != b.vm.lastAcceptedHeight+1 {
|
||||
return fmt.Errorf("dchain: block height %d != expected %d", b.height, b.vm.lastAcceptedHeight+1)
|
||||
}
|
||||
|
||||
overlay := versiondb.New(b.vm.db)
|
||||
result, err := b.execute(ctx, overlay)
|
||||
if err != nil {
|
||||
overlay.Abort()
|
||||
return err
|
||||
}
|
||||
|
||||
// Check the proposer's claimed root against the locally derived one.
|
||||
if result.root != b.execRoot {
|
||||
overlay.Abort()
|
||||
return fmt.Errorf("dchain: execution root mismatch: claimed %x derived %x", b.execRoot[:8], result.root[:8])
|
||||
}
|
||||
|
||||
b.overlay = overlay
|
||||
b.outcomes = result.outcomes
|
||||
return nil
|
||||
}
|
||||
|
||||
// Accept commits the verified overlay to durable zapdb atomically and advances
|
||||
// the VM's accepted state + in-RAM books. If Verify was skipped (overlay nil) it
|
||||
// runs execution now (the leader's own block path is Build->Verify->Accept, so
|
||||
// the overlay is normally present).
|
||||
func (b *Block) Accept(ctx context.Context) error {
|
||||
b.vm.mu.Lock()
|
||||
defer b.vm.mu.Unlock()
|
||||
|
||||
if b.overlay == nil {
|
||||
overlay := versiondb.New(b.vm.db)
|
||||
res, err := b.execute(ctx, overlay)
|
||||
if err != nil {
|
||||
overlay.Abort()
|
||||
return err
|
||||
}
|
||||
b.overlay = overlay
|
||||
b.outcomes = res.outcomes
|
||||
}
|
||||
|
||||
// Persist the accept watermarks into the SAME overlay so the commit is one
|
||||
// atomic batch: rows + meta land together or not at all.
|
||||
if err := writeLastAccepted(b.overlay, b.id); err != nil {
|
||||
b.overlay.Abort()
|
||||
return err
|
||||
}
|
||||
if err := writeHeight(b.overlay, b.height); err != nil {
|
||||
b.overlay.Abort()
|
||||
return err
|
||||
}
|
||||
if err := writeRoot(b.overlay, b.execRoot); err != nil {
|
||||
b.overlay.Abort()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.overlay.Commit(); err != nil {
|
||||
return fmt.Errorf("dchain: commit block %s: %w", b.id, err)
|
||||
}
|
||||
|
||||
// Advance in-RAM authoritative state.
|
||||
b.vm.lastAcceptedID = b.id
|
||||
b.vm.lastAcceptedHeight = b.height
|
||||
b.vm.lastRoot = b.execRoot
|
||||
b.vm.acceptedBlocks[b.id] = b
|
||||
b.vm.heightIndex[b.height] = b.id
|
||||
b.applyToMemBooks()
|
||||
b.status = statusAccepted
|
||||
b.overlay = nil
|
||||
|
||||
// Resolve any parked ZAP handlers with their tx's consensus outcome. This is
|
||||
// the only place an outcome becomes final: a rejected block never reaches
|
||||
// here (its txs return to the mempool). On a follower with no local waiters
|
||||
// this is a no-op.
|
||||
for _, o := range b.outcomes {
|
||||
b.vm.outcomes.resolve(o)
|
||||
}
|
||||
b.outcomes = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject aborts the overlay and returns the block's txs to the mempool for
|
||||
// reconsideration in a future block.
|
||||
func (b *Block) Reject(ctx context.Context) error {
|
||||
b.vm.mu.Lock()
|
||||
defer b.vm.mu.Unlock()
|
||||
if b.overlay != nil {
|
||||
b.overlay.Abort()
|
||||
b.overlay = nil
|
||||
}
|
||||
b.status = statusRejected
|
||||
b.vm.mempool.Requeue(b.txs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// execResult carries what execute derived: the per-market resulting rows, the
|
||||
// block's fills, the chained execution root, and the per-tx outcomes (so Accept
|
||||
// can resolve any parked ZAP handler with its tx's consensus result).
|
||||
type execResult struct {
|
||||
root [Size]byte
|
||||
fills []lx.DEXTrade
|
||||
rows []lx.DEXOrder // canonical resting rows across all touched markets
|
||||
outcomes []txOutcome // one per tx, in block order
|
||||
}
|
||||
|
||||
// execute runs the block's txs against the overlay, writing order/market/trade
|
||||
// rows into it and returning the derived fills + root. It rebuilds each touched
|
||||
// market's book from the overlay (so it sees prior in-block effects and committed
|
||||
// state), applies the tx, and writes back the affected rows. The in-RAM cached
|
||||
// books are NOT mutated here — only at Accept (applyToMemBooks) — so a rejected
|
||||
// block leaves the accelerator untouched.
|
||||
//
|
||||
// This is the single execution path shared by Verify and Accept: one body, no
|
||||
// drift between "what was verified" and "what was committed".
|
||||
func (b *Block) execute(ctx context.Context, overlay *versiondb.Database) (execResult, error) {
|
||||
// Books for markets touched in this block, rebuilt from the overlay on first
|
||||
// touch so in-block ordering is honored.
|
||||
books := map[[32]byte]*lx.OrderBook{}
|
||||
bookForPool := func(poolID [32]byte) (*lx.OrderBook, error) {
|
||||
if ob, ok := books[poolID]; ok {
|
||||
return ob, nil
|
||||
}
|
||||
symbol, exists, err := readMarketSymbol(overlay, poolID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("dchain: tx for unknown market %x", poolID[:8])
|
||||
}
|
||||
ob, err := rebuildBookFromDB(overlay, poolID, symbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
books[poolID] = ob
|
||||
return ob, nil
|
||||
}
|
||||
|
||||
var allFills []lx.DEXTrade
|
||||
var tradeSeq uint64
|
||||
outcomes := make([]txOutcome, 0, len(b.txs))
|
||||
for i, tx := range b.txs {
|
||||
poolID, ok := tx.poolID()
|
||||
if !ok {
|
||||
return execResult{}, fmt.Errorf("dchain: tx %d missing poolId", i)
|
||||
}
|
||||
|
||||
if tx.Type == TxEnsureMarket {
|
||||
// Idempotent market creation: record existence keyed by poolId. The
|
||||
// symbol is the hex of the poolId (the venue's market identity is the
|
||||
// poolId; a human symbol is a display concern handled at the gateway).
|
||||
if _, exists, err := readMarketSymbol(overlay, poolID); err != nil {
|
||||
return execResult{}, err
|
||||
} else if !exists {
|
||||
if err := writeMarket(overlay, poolID, poolSymbol(poolID)); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
}
|
||||
outcomes = append(outcomes, outcomeFromApply(tx, applyResult{}))
|
||||
continue
|
||||
}
|
||||
|
||||
// Idempotency: an order op whose tx id is already committed (or applied
|
||||
// earlier in THIS block, since seen-marks are written to the overlay) is a
|
||||
// deterministic no-op. This is read from the overlay so every validator —
|
||||
// and the proposer's probe — make the identical decision. A deduped write
|
||||
// resolves its waiter as rejected (already-processed), never re-executes,
|
||||
// so a relay retry of a dropped clob_submit can never double-fill/place.
|
||||
txID := tx.ID()
|
||||
if seen, err := isSeen(overlay, txID); err != nil {
|
||||
return execResult{}, err
|
||||
} else if seen {
|
||||
outcomes = append(outcomes, txOutcome{txID: txID, typ: tx.Type, status: zapwire.StatusRejected})
|
||||
continue
|
||||
}
|
||||
if err := markSeen(overlay, txID); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
|
||||
ob, err := bookForPool(poolID)
|
||||
if err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
|
||||
res, err := applyTx(ob, tx, b.height, b.timestamp, uint32(i))
|
||||
if err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
outcomes = append(outcomes, outcomeFromApply(tx, res))
|
||||
|
||||
// Persist resting deltas to the overlay.
|
||||
switch {
|
||||
case res.Placed != nil:
|
||||
if err := putOrderRow(overlay, poolID, lx.OrderToRow(res.Placed)); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
case res.Canceled != 0:
|
||||
if err := deleteOrderRow(overlay, poolID, res.Canceled); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
}
|
||||
// A submit touches makers: rewrite/delete their rows.
|
||||
for _, m := range res.Touched {
|
||||
if m.ID == 0 {
|
||||
continue
|
||||
}
|
||||
row := lx.OrderToRow(m)
|
||||
row.OrderID = m.ID
|
||||
if err := putOrderRow(overlay, poolID, row); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
}
|
||||
// Record fills in the trade log + the block's fill set.
|
||||
for _, f := range res.Fills {
|
||||
row := lx.TradeToRow(f, res.TakerSide)
|
||||
if err := overlay.Put(tradeKey(b.height, tradeSeq), lx.EncodeTrade(row)); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
tradeSeq++
|
||||
allFills = append(allFills, row)
|
||||
}
|
||||
}
|
||||
|
||||
// Persist each touched market's LastOrderID watermark and gather the
|
||||
// canonical resting rows for the book root.
|
||||
var allRows []lx.DEXOrder
|
||||
for poolID, ob := range books {
|
||||
if err := writeBookWatermark(overlay, poolID, ob.LastOrderID); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
allRows = append(allRows, lx.BookToRows(ob)...)
|
||||
}
|
||||
sortRowsCanonical(allRows)
|
||||
|
||||
root, _, _, _ := ExecutionRoot(b.vm.lastRoot, allRows, allFills, b.txs, b.height)
|
||||
return execResult{root: root, fills: allFills, rows: allRows, outcomes: outcomes}, nil
|
||||
}
|
||||
|
||||
// applyToMemBooks replays the block's resting effects onto the VM's cached in-RAM
|
||||
// books at Accept. The cache is an accelerator; the durable rows are truth. We
|
||||
// rebuild each touched market's cached book from the just-committed overlay state
|
||||
// so the cache is exactly consistent with disk.
|
||||
func (b *Block) applyToMemBooks() {
|
||||
touched := map[[32]byte]struct{}{}
|
||||
for _, tx := range b.txs {
|
||||
if poolID, ok := tx.poolID(); ok {
|
||||
touched[poolID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for poolID := range touched {
|
||||
symbol, exists, err := readMarketSymbol(b.vm.db, poolID)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
ob, err := rebuildBookFromDB(b.vm.db, poolID, symbol)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
b.vm.books[poolID] = ob
|
||||
}
|
||||
}
|
||||
|
||||
// rebuildBookFromDB streams the order:<poolID> rows from db and folds them into a
|
||||
// fresh OrderBook via RowsToBook — the rebuildable-accelerator path. This is the
|
||||
// SAME function used at startup (rebuild every book) and during execution (rebuild
|
||||
// one market into the overlay), so there is one rebuild path.
|
||||
func rebuildBookFromDB(db database.Iteratee, poolID [32]byte, symbol string) (*lx.OrderBook, error) {
|
||||
prefix := orderPrefixFor(poolID)
|
||||
it := db.NewIteratorWithPrefix(prefix)
|
||||
defer it.Release()
|
||||
|
||||
var rows []lx.DEXOrder
|
||||
for it.Next() {
|
||||
row, ok := lx.DecodeRow(it.Value())
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("dchain: corrupt order row under %x (len %d)", poolID[:8], len(it.Value()))
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
if err := it.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lx.RowsToBook(symbol, rows), nil
|
||||
}
|
||||
|
||||
// poolSymbol renders a poolId as its hex market symbol.
|
||||
func poolSymbol(poolID [32]byte) string {
|
||||
const hexdigits = "0123456789abcdef"
|
||||
out := make([]byte, 64)
|
||||
for i, c := range poolID {
|
||||
out[i*2] = hexdigits[c>>4]
|
||||
out[i*2+1] = hexdigits[c&0x0f]
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
)
|
||||
|
||||
// execute.go is the deterministic state-transition function. applyTx takes a tx,
|
||||
// the in-memory book for its market, the block height, the block timestamp, and
|
||||
// the tx's index in the block, and applies it — minting NOTHING from wall-clock
|
||||
// or a process-local counter. Every value that participates in matched state is
|
||||
// derived from block context, so two validators replaying the same ordered block
|
||||
// produce byte-identical results.
|
||||
|
||||
// blockDeterministicID derives a resting order's ID from (height, txIndex). It
|
||||
// is globally unique across the chain (height monotone, txIndex unique within a
|
||||
// block) and identical on every validator. The top 32 bits are the height and
|
||||
// the bottom 32 are the txIndex, with a +1 bias so an ID is never zero (zero is
|
||||
// the "unset" sentinel the matcher rejects). For heights/indices beyond 32 bits
|
||||
// this would alias; a venue block never holds 4 billion txs and a chain does not
|
||||
// reach height 4 billion in any realistic horizon, but if it ever must, widen to
|
||||
// a keccak-derived 64-bit id (kept simple here for a stable, debuggable id).
|
||||
func blockDeterministicID(height uint64, txIndex uint32) uint64 {
|
||||
id := (height << 32) | uint64(txIndex)
|
||||
return id + 1
|
||||
}
|
||||
|
||||
// applyResult is the outcome of applying one tx: the fills it produced (for a
|
||||
// submit) and the resting-order delta to persist. The VM writes these to the
|
||||
// versiondb overlay; on a place the placed order is the delta, on a cancel the
|
||||
// cancelled order id is removed, on a submit the affected makers are updated.
|
||||
type applyResult struct {
|
||||
// Fills are the trades a TxSubmit produced (empty for other tx types).
|
||||
Fills []lx.Trade
|
||||
// TakerSide is the side the submit took with (for encoding fill rows).
|
||||
TakerSide lx.Side
|
||||
// Placed is the order a TxPlace rested (nil otherwise).
|
||||
Placed *lx.Order
|
||||
// Canceled is the order id a TxCancel removed (0 otherwise).
|
||||
Canceled uint64
|
||||
// Touched are resting orders whose remaining changed due to a submit cross
|
||||
// (the makers); their rows must be rewritten/deleted.
|
||||
Touched []*lx.Order
|
||||
}
|
||||
|
||||
// applyTx applies a single transaction to the book deterministically. It is a
|
||||
// pure function of (tx, book state, height, ts, txIndex): no time.Now(), no
|
||||
// LastOrderID mint. EnsureMarket is handled by the VM (it has no book effect
|
||||
// beyond existence, recorded in state); applyTx handles place/cancel/submit.
|
||||
//
|
||||
// For TxPlace: the order's ID = blockDeterministicID(height, txIndex), its
|
||||
// Timestamp = ts, then ConsensusAddOrder rests it.
|
||||
// For TxCancel: CancelOrder removes the resting order.
|
||||
// For TxSubmit: the marketable order also gets a deterministic ID/ts (ephemeral —
|
||||
// it never rests) and SubmitMarketable crosses the book, returning its fills.
|
||||
func applyTx(book *lx.OrderBook, tx *Tx, height uint64, ts time.Time, txIndex uint32) (applyResult, error) {
|
||||
switch tx.Type {
|
||||
case TxPlace:
|
||||
return applyPlace(book, tx, height, ts, txIndex)
|
||||
case TxCancel:
|
||||
return applyCancel(book, tx)
|
||||
case TxSubmit:
|
||||
return applySubmit(book, tx, height, ts, txIndex)
|
||||
case TxEnsureMarket:
|
||||
// No book mutation; market existence is recorded in d-chain state by the
|
||||
// VM. Return an empty result.
|
||||
return applyResult{}, nil
|
||||
default:
|
||||
return applyResult{}, ErrUnknownTx
|
||||
}
|
||||
}
|
||||
|
||||
// applyPlace decodes a zapwire Place body and rests it via the deterministic
|
||||
// consensus entrypoint. Rejected orders (bad price/size, post-only-would-take,
|
||||
// self-trade) yield an empty result with no error: a rejected order is a valid,
|
||||
// deterministic outcome (no fills, no resting delta), not a chain-level failure.
|
||||
func applyPlace(book *lx.OrderBook, tx *Tx, height uint64, ts time.Time, txIndex uint32) (applyResult, error) {
|
||||
body := tx.Body
|
||||
// Place body: poolId[32] + side[1] + price[8] + size[8] + user[16].
|
||||
side := lx.Side(body[zapwire.PoolIDSize])
|
||||
price := zapwire.Float64(body[zapwire.PoolIDSize+1 : zapwire.PoolIDSize+9])
|
||||
size := zapwire.Float64(body[zapwire.PoolIDSize+9 : zapwire.PoolIDSize+17])
|
||||
user := string(trimNull(body[zapwire.PoolIDSize+17 : zapwire.PoolIDSize+17+zapwire.UserSize]))
|
||||
|
||||
if !(price > 0) || !(size > 0) {
|
||||
return applyResult{}, nil // deterministic reject
|
||||
}
|
||||
order := &lx.Order{
|
||||
ID: blockDeterministicID(height, txIndex),
|
||||
Type: lx.Limit,
|
||||
Side: side,
|
||||
Price: price,
|
||||
Size: size,
|
||||
User: user,
|
||||
UserID: user,
|
||||
Symbol: book.Symbol,
|
||||
Timestamp: ts,
|
||||
}
|
||||
if book.ConsensusAddOrder(order) == 0 {
|
||||
return applyResult{}, nil // rejected (e.g. post-only would take)
|
||||
}
|
||||
return applyResult{Placed: order}, nil
|
||||
}
|
||||
|
||||
// applyCancel decodes a zapwire Cancel body and removes the resting order. A
|
||||
// cancel of an unknown order is a deterministic no-op (the matcher returns an
|
||||
// error which we map to an empty result — every validator sees the same).
|
||||
func applyCancel(book *lx.OrderBook, tx *Tx) (applyResult, error) {
|
||||
orderID := binary.BigEndian.Uint64(tx.Body[zapwire.PoolIDSize : zapwire.PoolIDSize+8])
|
||||
// Snapshot the order before cancel so we know it existed (for the delta).
|
||||
if book.GetOrder(orderID) == nil {
|
||||
return applyResult{}, nil
|
||||
}
|
||||
if err := book.CancelOrder(orderID); err != nil {
|
||||
return applyResult{}, nil
|
||||
}
|
||||
return applyResult{Canceled: orderID}, nil
|
||||
}
|
||||
|
||||
// applySubmit decodes a zapwire Submit body and crosses the book. The marketable
|
||||
// order gets a deterministic (height,txIndex) ID and the block ts so the fills it
|
||||
// produces are byte-identical across validators; the order itself never rests
|
||||
// (SubmitMarketable is IOC). Touched makers are captured BEFORE the cross so the
|
||||
// VM can rewrite their rows; SubmitMarketable mutates resting orders in place.
|
||||
func applySubmit(book *lx.OrderBook, tx *Tx, height uint64, ts time.Time, txIndex uint32) (applyResult, error) {
|
||||
body := tx.Body
|
||||
// Submit body: poolId[32] + side[1] + isMarket[1] + price[8] + size[8] + user[16].
|
||||
side := lx.Side(body[zapwire.PoolIDSize])
|
||||
isMarket := body[zapwire.PoolIDSize+1] == 1
|
||||
limitPrice := zapwire.Float64(body[zapwire.PoolIDSize+2 : zapwire.PoolIDSize+10])
|
||||
size := zapwire.Float64(body[zapwire.PoolIDSize+10 : zapwire.PoolIDSize+18])
|
||||
user := string(trimNull(body[zapwire.PoolIDSize+18 : zapwire.PoolIDSize+18+zapwire.UserSize]))
|
||||
|
||||
if !(size > 0) {
|
||||
return applyResult{}, nil
|
||||
}
|
||||
order := &lx.Order{
|
||||
ID: blockDeterministicID(height, txIndex),
|
||||
Side: side,
|
||||
Size: size,
|
||||
User: user,
|
||||
UserID: user,
|
||||
Symbol: book.Symbol,
|
||||
Timestamp: ts,
|
||||
}
|
||||
if isMarket {
|
||||
order.Type = lx.Market
|
||||
} else {
|
||||
if !(limitPrice > 0) {
|
||||
return applyResult{}, nil
|
||||
}
|
||||
order.Type = lx.Limit
|
||||
order.Price = limitPrice
|
||||
}
|
||||
|
||||
fills, err := book.SubmitMarketable(order)
|
||||
if err != nil {
|
||||
return applyResult{}, nil // deterministic reject (e.g. invalid)
|
||||
}
|
||||
|
||||
// Collect the makers touched by these fills so the VM rewrites their rows.
|
||||
touched := make([]*lx.Order, 0, len(fills))
|
||||
seen := make(map[uint64]struct{}, len(fills))
|
||||
for _, f := range fills {
|
||||
makerID := f.SellOrder
|
||||
if side == lx.Sell {
|
||||
makerID = f.BuyOrder
|
||||
}
|
||||
if _, dup := seen[makerID]; dup {
|
||||
continue
|
||||
}
|
||||
seen[makerID] = struct{}{}
|
||||
if m := book.GetOrder(makerID); m != nil {
|
||||
touched = append(touched, m)
|
||||
} else {
|
||||
// Maker fully filled and removed from the book: synthesize a
|
||||
// tombstone row so the VM deletes its persisted row.
|
||||
touched = append(touched, &lx.Order{ID: makerID, Status: lx.Filled})
|
||||
}
|
||||
}
|
||||
|
||||
return applyResult{Fills: fills, TakerSide: side, Touched: touched}, nil
|
||||
}
|
||||
|
||||
// idempotencyKey derives the d-chain dedup key for a submit/place from the wire
|
||||
// user[16] and the per-user sequence. Dedup lives in STATE (this key), keeping
|
||||
// the 66B/65B frames FROZEN — no nonce field is added to the wire. The seq is the
|
||||
// caller's monotone counter; the VM stores seen keys and drops a replay.
|
||||
func idempotencyKey(user [zapwire.UserSize]byte, seq uint64) [32]byte {
|
||||
var s [8]byte
|
||||
binary.BigEndian.PutUint64(s[:], seq)
|
||||
return hash.ComputeKeccak256Array(user[:], s[:])
|
||||
}
|
||||
|
||||
// trimNull returns b with trailing NUL bytes removed (the inverse of zapwire's
|
||||
// null padding for the user field).
|
||||
func trimNull(b []byte) []byte {
|
||||
for i := len(b) - 1; i >= 0; i-- {
|
||||
if b[i] != 0 {
|
||||
return b[:i+1]
|
||||
}
|
||||
}
|
||||
return b[:0]
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
)
|
||||
|
||||
var testPool = [32]byte{0xaa, 0xbb, 0xcc}
|
||||
|
||||
func placeTx(t *testing.T, side uint8, price, size float64, user string) *Tx {
|
||||
t.Helper()
|
||||
tx, err := NewTx(TxPlace, zapwire.EncodePlace(testPool, side, price, size, user))
|
||||
if err != nil {
|
||||
t.Fatalf("NewTx place: %v", err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func submitTx(t *testing.T, side uint8, isMarket bool, price, size float64, user string) *Tx {
|
||||
t.Helper()
|
||||
tx, err := NewTx(TxSubmit, zapwire.EncodeSubmit(testPool, side, isMarket, price, size, user))
|
||||
if err != nil {
|
||||
t.Fatalf("NewTx submit: %v", err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
// applyAll replays a tx sequence into a fresh book, returning every fill as a
|
||||
// canonical DEXTrade row and the resulting book rows. height/txIndex are derived
|
||||
// exactly as the VM derives them (block height fixed, txIndex = position).
|
||||
func applyAll(t *testing.T, height uint64, ts time.Time, txs []*Tx) ([]lx.DEXTrade, []lx.DEXOrder) {
|
||||
t.Helper()
|
||||
book := lx.NewOrderBook("BTC-USD")
|
||||
var fills []lx.DEXTrade
|
||||
for i, tx := range txs {
|
||||
res, err := applyTx(book, tx, height, ts, uint32(i))
|
||||
if err != nil {
|
||||
t.Fatalf("applyTx[%d] (%s): %v", i, tx.Type, err)
|
||||
}
|
||||
for _, f := range res.Fills {
|
||||
fills = append(fills, lx.TradeToRow(f, res.TakerSide))
|
||||
}
|
||||
}
|
||||
return fills, lx.BookToRows(book)
|
||||
}
|
||||
|
||||
// TestApplyTxDeterminism is the step-3 determinism proof at the tx layer: the
|
||||
// same tx sequence applied twice (same height/ts/index) yields byte-identical
|
||||
// fills and book rows.
|
||||
func TestApplyTxDeterminism(t *testing.T) {
|
||||
ts := time.Unix(0, 1_700_000_000_000_000_000).UTC()
|
||||
txs := []*Tx{
|
||||
placeTx(t, zapwire.SideSell, 101.0, 5.0, "maker-a"),
|
||||
placeTx(t, zapwire.SideSell, 101.5, 3.0, "maker-b"),
|
||||
placeTx(t, zapwire.SideBuy, 99.0, 2.0, "maker-c"),
|
||||
submitTx(t, zapwire.SideBuy, false, 101.5, 6.0, "taker-x"),
|
||||
submitTx(t, zapwire.SideSell, true, 0, 1.0, "taker-y"),
|
||||
placeTx(t, zapwire.SideSell, 101.25, 4.0, "maker-d"),
|
||||
}
|
||||
|
||||
f1, r1 := applyAll(t, 42, ts, txs)
|
||||
f2, r2 := applyAll(t, 42, ts, txs)
|
||||
|
||||
if len(f1) == 0 {
|
||||
t.Fatal("expected fills from the submits, got none")
|
||||
}
|
||||
if len(f1) != len(f2) {
|
||||
t.Fatalf("fill count differs: %d vs %d", len(f1), len(f2))
|
||||
}
|
||||
for i := range f1 {
|
||||
if string(lx.EncodeTrade(f1[i])) != string(lx.EncodeTrade(f2[i])) {
|
||||
t.Errorf("fill[%d] not byte-identical across replays", i)
|
||||
}
|
||||
}
|
||||
if len(r1) != len(r2) {
|
||||
t.Fatalf("book row count differs: %d vs %d", len(r1), len(r2))
|
||||
}
|
||||
for i := range r1 {
|
||||
if string(lx.EncodeRow(r1[i])) != string(lx.EncodeRow(r2[i])) {
|
||||
t.Errorf("book row[%d] not byte-identical across replays", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBlockDeterministicID proves the id derivation is unique per (height,
|
||||
// txIndex) and never zero.
|
||||
func TestBlockDeterministicID(t *testing.T) {
|
||||
seen := map[uint64]struct{}{}
|
||||
for h := uint64(0); h < 4; h++ {
|
||||
for i := uint32(0); i < 4; i++ {
|
||||
id := blockDeterministicID(h, i)
|
||||
if id == 0 {
|
||||
t.Fatalf("id zero for height=%d idx=%d", h, i)
|
||||
}
|
||||
if _, dup := seen[id]; dup {
|
||||
t.Fatalf("id collision %d at height=%d idx=%d", id, h, i)
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyPlaceRestsDeterministicOrder proves a place rests with the
|
||||
// VM-supplied ID and timestamp (never minted), and the order is recoverable.
|
||||
func TestApplyPlaceRestsDeterministicOrder(t *testing.T) {
|
||||
book := lx.NewOrderBook("BTC-USD")
|
||||
ts := time.Unix(0, 1_700_000_000_000_000_000).UTC()
|
||||
tx := placeTx(t, zapwire.SideBuy, 100.0, 2.0, "alice")
|
||||
|
||||
res, err := applyTx(book, tx, 7, ts, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("applyTx: %v", err)
|
||||
}
|
||||
if res.Placed == nil {
|
||||
t.Fatal("expected a placed order")
|
||||
}
|
||||
wantID := blockDeterministicID(7, 3)
|
||||
if res.Placed.ID != wantID {
|
||||
t.Errorf("placed ID = %d, want deterministic %d", res.Placed.ID, wantID)
|
||||
}
|
||||
if !res.Placed.Timestamp.Equal(ts) {
|
||||
t.Errorf("placed timestamp = %v, want block ts %v", res.Placed.Timestamp, ts)
|
||||
}
|
||||
if o := book.GetOrder(wantID); o == nil {
|
||||
t.Error("placed order not resting in book")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyCancelRemovesResting proves cancel removes a resting order and an
|
||||
// unknown cancel is a deterministic no-op.
|
||||
func TestApplyCancelRemovesResting(t *testing.T) {
|
||||
book := lx.NewOrderBook("BTC-USD")
|
||||
ts := time.Unix(0, 1_700_000_000_000_000_000).UTC()
|
||||
res, _ := applyTx(book, placeTx(t, zapwire.SideBuy, 100.0, 2.0, "alice"), 1, ts, 0)
|
||||
id := res.Placed.ID
|
||||
|
||||
cancel, err := NewTx(TxCancel, zapwire.EncodeCancel(testPool, id))
|
||||
if err != nil {
|
||||
t.Fatalf("NewTx cancel: %v", err)
|
||||
}
|
||||
cres, err := applyTx(book, cancel, 1, ts, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("applyTx cancel: %v", err)
|
||||
}
|
||||
if cres.Canceled != id {
|
||||
t.Errorf("Canceled = %d, want %d", cres.Canceled, id)
|
||||
}
|
||||
|
||||
// Unknown cancel: deterministic no-op.
|
||||
unknown, _ := NewTx(TxCancel, zapwire.EncodeCancel(testPool, 999999))
|
||||
ures, err := applyTx(book, unknown, 1, ts, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("applyTx unknown cancel: %v", err)
|
||||
}
|
||||
if ures.Canceled != 0 {
|
||||
t.Errorf("unknown cancel reported Canceled=%d, want 0", ures.Canceled)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTxIDStable proves the TxID is a stable function of the canonical bytes.
|
||||
func TestTxIDStable(t *testing.T) {
|
||||
tx := placeTx(t, zapwire.SideBuy, 100.0, 2.0, "alice")
|
||||
if tx.ID() != tx.ID() {
|
||||
t.Error("TxID not stable")
|
||||
}
|
||||
// A different body yields a different id.
|
||||
other := placeTx(t, zapwire.SideBuy, 100.0, 3.0, "alice")
|
||||
if tx.ID() == other.ID() {
|
||||
t.Error("distinct txs share a TxID")
|
||||
}
|
||||
// Round-trips through Bytes/ParseTx unchanged.
|
||||
back, err := ParseTx(tx.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("ParseTx: %v", err)
|
||||
}
|
||||
if back.ID() != tx.ID() {
|
||||
t.Error("TxID changed across Bytes/ParseTx round-trip")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// handler.go is the d-chain's CLOB ZAP surface: the clob_* methods a client (the
|
||||
// chains/dexvm proxy relay, the EVM precompile adapter, the maker) calls over
|
||||
// github.com/luxfi/rpc. It is the gateway INTO consensus, not a matcher.
|
||||
//
|
||||
// THE CRITICAL DECOMPLECT: a write (place/cancel/submit) is NOT executed when it
|
||||
// arrives. The handler decodes the FROZEN zapwire frame into a Tx, Adds it to the
|
||||
// mempool, and BLOCKS until consensus decides it (an accepted block resolves the
|
||||
// waiter with the matched fills/ack). So the bytes a caller gets back are the
|
||||
// CONSENSUS-COMPUTED result — every validator re-derived the same fills at Verify
|
||||
// — not a synchronous, single-process book mutation. Reads (book/state) are
|
||||
// served directly from the authoritative in-RAM book (a fold of the committed
|
||||
// rows), needing no consensus round-trip.
|
||||
//
|
||||
// WIRE PARITY: every frame here is the FROZEN form from pkg/zapwire — the same
|
||||
// method names (clob_ensure_market/place/cancel/submit), the same request sizes,
|
||||
// and the same response codecs (zapwire.EncodeAck / zapwire.EncodeFills, 17-byte
|
||||
// fill). The chains/dexvm relay client and the precompile adapter re-define the
|
||||
// identical constants (they cannot import this cgo-tagged package), so a frame
|
||||
// built by any of them is byte-identical to what this server consumes/produces.
|
||||
|
||||
// RegisterCLOB wires the four clob_* handlers onto an rpc.Server. It is the
|
||||
// integration seam the venue entrypoint (cmd/dchain) and tests use to expose the
|
||||
// d-chain matcher over ZAP. Additive: the caller may register other methods on
|
||||
// the same server.
|
||||
func (vm *VM) RegisterCLOB(server rpc.Server) error {
|
||||
regs := []struct {
|
||||
method string
|
||||
handler rpc.RawHandler
|
||||
}{
|
||||
{zapwire.MethodEnsureMarket, vm.handleEnsureMarket},
|
||||
{zapwire.MethodPlace, vm.handlePlace},
|
||||
{zapwire.MethodCancel, vm.handleCancel},
|
||||
{zapwire.MethodSubmit, vm.handleSubmit},
|
||||
}
|
||||
for _, r := range regs {
|
||||
if err := server.RegisterRaw(r.method, r.handler); err != nil {
|
||||
return fmt.Errorf("dchain: register %s: %w", r.method, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// submitTx builds a Tx from a method's frozen payload, Adds it to the mempool,
|
||||
// and blocks until consensus resolves it (or ctx is done). It returns the
|
||||
// per-tx outcome the accepted block produced. This is the single write path for
|
||||
// all three write methods — one place that turns a wire frame into a
|
||||
// consensus-decided outcome.
|
||||
func (vm *VM) submitTx(ctx context.Context, t TxType, payload []byte) (txOutcome, error) {
|
||||
tx, err := NewTx(t, payload)
|
||||
if err != nil {
|
||||
return txOutcome{}, err
|
||||
}
|
||||
txID := tx.ID()
|
||||
|
||||
// Register the waiter BEFORE Add so the outcome cannot be resolved (by a fast
|
||||
// Accept) before we are listening. register is idempotent on txID, so a
|
||||
// duplicate in-flight frame shares the one waiter.
|
||||
ch := vm.outcomes.register(txID)
|
||||
|
||||
vm.mempool.Add(tx)
|
||||
|
||||
select {
|
||||
case o := <-ch:
|
||||
return o, nil
|
||||
case <-ctx.Done():
|
||||
// The caller gave up; drop the waiter so a later Accept does not leak it.
|
||||
// The tx may still be accepted (its effect is durable) — idempotency makes
|
||||
// a retry safe.
|
||||
vm.outcomes.cancel(txID)
|
||||
return txOutcome{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// handleEnsureMarket: payload = poolId[32]. Queues an idempotent market-create tx
|
||||
// and returns ack(0, placed) once consensus records the market.
|
||||
func (vm *VM) handleEnsureMarket(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < zapwire.EnsureMarketReqSize {
|
||||
return encodeReject(0, "clob_ensure_market: short payload"), nil
|
||||
}
|
||||
o, err := vm.submitTx(ctx, TxEnsureMarket, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zapwire.EncodeAck(0, o.status, 0), nil
|
||||
}
|
||||
|
||||
// handlePlace: payload = zapwire Place (65B). Queues a resting-limit tx and
|
||||
// returns ack(orderId, placed|rejected) with the CONSENSUS-assigned order id.
|
||||
func (vm *VM) handlePlace(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < zapwire.PlaceReqSize {
|
||||
return encodeReject(0, "clob_place: short payload"), nil
|
||||
}
|
||||
o, err := vm.submitTx(ctx, TxPlace, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if o.status == zapwire.StatusRejected {
|
||||
return encodeReject(0, "clob_place: order rejected"), nil
|
||||
}
|
||||
return zapwire.EncodeAck(o.orderID, o.status, 0), nil
|
||||
}
|
||||
|
||||
// handleCancel: payload = zapwire Cancel (40B). Queues a cancel tx and returns
|
||||
// ack(orderId, canceled|rejected).
|
||||
func (vm *VM) handleCancel(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < zapwire.CancelReqSize {
|
||||
return encodeReject(0, "clob_cancel: short payload"), nil
|
||||
}
|
||||
o, err := vm.submitTx(ctx, TxCancel, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if o.status == zapwire.StatusRejected {
|
||||
return encodeReject(o.orderID, "clob_cancel: order not canceled"), nil
|
||||
}
|
||||
return zapwire.EncodeAck(o.orderID, o.status, 0), nil
|
||||
}
|
||||
|
||||
// handleSubmit: payload = zapwire Submit (66B). Queues a marketable-order tx and
|
||||
// returns the CONSENSUS-computed fills in the frozen response form: count[4] then
|
||||
// count × (price[8] + size[8] + takerSide[1]). A deduped (replayed) submit
|
||||
// returns zero fills — it was already executed exactly once.
|
||||
func (vm *VM) handleSubmit(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < zapwire.SubmitReqSize {
|
||||
return nil, fmt.Errorf("clob_submit: short payload: %d", len(payload))
|
||||
}
|
||||
o, err := vm.submitTx(ctx, TxSubmit, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zapwire.EncodeFills(o.fills), nil
|
||||
}
|
||||
|
||||
// encodeReject builds the legacy reject frame the gateway uses:
|
||||
// orderId[8] + status[1=rejected] + reasonLen[2] + reason. The proxy's DecodeAck
|
||||
// reads status from byte 8 and ignores the rest, so a reject is wire-compatible
|
||||
// with an ack on the status-read path; DecodeFills on a reject errors (too
|
||||
// short), which the proxy treats as "no fills".
|
||||
func encodeReject(orderID uint64, reason string) []byte {
|
||||
rb := []byte(reason)
|
||||
if len(rb) > 256 {
|
||||
rb = rb[:256]
|
||||
}
|
||||
out := make([]byte, 11+len(rb))
|
||||
// orderId[0:8] big-endian.
|
||||
for i := 0; i < 8; i++ {
|
||||
out[7-i] = byte(orderID >> (8 * i))
|
||||
}
|
||||
out[8] = zapwire.StatusRejected
|
||||
out[9] = byte(len(rb) >> 8)
|
||||
out[10] = byte(len(rb))
|
||||
copy(out[11:], rb)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/consensus/engine/chain/block"
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// handler_test.go proves the d-chain CLOB ZAP surface end to end over a REAL
|
||||
// rpc.Listen socket, using the SAME client (rpc.ZAPDial) the chains/dexvm proxy
|
||||
// relay uses, and decoding responses with a locally-redefined framing that is
|
||||
// byte-identical to chains/dexvm/relay.go. This is the step-4 proxy-boundary
|
||||
// byte-parity proof: a frame built/sent/decoded exactly as the proxy would.
|
||||
//
|
||||
// Because a write blocks until consensus accepts it, the test runs an auto-sealer
|
||||
// goroutine that plays the consensus engine: on a PendingTxs signal it drains the
|
||||
// mempool via BuildBlock -> Verify -> Accept. This is the harness standing in for
|
||||
// the real engine that cmd/dchain wires via rpc.Serve.
|
||||
|
||||
// ---- relay-side framing, redefined locally to prove byte-parity (must equal
|
||||
// chains/dexvm/relay.go exactly: methods, FillWireSize=17, count[4]+N*17) ----
|
||||
|
||||
const relayFillWireSize = 17
|
||||
|
||||
const (
|
||||
relayMethodEnsureMarket = "clob_ensure_market"
|
||||
relayMethodPlace = "clob_place"
|
||||
relayMethodCancel = "clob_cancel"
|
||||
relayMethodSubmit = "clob_submit"
|
||||
)
|
||||
|
||||
type relayFill struct {
|
||||
Price float64
|
||||
Size float64
|
||||
Side uint8
|
||||
}
|
||||
|
||||
// relayDecodeFills mirrors chains/dexvm/relay.go::DecodeFills byte for byte,
|
||||
// including the finite-positive and side∈{0,1} validation, so a successful decode
|
||||
// here proves the d-chain's clob_submit response is exactly what the proxy parses.
|
||||
func relayDecodeFills(data []byte) ([]relayFill, error) {
|
||||
if len(data) < 4 {
|
||||
return nil, fmt.Errorf("fills response too short: %d", len(data))
|
||||
}
|
||||
n := int(binary.BigEndian.Uint32(data[0:4]))
|
||||
if 4+n*relayFillWireSize > len(data) {
|
||||
return nil, fmt.Errorf("fills response truncated: count=%d len=%d", n, len(data))
|
||||
}
|
||||
fills := make([]relayFill, 0, n)
|
||||
off := 4
|
||||
for i := 0; i < n; i++ {
|
||||
p := math.Float64frombits(binary.BigEndian.Uint64(data[off : off+8]))
|
||||
s := math.Float64frombits(binary.BigEndian.Uint64(data[off+8 : off+16]))
|
||||
side := data[off+16]
|
||||
off += relayFillWireSize
|
||||
if !(p > 0) || math.IsInf(p, 0) {
|
||||
return nil, fmt.Errorf("fill %d: invalid price %v", i, p)
|
||||
}
|
||||
if !(s > 0) || math.IsInf(s, 0) {
|
||||
return nil, fmt.Errorf("fill %d: invalid size %v", i, s)
|
||||
}
|
||||
if side > 1 {
|
||||
return nil, fmt.Errorf("fill %d: invalid side %d", i, side)
|
||||
}
|
||||
fills = append(fills, relayFill{Price: p, Size: s, Side: side})
|
||||
}
|
||||
return fills, nil
|
||||
}
|
||||
|
||||
// relayDecodeAck mirrors zapwire/relay ack-read: orderId[0:8], status[8].
|
||||
func relayDecodeAck(resp []byte) (orderID uint64, status uint8, err error) {
|
||||
if len(resp) < 9 {
|
||||
return 0, 0, fmt.Errorf("ack too short: %d", len(resp))
|
||||
}
|
||||
return binary.BigEndian.Uint64(resp[0:8]), resp[8], nil
|
||||
}
|
||||
|
||||
// autoSealer plays the consensus engine for tests: it waits for PendingTxs and
|
||||
// seals every pending tx into an accepted block (Build -> Verify -> Accept). It
|
||||
// stops when ctx is cancelled.
|
||||
func autoSealer(ctx context.Context, t *testing.T, vm *VM) {
|
||||
t.Helper()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
msg, err := vm.WaitForEvent(ctx)
|
||||
if err != nil {
|
||||
return // ctx cancelled
|
||||
}
|
||||
if msg.Type != block.PendingTxs {
|
||||
continue
|
||||
}
|
||||
blk, err := vm.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
// Empty mempool race: nothing to do, loop.
|
||||
continue
|
||||
}
|
||||
if err := blk.Verify(ctx); err != nil {
|
||||
t.Errorf("autoSealer Verify: %v", err)
|
||||
return
|
||||
}
|
||||
if err := blk.Accept(ctx); err != nil {
|
||||
t.Errorf("autoSealer Accept: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startServer brings up a VM + a real rpc.Listen ZAP server with the clob_*
|
||||
// handlers, plus the auto-sealer. Returns the dial address and a stop func.
|
||||
func startServer(t *testing.T) (*VM, string, func()) {
|
||||
t.Helper()
|
||||
db := memdb.New()
|
||||
vm := &VM{}
|
||||
toEngine := make(chan block.Message, 64)
|
||||
if err := vm.Initialize(context.Background(), block.Init{
|
||||
DB: db,
|
||||
Log: log.NewNoOpLogger(),
|
||||
ToEngine: toEngine,
|
||||
}); err != nil {
|
||||
t.Fatalf("Initialize: %v", err)
|
||||
}
|
||||
|
||||
server, err := rpc.Listen("127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("rpc.Listen: %v", err)
|
||||
}
|
||||
if err := vm.RegisterCLOB(server); err != nil {
|
||||
t.Fatalf("RegisterCLOB: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); _ = server.Serve(ctx) }()
|
||||
go func() { defer wg.Done(); autoSealer(ctx, t, vm) }()
|
||||
|
||||
addr := server.Addr()
|
||||
stop := func() {
|
||||
cancel()
|
||||
_ = server.Close()
|
||||
_ = vm.Shutdown(context.Background())
|
||||
wg.Wait()
|
||||
}
|
||||
return vm, addr, stop
|
||||
}
|
||||
|
||||
// TestHandlerByteParityOverSocket is the step-4 proxy-boundary proof: build the
|
||||
// FROZEN frames, send them over a real ZAP socket via the proxy's exact client,
|
||||
// and decode every response with the proxy's exact framing. ensure_market ->
|
||||
// place a maker -> submit a crossing taker -> assert the fills decode (proving
|
||||
// the d-chain returns CONSENSUS-computed fills in the byte-identical wire form).
|
||||
func TestHandlerByteParityOverSocket(t *testing.T) {
|
||||
_, addr, stop := startServer(t)
|
||||
defer stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := rpc.ZAPDial(ctx, addr)
|
||||
if err != nil {
|
||||
t.Fatalf("ZAPDial: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var pool [32]byte
|
||||
pool[0], pool[1] = 0xbe, 0xef
|
||||
|
||||
// 1) ensure_market — frozen EnsureMarket frame, ack response.
|
||||
resp, err := conn.Call(ctx, relayMethodEnsureMarket, zapwire.EncodeEnsureMarket(pool))
|
||||
if err != nil {
|
||||
t.Fatalf("ensure_market Call: %v", err)
|
||||
}
|
||||
if _, status, err := relayDecodeAck(resp); err != nil || status != zapwire.StatusPlaced {
|
||||
t.Fatalf("ensure_market ack: status=%d err=%v", status, err)
|
||||
}
|
||||
|
||||
// 2) place a resting ask @101 size 5 — frozen Place frame, ack with order id.
|
||||
resp, err = conn.Call(ctx, relayMethodPlace,
|
||||
zapwire.EncodePlace(pool, zapwire.SideSell, 101.0, 5.0, "maker"))
|
||||
if err != nil {
|
||||
t.Fatalf("place Call: %v", err)
|
||||
}
|
||||
makerID, status, err := relayDecodeAck(resp)
|
||||
if err != nil || status != zapwire.StatusPlaced {
|
||||
t.Fatalf("place ack: status=%d err=%v", status, err)
|
||||
}
|
||||
if makerID == 0 {
|
||||
t.Fatal("place returned zero order id")
|
||||
}
|
||||
|
||||
// 3) submit a buy that crosses — frozen Submit frame, FILLS response decoded
|
||||
// with the proxy's exact DecodeFills (the byte-parity assertion).
|
||||
resp, err = conn.Call(ctx, relayMethodSubmit,
|
||||
zapwire.EncodeSubmit(pool, zapwire.SideBuy, false, 101.0, 3.0, "taker"))
|
||||
if err != nil {
|
||||
t.Fatalf("submit Call: %v", err)
|
||||
}
|
||||
fills, err := relayDecodeFills(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("proxy DecodeFills rejected the d-chain response (BYTE PARITY BROKEN): %v", err)
|
||||
}
|
||||
if len(fills) != 1 {
|
||||
t.Fatalf("expected 1 fill, got %d", len(fills))
|
||||
}
|
||||
if fills[0].Side != zapwire.SideBuy {
|
||||
t.Errorf("fill taker side = %d, want buy(0)", fills[0].Side)
|
||||
}
|
||||
if fills[0].Size != 3.0 {
|
||||
t.Errorf("fill size = %v, want 3.0", fills[0].Size)
|
||||
}
|
||||
if fills[0].Price != 101.0 {
|
||||
t.Errorf("fill price = %v, want 101.0", fills[0].Price)
|
||||
}
|
||||
|
||||
// Cross-check: the same response decodes identically with the dex's own
|
||||
// zapwire.DecodeFills (the two definitions are byte-identical).
|
||||
zfills, err := zapwire.DecodeFills(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("zapwire.DecodeFills: %v", err)
|
||||
}
|
||||
if len(zfills) != len(fills) || zfills[0].Price != fills[0].Price ||
|
||||
zfills[0].Size != fills[0].Size || zfills[0].TakerSide != fills[0].Side {
|
||||
t.Error("zapwire and relay fill decodes disagree (frame not byte-identical)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlerIdempotentReplay proves the d-chain STATE idempotency: replaying the
|
||||
// identical clob_submit frame (a proxy retry on a dropped socket) executes the
|
||||
// order EXACTLY ONCE — the second call returns zero fills, and the resting book
|
||||
// is consumed only once.
|
||||
func TestHandlerIdempotentReplay(t *testing.T) {
|
||||
vm, addr, stop := startServer(t)
|
||||
defer stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := rpc.ZAPDial(ctx, addr)
|
||||
if err != nil {
|
||||
t.Fatalf("ZAPDial: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var pool [32]byte
|
||||
pool[0] = 0x11
|
||||
|
||||
mustCall := func(method string, payload []byte) []byte {
|
||||
t.Helper()
|
||||
resp, err := conn.Call(ctx, method, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", method, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
mustCall(relayMethodEnsureMarket, zapwire.EncodeEnsureMarket(pool))
|
||||
mustCall(relayMethodPlace, zapwire.EncodePlace(pool, zapwire.SideSell, 50.0, 10.0, "maker"))
|
||||
|
||||
// First submit: a 4-unit buy crosses for 4 units.
|
||||
submit := zapwire.EncodeSubmit(pool, zapwire.SideBuy, false, 50.0, 4.0, "taker")
|
||||
fills1, err := zapwire.DecodeFills(mustCall(relayMethodSubmit, submit))
|
||||
if err != nil {
|
||||
t.Fatalf("submit1 decode: %v", err)
|
||||
}
|
||||
if len(fills1) != 1 || fills1[0].Size != 4.0 {
|
||||
t.Fatalf("submit1 fills = %+v, want one 4.0 fill", fills1)
|
||||
}
|
||||
|
||||
// Replay the EXACT same submit frame (same bytes -> same tx id). Idempotency
|
||||
// must make it a no-op: zero fills, and the resting maker keeps the 6 units
|
||||
// the first submit left (a double-execution would consume 4 more).
|
||||
fills2, err := zapwire.DecodeFills(mustCall(relayMethodSubmit, submit))
|
||||
if err != nil {
|
||||
t.Fatalf("submit2 (replay) decode: %v", err)
|
||||
}
|
||||
if len(fills2) != 0 {
|
||||
t.Fatalf("replayed submit executed again: got %d fills, want 0 (idempotency broken)", len(fills2))
|
||||
}
|
||||
|
||||
// The maker's remaining size confirms exactly-once: 10 - 4 = 6 left.
|
||||
ob := vm.books[pool]
|
||||
if ob == nil {
|
||||
t.Fatal("market book missing")
|
||||
}
|
||||
var remaining float64
|
||||
for _, o := range ob.Orders {
|
||||
remaining += o.RemainingSize
|
||||
}
|
||||
if remaining != 6.0 {
|
||||
t.Errorf("resting size = %v, want 6.0 (replay must not double-consume)", remaining)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build dchain && cgo
|
||||
// +build dchain,cgo
|
||||
|
||||
package dchain
|
||||
|
||||
// localnet_e2e_test.go is the CAPSTONE venue-level localnet proof for the
|
||||
// standalone D-Chain DEX VM. Unlike handler_test.go (memdb, single market), it
|
||||
// stands the venue up exactly as cmd/dchain would at runtime:
|
||||
//
|
||||
// - a REAL on-disk zapdb (the authoritative persisted chainstate, not memdb),
|
||||
// - the clob_* ZAP surface served over a REAL rpc.Listen socket (the same
|
||||
// transport the chains/dexvm proxy + 0x9010 precompile dial),
|
||||
// - an auto-sealer goroutine playing the consensus engine (BuildBlock ->
|
||||
// Verify -> Accept) — the harness that cmd/dchain's rpc.Serve gets from the
|
||||
// node runtime,
|
||||
// - the TWO canonical first-pair markets the maker seeds: LUX/LUSD and
|
||||
// LUX/LETH (dex-architecture-canonical: maker seeds REAL D-Chain pools via
|
||||
// resting limit orders = V4 "liquidity"),
|
||||
// - an external ZAP client (rpc.ZAPDial) that SEEDS resting liquidity
|
||||
// (clob_place), then crosses it with marketable takers (clob_submit), and
|
||||
// verifies the CONSENSUS-COMPUTED fills + the resulting book,
|
||||
// - a Shutdown + restart that rebuilds the authoritative book from persisted
|
||||
// rows and proves the resting liquidity SURVIVED (it is a real chain, not an
|
||||
// in-memory authority).
|
||||
//
|
||||
// Every order crosses through the full match -> Verify -> Accept consensus path:
|
||||
// the fills a caller receives are what every validator re-derived at Verify, not
|
||||
// a synchronous single-process book mutation. This is the on-chain e2e the
|
||||
// capstone requires, at the venue (single-operator d-chain) layer.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/consensus/engine/chain/block"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/badgerdb"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// poolIDForSymbol derives a deterministic 32-byte poolId from a market symbol.
|
||||
// The real V4 poolId is keccak256(PoolKey); for the venue e2e any stable,
|
||||
// collision-free 32-byte identity is sufficient (the d-chain keys markets by the
|
||||
// raw 32 bytes). We embed the ASCII symbol so logs are human-readable and two
|
||||
// distinct symbols map to two distinct books.
|
||||
func poolIDForSymbol(sym string) [32]byte {
|
||||
var p [32]byte
|
||||
copy(p[:], sym)
|
||||
// Tag the high byte so an all-symbol-prefix pool can't collide with a zero id.
|
||||
p[31] = 0xD0
|
||||
return p
|
||||
}
|
||||
|
||||
// venue is a booted standalone D-Chain venue: an on-disk VM + real ZAP socket +
|
||||
// auto-sealer, plus the dial address. dir is the zapdb path so a restart can
|
||||
// reopen the same authoritative state.
|
||||
type venue struct {
|
||||
vm *VM
|
||||
db database.Database
|
||||
addr string
|
||||
dir string
|
||||
cancel context.CancelFunc
|
||||
wg *sync.WaitGroup
|
||||
server rpc.Server
|
||||
}
|
||||
|
||||
// bootVenue stands up the venue on an on-disk badger/zapdb at dir. If dir
|
||||
// already holds a chain (restart), Initialize rebuilds the authoritative book
|
||||
// from the persisted order rows rather than bootstrapping a fresh genesis.
|
||||
func bootVenue(t *testing.T, dir string) *venue {
|
||||
t.Helper()
|
||||
db, err := badgerdb.New(dir, nil, "dchain", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("badgerdb.New(%s): %v", dir, err)
|
||||
}
|
||||
vm := &VM{}
|
||||
toEngine := make(chan block.Message, 256)
|
||||
if err := vm.Initialize(context.Background(), block.Init{
|
||||
DB: db,
|
||||
Log: log.NewNoOpLogger(),
|
||||
ToEngine: toEngine,
|
||||
}); err != nil {
|
||||
t.Fatalf("VM.Initialize: %v", err)
|
||||
}
|
||||
|
||||
server, err := rpc.Listen("127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("rpc.Listen: %v", err)
|
||||
}
|
||||
if err := vm.RegisterCLOB(server); err != nil {
|
||||
t.Fatalf("RegisterCLOB: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); _ = server.Serve(ctx) }()
|
||||
go func() { defer wg.Done(); autoSealer(ctx, t, vm) }()
|
||||
|
||||
return &venue{vm: vm, db: db, addr: server.Addr(), dir: dir, cancel: cancel, wg: wg, server: server}
|
||||
}
|
||||
|
||||
// stop tears down the serving goroutines, shuts the VM down, and closes the
|
||||
// on-disk db (the VM does NOT own the injected db — at runtime the node runtime
|
||||
// owns it; here the harness owns it), releasing the badger directory lock so a
|
||||
// restart can reopen the same authoritative chainstate.
|
||||
func (v *venue) stop() {
|
||||
v.cancel()
|
||||
_ = v.server.Close()
|
||||
_ = v.vm.Shutdown(context.Background())
|
||||
v.wg.Wait()
|
||||
_ = v.db.Close()
|
||||
}
|
||||
|
||||
// e2eClient is an external ZAP client speaking the FROZEN clob_* frames — the
|
||||
// exact surface the chains/dexvm proxy relay and the 0x9010 precompile use.
|
||||
type e2eClient struct {
|
||||
conn *rpc.ZAPConn
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func dialVenue(t *testing.T, addr string) (*e2eClient, func()) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
conn, err := rpc.ZAPDial(ctx, addr)
|
||||
if err != nil {
|
||||
cancel()
|
||||
t.Fatalf("ZAPDial(%s): %v", addr, err)
|
||||
}
|
||||
return &e2eClient{conn: conn, ctx: ctx}, func() { _ = conn.Close(); cancel() }
|
||||
}
|
||||
|
||||
func (c *e2eClient) ensureMarket(t *testing.T, pool [32]byte) {
|
||||
t.Helper()
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodEnsureMarket, zapwire.EncodeEnsureMarket(pool))
|
||||
if err != nil {
|
||||
t.Fatalf("clob_ensure_market: %v", err)
|
||||
}
|
||||
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
|
||||
t.Fatalf("clob_ensure_market not placed: resp=%x", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// place rests a limit order (maker liquidity). Returns the consensus-assigned id.
|
||||
func (c *e2eClient) place(t *testing.T, pool [32]byte, side uint8, price, size float64, user string) uint64 {
|
||||
t.Helper()
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodPlace, zapwire.EncodePlace(pool, side, price, size, user))
|
||||
if err != nil {
|
||||
t.Fatalf("clob_place %s %v@%v: %v", sideName(side), size, price, err)
|
||||
}
|
||||
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
|
||||
t.Fatalf("clob_place rejected %s %v@%v: resp=%x", sideName(side), size, price, resp)
|
||||
}
|
||||
return binary.BigEndian.Uint64(resp[0:8])
|
||||
}
|
||||
|
||||
// submit crosses a marketable order against the book and returns the
|
||||
// consensus-computed fills (decoded via the proxy's exact frame).
|
||||
func (c *e2eClient) submit(t *testing.T, pool [32]byte, side uint8, limit, size float64, user string) []zapwire.Fill {
|
||||
t.Helper()
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodSubmit,
|
||||
zapwire.EncodeSubmit(pool, side, false, limit, size, user))
|
||||
if err != nil {
|
||||
t.Fatalf("clob_submit %s %v@%v: %v", sideName(side), size, limit, err)
|
||||
}
|
||||
fills, err := zapwire.DecodeFills(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("clob_submit fills decode (BYTE PARITY): %v (resp=%x)", err, resp)
|
||||
}
|
||||
return fills
|
||||
}
|
||||
|
||||
func sideName(s uint8) string {
|
||||
if s == zapwire.SideBuy {
|
||||
return "buy"
|
||||
}
|
||||
return "sell"
|
||||
}
|
||||
|
||||
// restingByMarket sums the remaining resting size on a market's authoritative
|
||||
// in-RAM book (a fold of the committed rows).
|
||||
func (v *venue) restingByMarket(pool [32]byte) (orders int, remaining float64) {
|
||||
ob := v.vm.books[pool]
|
||||
if ob == nil {
|
||||
return 0, 0
|
||||
}
|
||||
for _, o := range ob.Orders {
|
||||
orders++
|
||||
remaining += o.RemainingSize
|
||||
}
|
||||
return orders, remaining
|
||||
}
|
||||
|
||||
// TestLocalnetVenueE2E is the capstone venue bring-up + on-chain e2e:
|
||||
// 1. boot the venue on an on-disk zapdb (real persisted chainstate),
|
||||
// 2. seed LUX/LUSD and LUX/LETH with resting maker liquidity (clob_place),
|
||||
// 3. cross both books with marketable takers (clob_submit),
|
||||
// 4. assert the consensus-computed fills are correct + books updated,
|
||||
// 5. restart the venue and prove the resting liquidity SURVIVED (real chain).
|
||||
func TestLocalnetVenueE2E(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
v := bootVenue(t, dir)
|
||||
t.Logf("venue d-chain up: ZAP endpoint=%s zapdb=%s", v.addr, dir)
|
||||
|
||||
cli, closeCli := dialVenue(t, v.addr)
|
||||
|
||||
lusd := poolIDForSymbol("LUX/LUSD")
|
||||
leth := poolIDForSymbol("LUX/LETH")
|
||||
|
||||
// --- maker seeding: ensure markets, then rest two-sided liquidity. This is
|
||||
// what `maker -dex <endpoint>` does for the first pairs, in clob_* form. ---
|
||||
cli.ensureMarket(t, lusd)
|
||||
cli.ensureMarket(t, leth)
|
||||
t.Logf("markets ensured: LUX/LUSD, LUX/LETH")
|
||||
|
||||
// LUX/LUSD book: asks 10@100, 5@101 ; bids 8@99, 12@98.
|
||||
cli.place(t, lusd, zapwire.SideSell, 100.0, 10.0, "maker")
|
||||
cli.place(t, lusd, zapwire.SideSell, 101.0, 5.0, "maker")
|
||||
cli.place(t, lusd, zapwire.SideBuy, 99.0, 8.0, "maker")
|
||||
cli.place(t, lusd, zapwire.SideBuy, 98.0, 12.0, "maker")
|
||||
|
||||
// LUX/LETH book: asks [email protected], [email protected] ; bids [email protected].
|
||||
cli.place(t, leth, zapwire.SideSell, 0.050, 4.0, "maker")
|
||||
cli.place(t, leth, zapwire.SideSell, 0.051, 6.0, "maker")
|
||||
cli.place(t, leth, zapwire.SideBuy, 0.049, 3.0, "maker")
|
||||
|
||||
if n, rem := v.restingByMarket(lusd); n != 4 || rem != 35.0 {
|
||||
t.Fatalf("LUX/LUSD seeded book: orders=%d remaining=%v, want 4 / 35.0", n, rem)
|
||||
}
|
||||
if n, rem := v.restingByMarket(leth); n != 3 || rem != 13.0 {
|
||||
t.Fatalf("LUX/LETH seeded book: orders=%d remaining=%v, want 3 / 13.0", n, rem)
|
||||
}
|
||||
t.Logf("seeded LUX/LUSD: 4 resting orders / 35.0 total; LUX/LETH: 3 resting / 13.0 total")
|
||||
|
||||
// --- on-chain cross #1: a 12-unit buy @101 on LUX/LUSD sweeps 10@100 +
|
||||
// 2@101 (two fills, multi-level), VWAP between 100 and 101. ---
|
||||
f1 := cli.submit(t, lusd, zapwire.SideBuy, 101.0, 12.0, "taker")
|
||||
if len(f1) != 2 {
|
||||
t.Fatalf("LUX/LUSD cross: got %d fills, want 2 (multi-level sweep)", len(f1))
|
||||
}
|
||||
var got1 float64
|
||||
for i, f := range f1 {
|
||||
if f.TakerSide != zapwire.SideBuy {
|
||||
t.Errorf("fill %d takerSide=%d want buy", i, f.TakerSide)
|
||||
}
|
||||
got1 += f.Size
|
||||
t.Logf(" LUX/LUSD fill %d: %v @ %v (takerSide=%s)", i, f.Size, f.Price, sideName(f.TakerSide))
|
||||
}
|
||||
if got1 != 12.0 {
|
||||
t.Fatalf("LUX/LUSD filled %v, want 12.0", got1)
|
||||
}
|
||||
if f1[0].Price != 100.0 || f1[1].Price != 101.0 {
|
||||
t.Fatalf("LUX/LUSD fill prices = %v,%v want 100,101 (price-time priority)", f1[0].Price, f1[1].Price)
|
||||
}
|
||||
// After: the 5@101 ask is fully consumed by the 2-unit take? No: 10@100 fully
|
||||
// + 2 of 5@101 -> 3@101 remains. Asks remaining = 3; bids untouched = 20.
|
||||
if n, rem := v.restingByMarket(lusd); rem != 23.0 {
|
||||
t.Fatalf("LUX/LUSD post-cross resting=%v (orders=%d), want 23.0 (3@101 ask + 8@99 + 12@98 bids)", rem, n)
|
||||
}
|
||||
t.Logf("LUX/LUSD post-cross: 23.0 resting (3@101 ask + 20 bid) — consensus-settled")
|
||||
|
||||
// --- on-chain cross #2: a 5-unit sell @0.049 on LUX/LETH hits the [email protected]
|
||||
// bid (one fill of 3), residual 2 does NOT rest (clob_submit is IOC-style:
|
||||
// marketable, unfilled remainder dropped). ---
|
||||
f2 := cli.submit(t, leth, zapwire.SideSell, 0.049, 5.0, "taker")
|
||||
if len(f2) != 1 {
|
||||
t.Fatalf("LUX/LETH cross: got %d fills, want 1", len(f2))
|
||||
}
|
||||
if f2[0].Size != 3.0 || f2[0].Price != 0.049 || f2[0].TakerSide != zapwire.SideSell {
|
||||
t.Fatalf("LUX/LETH fill = %v@%v side=%d, want [email protected] sell", f2[0].Size, f2[0].Price, f2[0].TakerSide)
|
||||
}
|
||||
t.Logf(" LUX/LETH fill: %v @ %v (takerSide=sell)", f2[0].Size, f2[0].Price)
|
||||
// LUX/LETH bids now empty; asks ([email protected] + [email protected] = 10) untouched.
|
||||
if n, rem := v.restingByMarket(leth); rem != 10.0 {
|
||||
t.Fatalf("LUX/LETH post-cross resting=%v (orders=%d), want 10.0 (asks only)", rem, n)
|
||||
}
|
||||
t.Logf("LUX/LETH post-cross: 10.0 resting (asks only, bid fully consumed) — consensus-settled")
|
||||
|
||||
lastHeightBefore := v.vm.lastAcceptedHeight
|
||||
closeCli()
|
||||
v.stop()
|
||||
t.Logf("venue stopped at height=%d; restarting from the same zapdb...", lastHeightBefore)
|
||||
|
||||
// --- restart: reopen the SAME on-disk zapdb. The authoritative book is
|
||||
// rebuilt from persisted rows; resting liquidity must be byte-identical. ---
|
||||
v2 := bootVenue(t, dir)
|
||||
defer v2.stop()
|
||||
|
||||
if h := v2.vm.lastAcceptedHeight; h != lastHeightBefore {
|
||||
t.Fatalf("restart height=%d, want %d (persisted chainstate must survive)", h, lastHeightBefore)
|
||||
}
|
||||
if n, rem := v2.restingByMarket(lusd); rem != 23.0 {
|
||||
t.Fatalf("LUX/LUSD after restart resting=%v (orders=%d), want 23.0 (must survive restart)", rem, n)
|
||||
}
|
||||
if n, rem := v2.restingByMarket(leth); rem != 10.0 {
|
||||
t.Fatalf("LUX/LETH after restart resting=%v (orders=%d), want 10.0 (must survive restart)", rem, n)
|
||||
}
|
||||
t.Logf("RESTART OK: height=%d preserved; LUX/LUSD=23.0, LUX/LETH=10.0 resting rebuilt from persisted rows", v2.vm.lastAcceptedHeight)
|
||||
|
||||
// --- prove the venue still matches after restart: cross the surviving
|
||||
// LUX/LUSD 3@101 ask with a 3@101 buy. One fill, book empties to bids only. ---
|
||||
cli2, closeCli2 := dialVenue(t, v2.addr)
|
||||
defer closeCli2()
|
||||
f3 := cli2.submit(t, lusd, zapwire.SideBuy, 101.0, 3.0, "taker2")
|
||||
if len(f3) != 1 || f3[0].Size != 3.0 || f3[0].Price != 101.0 {
|
||||
t.Fatalf("post-restart LUX/LUSD cross = %+v, want one 3.0@101 fill", f3)
|
||||
}
|
||||
if _, rem := v2.restingByMarket(lusd); rem != 20.0 {
|
||||
t.Fatalf("post-restart LUX/LUSD resting=%v, want 20.0 (bids only, ask fully consumed)", rem)
|
||||
}
|
||||
t.Logf("post-restart cross OK: LUX/LUSD ask consumed, 20.0 bid liquidity remains")
|
||||
|
||||
fmt.Printf("\n=== VENUE LOCALNET E2E: PASS ===\n"+
|
||||
" endpoint=%s\n"+
|
||||
" LUX/LUSD + LUX/LETH seeded, crossed on-chain (match->Verify->Accept), restarted, re-crossed\n"+
|
||||
" final: LUX/LUSD 20.0 bid liquidity; height=%d (persisted)\n",
|
||||
v2.addr, v2.vm.lastAcceptedHeight)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/consensus/engine/chain/block"
|
||||
)
|
||||
|
||||
// mempool.go is the d-chain pending-transaction queue. ZAP write frames
|
||||
// (clob_place/cancel/submit) are NOT executed synchronously — the handler decodes
|
||||
// the frame into a Tx, Adds it here, and the mempool signals the consensus engine
|
||||
// (init.ToEngine <- PendingTxs) that there is work. The engine responds by
|
||||
// calling VM.BuildBlock, which drains the mempool in arrival (sequence) order.
|
||||
// This is the single seam between "a client asked for an order" and "consensus
|
||||
// decides the order's effect": no order mutates the book outside a verified,
|
||||
// accepted block.
|
||||
|
||||
// mempool is a FIFO of pending transactions guarded by a mutex. It is small and
|
||||
// deliberately simple: ordering is arrival order, which is the sequence the
|
||||
// matcher consumes. (A fee-priority mempool is a future concern; the venue's CLOB
|
||||
// is sequence-fair by design.)
|
||||
type mempool struct {
|
||||
mu sync.Mutex
|
||||
txs []*Tx
|
||||
|
||||
// toEngine is the VM->engine notification channel from block.Init. Sending
|
||||
// PendingTxs tells the engine to build a block. nil before Initialize.
|
||||
toEngine chan<- block.Message
|
||||
}
|
||||
|
||||
// newMempool creates an empty mempool wired to the engine notification channel.
|
||||
func newMempool(toEngine chan<- block.Message) *mempool {
|
||||
return &mempool{toEngine: toEngine}
|
||||
}
|
||||
|
||||
// Add appends a tx and signals the engine that a block can be built. The signal
|
||||
// is non-blocking: PendingTxs is a level-trigger ("there is work"), so a full
|
||||
// channel (engine already notified) is fine to drop — the engine will drain the
|
||||
// mempool fully on its next BuildBlock regardless of how many signals it saw.
|
||||
func (m *mempool) Add(tx *Tx) {
|
||||
m.mu.Lock()
|
||||
m.txs = append(m.txs, tx)
|
||||
m.mu.Unlock()
|
||||
m.signal()
|
||||
}
|
||||
|
||||
// signal sends a non-blocking PendingTxs to the engine.
|
||||
func (m *mempool) signal() {
|
||||
if m.toEngine == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case m.toEngine <- block.Message{Type: block.PendingTxs}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of pending txs.
|
||||
func (m *mempool) Len() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.txs)
|
||||
}
|
||||
|
||||
// Drain removes and returns up to max pending txs in arrival order (all of them
|
||||
// if max <= 0). The returned slice is owned by the caller.
|
||||
func (m *mempool) Drain(max int) []*Tx {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
n := len(m.txs)
|
||||
if max > 0 && max < n {
|
||||
n = max
|
||||
}
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]*Tx, n)
|
||||
copy(out, m.txs[:n])
|
||||
m.txs = m.txs[n:]
|
||||
// Reclaim the backing array when fully drained.
|
||||
if len(m.txs) == 0 {
|
||||
m.txs = nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Requeue puts txs back at the FRONT of the mempool in their original order. Used
|
||||
// when a built block is rejected: its txs return to the pool to be reconsidered
|
||||
// in the next block, preserving arrival order.
|
||||
func (m *mempool) Requeue(txs []*Tx) {
|
||||
if len(txs) == 0 {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.txs = append(txs, m.txs...)
|
||||
m.mu.Unlock()
|
||||
m.signal()
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// results.go is the seam between consensus and a synchronous client: the per-tx
|
||||
// OUTCOME a block's execution produced, and the VM's registry that lets a ZAP
|
||||
// handler block until "its" tx is decided by an accepted block.
|
||||
//
|
||||
// The decomplected shape: a clob_* write is NOT executed when it arrives — it is
|
||||
// queued, ordered by consensus, matched at Verify, committed at Accept. But the
|
||||
// caller (the proxy's synchronous Relay, the precompile's swap) needs the
|
||||
// resulting fills/ack back. So the handler registers a waiter keyed by the tx's
|
||||
// content-addressed id, Adds the tx to the mempool, and parks. When the block
|
||||
// carrying that tx is Accepted, the VM resolves the waiter with the consensus
|
||||
// outcome. Verify/Reject never resolve (a rejected block's txs return to the
|
||||
// mempool to be retried; only an accepted block produces a final outcome).
|
||||
|
||||
// txOutcome is the consensus-decided result of one transaction, attributed by tx
|
||||
// id. For a submit it carries the fills; for a place it carries the resting
|
||||
// order id; for a cancel the cancelled id; for ensure_market just status. status
|
||||
// uses the FROZEN zapwire status bytes so the handler encodes a byte-identical
|
||||
// ack/fills response without re-deriving anything.
|
||||
type txOutcome struct {
|
||||
txID ids.ID
|
||||
typ TxType
|
||||
orderID uint64 // placed / cancelled order id (0 for submit/ensure)
|
||||
fills []zapwire.Fill // submit fills (nil otherwise)
|
||||
status uint8 // zapwire.StatusPlaced / StatusCanceled / StatusRejected
|
||||
}
|
||||
|
||||
// toWireFills converts a submit's matched trades into the FROZEN wire Fill form
|
||||
// (price, size, takerSide). This is the single conversion from the internal
|
||||
// lx.Trade to the 17-byte wire fill; the handler then calls zapwire.EncodeFills.
|
||||
func toWireFills(fills []lx.Trade, takerSide lx.Side) []zapwire.Fill {
|
||||
out := make([]zapwire.Fill, len(fills))
|
||||
for i, f := range fills {
|
||||
out[i] = zapwire.Fill{
|
||||
Price: f.Price,
|
||||
Size: f.Size,
|
||||
TakerSide: uint8(takerSide),
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// outcomeFromApply builds the per-tx outcome for an applied tx. It is called in
|
||||
// execution order so the outcome's identity (the tx id) is exactly what the
|
||||
// handler registered its waiter under.
|
||||
func outcomeFromApply(tx *Tx, res applyResult) txOutcome {
|
||||
o := txOutcome{txID: tx.ID(), typ: tx.Type}
|
||||
switch tx.Type {
|
||||
case TxPlace:
|
||||
if res.Placed != nil {
|
||||
o.orderID = res.Placed.ID
|
||||
o.status = zapwire.StatusPlaced
|
||||
} else {
|
||||
o.status = zapwire.StatusRejected
|
||||
}
|
||||
case TxCancel:
|
||||
if res.Canceled != 0 {
|
||||
o.orderID = res.Canceled
|
||||
o.status = zapwire.StatusCanceled
|
||||
} else {
|
||||
o.status = zapwire.StatusRejected
|
||||
}
|
||||
case TxSubmit:
|
||||
o.fills = toWireFills(res.Fills, res.TakerSide)
|
||||
o.status = zapwire.StatusPlaced
|
||||
case TxEnsureMarket:
|
||||
o.status = zapwire.StatusPlaced
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// outcomeRegistry maps a pending tx id to the channel a parked handler waits on.
|
||||
// A buffered (cap 1) channel means resolve never blocks even if the waiter has
|
||||
// already given up (ctx timeout) and stopped receiving.
|
||||
type outcomeRegistry struct {
|
||||
mu sync.Mutex
|
||||
waiters map[ids.ID]chan txOutcome
|
||||
}
|
||||
|
||||
func newOutcomeRegistry() *outcomeRegistry {
|
||||
return &outcomeRegistry{waiters: map[ids.ID]chan txOutcome{}}
|
||||
}
|
||||
|
||||
// register returns a channel that will receive the outcome for txID. If a waiter
|
||||
// already exists for the id (a duplicate in-flight submit), the existing channel
|
||||
// is returned so both callers observe the same single consensus outcome.
|
||||
func (r *outcomeRegistry) register(txID ids.ID) chan txOutcome {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ch, ok := r.waiters[txID]; ok {
|
||||
return ch
|
||||
}
|
||||
ch := make(chan txOutcome, 1)
|
||||
r.waiters[txID] = ch
|
||||
return ch
|
||||
}
|
||||
|
||||
// resolve delivers an outcome to its waiter (if any) and removes the entry. The
|
||||
// send is non-blocking on a cap-1 channel; a missing waiter (no local handler
|
||||
// parked — e.g. on a follower validator) is a no-op.
|
||||
func (r *outcomeRegistry) resolve(o txOutcome) {
|
||||
r.mu.Lock()
|
||||
ch, ok := r.waiters[o.txID]
|
||||
if ok {
|
||||
delete(r.waiters, o.txID)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
if ok {
|
||||
ch <- o // cap-1, never blocks
|
||||
}
|
||||
}
|
||||
|
||||
// cancel drops a waiter without delivering (used when the handler abandons the
|
||||
// wait, so a later resolve does not leak the entry).
|
||||
func (r *outcomeRegistry) cancel(txID ids.ID) {
|
||||
r.mu.Lock()
|
||||
delete(r.waiters, txID)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/crypto/merkle"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// state.go is the d-chain key/value schema and the state root. The schema lives
|
||||
// in the VM's init.DB namespace; the proxy (chains/dexvm) holds NONE of these
|
||||
// keys — proxy is transport, d-chain is state.
|
||||
//
|
||||
// KEY SCHEMA (all values are fixed-width images from pkg/lx/persist.go):
|
||||
//
|
||||
// order:<poolID:32><orderID:8> -> DEXOrder (64B) resting liquidity
|
||||
// book:<poolID:32> -> uint64 LastOrderID watermark for the market
|
||||
// market:<poolID:32> -> symbol bytes market existence + symbol
|
||||
// trade:<height:8><seq:8> -> DEXTrade (80B) append-only fill log
|
||||
// meta:lastAccepted -> ids.ID (32B) last accepted block id
|
||||
// meta:height -> uint64 (8B) last accepted height
|
||||
// meta:root -> [32]byte last execution root
|
||||
//
|
||||
// The order:* rows are sufficient to rebuild every book on restart (the in-RAM
|
||||
// lx.OrderBook is a fold of these). trade:* is the durable event log. The roots
|
||||
// are derived, not stored as truth — meta:root caches the parent root for the
|
||||
// next block's chained composition.
|
||||
const (
|
||||
prefixOrder = "order:"
|
||||
prefixBook = "book:"
|
||||
prefixMarket = "market:"
|
||||
prefixTrade = "trade:"
|
||||
prefixSeen = "seen:" // processed tx ids (idempotency / replay dedup)
|
||||
prefixMeta = "meta:"
|
||||
|
||||
metaLastAccepted = prefixMeta + "lastAccepted"
|
||||
metaHeight = prefixMeta + "height"
|
||||
metaRoot = prefixMeta + "root"
|
||||
)
|
||||
|
||||
// seenKey builds seen:<txID:32>. A tx id committed here has been applied by an
|
||||
// accepted block; re-applying it is a no-op. This is the d-chain STATE
|
||||
// idempotency index: the frozen zapwire frame carries no nonce, so dedup is
|
||||
// content-addressed on the tx id (which subsumes the wire user[16] and the full
|
||||
// payload). A relay that retries a dropped clob_submit re-sends the identical
|
||||
// bytes -> identical tx id -> deduped, exactly once.
|
||||
func seenKey(txID ids.ID) []byte {
|
||||
k := make([]byte, len(prefixSeen)+32)
|
||||
copy(k, prefixSeen)
|
||||
copy(k[len(prefixSeen):], txID[:])
|
||||
return k
|
||||
}
|
||||
|
||||
// isSeen reports whether a tx id has already been applied by an accepted block.
|
||||
func isSeen(db database.KeyValueReader, txID ids.ID) (bool, error) {
|
||||
_, err := db.Get(seenKey(txID))
|
||||
if err == database.ErrNotFound {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// markSeen records a tx id as applied (written into the block's overlay so it
|
||||
// commits atomically with the tx's effects).
|
||||
func markSeen(db database.KeyValueWriter, txID ids.ID) error {
|
||||
return db.Put(seenKey(txID), []byte{1})
|
||||
}
|
||||
|
||||
// orderKey builds order:<poolID:32><orderID:8>.
|
||||
func orderKey(poolID [32]byte, orderID uint64) []byte {
|
||||
k := make([]byte, len(prefixOrder)+32+8)
|
||||
copy(k, prefixOrder)
|
||||
copy(k[len(prefixOrder):], poolID[:])
|
||||
binary.BigEndian.PutUint64(k[len(prefixOrder)+32:], orderID)
|
||||
return k
|
||||
}
|
||||
|
||||
// orderPrefixFor builds the order:<poolID:32> iteration prefix for one market.
|
||||
func orderPrefixFor(poolID [32]byte) []byte {
|
||||
k := make([]byte, len(prefixOrder)+32)
|
||||
copy(k, prefixOrder)
|
||||
copy(k[len(prefixOrder):], poolID[:])
|
||||
return k
|
||||
}
|
||||
|
||||
// marketKey builds market:<poolID:32>.
|
||||
func marketKey(poolID [32]byte) []byte {
|
||||
k := make([]byte, len(prefixMarket)+32)
|
||||
copy(k, prefixMarket)
|
||||
copy(k[len(prefixMarket):], poolID[:])
|
||||
return k
|
||||
}
|
||||
|
||||
// bookKey builds book:<poolID:32> (the per-market LastOrderID watermark).
|
||||
func bookKey(poolID [32]byte) []byte {
|
||||
k := make([]byte, len(prefixBook)+32)
|
||||
copy(k, prefixBook)
|
||||
copy(k[len(prefixBook):], poolID[:])
|
||||
return k
|
||||
}
|
||||
|
||||
// tradeKey builds trade:<height:8><seq:8>.
|
||||
func tradeKey(height, seq uint64) []byte {
|
||||
k := make([]byte, len(prefixTrade)+16)
|
||||
copy(k, prefixTrade)
|
||||
binary.BigEndian.PutUint64(k[len(prefixTrade):], height)
|
||||
binary.BigEndian.PutUint64(k[len(prefixTrade)+8:], seq)
|
||||
return k
|
||||
}
|
||||
|
||||
// putOrderRow writes a resting order row, or deletes it when the order is no
|
||||
// longer live (filled or cancelled or zero remaining). The book is the live
|
||||
// resting set; spent rows are removed so iteration over order:* yields exactly
|
||||
// the rebuildable liquidity.
|
||||
func putOrderRow(db database.KeyValueWriterDeleter, poolID [32]byte, row lx.DEXOrder) error {
|
||||
key := orderKey(poolID, row.OrderID)
|
||||
if row.Status == lx.DEXStatusFilled || row.Status == lx.DEXStatusCancelled || row.Remaining == 0 {
|
||||
return db.Delete(key)
|
||||
}
|
||||
return db.Put(key, lx.EncodeRow(row))
|
||||
}
|
||||
|
||||
// deleteOrderRow removes a resting order row (used on cancel).
|
||||
func deleteOrderRow(db database.KeyValueWriterDeleter, poolID [32]byte, orderID uint64) error {
|
||||
return db.Delete(orderKey(poolID, orderID))
|
||||
}
|
||||
|
||||
// readMarketSymbol returns the market's symbol, or ok=false if the market does
|
||||
// not exist.
|
||||
func readMarketSymbol(db database.KeyValueReader, poolID [32]byte) (string, bool, error) {
|
||||
v, err := db.Get(marketKey(poolID))
|
||||
if err == database.ErrNotFound {
|
||||
return "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return string(v), true, nil
|
||||
}
|
||||
|
||||
// writeMarket records a market's existence + symbol (idempotent).
|
||||
func writeMarket(db database.KeyValueWriter, poolID [32]byte, symbol string) error {
|
||||
return db.Put(marketKey(poolID), []byte(symbol))
|
||||
}
|
||||
|
||||
// readBookWatermark returns the market's LastOrderID watermark (0 if absent).
|
||||
func readBookWatermark(db database.KeyValueReader, poolID [32]byte) (uint64, error) {
|
||||
v, err := db.Get(bookKey(poolID))
|
||||
if err == database.ErrNotFound {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(v) != 8 {
|
||||
return 0, fmt.Errorf("dchain: corrupt book watermark len=%d", len(v))
|
||||
}
|
||||
return binary.BigEndian.Uint64(v), nil
|
||||
}
|
||||
|
||||
// writeBookWatermark persists the market's LastOrderID watermark.
|
||||
func writeBookWatermark(db database.KeyValueWriter, poolID [32]byte, watermark uint64) error {
|
||||
var b [8]byte
|
||||
binary.BigEndian.PutUint64(b[:], watermark)
|
||||
return db.Put(bookKey(poolID), b[:])
|
||||
}
|
||||
|
||||
// readMeta helpers.
|
||||
|
||||
func readLastAccepted(db database.KeyValueReader) (ids.ID, error) {
|
||||
v, err := db.Get([]byte(metaLastAccepted))
|
||||
if err != nil {
|
||||
return ids.Empty, err
|
||||
}
|
||||
return ids.ToID(v)
|
||||
}
|
||||
|
||||
func writeLastAccepted(db database.KeyValueWriter, id ids.ID) error {
|
||||
return db.Put([]byte(metaLastAccepted), id[:])
|
||||
}
|
||||
|
||||
func readHeight(db database.KeyValueReader) (uint64, error) {
|
||||
v, err := db.Get([]byte(metaHeight))
|
||||
if err == database.ErrNotFound {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(v) != 8 {
|
||||
return 0, fmt.Errorf("dchain: corrupt height len=%d", len(v))
|
||||
}
|
||||
return binary.BigEndian.Uint64(v), nil
|
||||
}
|
||||
|
||||
func writeHeight(db database.KeyValueWriter, height uint64) error {
|
||||
var b [8]byte
|
||||
binary.BigEndian.PutUint64(b[:], height)
|
||||
return db.Put([]byte(metaHeight), b[:])
|
||||
}
|
||||
|
||||
func readRoot(db database.KeyValueReader) ([32]byte, error) {
|
||||
var r [32]byte
|
||||
v, err := db.Get([]byte(metaRoot))
|
||||
if err == database.ErrNotFound {
|
||||
return r, nil // genesis parent root is zero
|
||||
}
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if len(v) != 32 {
|
||||
return r, fmt.Errorf("dchain: corrupt root len=%d", len(v))
|
||||
}
|
||||
copy(r[:], v)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func writeRoot(db database.KeyValueWriter, root [32]byte) error {
|
||||
return db.Put([]byte(metaRoot), root[:])
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// State root — REUSES the audited xvm execution_root construction:
|
||||
// keccak256 tagged binary Merkle (RFC-6962, github.com/luxfi/crypto/merkle) for
|
||||
// each variable-length sub-root, and a fixed-shape keccak256 composition for the
|
||||
// final root. Identical primitives to vms/xvm/state/xvmroot; the leaves here are
|
||||
// the d-chain's fixed-point DEXOrder / DEXTrade rows.
|
||||
// =========================================================================
|
||||
|
||||
// Size is the byte length of a root (a keccak256 digest).
|
||||
const Size = hash.KeccakSize
|
||||
|
||||
// bookRoot is the RFC-6962 tagged binary Merkle root over the canonical resting
|
||||
// book rows. The leaf digest is keccak256 over the order's deterministic
|
||||
// (Q64.64) field image; rows MUST already be in canonical order (BookToRows /
|
||||
// sortRows) so the root is insertion-order-independent. An empty book yields
|
||||
// merkle.EmptyRoot.
|
||||
func bookRoot(rows []lx.DEXOrder) [Size]byte {
|
||||
leaves := make([][Size]byte, len(rows))
|
||||
for i := range rows {
|
||||
leaves[i] = orderLeafDigest(rows[i], uint32(i))
|
||||
}
|
||||
return merkle.Root(leaves)
|
||||
}
|
||||
|
||||
// tradeRoot is the RFC-6962 tagged Merkle root over the block's fill rows (the
|
||||
// event root). Trades are folded in production order (matcher output order),
|
||||
// each bound to its index.
|
||||
func tradeRoot(trades []lx.DEXTrade) [Size]byte {
|
||||
leaves := make([][Size]byte, len(trades))
|
||||
for i := range trades {
|
||||
leaves[i] = tradeLeafDigest(trades[i], uint32(i))
|
||||
}
|
||||
return merkle.Root(leaves)
|
||||
}
|
||||
|
||||
// txRoot is the RFC-6962 tagged Merkle root over the block's transactions (the
|
||||
// log root). The leaf is keccak256 over the canonical [type][body] tx bytes.
|
||||
func txRoot(txs []*Tx) [Size]byte {
|
||||
leaves := make([][Size]byte, len(txs))
|
||||
for i := range txs {
|
||||
d := hash.ComputeKeccak256Array(txs[i].Bytes())
|
||||
var idx [4]byte
|
||||
binary.LittleEndian.PutUint32(idx[:], uint32(i))
|
||||
leaves[i] = hash.ComputeKeccak256Array(d[:], idx[:])
|
||||
}
|
||||
return merkle.Root(leaves)
|
||||
}
|
||||
|
||||
// orderLeafDigest = keccak256(canonical 64B row image ‖ index_le). Binding the
|
||||
// index makes the leaf position-aware, matching the xvmroot leaf discipline. The
|
||||
// row image is already the deterministic Q64.64 form, so the digest is
|
||||
// cross-architecture stable.
|
||||
func orderLeafDigest(row lx.DEXOrder, i uint32) [Size]byte {
|
||||
img := lx.EncodeRow(row)
|
||||
var idx [4]byte
|
||||
binary.LittleEndian.PutUint32(idx[:], i)
|
||||
return hash.ComputeKeccak256Array(img, idx[:])
|
||||
}
|
||||
|
||||
// tradeLeafDigest = keccak256(canonical 80B trade image ‖ index_le).
|
||||
func tradeLeafDigest(t lx.DEXTrade, i uint32) [Size]byte {
|
||||
img := lx.EncodeTrade(t)
|
||||
var idx [4]byte
|
||||
binary.LittleEndian.PutUint32(idx[:], i)
|
||||
return hash.ComputeKeccak256Array(img, idx[:])
|
||||
}
|
||||
|
||||
// ComposeRoot returns the d-chain execution root from the parent root, the three
|
||||
// sub-roots, and the height — the SAME fixed-shape un-tagged keccak256
|
||||
// composition xvmroot.Compose uses (this is NOT a Merkle node):
|
||||
//
|
||||
// dex_execution_root = keccak256(
|
||||
// parent ‖ bookRoot ‖ tradeRoot ‖ txRoot ‖ height_u64_le )
|
||||
//
|
||||
// Chaining the parent root makes the root a commitment to the whole accepted
|
||||
// history, not just the current snapshot — a validator that forked at any past
|
||||
// block produces a different root here.
|
||||
func ComposeRoot(parent, book, trade, txr [Size]byte, height uint64) [Size]byte {
|
||||
var h [8]byte
|
||||
binary.LittleEndian.PutUint64(h[:], height)
|
||||
return hash.ComputeKeccak256Array(parent[:], book[:], trade[:], txr[:], h[:])
|
||||
}
|
||||
|
||||
// sortRowsCanonical imposes a total, deterministic order on resting rows that may
|
||||
// span multiple markets (the per-block book root is over every touched market's
|
||||
// liquidity). Ordering: by side (bids before asks), then — to disambiguate rows
|
||||
// from different markets at the same price — by orderID, which is globally unique
|
||||
// across the chain (blockDeterministicID). Within a single market this matches
|
||||
// lx.sortRows; across markets the unique orderID makes it total.
|
||||
func sortRowsCanonical(rows []lx.DEXOrder) {
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
a, b := rows[i], rows[j]
|
||||
if a.Side != b.Side {
|
||||
return a.Side == lx.DEXSideBid
|
||||
}
|
||||
if a.Side == lx.DEXSideBid {
|
||||
if a.Price.Integer != b.Price.Integer {
|
||||
return a.Price.Integer > b.Price.Integer
|
||||
}
|
||||
if a.Price.Fraction != b.Price.Fraction {
|
||||
return a.Price.Fraction > b.Price.Fraction
|
||||
}
|
||||
} else {
|
||||
if a.Price.Integer != b.Price.Integer {
|
||||
return a.Price.Integer < b.Price.Integer
|
||||
}
|
||||
if a.Price.Fraction != b.Price.Fraction {
|
||||
return a.Price.Fraction < b.Price.Fraction
|
||||
}
|
||||
}
|
||||
return a.OrderID < b.OrderID
|
||||
})
|
||||
}
|
||||
|
||||
// ExecutionRoot computes the full d-chain execution root over a block's resulting
|
||||
// state: the canonical resting rows (book), the block's fills (trades), and the
|
||||
// block's transactions (txs), composed with the parent root and height. Returns
|
||||
// the execution root and the three sub-roots (for receipts / debugging).
|
||||
func ExecutionRoot(parent [Size]byte, rows []lx.DEXOrder, trades []lx.DEXTrade, txs []*Tx, height uint64) (root, book, trade, txr [Size]byte) {
|
||||
book = bookRoot(rows)
|
||||
trade = tradeRoot(trades)
|
||||
txr = txRoot(txs)
|
||||
root = ComposeRoot(parent, book, trade, txr, height)
|
||||
return root, book, trade, txr
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
)
|
||||
|
||||
// TestExecutionRootDeterministic proves the state root is a pure function of the
|
||||
// committed rows/trades/txs + parent + height: same inputs -> identical root,
|
||||
// computed independently twice.
|
||||
func TestExecutionRootDeterministic(t *testing.T) {
|
||||
ts := time.Unix(0, 1_700_000_000_000_000_000).UTC()
|
||||
txs := []*Tx{
|
||||
placeTx(t, zapwire.SideSell, 101.0, 5.0, "maker-a"),
|
||||
submitTx(t, zapwire.SideBuy, false, 101.0, 2.0, "taker-x"),
|
||||
}
|
||||
fills, rows := applyAll(t, 10, ts, txs)
|
||||
|
||||
var parent [Size]byte
|
||||
r1, b1, tr1, x1 := ExecutionRoot(parent, rows, fills, txs, 10)
|
||||
r2, b2, tr2, x2 := ExecutionRoot(parent, rows, fills, txs, 10)
|
||||
|
||||
if r1 != r2 || b1 != b2 || tr1 != tr2 || x1 != x2 {
|
||||
t.Fatal("ExecutionRoot not deterministic across two computations")
|
||||
}
|
||||
|
||||
// The root chains the parent: a different parent -> different root.
|
||||
parent2 := [Size]byte{0x01}
|
||||
r3, _, _, _ := ExecutionRoot(parent2, rows, fills, txs, 10)
|
||||
if r3 == r1 {
|
||||
t.Error("root did not change with a different parent (no history binding)")
|
||||
}
|
||||
|
||||
// The root binds the height: a different height -> different root.
|
||||
r4, _, _, _ := ExecutionRoot(parent, rows, fills, txs, 11)
|
||||
if r4 == r1 {
|
||||
t.Error("root did not change with a different height")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutionRootChangesWithState proves any change to committed state moves
|
||||
// the root (it actually commits the book/fills, not a constant).
|
||||
func TestExecutionRootChangesWithState(t *testing.T) {
|
||||
ts := time.Unix(0, 1_700_000_000_000_000_000).UTC()
|
||||
var parent [Size]byte
|
||||
|
||||
txsA := []*Tx{placeTx(t, zapwire.SideSell, 101.0, 5.0, "maker-a")}
|
||||
_, rowsA := applyAll(t, 1, ts, txsA)
|
||||
rootA, _, _, _ := ExecutionRoot(parent, rowsA, nil, txsA, 1)
|
||||
|
||||
// Different resting price -> different book root -> different exec root.
|
||||
txsB := []*Tx{placeTx(t, zapwire.SideSell, 102.0, 5.0, "maker-a")}
|
||||
_, rowsB := applyAll(t, 1, ts, txsB)
|
||||
rootB, _, _, _ := ExecutionRoot(parent, rowsB, nil, txsB, 1)
|
||||
|
||||
if rootA == rootB {
|
||||
t.Error("execution root identical for different book state")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmptyRootsAreStable pins the empty sub-root values to merkle.EmptyRoot via
|
||||
// the public ExecutionRoot path (no rows/trades/txs).
|
||||
func TestEmptyRootsAreStable(t *testing.T) {
|
||||
var parent [Size]byte
|
||||
r1, b1, tr1, x1 := ExecutionRoot(parent, nil, nil, nil, 0)
|
||||
r2, b2, tr2, x2 := ExecutionRoot(parent, nil, nil, nil, 0)
|
||||
if r1 != r2 || b1 != b2 || tr1 != tr2 || x1 != x2 {
|
||||
t.Fatal("empty roots not stable")
|
||||
}
|
||||
// Sub-roots over empty leaf sets must equal each other (all merkle.EmptyRoot).
|
||||
if b1 != tr1 || tr1 != x1 {
|
||||
t.Error("empty sub-roots differ; expected all merkle.EmptyRoot")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBookRootIgnoresInsertionOrder proves the book root is computed over the
|
||||
// canonical row stream, so it is independent of how rows are presented (rows go
|
||||
// through BookToRows which canonicalizes).
|
||||
func TestBookRootIgnoresInsertionOrder(t *testing.T) {
|
||||
ts := time.Unix(0, 1_700_000_000_000_000_000).UTC()
|
||||
|
||||
b1 := lx.NewOrderBook("X")
|
||||
b1.ConsensusAddOrder(&lx.Order{ID: 1, Type: lx.Limit, Side: lx.Buy, Price: 100, Size: 1, User: "a", Timestamp: ts})
|
||||
b1.ConsensusAddOrder(&lx.Order{ID: 2, Type: lx.Limit, Side: lx.Sell, Price: 101, Size: 1, User: "b", Timestamp: ts})
|
||||
|
||||
b2 := lx.NewOrderBook("X")
|
||||
b2.ConsensusAddOrder(&lx.Order{ID: 2, Type: lx.Limit, Side: lx.Sell, Price: 101, Size: 1, User: "b", Timestamp: ts})
|
||||
b2.ConsensusAddOrder(&lx.Order{ID: 1, Type: lx.Limit, Side: lx.Buy, Price: 100, Size: 1, User: "a", Timestamp: ts})
|
||||
|
||||
if bookRoot(lx.BookToRows(b1)) != bookRoot(lx.BookToRows(b2)) {
|
||||
t.Error("book root depends on insertion order")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package dchain is the standalone D-Chain DEX virtual machine: a
|
||||
// block.ChainVM (github.com/luxfi/consensus/engine/chain/block) that runs the
|
||||
// lx.OrderBook matcher inside consensus. Orders arrive as ZAP frames, are
|
||||
// queued in a mempool (never executed synchronously), drained into a block in
|
||||
// sequence order, matched at Block.Verify against a versiondb overlay so every
|
||||
// validator re-derives the fills, and committed to durable zapdb at
|
||||
// Block.Accept. The persisted chainstate is authoritative; the in-RAM book is a
|
||||
// rebuildable accelerator folded from the order:* rows on restart.
|
||||
//
|
||||
// This file defines the transaction layer: the four DEX operations, their
|
||||
// canonical [type][body] wire form, and the deterministic TxID.
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// TxType is the one-byte discriminant prefixing every transaction body. The body
|
||||
// for the order operations is the FROZEN zapwire payload verbatim (see
|
||||
// pkg/zapwire), so the chains/dexvm proxy can forward a client frame into a
|
||||
// d-chain tx without re-encoding, and the precompile/maker/server all speak the
|
||||
// same bytes.
|
||||
type TxType uint8
|
||||
|
||||
const (
|
||||
// TxEnsureMarket idempotently creates a market (order book) for a poolId.
|
||||
// Body = zapwire EnsureMarket payload: poolId[32].
|
||||
TxEnsureMarket TxType = iota + 1
|
||||
// TxPlace rests a limit order. Body = zapwire Place payload (65B):
|
||||
// poolId[32] + side[1] + price[8] + size[8] + user[16].
|
||||
TxPlace
|
||||
// TxCancel cancels a resting order. Body = zapwire Cancel payload (40B):
|
||||
// poolId[32] + orderId[8].
|
||||
TxCancel
|
||||
// TxSubmit crosses the book with a marketable order. Body = zapwire Submit
|
||||
// payload (66B): poolId[32] + side[1] + isMarket[1] + price[8] + size[8] +
|
||||
// user[16].
|
||||
TxSubmit
|
||||
)
|
||||
|
||||
func (t TxType) String() string {
|
||||
switch t {
|
||||
case TxEnsureMarket:
|
||||
return "ensure_market"
|
||||
case TxPlace:
|
||||
return "place"
|
||||
case TxCancel:
|
||||
return "cancel"
|
||||
case TxSubmit:
|
||||
return "submit"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Errors returned by the tx layer.
|
||||
var (
|
||||
ErrEmptyTx = errors.New("dchain: empty transaction")
|
||||
ErrUnknownTx = errors.New("dchain: unknown transaction type")
|
||||
ErrShortTxBody = errors.New("dchain: transaction body too short for its type")
|
||||
ErrEmptyMempool = errors.New("dchain: no pending transactions")
|
||||
)
|
||||
|
||||
// Tx is a parsed DEX transaction: the type discriminant plus the raw zapwire body
|
||||
// for that type. The raw bytes are retained so the tx is re-serialized
|
||||
// byte-identically (Bytes) and the TxID is stable.
|
||||
type Tx struct {
|
||||
Type TxType
|
||||
Body []byte // the zapwire payload for Type (verbatim)
|
||||
}
|
||||
|
||||
// poolID returns the 32-byte market id every tx body begins with. All four tx
|
||||
// bodies start with poolId[32]; the type-specific decode is done in execute.go.
|
||||
func (tx *Tx) poolID() ([32]byte, bool) {
|
||||
var id [32]byte
|
||||
if len(tx.Body) < zapwire.PoolIDSize {
|
||||
return id, false
|
||||
}
|
||||
copy(id[:], tx.Body[:zapwire.PoolIDSize])
|
||||
return id, true
|
||||
}
|
||||
|
||||
// NewTx builds a Tx from a type and its zapwire body, validating the body length
|
||||
// matches the type's fixed frame size. A malformed length is rejected here so a
|
||||
// short/garbage body can never reach the matcher.
|
||||
func NewTx(t TxType, body []byte) (*Tx, error) {
|
||||
want, ok := bodySize(t)
|
||||
if !ok {
|
||||
return nil, ErrUnknownTx
|
||||
}
|
||||
if len(body) < want {
|
||||
return nil, fmt.Errorf("%w: type=%s have=%d want=%d", ErrShortTxBody, t, len(body), want)
|
||||
}
|
||||
// Copy so the tx owns its bytes independent of the caller's buffer.
|
||||
b := make([]byte, want)
|
||||
copy(b, body[:want])
|
||||
return &Tx{Type: t, Body: b}, nil
|
||||
}
|
||||
|
||||
// bodySize returns the fixed zapwire body length for a tx type.
|
||||
func bodySize(t TxType) (int, bool) {
|
||||
switch t {
|
||||
case TxEnsureMarket:
|
||||
return zapwire.EnsureMarketReqSize, true
|
||||
case TxPlace:
|
||||
return zapwire.PlaceReqSize, true
|
||||
case TxCancel:
|
||||
return zapwire.CancelReqSize, true
|
||||
case TxSubmit:
|
||||
return zapwire.SubmitReqSize, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes returns the canonical wire form: [type:1][body]. This is what a block
|
||||
// stores and what the TxID hashes — deterministic and self-describing.
|
||||
func (tx *Tx) Bytes() []byte {
|
||||
out := make([]byte, 1+len(tx.Body))
|
||||
out[0] = byte(tx.Type)
|
||||
copy(out[1:], tx.Body)
|
||||
return out
|
||||
}
|
||||
|
||||
// ID is the deterministic transaction id: Checksum256 over the canonical bytes.
|
||||
// ids.Checksum256 is sha256(sha256(b))-style content addressing used across the
|
||||
// Lux stack, so the same tx bytes always yield the same id on every node.
|
||||
func (tx *Tx) ID() ids.ID {
|
||||
return ids.Checksum256(tx.Bytes())
|
||||
}
|
||||
|
||||
// ParseTx decodes a [type:1][body] wire transaction, validating the type and the
|
||||
// body length. Used by ParseBlock and by the mempool when accepting a relayed
|
||||
// frame.
|
||||
func ParseTx(b []byte) (*Tx, error) {
|
||||
if len(b) == 0 {
|
||||
return nil, ErrEmptyTx
|
||||
}
|
||||
t := TxType(b[0])
|
||||
return NewTx(t, b[1:])
|
||||
}
|
||||
|
||||
// encodeTxList serializes a list of txs as [count:4][len:4 body]*, the block body
|
||||
// format. Each tx is length-prefixed so ParseBlock can split them back exactly.
|
||||
func encodeTxList(txs []*Tx) []byte {
|
||||
size := 4
|
||||
for _, tx := range txs {
|
||||
size += 4 + 1 + len(tx.Body)
|
||||
}
|
||||
out := make([]byte, size)
|
||||
binary.BigEndian.PutUint32(out[0:4], uint32(len(txs)))
|
||||
off := 4
|
||||
for _, tx := range txs {
|
||||
raw := tx.Bytes()
|
||||
binary.BigEndian.PutUint32(out[off:off+4], uint32(len(raw)))
|
||||
off += 4
|
||||
copy(out[off:off+len(raw)], raw)
|
||||
off += len(raw)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// decodeTxList parses a [count:4][len:4 body]* tx list. Every length is bounds
|
||||
// checked so a truncated block body is rejected rather than panicking.
|
||||
func decodeTxList(b []byte) ([]*Tx, error) {
|
||||
if len(b) < 4 {
|
||||
return nil, fmt.Errorf("dchain: tx list too short: %d", len(b))
|
||||
}
|
||||
n := int(binary.BigEndian.Uint32(b[0:4]))
|
||||
off := 4
|
||||
txs := make([]*Tx, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
if off+4 > len(b) {
|
||||
return nil, fmt.Errorf("dchain: tx list truncated at tx %d (len header)", i)
|
||||
}
|
||||
l := int(binary.BigEndian.Uint32(b[off : off+4]))
|
||||
off += 4
|
||||
if off+l > len(b) {
|
||||
return nil, fmt.Errorf("dchain: tx list truncated at tx %d (body): need %d have %d", i, l, len(b)-off)
|
||||
}
|
||||
tx, err := ParseTx(b[off : off+l])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dchain: tx %d: %w", i, err)
|
||||
}
|
||||
off += l
|
||||
txs = append(txs, tx)
|
||||
}
|
||||
return txs, nil
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/consensus/engine/chain/block"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/versiondb"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/version"
|
||||
)
|
||||
|
||||
// vm.go is the standalone D-Chain DEX VM: an implementation of
|
||||
// block.ChainVM (== github.com/luxfi/vm/chain.ChainVM) served as an OS process by
|
||||
// github.com/luxfi/vm/rpc.Serve — the same ZAP plugin harness lux/evm boots
|
||||
// through. rpc.Serve opens the chain's badgerdb/zapdb at ChainDataDir/db and
|
||||
// hands it to Initialize as init.DB; the VM treats that as its sole authoritative
|
||||
// store.
|
||||
//
|
||||
// The VM owns: the durable state (init.DB), the in-RAM book cache (rebuilt from
|
||||
// order:* rows on Initialize), the mempool, and the accepted-block index. It is
|
||||
// deterministic end to end: BuildBlock drains the mempool in sequence order,
|
||||
// Block.Verify runs the matcher against a versiondb overlay, Block.Accept commits
|
||||
// the overlay atomically.
|
||||
|
||||
// Version is the VM's semantic version string.
|
||||
const Version = "dchain/v1.0.0"
|
||||
|
||||
// genesisParent is the synthetic parent id of the genesis block (all zero).
|
||||
var genesisParent = ids.Empty
|
||||
|
||||
// VM implements block.ChainVM for the d-chain DEX.
|
||||
type VM struct {
|
||||
// mu guards all mutable VM state below. Block lifecycle methods
|
||||
// (Verify/Accept/Reject) and BuildBlock take it; the matcher's own ob.mu is
|
||||
// nested under it.
|
||||
mu sync.Mutex
|
||||
|
||||
log log.Logger
|
||||
db database.Database
|
||||
|
||||
mempool *mempool
|
||||
|
||||
// outcomes lets a ZAP handler block until its tx is decided by an accepted
|
||||
// block (Accept resolves the waiter with the consensus fills/ack).
|
||||
outcomes *outcomeRegistry
|
||||
|
||||
// In-RAM accelerator: poolId -> rebuildable book. Truth is the order:* rows.
|
||||
books map[[32]byte]*lx.OrderBook
|
||||
|
||||
// Accepted-block index + linear-chain head.
|
||||
acceptedBlocks map[ids.ID]*Block
|
||||
heightIndex map[uint64]ids.ID
|
||||
lastAcceptedID ids.ID
|
||||
lastAcceptedHeight uint64
|
||||
lastRoot [Size]byte
|
||||
|
||||
// preferred is the block the engine asked us to build on next.
|
||||
preferred ids.ID
|
||||
|
||||
// toEngine is retained for the mempool signal path.
|
||||
toEngine chan<- block.Message
|
||||
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// Initialize wires the VM to its runtime, durable store, and engine channel,
|
||||
// then rebuilds in-RAM state from the durable rows. On a fresh chain it commits
|
||||
// the genesis block; on restart it loads the last-accepted head and folds every
|
||||
// market's book from its order:* rows (the rebuildable-accelerator path).
|
||||
func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
|
||||
if init.DB == nil {
|
||||
return fmt.Errorf("dchain: Initialize requires a non-nil DB")
|
||||
}
|
||||
vm.log = init.Log
|
||||
if vm.log == nil {
|
||||
vm.log = log.NewNoOpLogger()
|
||||
}
|
||||
vm.db = init.DB
|
||||
vm.toEngine = init.ToEngine
|
||||
vm.mempool = newMempool(init.ToEngine)
|
||||
vm.outcomes = newOutcomeRegistry()
|
||||
vm.books = map[[32]byte]*lx.OrderBook{}
|
||||
vm.acceptedBlocks = map[ids.ID]*Block{}
|
||||
vm.heightIndex = map[uint64]ids.ID{}
|
||||
|
||||
// Load the head, or bootstrap genesis.
|
||||
if _, err := readLastAccepted(vm.db); err == database.ErrNotFound {
|
||||
if err := vm.bootstrapGenesis(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("dchain: read last accepted: %w", err)
|
||||
} else {
|
||||
if err := vm.loadHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vm.rebuildAllBooks(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
vm.preferred = vm.lastAcceptedID
|
||||
vm.initialized = true
|
||||
vm.log.Info("dchain VM initialized",
|
||||
"height", vm.lastAcceptedHeight,
|
||||
"lastAccepted", vm.lastAcceptedID,
|
||||
"markets", len(vm.books),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// bootstrapGenesis writes the genesis block (height 0, empty, zero parent root)
|
||||
// and sets it as the accepted head. Must be called under vm.mu on a fresh DB.
|
||||
func (vm *VM) bootstrapGenesis() error {
|
||||
var parentRoot [Size]byte // zero
|
||||
root, _, _, _ := ExecutionRoot(parentRoot, nil, nil, nil, 0)
|
||||
gen := newBlock(vm, genesisParent, 0, time.Unix(0, 0).UTC(), root, nil)
|
||||
|
||||
batch := vm.db.NewBatch()
|
||||
if err := writeLastAccepted(batch, gen.id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeHeight(batch, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeRoot(batch, root); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := batch.Write(); err != nil {
|
||||
return fmt.Errorf("dchain: write genesis: %w", err)
|
||||
}
|
||||
|
||||
gen.status = statusAccepted
|
||||
vm.lastAcceptedID = gen.id
|
||||
vm.lastAcceptedHeight = 0
|
||||
vm.lastRoot = root
|
||||
vm.acceptedBlocks[gen.id] = gen
|
||||
vm.heightIndex[0] = gen.id
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadHead reads the accepted head (id, height, root) from durable meta. Must be
|
||||
// called under vm.mu.
|
||||
func (vm *VM) loadHead() error {
|
||||
id, err := readLastAccepted(vm.db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dchain: load head id: %w", err)
|
||||
}
|
||||
height, err := readHeight(vm.db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dchain: load head height: %w", err)
|
||||
}
|
||||
root, err := readRoot(vm.db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dchain: load head root: %w", err)
|
||||
}
|
||||
vm.lastAcceptedID = id
|
||||
vm.lastAcceptedHeight = height
|
||||
vm.lastRoot = root
|
||||
vm.heightIndex[height] = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// rebuildAllBooks folds every market's book from its order:* rows. This is the
|
||||
// restart path: after a crash the in-RAM books are reconstructed exactly from the
|
||||
// durable resting set, so trading resumes with the committed state. Must be
|
||||
// called under vm.mu.
|
||||
func (vm *VM) rebuildAllBooks() error {
|
||||
it := vm.db.NewIteratorWithPrefix([]byte(prefixMarket))
|
||||
defer it.Release()
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
if len(key) != len(prefixMarket)+32 {
|
||||
continue
|
||||
}
|
||||
var poolID [32]byte
|
||||
copy(poolID[:], key[len(prefixMarket):])
|
||||
symbol := string(it.Value())
|
||||
ob, err := rebuildBookFromDB(vm.db, poolID, symbol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dchain: rebuild book %x: %w", poolID[:8], err)
|
||||
}
|
||||
vm.books[poolID] = ob
|
||||
}
|
||||
return it.Error()
|
||||
}
|
||||
|
||||
// BuildBlock drains the mempool into a new block on top of the preferred head,
|
||||
// executes it locally to compute the execution root (the proposer's claim), and
|
||||
// returns it. The block is NOT accepted here — consensus votes, then calls
|
||||
// Verify/Accept. Returns an error when the mempool is empty (the engine only
|
||||
// calls BuildBlock after a PendingTxs signal, but a race can leave it empty).
|
||||
func (vm *VM) BuildBlock(ctx context.Context) (block.Block, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
|
||||
txs := vm.mempool.Drain(0)
|
||||
if len(txs) == 0 {
|
||||
return nil, ErrEmptyMempool
|
||||
}
|
||||
|
||||
height := vm.lastAcceptedHeight + 1
|
||||
ts := vm.blockTimestamp()
|
||||
|
||||
// Execute against a throwaway overlay to derive the root the proposer claims.
|
||||
// (Accept re-executes against a fresh overlay; this overlay is discarded.)
|
||||
probe := newBlock(vm, vm.lastAcceptedID, height, ts, [Size]byte{}, txs)
|
||||
overlay := versiondb.New(vm.db)
|
||||
res, err := probe.execute(ctx, overlay)
|
||||
overlay.Abort()
|
||||
if err != nil {
|
||||
// A tx that cannot execute (e.g. unknown market) must not wedge the
|
||||
// chain: drop the batch back to the mempool and surface the error.
|
||||
vm.mempool.Requeue(txs)
|
||||
return nil, fmt.Errorf("dchain: build execute: %w", err)
|
||||
}
|
||||
|
||||
blk := newBlock(vm, vm.lastAcceptedID, height, ts, res.root, txs)
|
||||
vm.log.Debug("dchain built block", "height", height, "txs", len(txs), "fills", len(res.fills))
|
||||
return blk, nil
|
||||
}
|
||||
|
||||
// blockTimestamp returns a deterministic, monotone block timestamp: the max of
|
||||
// "now" and (last accepted ts + 1ns). The block's own timestamp is what feeds
|
||||
// applyTx, so it is recorded in the block bytes and replayed identically by every
|
||||
// validator (they read it from the block, never re-sample the clock). Must be
|
||||
// called under vm.mu.
|
||||
func (vm *VM) blockTimestamp() time.Time {
|
||||
now := time.Now().UTC()
|
||||
if last, ok := vm.acceptedBlocks[vm.lastAcceptedID]; ok {
|
||||
min := last.timestamp.Add(time.Nanosecond)
|
||||
if now.Before(min) {
|
||||
return min
|
||||
}
|
||||
}
|
||||
return now
|
||||
}
|
||||
|
||||
// ParseBlock decodes block bytes into a Block. Used by the engine to reconstruct
|
||||
// a peer's proposed block; Verify then re-executes it.
|
||||
func (vm *VM) ParseBlock(ctx context.Context, b []byte) (block.Block, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
// Return the cached accepted block if we already have it (stable identity).
|
||||
blk, err := parseBlock(vm, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing, ok := vm.acceptedBlocks[blk.id]; ok {
|
||||
return existing, nil
|
||||
}
|
||||
return blk, nil
|
||||
}
|
||||
|
||||
// GetBlock returns an accepted block by id, or database.ErrNotFound.
|
||||
func (vm *VM) GetBlock(ctx context.Context, id ids.ID) (block.Block, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
if blk, ok := vm.acceptedBlocks[id]; ok {
|
||||
return blk, nil
|
||||
}
|
||||
return nil, database.ErrNotFound
|
||||
}
|
||||
|
||||
// LastAccepted returns the id of the last accepted block.
|
||||
func (vm *VM) LastAccepted(ctx context.Context) (ids.ID, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
return vm.lastAcceptedID, nil
|
||||
}
|
||||
|
||||
// SetPreference records the block the engine wants the VM to build on. The
|
||||
// d-chain is linear; the preference always tracks the last accepted block.
|
||||
func (vm *VM) SetPreference(ctx context.Context, id ids.ID) error {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
vm.preferred = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBlockIDAtHeight returns the accepted block id at a height (height indexer).
|
||||
func (vm *VM) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
if id, ok := vm.heightIndex[height]; ok {
|
||||
return id, nil
|
||||
}
|
||||
return ids.Empty, database.ErrNotFound
|
||||
}
|
||||
|
||||
// SetState transitions the VM lifecycle state (bootstrapping/normal-op/etc).
|
||||
// The d-chain has no state-specific behavior beyond logging — it is ready to
|
||||
// build/verify as soon as Initialize completes.
|
||||
func (vm *VM) SetState(ctx context.Context, state uint32) error {
|
||||
vm.log.Debug("dchain SetState", "state", state)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Version returns the VM version string.
|
||||
func (vm *VM) Version(ctx context.Context) (string, error) { return Version, nil }
|
||||
|
||||
// NewHTTPHandler returns the VM's HTTP handler. The d-chain serves its CLOB API
|
||||
// over the ZAP gateway (handler.go), not HTTP, so this is nil.
|
||||
func (vm *VM) NewHTTPHandler(ctx context.Context) (http.Handler, error) { return nil, nil }
|
||||
|
||||
// CreateHandlers returns named HTTP handlers. None — the CLOB surface is ZAP.
|
||||
func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) {
|
||||
return map[string]http.Handler{}, nil
|
||||
}
|
||||
|
||||
// Connected/Disconnected are p2p lifecycle callbacks; the d-chain matcher needs
|
||||
// no per-peer state, so these are no-ops.
|
||||
func (vm *VM) Connected(ctx context.Context, nodeID ids.NodeID, app *version.Application) error {
|
||||
return nil
|
||||
}
|
||||
func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error { return nil }
|
||||
|
||||
// HealthCheck reports the VM healthy once initialized, with the current height.
|
||||
func (vm *VM) HealthCheck(ctx context.Context) (block.HealthCheckResult, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
return block.HealthCheckResult{
|
||||
Healthy: vm.initialized,
|
||||
Details: map[string]string{
|
||||
"height": fmt.Sprintf("%d", vm.lastAcceptedHeight),
|
||||
"markets": fmt.Sprintf("%d", len(vm.books)),
|
||||
"pending": fmt.Sprintf("%d", vm.mempool.Len()),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WaitForEvent blocks until there is work to build a block. It returns PendingTxs
|
||||
// when the mempool is non-empty. The engine calls this in a loop; we poll the
|
||||
// mempool with a short backoff rather than holding a dedicated condition var,
|
||||
// keeping the VM's locking simple (the mempool also pushes a PendingTxs signal on
|
||||
// Add, so latency is bounded by the push, not the poll).
|
||||
func (vm *VM) WaitForEvent(ctx context.Context) (block.Message, error) {
|
||||
ticker := time.NewTicker(5 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
if vm.mempool != nil && vm.mempool.Len() > 0 {
|
||||
return block.Message{Type: block.PendingTxs}, nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return block.Message{}, ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown releases VM resources. The durable DB is owned and closed by the
|
||||
// rpc.Serve harness (it opened it), so we do not close it here — closing it twice
|
||||
// is an error. We only drop in-RAM state.
|
||||
func (vm *VM) Shutdown(ctx context.Context) error {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
vm.books = nil
|
||||
vm.acceptedBlocks = nil
|
||||
vm.heightIndex = nil
|
||||
vm.initialized = false
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/consensus/engine/chain/block"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// vm_test.go drives the real dchain.VM through its block.ChainVM lifecycle —
|
||||
// the methods the consensus engine calls, in the exact order it calls them:
|
||||
// Initialize -> WaitForEvent -> BuildBlock -> Verify -> Accept, then a restart
|
||||
// (a fresh VM over the SAME durable DB) to prove the chainstate survives and the
|
||||
// books rebuild from the order:* rows. This is the step-3 acceptance proof.
|
||||
|
||||
// newTestVM builds a VM over db with a buffered toEngine channel and Initializes
|
||||
// it. The buffered channel mirrors the engine's receive side so the mempool's
|
||||
// non-blocking PendingTxs signal never blocks the test.
|
||||
func newTestVM(t *testing.T, db database.Database) (*VM, chan block.Message) {
|
||||
t.Helper()
|
||||
toEngine := make(chan block.Message, 16)
|
||||
vm := &VM{}
|
||||
if err := vm.Initialize(context.Background(), block.Init{
|
||||
DB: db,
|
||||
Log: log.NewNoOpLogger(),
|
||||
ToEngine: toEngine,
|
||||
}); err != nil {
|
||||
t.Fatalf("Initialize: %v", err)
|
||||
}
|
||||
return vm, toEngine
|
||||
}
|
||||
|
||||
// submitFrame builds the [type:1][body] tx bytes for a clob op, the exact bytes
|
||||
// the handler/proxy would Add to the mempool.
|
||||
func ensureMarketTx(t *testing.T, pool [32]byte) *Tx {
|
||||
t.Helper()
|
||||
tx, err := NewTx(TxEnsureMarket, zapwire.EncodeEnsureMarket(pool))
|
||||
if err != nil {
|
||||
t.Fatalf("NewTx ensure: %v", err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func placePoolTx(t *testing.T, pool [32]byte, side uint8, price, size float64, user string) *Tx {
|
||||
t.Helper()
|
||||
tx, err := NewTx(TxPlace, zapwire.EncodePlace(pool, side, price, size, user))
|
||||
if err != nil {
|
||||
t.Fatalf("NewTx place: %v", err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func submitPoolTx(t *testing.T, pool [32]byte, side uint8, isMarket bool, price, size float64, user string) *Tx {
|
||||
t.Helper()
|
||||
tx, err := NewTx(TxSubmit, zapwire.EncodeSubmit(pool, side, isMarket, price, size, user))
|
||||
if err != nil {
|
||||
t.Fatalf("NewTx submit: %v", err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
// buildVerifyAccept runs the proposer+validator path for the txs currently in the
|
||||
// mempool: BuildBlock (proposer) -> ParseBlock+Verify (validator re-derives) ->
|
||||
// Accept (commit). It asserts the parsed/verified block is byte-identical to the
|
||||
// built one (a peer reconstructs the same block) and returns the accepted block.
|
||||
func buildVerifyAccept(t *testing.T, vm *VM) *Block {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
built, err := vm.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildBlock: %v", err)
|
||||
}
|
||||
|
||||
// A validator re-parses the proposer's bytes and verifies independently.
|
||||
reparsed, err := vm.ParseBlock(ctx, built.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock: %v", err)
|
||||
}
|
||||
if string(reparsed.Bytes()) != string(built.Bytes()) {
|
||||
t.Fatal("re-parsed block bytes differ from built block")
|
||||
}
|
||||
if err := reparsed.Verify(ctx); err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
if err := reparsed.Accept(ctx); err != nil {
|
||||
t.Fatalf("Accept: %v", err)
|
||||
}
|
||||
return reparsed.(*Block)
|
||||
}
|
||||
|
||||
// TestVMLifecycle is the step-3 end-to-end proof: Initialize a fresh VM, place
|
||||
// resting liquidity through a built+verified+accepted block, cross it with a
|
||||
// submit in a second block, then restart (new VM, same DB) and confirm the book
|
||||
// and the accepted head survive — rebuilt purely from the durable rows.
|
||||
func TestVMLifecycle(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := memdb.New()
|
||||
pool := [32]byte{0x01, 0x02, 0x03}
|
||||
|
||||
vm, toEngine := newTestVM(t, db)
|
||||
|
||||
// Genesis: fresh chain is at height 0 with no markets.
|
||||
if vm.lastAcceptedHeight != 0 {
|
||||
t.Fatalf("fresh VM height = %d, want 0", vm.lastAcceptedHeight)
|
||||
}
|
||||
genesisID := vm.lastAcceptedID
|
||||
|
||||
// ---- Block 1: ensure market + rest three orders ----
|
||||
vm.mempool.Add(ensureMarketTx(t, pool))
|
||||
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideSell, 101.0, 5.0, "maker-a"))
|
||||
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideSell, 102.0, 3.0, "maker-b"))
|
||||
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideBuy, 99.0, 4.0, "maker-c"))
|
||||
|
||||
// The mempool signalled the engine; WaitForEvent must return PendingTxs.
|
||||
msg, err := vm.WaitForEvent(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("WaitForEvent: %v", err)
|
||||
}
|
||||
if msg.Type != block.PendingTxs {
|
||||
t.Fatalf("WaitForEvent type = %v, want PendingTxs", msg.Type)
|
||||
}
|
||||
drainSignals(toEngine)
|
||||
|
||||
blk1 := buildVerifyAccept(t, vm)
|
||||
if blk1.Height() != 1 {
|
||||
t.Fatalf("block1 height = %d, want 1", blk1.Height())
|
||||
}
|
||||
if blk1.Parent() != genesisID {
|
||||
t.Fatal("block1 parent != genesis")
|
||||
}
|
||||
if vm.lastAcceptedID != blk1.ID() {
|
||||
t.Fatal("VM head not advanced to block1")
|
||||
}
|
||||
|
||||
// Book now has 3 resting orders (2 asks, 1 bid).
|
||||
ob := vm.books[pool]
|
||||
if ob == nil {
|
||||
t.Fatal("market book not present after accept")
|
||||
}
|
||||
if got := len(ob.Orders); got != 3 {
|
||||
t.Fatalf("resting orders = %d, want 3", got)
|
||||
}
|
||||
|
||||
// ---- Block 2: a buy submit crosses the 101 ask fully + part of 102 ----
|
||||
vm.mempool.Add(submitPoolTx(t, pool, zapwire.SideBuy, false, 102.0, 6.0, "taker-x"))
|
||||
drainSignals(toEngine)
|
||||
blk2 := buildVerifyAccept(t, vm)
|
||||
if blk2.Height() != 2 {
|
||||
t.Fatalf("block2 height = %d, want 2", blk2.Height())
|
||||
}
|
||||
|
||||
// After the cross: the 101 ask (5.0) is gone, the 102 ask has 2.0 left, the
|
||||
// 99 bid is untouched. So 2 resting orders remain.
|
||||
ob = vm.books[pool]
|
||||
if got := len(ob.Orders); got != 2 {
|
||||
t.Fatalf("resting orders after cross = %d, want 2", got)
|
||||
}
|
||||
|
||||
// Capture the authoritative head + a canonical book image before restart.
|
||||
headBefore := vm.lastAcceptedID
|
||||
heightBefore := vm.lastAcceptedHeight
|
||||
rootBefore := vm.lastRoot
|
||||
rowsBefore := lx.BookToRows(ob)
|
||||
|
||||
if err := vm.Shutdown(ctx); err != nil {
|
||||
t.Fatalf("Shutdown: %v", err)
|
||||
}
|
||||
|
||||
// ---- RESTART: new VM over the SAME db (the rpc.Serve harness reopens the
|
||||
// persistent store; here memdb persists in-process) ----
|
||||
vm2, _ := newTestVM(t, db)
|
||||
|
||||
if vm2.lastAcceptedID != headBefore {
|
||||
t.Fatalf("restart head = %s, want %s", vm2.lastAcceptedID, headBefore)
|
||||
}
|
||||
if vm2.lastAcceptedHeight != heightBefore {
|
||||
t.Fatalf("restart height = %d, want %d", vm2.lastAcceptedHeight, heightBefore)
|
||||
}
|
||||
if vm2.lastRoot != rootBefore {
|
||||
t.Fatalf("restart root = %x, want %x", vm2.lastRoot[:8], rootBefore[:8])
|
||||
}
|
||||
|
||||
// The book rebuilt from the durable order:* rows must be byte-identical.
|
||||
ob2 := vm2.books[pool]
|
||||
if ob2 == nil {
|
||||
t.Fatal("market book not rebuilt after restart")
|
||||
}
|
||||
rowsAfter := lx.BookToRows(ob2)
|
||||
if len(rowsAfter) != len(rowsBefore) {
|
||||
t.Fatalf("rebuilt book has %d rows, want %d", len(rowsAfter), len(rowsBefore))
|
||||
}
|
||||
for i := range rowsBefore {
|
||||
if string(lx.EncodeRow(rowsBefore[i])) != string(lx.EncodeRow(rowsAfter[i])) {
|
||||
t.Errorf("rebuilt row %d differs after restart", i)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- The restarted VM is fully live: it can build/verify/accept again, and
|
||||
// the new block's root chains the surviving parent root. ----
|
||||
vm2.mempool.Add(placePoolTx(t, pool, zapwire.SideBuy, 100.0, 1.0, "maker-d"))
|
||||
blk3 := buildVerifyAccept(t, vm2)
|
||||
if blk3.Height() != heightBefore+1 {
|
||||
t.Fatalf("post-restart block height = %d, want %d", blk3.Height(), heightBefore+1)
|
||||
}
|
||||
if blk3.Parent() != headBefore {
|
||||
t.Fatal("post-restart block does not chain the surviving head")
|
||||
}
|
||||
if got := len(vm2.books[pool].Orders); got != 3 {
|
||||
t.Fatalf("resting orders after post-restart place = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVMRejectAbortsState proves a rejected block leaves no durable trace and
|
||||
// returns its txs to the mempool: Verify stages an overlay, Reject aborts it, the
|
||||
// committed state and in-RAM book are unchanged, and the dropped txs can be
|
||||
// rebuilt into a later block.
|
||||
func TestVMRejectAbortsState(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := memdb.New()
|
||||
pool := [32]byte{0x09}
|
||||
vm, toEngine := newTestVM(t, db)
|
||||
|
||||
// Establish a market with one resting ask via an accepted block.
|
||||
vm.mempool.Add(ensureMarketTx(t, pool))
|
||||
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideSell, 50.0, 2.0, "maker"))
|
||||
drainSignals(toEngine)
|
||||
buildVerifyAccept(t, vm)
|
||||
|
||||
headBefore := vm.lastAcceptedID
|
||||
heightBefore := vm.lastAcceptedHeight
|
||||
restingBefore := len(vm.books[pool].Orders)
|
||||
|
||||
// Build a block that rests another order, Verify it (stages overlay), then
|
||||
// Reject it.
|
||||
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideBuy, 49.0, 1.0, "maker2"))
|
||||
drainSignals(toEngine)
|
||||
built, err := vm.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildBlock: %v", err)
|
||||
}
|
||||
if err := built.Verify(ctx); err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
if err := built.Reject(ctx); err != nil {
|
||||
t.Fatalf("Reject: %v", err)
|
||||
}
|
||||
|
||||
// State must be exactly as before the rejected block.
|
||||
if vm.lastAcceptedID != headBefore || vm.lastAcceptedHeight != heightBefore {
|
||||
t.Fatal("rejected block advanced the head")
|
||||
}
|
||||
if got := len(vm.books[pool].Orders); got != restingBefore {
|
||||
t.Fatalf("rejected block mutated the in-RAM book: %d != %d", got, restingBefore)
|
||||
}
|
||||
// Nothing durable was written for height+1.
|
||||
if _, err := db.Get(orderKey(pool, blockDeterministicID(heightBefore+1, 1))); err != database.ErrNotFound {
|
||||
t.Fatalf("rejected order row leaked to disk: err=%v", err)
|
||||
}
|
||||
|
||||
// The rejected txs are back in the mempool and can be rebuilt+accepted.
|
||||
drainSignals(toEngine)
|
||||
if vm.mempool.Len() == 0 {
|
||||
t.Fatal("rejected block did not requeue its txs")
|
||||
}
|
||||
blk := buildVerifyAccept(t, vm)
|
||||
if blk.Height() != heightBefore+1 {
|
||||
t.Fatalf("requeued block height = %d, want %d", blk.Height(), heightBefore+1)
|
||||
}
|
||||
if got := len(vm.books[pool].Orders); got != restingBefore+1 {
|
||||
t.Fatalf("requeued+accepted order not resting: %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVMVerifyRejectsLyingRoot proves the matcher-at-Verify guarantee: a block
|
||||
// whose claimed execution root does not match the locally derived one is rejected
|
||||
// by Verify. This is what lets every validator reject a lying proposer.
|
||||
func TestVMVerifyRejectsLyingRoot(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := memdb.New()
|
||||
pool := [32]byte{0x07}
|
||||
vm, toEngine := newTestVM(t, db)
|
||||
|
||||
vm.mempool.Add(ensureMarketTx(t, pool))
|
||||
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideSell, 10.0, 1.0, "m"))
|
||||
drainSignals(toEngine)
|
||||
built, err := vm.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildBlock: %v", err)
|
||||
}
|
||||
good := built.(*Block)
|
||||
|
||||
// Forge a block with the same txs but a tampered execution root.
|
||||
bad := newBlock(vm, good.parentID, good.height, good.timestamp, [Size]byte{0xde, 0xad}, good.txs)
|
||||
if err := bad.Verify(ctx); err == nil {
|
||||
t.Fatal("Verify accepted a block with a lying execution root")
|
||||
}
|
||||
|
||||
// The honest block still verifies + accepts.
|
||||
if err := good.Verify(ctx); err != nil {
|
||||
t.Fatalf("honest Verify: %v", err)
|
||||
}
|
||||
if err := good.Accept(ctx); err != nil {
|
||||
t.Fatalf("honest Accept: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVMDeterministicAcrossInstances proves the cross-validator determinism the
|
||||
// whole design rests on: a block PROPOSED by one VM (carrying the proposer's
|
||||
// timestamp in its bytes) is re-executed by a SECOND, independent VM (separate
|
||||
// DB) to the byte-identical execution root. This models real consensus — the
|
||||
// proposer mints the timestamp once, every validator replays the carried value
|
||||
// (never re-samples its own clock), so the derived root is identical everywhere.
|
||||
//
|
||||
// (Two INDEPENDENTLY-proposed blocks legitimately differ by the proposer's
|
||||
// timestamp; that non-determinism lives only in BuildBlock and is committed to
|
||||
// the block bytes, which is why Verify replays the carried timestamp.)
|
||||
func TestVMDeterministicAcrossInstances(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
pool := [32]byte{0x42}
|
||||
|
||||
// fresh brings up a new VM over its own DB (a distinct validator).
|
||||
fresh := func() (*VM, chan block.Message) {
|
||||
db := memdb.New()
|
||||
return newTestVM(t, db)
|
||||
}
|
||||
|
||||
// applyProposed makes vm adopt a proposer's exact block bytes (ParseBlock ->
|
||||
// Verify -> Accept). Every validator runs this on the SAME bytes, so they all
|
||||
// replay the proposer's carried timestamp and converge to the identical head
|
||||
// and parent root — the precondition for block N+1's root to match too.
|
||||
applyProposed := func(vm *VM, raw []byte) *Block {
|
||||
t.Helper()
|
||||
blk, err := vm.ParseBlock(ctx, raw)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock: %v", err)
|
||||
}
|
||||
if err := blk.Verify(ctx); err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
if err := blk.Accept(ctx); err != nil {
|
||||
t.Fatalf("Accept: %v", err)
|
||||
}
|
||||
return blk.(*Block)
|
||||
}
|
||||
|
||||
// vm1 is the proposer; vm2 is an independent validator on a separate DB.
|
||||
vm1, eng1 := fresh()
|
||||
vm2, _ := fresh()
|
||||
|
||||
// ---- Block 1: vm1 proposes; BOTH adopt the identical bytes. ----
|
||||
vm1.mempool.Add(ensureMarketTx(t, pool))
|
||||
vm1.mempool.Add(placePoolTx(t, pool, zapwire.SideSell, 101.0, 5.0, "maker-a"))
|
||||
vm1.mempool.Add(placePoolTx(t, pool, zapwire.SideBuy, 99.0, 2.0, "maker-c"))
|
||||
drainSignals(eng1)
|
||||
prop1, err := vm1.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildBlock 1: %v", err)
|
||||
}
|
||||
b1a := applyProposed(vm1, prop1.Bytes())
|
||||
b1b := applyProposed(vm2, prop1.Bytes())
|
||||
if b1a.execRoot != b1b.execRoot {
|
||||
t.Fatalf("block 1 root diverged: %x vs %x", b1a.execRoot[:8], b1b.execRoot[:8])
|
||||
}
|
||||
|
||||
// ---- Block 2: vm1 proposes a cross; vm2 re-derives the same root. ----
|
||||
vm1.mempool.Add(submitPoolTx(t, pool, zapwire.SideBuy, false, 101.0, 3.0, "taker"))
|
||||
drainSignals(eng1)
|
||||
prop2, err := vm1.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildBlock 2: %v", err)
|
||||
}
|
||||
// vm2 re-parses + re-executes the proposer's exact bytes. Verify enforces the
|
||||
// derived root equals the claimed root; we assert vm2's derived root matches.
|
||||
reparsed, err := vm2.ParseBlock(ctx, prop2.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock 2 on validator: %v", err)
|
||||
}
|
||||
if err := reparsed.Verify(ctx); err != nil {
|
||||
t.Fatalf("validator Verify 2 (root divergence surfaces here): %v", err)
|
||||
}
|
||||
if reparsed.(*Block).execRoot != prop2.(*Block).execRoot {
|
||||
t.Fatalf("execution root diverged across instances: %x vs %x",
|
||||
prop2.(*Block).execRoot[:8], reparsed.(*Block).execRoot[:8])
|
||||
}
|
||||
}
|
||||
|
||||
// drainSignals empties the buffered toEngine channel so a later WaitForEvent /
|
||||
// Add does not see a stale signal. Tests drive BuildBlock directly, so the signal
|
||||
// is only asserted once (TestVMLifecycle); elsewhere we just clear it.
|
||||
func drainSignals(ch chan block.Message) {
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure the VM satisfies the consensus ChainVM interface at compile time.
|
||||
var _ block.ChainVM = (*VM)(nil)
|
||||
|
||||
// silence unused import in case time is only used transitively.
|
||||
var _ = time.Now
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package lx
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// consensus.go holds the determinism guards that let an OrderBook be driven from
|
||||
// a consensus VM (the d-chain) without any non-deterministic input leaking into
|
||||
// the matched state. There is exactly one rule: in the consensus path the VM owns
|
||||
// the order's ID and timestamp; the book MUST NOT mint a process-local
|
||||
// LastOrderID nor stamp wall-clock time.Now(), because both fork across
|
||||
// validators replaying the same ordered block.
|
||||
//
|
||||
// This does NOT fork the matcher. ConsensusAddOrder reuses the identical resting
|
||||
// insertion that AddOrder uses (addRestingLocked); it only removes the two
|
||||
// non-deterministic side effects. The taker path (SubmitMarketable) was already
|
||||
// made deterministic upstream (it never mints/stamps), so the two consensus
|
||||
// entrypoints are: ConsensusAddOrder (rest a limit order) and SubmitMarketable
|
||||
// (cross a marketable order). The d-chain never calls MatchOrders — that
|
||||
// resting-cross sweep stamps time.Now() (orderbook.go) and is for the
|
||||
// non-consensus, single-process server only.
|
||||
|
||||
// nanosToTime converts unix-nanoseconds (as carried by a DEXOrder row and by the
|
||||
// block timestamp) back to a time.Time. A zero value maps to the zero time so a
|
||||
// never-stamped order stays unstamped.
|
||||
func nanosToTime(nanos int64) time.Time {
|
||||
if nanos == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(0, nanos).UTC()
|
||||
}
|
||||
|
||||
// ConsensusAddOrder rests a limit order using a caller-supplied, deterministic
|
||||
// ID and Timestamp. It is the d-chain's single entry for placing resting
|
||||
// liquidity. Unlike AddOrder it never mints order.ID from the LastOrderID counter
|
||||
// and never stamps order.Timestamp from time.Now(): the VM derives both from
|
||||
// block context (height/txIndex for the ID, block timestamp for the stamp) before
|
||||
// calling this, so every validator replaying the same block produces the
|
||||
// byte-identical resting order.
|
||||
//
|
||||
// Preconditions (the VM guarantees them; we do not silently repair a violation
|
||||
// because a zero ID/timestamp in consensus is a determinism bug, not a default):
|
||||
// - order.ID != 0
|
||||
// - !order.Timestamp.IsZero()
|
||||
//
|
||||
// If either preconditions is violated the order is rejected (returns 0) rather
|
||||
// than minting a non-deterministic value. Returns the order ID on success.
|
||||
func (ob *OrderBook) ConsensusAddOrder(order *Order) uint64 {
|
||||
if order == nil || order.ID == 0 || order.Timestamp.IsZero() {
|
||||
return 0
|
||||
}
|
||||
if order.Status == "" {
|
||||
order.Status = Open
|
||||
}
|
||||
if order.RemainingSize == 0 && order.Filled == 0 {
|
||||
order.RemainingSize = order.Size
|
||||
}
|
||||
return ob.addRestingLocked(order)
|
||||
}
|
||||
|
||||
// addRestingLocked is the resting-insertion core shared by the consensus path. It
|
||||
// validates the order, runs self-trade / post-only guards, inserts into the
|
||||
// correct price tree under ob.mu, and tracks it in the order maps. It performs NO
|
||||
// matching: a resting limit order placed via the CLOB never crosses on placement
|
||||
// (takers cross via SubmitMarketable). It assumes order.ID and order.RemainingSize
|
||||
// are already set.
|
||||
//
|
||||
// This is the same insertion AddOrder performs for a plain GTC limit order; it is
|
||||
// factored out so the consensus entry and the legacy entry share one body (no
|
||||
// second copy of the tree-insert/tracking logic to drift).
|
||||
func (ob *OrderBook) addRestingLocked(order *Order) uint64 {
|
||||
if err := ob.validateOrder(order); err != nil {
|
||||
order.Status = Rejected
|
||||
return 0
|
||||
}
|
||||
|
||||
priceInt := PriceInt(order.Price * PriceMultiplier)
|
||||
|
||||
ob.mu.Lock()
|
||||
defer ob.mu.Unlock()
|
||||
|
||||
// Self-trade prevention against the resting book.
|
||||
userIdentifier := order.User
|
||||
if userIdentifier == "" {
|
||||
userIdentifier = order.UserID
|
||||
}
|
||||
if userIdentifier != "" && ob.checkSelfTrade(order) {
|
||||
order.Status = Rejected
|
||||
return 0
|
||||
}
|
||||
|
||||
// Post-only: a resting order that would take is rejected.
|
||||
if order.PostOnly || order.Flags&OrderFlagPostOnly != 0 {
|
||||
if ob.wouldTakeLiquidity(order) {
|
||||
order.Status = Rejected
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
var tree *OrderTree
|
||||
if order.Side == Buy {
|
||||
tree = (*OrderTree)(atomic.LoadPointer(&ob.bids))
|
||||
} else {
|
||||
tree = (*OrderTree)(atomic.LoadPointer(&ob.asks))
|
||||
}
|
||||
ob.addToTreeOptimized(tree, order, priceInt)
|
||||
|
||||
ob.Orders[order.ID] = order
|
||||
ob.ordersMap.Store(order.ID, order)
|
||||
if ob.UserOrders[order.User] == nil {
|
||||
ob.UserOrders[order.User] = make([]uint64, 0)
|
||||
}
|
||||
ob.UserOrders[order.User] = append(ob.UserOrders[order.User], order.ID)
|
||||
|
||||
return order.ID
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -715,6 +716,22 @@ func (ob *OrderBook) tryMatchImmediateLocked(order *Order) uint64 {
|
||||
}
|
||||
tradeSize = math.Min(order.RemainingSize, bestRemaining)
|
||||
|
||||
// EXACT-INTEGER QUANTITY LANE (value conservation). When BOTH the taker
|
||||
// and the resting maker carry the integer quantity lane (CLOB / consensus
|
||||
// path), the matched base is min(remainingUnits) computed in big.Int — the
|
||||
// authoritative quantity, immune to the float64 precision loss that mints
|
||||
// or burns value for 18-decimal token amounts above 2^53. The float
|
||||
// tradeSize above still drives the crossing loop and legacy bookkeeping;
|
||||
// the integer base/quote below are what settlement is derived from. If
|
||||
// either side lacks the lane (legacy float API), tradeUnits stays nil and
|
||||
// the fill carries no integer truth, preserving legacy behavior exactly.
|
||||
takerUnits := ensureRemainingUnits(order)
|
||||
makerUnits := ensureRemainingUnits(bestOrder)
|
||||
var tradeUnits *big.Int
|
||||
if takerUnits != nil && makerUnits != nil {
|
||||
tradeUnits = minBig(takerUnits, makerUnits)
|
||||
}
|
||||
|
||||
// Create trade. CONSENSUS DETERMINISM: stamp the trade with the
|
||||
// INCOMING order's timestamp (the d-chain supplies a block-derived,
|
||||
// deterministic value via SubmitMarketable) rather than wall-clock, so
|
||||
@@ -735,6 +752,17 @@ func (ob *OrderBook) tryMatchImmediateLocked(order *Order) uint64 {
|
||||
Timestamp: ts,
|
||||
TakerSide: order.Side,
|
||||
}
|
||||
if tradeUnits != nil {
|
||||
// Quote is an exact integer function of the matched base and the
|
||||
// maker's resting price (its PriceInt key) — never of any float size.
|
||||
priceInt := PriceInt(bestOrder.Price * PriceMultiplier)
|
||||
trade.BaseUnits = tradeUnits
|
||||
trade.QuoteUnits = quoteUnitsFor(tradeUnits, priceInt)
|
||||
// Decrement the exact integer remainders so a multi-level / partial
|
||||
// sweep stays integer-consistent across iterations.
|
||||
takerUnits.Sub(takerUnits, tradeUnits)
|
||||
makerUnits.Sub(makerUnits, tradeUnits)
|
||||
}
|
||||
if order.Side == Buy {
|
||||
trade.BuyOrder, trade.SellOrder = order.ID, bestOrder.ID
|
||||
trade.Buyer, trade.Seller = order.User, bestOrder.User
|
||||
|
||||
@@ -6,6 +6,7 @@ package lx
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestMatchOrder_GPUMatchesCPU pins the field-equality contract between
|
||||
@@ -89,3 +90,127 @@ func TestMatchOrder_GPUMatchesCPU(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// clobTx is one operation in a deterministic CLOB transaction sequence: either
|
||||
// rest a limit order (place) or cross the book with a marketable order (submit).
|
||||
// IDs and timestamps are supplied explicitly — exactly as the d-chain VM supplies
|
||||
// them from block context — so the replay is a pure function of the sequence.
|
||||
type clobTx struct {
|
||||
submit bool // false = place (rest), true = submit (cross)
|
||||
id uint64
|
||||
side Side
|
||||
price float64
|
||||
size float64
|
||||
user string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// replaySequence runs a CLOB tx sequence through a fresh book via the
|
||||
// deterministic consensus entrypoints (ConsensusAddOrder for places,
|
||||
// SubmitMarketable for submits, with VM-supplied ID/timestamp). It returns the
|
||||
// concatenated fills (as canonical DEXTrade rows, taker side bound in) and the
|
||||
// final book as canonical DEXOrder rows — the two byte-streams a validator
|
||||
// commits. It NEVER calls AddOrder (would mint) or MatchOrders (would stamp
|
||||
// time.Now()).
|
||||
func replaySequence(symbol string, txs []clobTx) (fills []DEXTrade, rows []DEXOrder) {
|
||||
ob := NewOrderBook(symbol)
|
||||
for _, tx := range txs {
|
||||
if tx.submit {
|
||||
o := &Order{
|
||||
ID: tx.id, Type: Limit, Side: tx.side, Price: tx.price, Size: tx.size,
|
||||
User: tx.user, UserID: tx.user, Symbol: symbol, Timestamp: tx.timestamp,
|
||||
}
|
||||
trades, err := ob.SubmitMarketable(o)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, tr := range trades {
|
||||
fills = append(fills, TradeToRow(tr, tx.side))
|
||||
}
|
||||
} else {
|
||||
o := &Order{
|
||||
ID: tx.id, Type: Limit, Side: tx.side, Price: tx.price, Size: tx.size,
|
||||
User: tx.user, UserID: tx.user, Symbol: symbol, Timestamp: tx.timestamp,
|
||||
}
|
||||
ob.ConsensusAddOrder(o)
|
||||
}
|
||||
}
|
||||
return fills, BookToRows(ob)
|
||||
}
|
||||
|
||||
// TestConsensusReplayDeterminism is the CRITICAL step-2 determinism proof: two
|
||||
// replays of the IDENTICAL CLOB tx sequence produce BYTE-IDENTICAL fills and
|
||||
// BYTE-IDENTICAL book rows. This is the property a multi-validator d-chain
|
||||
// depends on — every honest validator that replays the same ordered block
|
||||
// derives the same fills and the same committed state. The consensus path
|
||||
// (ConsensusAddOrder + SubmitMarketable) supplies ID/timestamp, so nothing
|
||||
// non-deterministic (no LastOrderID mint, no time.Now()) leaks in.
|
||||
func TestConsensusReplayDeterminism(t *testing.T) {
|
||||
base := time.Unix(0, 1_700_000_000_000_000_000).UTC()
|
||||
ts := func(i int) time.Time { return base.Add(time.Duration(i) * time.Millisecond) }
|
||||
|
||||
// A representative sequence: rest both sides, then cross with marketable
|
||||
// orders that partially fill across multiple price levels, then rest more.
|
||||
txs := []clobTx{
|
||||
{false, 1, Sell, 101.0, 5.0, "maker-a", ts(0)},
|
||||
{false, 2, Sell, 101.5, 3.0, "maker-b", ts(1)},
|
||||
{false, 3, Sell, 102.0, 4.0, "maker-c", ts(2)},
|
||||
{false, 4, Buy, 99.0, 2.0, "maker-d", ts(3)},
|
||||
{false, 5, Buy, 98.5, 6.0, "maker-e", ts(4)},
|
||||
{true, 6, Buy, 102.0, 7.0, "taker-x", ts(5)}, // crosses 101.0 + 101.5 levels
|
||||
{true, 7, Sell, 98.5, 3.0, "taker-y", ts(6)}, // crosses 99.0 + 98.5 levels
|
||||
{false, 8, Sell, 101.25, 2.0, "maker-f", ts(7)},
|
||||
{true, 9, Buy, 101.25, 1.0, "taker-z", ts(8)}, // partial against maker-f
|
||||
}
|
||||
|
||||
fills1, rows1 := replaySequence("BTC-USD", txs)
|
||||
fills2, rows2 := replaySequence("BTC-USD", txs)
|
||||
|
||||
// The sequence must actually produce fills, or the test proves nothing.
|
||||
if len(fills1) == 0 {
|
||||
t.Fatal("expected the marketable submits to produce fills; sequence produced none")
|
||||
}
|
||||
|
||||
if len(fills1) != len(fills2) {
|
||||
t.Fatalf("fill count differs across replays: %d vs %d", len(fills1), len(fills2))
|
||||
}
|
||||
for i := range fills1 {
|
||||
if string(EncodeTrade(fills1[i])) != string(EncodeTrade(fills2[i])) {
|
||||
t.Errorf("fill[%d] NOT byte-identical across replays:\n a=%+v\n b=%+v",
|
||||
i, fills1[i], fills2[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(rows1) != len(rows2) {
|
||||
t.Fatalf("book row count differs across replays: %d vs %d", len(rows1), len(rows2))
|
||||
}
|
||||
for i := range rows1 {
|
||||
if string(EncodeRow(rows1[i])) != string(EncodeRow(rows2[i])) {
|
||||
t.Errorf("book row[%d] NOT byte-identical across replays:\n a=%+v\n b=%+v",
|
||||
i, rows1[i], rows2[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConsensusAddOrderRejectsNonDeterministicInput proves the guard refuses to
|
||||
// mint: an order with a zero ID or zero timestamp is rejected (returns 0) rather
|
||||
// than silently acquiring a process-local counter value or wall-clock stamp.
|
||||
func TestConsensusAddOrderRejectsNonDeterministicInput(t *testing.T) {
|
||||
ob := NewOrderBook("BTC-USD")
|
||||
ts := time.Unix(0, 1_700_000_000_000_000_000).UTC()
|
||||
|
||||
if got := ob.ConsensusAddOrder(&Order{ID: 0, Type: Limit, Side: Buy, Price: 100, Size: 1, User: "a", Timestamp: ts}); got != 0 {
|
||||
t.Errorf("ConsensusAddOrder accepted a zero-ID order: returned %d", got)
|
||||
}
|
||||
if got := ob.ConsensusAddOrder(&Order{ID: 1, Type: Limit, Side: Buy, Price: 100, Size: 1, User: "a"}); got != 0 {
|
||||
t.Errorf("ConsensusAddOrder accepted a zero-timestamp order: returned %d", got)
|
||||
}
|
||||
// LastOrderID must not have moved — no minting happened.
|
||||
if ob.LastOrderID != 0 {
|
||||
t.Errorf("ConsensusAddOrder minted LastOrderID despite rejection: %d", ob.LastOrderID)
|
||||
}
|
||||
// A well-formed order is accepted with its supplied ID.
|
||||
if got := ob.ConsensusAddOrder(&Order{ID: 7, Type: Limit, Side: Buy, Price: 100, Size: 1, User: "a", Timestamp: ts}); got != 7 {
|
||||
t.Errorf("ConsensusAddOrder rejected a valid order: returned %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package lx
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"sort"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// persist.go is the DEXOrder <-> OrderBook bridge: the deterministic, flat-row
|
||||
// serialization the d-chain VM uses to persist a book to durable storage and to
|
||||
// rebuild it on restart. pkg/lx has no other persistence; this is the single,
|
||||
// canonical encoding.
|
||||
//
|
||||
// THE ROW IS THE GPU ABI. A persisted resting order is a 64-byte DEXOrder
|
||||
// (orderbook_cuda_types.go) — the exact, byte-for-byte layout the CUDA/HIP/Metal
|
||||
// matching kernels consume. Using one representation for "the order as stored"
|
||||
// and "the order as fed to the matcher" means there is exactly one row format in
|
||||
// the system: zero divergence between what consensus commits and what the
|
||||
// accelerator matches.
|
||||
//
|
||||
// PRICE/SIZE ARE Q64.64 FIXED-POINT, NEVER float64. The book's in-RAM Order
|
||||
// carries float64 Price/Size for the legacy API, but float64 is NOT
|
||||
// byte-reproducible across architectures (x86_64 vs aarch64 rounding differs at
|
||||
// the ULP), so persisting or merkleizing the float would fork consensus across a
|
||||
// heterogeneous validator set. The row therefore stores the integer Q64.64 form
|
||||
// (DEXPrice{Integer, Fraction}) and an integer base quantity in 1e8 fixed units.
|
||||
// The conversion here is the ONE place float64 crosses into the deterministic
|
||||
// domain, and it is a pure, total, deterministic function of the float bits.
|
||||
//
|
||||
// USER IDENTITY IN A ROW IS 8 BYTES. DEXOrder.UserID is a uint64 (the GPU ABI's
|
||||
// user field, used for on-device self-trade prevention). The wire/protocol user
|
||||
// is a 16-byte field and the book's Order.User is a string; the row folds that to
|
||||
// the leading 8 bytes (big-endian). This is sufficient for the matcher's STP and
|
||||
// for rebuilding price-time priority. The CANONICAL 16-byte settlement/dedup
|
||||
// identity is NOT a property of the order-book row — it lives in d-chain state
|
||||
// (the idempotency index keyed on user[16]+seq). Keeping the row at the GPU ABI's
|
||||
// 8-byte user is the decomplected choice: the book row models matching, d-chain
|
||||
// state models settlement identity.
|
||||
|
||||
// QuantityScale is the fixed-point scale for the integer base quantity stored in
|
||||
// a DEXOrder row (Quantity / Remaining). A float64 size s is stored as
|
||||
// round(s * 1e8); 1e8 matches PriceMultiplier so the integer domain is uniform
|
||||
// across price and size. The matcher's resting quantities are always
|
||||
// non-negative, so the unsigned domain is exact for any size representable in
|
||||
// 1e8 units up to ~1.8e11 base units.
|
||||
const QuantityScale = float64(PriceMultiplier) // 1e8
|
||||
|
||||
// floatToFixedPrice converts a float64 price to the Q64.64 DEXPrice form
|
||||
// deterministically. The integer part is the floor; the fractional part is the
|
||||
// remaining mantissa scaled by 2^64 and rounded to nearest. This is a pure
|
||||
// function of the IEEE-754 value: every architecture that agrees on the float
|
||||
// bits (which they do for a value carried verbatim on the wire) computes the
|
||||
// identical {Integer, Fraction}. Negative or non-finite inputs clamp to zero;
|
||||
// the matcher rejects those upstream so they never reach a persisted row.
|
||||
func floatToFixedPrice(price float64) DEXPrice {
|
||||
if !(price > 0) { // also catches NaN
|
||||
return DEXPrice{}
|
||||
}
|
||||
intPart := math.Floor(price)
|
||||
if intPart > math.MaxUint64 {
|
||||
// Saturate rather than wrap; safePriceToInt already bounds real prices
|
||||
// far below this, so this is a defensive clamp, not a live path.
|
||||
return DEXPrice{Integer: math.MaxUint64, Fraction: math.MaxUint64}
|
||||
}
|
||||
frac := price - intPart
|
||||
// frac in [0,1); scale to the 64-bit fraction domain. math.Round gives the
|
||||
// nearest integer; 2^64 is represented exactly as a float64.
|
||||
fracUnits := math.Round(frac * 18446744073709551616.0) // 2^64
|
||||
var fraction uint64
|
||||
if fracUnits >= 18446744073709551616.0 {
|
||||
// Rounding pushed it to a full unit: carry into the integer part.
|
||||
return DEXPrice{Integer: uint64(intPart) + 1, Fraction: 0}
|
||||
}
|
||||
fraction = uint64(fracUnits)
|
||||
return DEXPrice{Integer: uint64(intPart), Fraction: fraction}
|
||||
}
|
||||
|
||||
// fixedPriceToFloat converts a Q64.64 DEXPrice back to float64. This is the
|
||||
// inverse used only to repopulate the in-RAM Order.Price when rebuilding the
|
||||
// book from rows; the deterministic state (rows, merkle leaves) always uses the
|
||||
// integer form, so any float rounding here never affects consensus.
|
||||
func fixedPriceToFloat(p DEXPrice) float64 {
|
||||
return float64(p.Integer) + float64(p.Fraction)/18446744073709551616.0 // /2^64
|
||||
}
|
||||
|
||||
// floatToFixedQty converts a float64 base quantity to the integer 1e8 fixed-unit
|
||||
// form stored in a row. Rounds to nearest; clamps a negative/NaN to zero.
|
||||
func floatToFixedQty(qty float64) uint64 {
|
||||
if !(qty > 0) {
|
||||
return 0
|
||||
}
|
||||
scaled := math.Round(qty * QuantityScale)
|
||||
if scaled >= math.MaxUint64 {
|
||||
return math.MaxUint64
|
||||
}
|
||||
return uint64(scaled)
|
||||
}
|
||||
|
||||
// fixedQtyToFloat converts an integer 1e8 fixed-unit quantity back to float64.
|
||||
func fixedQtyToFloat(q uint64) float64 {
|
||||
return float64(q) / QuantityScale
|
||||
}
|
||||
|
||||
// userToUint64 folds a user identity string to the row's 8-byte UserID
|
||||
// (big-endian over the leading 8 bytes, zero-padded). Deterministic and total.
|
||||
func userToUint64(user string) uint64 {
|
||||
var b [8]byte
|
||||
copy(b[:], user)
|
||||
return binary.BigEndian.Uint64(b[:])
|
||||
}
|
||||
|
||||
// uint64ToUser renders an 8-byte UserID back to the trimmed string form used by
|
||||
// the in-RAM book. Trailing NULs are removed so a short user round-trips to its
|
||||
// original string (the common case where the user fits in 8 bytes).
|
||||
func uint64ToUser(id uint64) string {
|
||||
var b [8]byte
|
||||
binary.BigEndian.PutUint64(b[:], id)
|
||||
n := 8
|
||||
for n > 0 && b[n-1] == 0 {
|
||||
n--
|
||||
}
|
||||
return string(b[:n])
|
||||
}
|
||||
|
||||
// sideToDEX maps the book Side to the DEXOrder side byte.
|
||||
func sideToDEX(s Side) uint8 {
|
||||
if s == Sell {
|
||||
return DEXSideAsk
|
||||
}
|
||||
return DEXSideBid
|
||||
}
|
||||
|
||||
// dexToSide maps a DEXOrder side byte back to the book Side.
|
||||
func dexToSide(s uint8) Side {
|
||||
if s == DEXSideAsk {
|
||||
return Sell
|
||||
}
|
||||
return Buy
|
||||
}
|
||||
|
||||
// typeToDEX maps the book OrderType to the DEXOrder type byte. Only the resting
|
||||
// forms are persistable (a resting order is always a Limit by construction in
|
||||
// the consensus CLOB path); Market/IOC/FOK takers never rest and so never reach
|
||||
// a row. Anything non-Limit maps to DEXOrderLimit defensively.
|
||||
func typeToDEX(t OrderType) uint8 {
|
||||
switch t {
|
||||
case Market:
|
||||
return DEXOrderMarket
|
||||
default:
|
||||
return DEXOrderLimit
|
||||
}
|
||||
}
|
||||
|
||||
// dexToType maps a DEXOrder type byte back to the book OrderType.
|
||||
func dexToType(t uint8) OrderType {
|
||||
switch t {
|
||||
case DEXOrderMarket:
|
||||
return Market
|
||||
default:
|
||||
return Limit
|
||||
}
|
||||
}
|
||||
|
||||
// statusToDEX maps a book status string to the DEXOrder status byte.
|
||||
func statusToDEX(status string, remaining, size float64) uint8 {
|
||||
switch status {
|
||||
case Canceled, StatusCancelled:
|
||||
return DEXStatusCancelled
|
||||
case Filled:
|
||||
return DEXStatusFilled
|
||||
}
|
||||
if remaining < size {
|
||||
return DEXStatusPartial
|
||||
}
|
||||
return DEXStatusOpen
|
||||
}
|
||||
|
||||
// dexToStatus maps a DEXOrder status byte back to a book status string.
|
||||
func dexToStatus(s uint8) string {
|
||||
switch s {
|
||||
case DEXStatusFilled:
|
||||
return Filled
|
||||
case DEXStatusCancelled:
|
||||
return Canceled
|
||||
case DEXStatusPartial:
|
||||
return PartiallyFilled
|
||||
default:
|
||||
return Open
|
||||
}
|
||||
}
|
||||
|
||||
// OrderToRow converts a resting Order to its canonical 64-byte DEXOrder row. The
|
||||
// price/size are committed to the deterministic Q64.64 / 1e8-integer domain; the
|
||||
// padding is zeroed so two equal orders always produce byte-identical rows.
|
||||
func OrderToRow(o *Order) DEXOrder {
|
||||
remaining := o.RemainingSize
|
||||
if remaining == 0 && o.Size > 0 && o.Filled == 0 {
|
||||
remaining = o.Size
|
||||
}
|
||||
user := o.User
|
||||
if user == "" {
|
||||
user = o.UserID
|
||||
}
|
||||
row := DEXOrder{
|
||||
OrderID: o.ID,
|
||||
UserID: userToUint64(user),
|
||||
Price: floatToFixedPrice(o.Price),
|
||||
Quantity: floatToFixedQty(o.Size),
|
||||
Remaining: floatToFixedQty(remaining),
|
||||
Timestamp: uint64(o.Timestamp.UnixNano()),
|
||||
Side: sideToDEX(o.Side),
|
||||
Type: typeToDEX(o.Type),
|
||||
Status: statusToDEX(o.Status, remaining, o.Size),
|
||||
}
|
||||
row.ClearPadding()
|
||||
return row
|
||||
}
|
||||
|
||||
// RowToOrder reconstructs an in-RAM Order from a canonical DEXOrder row. The
|
||||
// deterministic integer fields are the source of truth; the float Price/Size are
|
||||
// derived for the legacy book API only. Timestamp is rebuilt from the stored
|
||||
// unix-nanos so the resting order keeps its consensus-assigned, block-derived
|
||||
// stamp (price-time priority is preserved exactly across a restart).
|
||||
func RowToOrder(row DEXOrder) *Order {
|
||||
user := uint64ToUser(row.UserID)
|
||||
return &Order{
|
||||
ID: row.OrderID,
|
||||
Type: dexToType(row.Type),
|
||||
Side: dexToSide(row.Side),
|
||||
Price: fixedPriceToFloat(row.Price),
|
||||
Size: fixedQtyToFloat(row.Quantity),
|
||||
RemainingSize: fixedQtyToFloat(row.Remaining),
|
||||
Timestamp: nanosToTime(int64(row.Timestamp)),
|
||||
User: user,
|
||||
UserID: user,
|
||||
Status: dexToStatus(row.Status),
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeRow writes a DEXOrder as its 64 byte on-disk value. The bytes are the
|
||||
// raw little-endian struct image (matching the GPU ABI's in-memory layout on the
|
||||
// little-endian targets the venue runs); a fixed 64-byte record means a row is a
|
||||
// single LSM value with zero framing overhead.
|
||||
func EncodeRow(row DEXOrder) []byte {
|
||||
row.ClearPadding()
|
||||
out := make([]byte, unsafe.Sizeof(DEXOrder{}))
|
||||
*(*DEXOrder)(unsafe.Pointer(&out[0])) = row
|
||||
return out
|
||||
}
|
||||
|
||||
// DecodeRow reads a 64-byte on-disk value back into a DEXOrder. Returns ok=false
|
||||
// for any value that is not exactly the row width, so a truncated/garbage record
|
||||
// can never be silently reinterpreted into a different order.
|
||||
func DecodeRow(b []byte) (DEXOrder, bool) {
|
||||
if uintptr(len(b)) != unsafe.Sizeof(DEXOrder{}) {
|
||||
return DEXOrder{}, false
|
||||
}
|
||||
row := *(*DEXOrder)(unsafe.Pointer(&b[0]))
|
||||
row.ClearPadding()
|
||||
return row, true
|
||||
}
|
||||
|
||||
// EncodeTrade writes a DEXTrade as its 80 byte on-disk value (the trade-log
|
||||
// record). Same flat-image discipline as EncodeRow.
|
||||
func EncodeTrade(t DEXTrade) []byte {
|
||||
t.ClearPadding()
|
||||
out := make([]byte, unsafe.Sizeof(DEXTrade{}))
|
||||
*(*DEXTrade)(unsafe.Pointer(&out[0])) = t
|
||||
return out
|
||||
}
|
||||
|
||||
// DecodeTrade reads an 80-byte on-disk value back into a DEXTrade.
|
||||
func DecodeTrade(b []byte) (DEXTrade, bool) {
|
||||
if uintptr(len(b)) != unsafe.Sizeof(DEXTrade{}) {
|
||||
return DEXTrade{}, false
|
||||
}
|
||||
t := *(*DEXTrade)(unsafe.Pointer(&b[0]))
|
||||
t.ClearPadding()
|
||||
return t, true
|
||||
}
|
||||
|
||||
// TradeToRow converts a matched Trade to its canonical 80-byte DEXTrade row,
|
||||
// committing price/size to the deterministic domain. takerSide is the side the
|
||||
// taker submitted with (the resting maker is the opposite side); it is supplied
|
||||
// by the caller because the in-RAM Trade does not always carry it.
|
||||
func TradeToRow(t Trade, takerSide Side) DEXTrade {
|
||||
row := DEXTrade{
|
||||
TradeID: t.ID,
|
||||
MakerUserID: userToUint64(makerUser(t, takerSide)),
|
||||
TakerUserID: userToUint64(takerUser(t, takerSide)),
|
||||
Price: floatToFixedPrice(t.Price),
|
||||
Quantity: floatToFixedQty(t.Size),
|
||||
Timestamp: uint64(t.Timestamp.UnixNano()),
|
||||
TakerSide: sideToDEX(takerSide),
|
||||
}
|
||||
// Maker/taker order IDs: the buy order is BuyOrder, the sell is SellOrder;
|
||||
// the taker is whichever matches takerSide.
|
||||
if takerSide == Buy {
|
||||
row.TakerOrderID = t.BuyOrder
|
||||
row.MakerOrderID = t.SellOrder
|
||||
} else {
|
||||
row.TakerOrderID = t.SellOrder
|
||||
row.MakerOrderID = t.BuyOrder
|
||||
}
|
||||
row.ClearPadding()
|
||||
return row
|
||||
}
|
||||
|
||||
// makerUser returns the resting (maker) user string for a trade given the taker
|
||||
// side. If the taker bought, the maker sold (Seller); else the maker bought.
|
||||
func makerUser(t Trade, takerSide Side) string {
|
||||
if takerSide == Buy {
|
||||
return pickUser(t.Seller, t.SellUserID)
|
||||
}
|
||||
return pickUser(t.Buyer, t.BuyUserID)
|
||||
}
|
||||
|
||||
// takerUser returns the taker user string for a trade given the taker side.
|
||||
func takerUser(t Trade, takerSide Side) string {
|
||||
if takerSide == Buy {
|
||||
return pickUser(t.Buyer, t.BuyUserID)
|
||||
}
|
||||
return pickUser(t.Seller, t.SellUserID)
|
||||
}
|
||||
|
||||
func pickUser(primary, fallback string) string {
|
||||
if primary != "" {
|
||||
return primary
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// BookToRows serializes every resting order in the book to canonical DEXOrder
|
||||
// rows, ordered deterministically: bids by descending price then ascending
|
||||
// orderID, then asks by ascending price then ascending orderID. This ordering is
|
||||
// the canonical book ordering used for the book merkle root, so the row stream is
|
||||
// itself deterministic and a round-trip is byte-stable.
|
||||
//
|
||||
// Only the resting side of the book is serialized — the trade log and meta are
|
||||
// persisted separately by the d-chain state layer. The book is, per the
|
||||
// cryptographer's design, a rebuildable accelerator: these rows are sufficient to
|
||||
// reconstruct it exactly.
|
||||
func BookToRows(ob *OrderBook) []DEXOrder {
|
||||
if ob == nil {
|
||||
return nil
|
||||
}
|
||||
ob.mu.RLock()
|
||||
defer ob.mu.RUnlock()
|
||||
|
||||
rows := make([]DEXOrder, 0, len(ob.Orders))
|
||||
for _, o := range ob.Orders {
|
||||
if o == nil {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, OrderToRow(o))
|
||||
}
|
||||
sortRows(rows)
|
||||
return rows
|
||||
}
|
||||
|
||||
// sortRows imposes the canonical book ordering on a row slice: bids (side=bid)
|
||||
// first by descending price, asks by ascending price, ties broken by ascending
|
||||
// orderID. Deterministic and total.
|
||||
func sortRows(rows []DEXOrder) {
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
a, b := rows[i], rows[j]
|
||||
// bids before asks
|
||||
if a.Side != b.Side {
|
||||
return a.Side == DEXSideBid
|
||||
}
|
||||
if a.Side == DEXSideBid {
|
||||
// descending price for bids
|
||||
if a.Price.Integer != b.Price.Integer {
|
||||
return a.Price.Integer > b.Price.Integer
|
||||
}
|
||||
if a.Price.Fraction != b.Price.Fraction {
|
||||
return a.Price.Fraction > b.Price.Fraction
|
||||
}
|
||||
} else {
|
||||
// ascending price for asks
|
||||
if a.Price.Integer != b.Price.Integer {
|
||||
return a.Price.Integer < b.Price.Integer
|
||||
}
|
||||
if a.Price.Fraction != b.Price.Fraction {
|
||||
return a.Price.Fraction < b.Price.Fraction
|
||||
}
|
||||
}
|
||||
return a.OrderID < b.OrderID
|
||||
})
|
||||
}
|
||||
|
||||
// RowsToBook reconstructs a fresh OrderBook for symbol from a row stream. Each
|
||||
// row becomes a resting Order inserted via ConsensusAddOrder (deterministic
|
||||
// path: the row's own ID and timestamp are authoritative, never re-minted). The
|
||||
// LastOrderID watermark is advanced past the highest seen ID so any future
|
||||
// consensus order keeps a monotonic ID space. Fully-filled / cancelled rows are
|
||||
// skipped — only live resting liquidity is rebuilt.
|
||||
func RowsToBook(symbol string, rows []DEXOrder) *OrderBook {
|
||||
ob := NewOrderBook(symbol)
|
||||
var maxID uint64
|
||||
for _, row := range rows {
|
||||
if row.OrderID > maxID {
|
||||
maxID = row.OrderID
|
||||
}
|
||||
if row.Status == DEXStatusFilled || row.Status == DEXStatusCancelled {
|
||||
continue
|
||||
}
|
||||
if row.Remaining == 0 {
|
||||
continue
|
||||
}
|
||||
o := RowToOrder(row)
|
||||
o.Symbol = symbol
|
||||
ob.ConsensusAddOrder(o)
|
||||
}
|
||||
ob.LastOrderID = maxID
|
||||
return ob
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package lx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestFixedPointRoundTrip proves the Q64.64 price and 1e8 quantity conversions
|
||||
// are stable: encoding a float into the deterministic integer domain and back
|
||||
// recovers the value to within one ULP of the fixed-point grid, AND — the part
|
||||
// that matters for consensus — the integer form itself is identical for the same
|
||||
// input every time (it is a pure function of the float bits).
|
||||
func TestFixedPointRoundTrip(t *testing.T) {
|
||||
prices := []float64{1, 100, 100.5, 0.00000001, 12345.6789, 999999.99999999, 0.1, 3.14159265358979}
|
||||
for _, p := range prices {
|
||||
fx := floatToFixedPrice(p)
|
||||
// determinism: same input -> same fixed-point.
|
||||
if fx != floatToFixedPrice(p) {
|
||||
t.Fatalf("floatToFixedPrice(%v) not deterministic", p)
|
||||
}
|
||||
back := fixedPriceToFloat(fx)
|
||||
// The grid step for the fraction is 2^-64, far finer than 1e-8, so the
|
||||
// recovered value must match to 1e-8.
|
||||
if diff := back - p; diff > 1e-8 || diff < -1e-8 {
|
||||
t.Errorf("price round-trip %v -> %+v -> %v (diff %v)", p, fx, back, diff)
|
||||
}
|
||||
}
|
||||
|
||||
sizes := []float64{1, 0.5, 0.00000001, 1000000, 42.123456}
|
||||
for _, s := range sizes {
|
||||
q := floatToFixedQty(s)
|
||||
if q != floatToFixedQty(s) {
|
||||
t.Fatalf("floatToFixedQty(%v) not deterministic", s)
|
||||
}
|
||||
back := fixedQtyToFloat(q)
|
||||
if diff := back - s; diff > 1e-8 || diff < -1e-8 {
|
||||
t.Errorf("qty round-trip %v -> %d -> %v (diff %v)", s, q, back, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRowEncodeDecode proves the 64-byte row image round-trips and rejects
|
||||
// mis-sized input.
|
||||
func TestRowEncodeDecode(t *testing.T) {
|
||||
row := DEXOrder{
|
||||
OrderID: 42,
|
||||
UserID: userToUint64("alice"),
|
||||
Price: floatToFixedPrice(100.25),
|
||||
Quantity: floatToFixedQty(5),
|
||||
Remaining: floatToFixedQty(3),
|
||||
Timestamp: 1700000000000000000,
|
||||
Side: DEXSideBid,
|
||||
Type: DEXOrderLimit,
|
||||
Status: DEXStatusPartial,
|
||||
}
|
||||
b := EncodeRow(row)
|
||||
if len(b) != 64 {
|
||||
t.Fatalf("encoded row len = %d, want 64", len(b))
|
||||
}
|
||||
got, ok := DecodeRow(b)
|
||||
if !ok {
|
||||
t.Fatal("DecodeRow rejected a valid 64-byte row")
|
||||
}
|
||||
if !got.EqualFields(row) {
|
||||
t.Errorf("row decode mismatch:\n got %+v\nwant %+v", got, row)
|
||||
}
|
||||
if _, ok := DecodeRow(b[:63]); ok {
|
||||
t.Error("DecodeRow accepted a 63-byte value")
|
||||
}
|
||||
if _, ok := DecodeRow(append(b, 0)); ok {
|
||||
t.Error("DecodeRow accepted a 65-byte value")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBookToRowsRoundTrip is the CRITICAL step-1 test: a populated book
|
||||
// serialized to rows and rebuilt must be EQUAL to the original — same resting
|
||||
// orders, same price-time priority, same best bid/ask, same depth. Equality is
|
||||
// checked through the canonical row stream (the deterministic representation),
|
||||
// which is exactly what the merkle root and persistence consume.
|
||||
func TestBookToRowsRoundTrip(t *testing.T) {
|
||||
ts := time.Unix(0, 1700000000000000000).UTC()
|
||||
ob := NewOrderBook("BTC-USD")
|
||||
|
||||
// Rest a deterministic spread: 3 bids, 3 asks, with a couple of multi-order
|
||||
// price levels to exercise time priority.
|
||||
place := func(id uint64, side Side, price, size float64, user string, dt time.Duration) {
|
||||
o := &Order{
|
||||
ID: id, Type: Limit, Side: side, Price: price, Size: size,
|
||||
User: user, UserID: user, Symbol: "BTC-USD",
|
||||
Timestamp: ts.Add(dt),
|
||||
}
|
||||
if got := ob.ConsensusAddOrder(o); got != id {
|
||||
t.Fatalf("ConsensusAddOrder(id=%d) = %d", id, got)
|
||||
}
|
||||
}
|
||||
place(1, Buy, 100.0, 2.0, "alice", 0)
|
||||
place(2, Buy, 100.0, 1.5, "bob", 1) // same level, later -> behind id1
|
||||
place(3, Buy, 99.5, 3.0, "carol", 2)
|
||||
place(4, Sell, 101.0, 2.5, "dave", 3)
|
||||
place(5, Sell, 101.5, 1.0, "erin", 4)
|
||||
place(6, Sell, 101.0, 0.5, "frank", 5) // same level, later -> behind id4
|
||||
|
||||
rows1 := BookToRows(ob)
|
||||
if len(rows1) != 6 {
|
||||
t.Fatalf("expected 6 rows, got %d", len(rows1))
|
||||
}
|
||||
|
||||
// Rebuild and re-serialize.
|
||||
ob2 := RowsToBook("BTC-USD", rows1)
|
||||
rows2 := BookToRows(ob2)
|
||||
|
||||
if len(rows2) != len(rows1) {
|
||||
t.Fatalf("rebuilt book row count %d != original %d", len(rows2), len(rows1))
|
||||
}
|
||||
for i := range rows1 {
|
||||
if !rows1[i].EqualFields(rows2[i]) {
|
||||
t.Errorf("row %d differs after round-trip:\n orig %+v\nrebuilt %+v", i, rows1[i], rows2[i])
|
||||
}
|
||||
// Byte-image equality too.
|
||||
if string(EncodeRow(rows1[i])) != string(EncodeRow(rows2[i])) {
|
||||
t.Errorf("row %d byte image differs after round-trip", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Functional equality: best bid/ask preserved.
|
||||
if got := len(ob2.Orders); got != 6 {
|
||||
t.Errorf("rebuilt book has %d orders, want 6", got)
|
||||
}
|
||||
if ob.LastOrderID != 0 && ob2.LastOrderID < 6 {
|
||||
t.Errorf("rebuilt LastOrderID watermark = %d, want >= 6", ob2.LastOrderID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRowStreamIsCanonicallyOrdered proves BookToRows emits a deterministic
|
||||
// order regardless of insertion order: two books built with the same orders in
|
||||
// different insertion sequences produce the identical row stream. This is what
|
||||
// makes the book merkle root insertion-order-independent.
|
||||
func TestRowStreamIsCanonicallyOrdered(t *testing.T) {
|
||||
ts := time.Unix(0, 1700000000000000000).UTC()
|
||||
build := func(order []uint64) []DEXOrder {
|
||||
ob := NewOrderBook("ETH-USD")
|
||||
spec := map[uint64]struct {
|
||||
side Side
|
||||
price float64
|
||||
size float64
|
||||
user string
|
||||
}{
|
||||
10: {Buy, 50.0, 1, "a"},
|
||||
11: {Buy, 51.0, 2, "b"},
|
||||
12: {Sell, 52.0, 1, "c"},
|
||||
13: {Sell, 53.0, 2, "d"},
|
||||
}
|
||||
for _, id := range order {
|
||||
s := spec[id]
|
||||
// Timestamp is tied to the ID (not the insertion index) so the same
|
||||
// order produces the same row regardless of when it was inserted —
|
||||
// the row is a pure function of the order's identity, and the only
|
||||
// thing the canonical sort reorders is position, not content.
|
||||
ob.ConsensusAddOrder(&Order{
|
||||
ID: id, Type: Limit, Side: s.side, Price: s.price, Size: s.size,
|
||||
User: s.user, UserID: s.user, Symbol: "ETH-USD",
|
||||
Timestamp: ts.Add(time.Duration(id) * time.Nanosecond),
|
||||
})
|
||||
}
|
||||
return BookToRows(ob)
|
||||
}
|
||||
a := build([]uint64{10, 11, 12, 13})
|
||||
b := build([]uint64{13, 12, 11, 10})
|
||||
if len(a) != len(b) {
|
||||
t.Fatalf("row counts differ: %d vs %d", len(a), len(b))
|
||||
}
|
||||
for i := range a {
|
||||
if string(EncodeRow(a[i])) != string(EncodeRow(b[i])) {
|
||||
t.Errorf("canonical order not insertion-independent at row %d", i)
|
||||
}
|
||||
}
|
||||
// Canonical ordering: bids (descending price) then asks (ascending price).
|
||||
if a[0].Side != DEXSideBid || a[len(a)-1].Side != DEXSideAsk {
|
||||
t.Errorf("expected bids first then asks, got sides %d..%d", a[0].Side, a[len(a)-1].Side)
|
||||
}
|
||||
if a[0].Price.Integer != 51 || a[1].Price.Integer != 50 {
|
||||
t.Errorf("bids not descending: %d then %d", a[0].Price.Integer, a[1].Price.Integer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package lx
|
||||
|
||||
import "math/big"
|
||||
|
||||
// settlement_units.go is the SINGLE home for the exact-integer quantity lane the
|
||||
// CLOB / consensus path uses for value conservation. The matcher's price grid
|
||||
// stays float64 (a ratio is fine as a float, and price-time priority is unchanged
|
||||
// by it), but the SETTLED QUANTITY — the base units a fill moves and the quote
|
||||
// units owed for them — must be exact: an 18-decimal token amount (e.g. 1e20)
|
||||
// exceeds float64's 2^53 exact-integer ceiling, so deriving settlement from a
|
||||
// float quantity mints or burns real on-chain value on every realistic trade.
|
||||
//
|
||||
// Decomplected (Rich Hickey): "price" (a ratio) and "quantity" (a count of atomic
|
||||
// units) are different values and live in different lanes. The float lane keeps
|
||||
// serving the legacy single-process API and the matcher's crossing decision; this
|
||||
// integer lane owns the value-bearing quantity end to end (wire -> match -> fill
|
||||
// -> settlement). Nothing here touches the float crossing logic.
|
||||
//
|
||||
// priceMultiplierBig is PriceMultiplier as a big.Int (the 8-decimal fixed-point
|
||||
// scale the engine quotes prices in). quoteUnits = baseUnits * priceInt /
|
||||
// PriceMultiplier in exact integer arithmetic.
|
||||
var priceMultiplierBig = big.NewInt(PriceMultiplier)
|
||||
|
||||
// quoteUnitsFor computes the exact quote owed for baseUnits at the maker's resting
|
||||
// price, in atomic quote units. The price is taken as its fixed-point integer
|
||||
// form (priceInt = round(price * PriceMultiplier)) — the SAME PriceInt the book
|
||||
// keys levels by — so the quote is a pure-integer function of the matched base and
|
||||
// the resting price, identical on every validator.
|
||||
//
|
||||
// Rounding: integer division floors. quote = floor(baseUnits * priceInt /
|
||||
// PriceMultiplier). Floor (truncation toward zero for non-negative operands) means
|
||||
// the quote charged to a taker buy / credited to a taker sell never EXCEEDS the
|
||||
// mathematical product, so the matcher never fabricates a sub-unit of quote. The
|
||||
// dropped remainder (< 1 atomic unit) stays with the counterparty — value is
|
||||
// conserved across the maker/taker boundary because BOTH legs are derived from the
|
||||
// SAME floored integer (the taker pays exactly what the maker is credited).
|
||||
func quoteUnitsFor(baseUnits *big.Int, priceInt PriceInt) *big.Int {
|
||||
if baseUnits == nil || baseUnits.Sign() <= 0 || priceInt <= 0 {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
q := new(big.Int).Mul(baseUnits, big.NewInt(int64(priceInt)))
|
||||
q.Quo(q, priceMultiplierBig)
|
||||
return q
|
||||
}
|
||||
|
||||
// ensureRemainingUnits lazily initializes an order's exact integer remainder from
|
||||
// its SizeUnits the first time the matcher touches it on the CLOB path. An order
|
||||
// without SizeUnits (legacy float API) yields nil — the integer lane stays off and
|
||||
// the caller falls back to the float quantity, preserving legacy behavior.
|
||||
func ensureRemainingUnits(o *Order) *big.Int {
|
||||
if o.SizeUnits == nil {
|
||||
return nil
|
||||
}
|
||||
if o.RemainingUnits == nil {
|
||||
o.RemainingUnits = new(big.Int).Set(o.SizeUnits)
|
||||
}
|
||||
return o.RemainingUnits
|
||||
}
|
||||
|
||||
// minBig returns a fresh big.Int equal to the smaller of a and b.
|
||||
func minBig(a, b *big.Int) *big.Int {
|
||||
if a.Cmp(b) <= 0 {
|
||||
return new(big.Int).Set(a)
|
||||
}
|
||||
return new(big.Int).Set(b)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package lx
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -58,6 +59,17 @@ type Order struct {
|
||||
Status string
|
||||
Flags OrderFlags
|
||||
ExecutedSize float64 // Total executed size
|
||||
|
||||
// Exact-integer quantity lane (CLOB / consensus path only). The float Size
|
||||
// above is the price-grid/validation/legacy quantity; it loses precision for
|
||||
// 18-decimal token amounts above 2^53. SizeUnits is the AUTHORITATIVE base
|
||||
// quantity in atomic token units (no float, no rounding) and is the only
|
||||
// quantity settlement is derived from. RemainingUnits tracks the unfilled
|
||||
// integer remainder as the order crosses. Both are nil on the legacy float
|
||||
// API path (AddOrder/MatchOrders), set on the CLOB path (ConsensusAddOrder /
|
||||
// SubmitMarketable) by the wire decoder. See settlement_units.go.
|
||||
SizeUnits *big.Int
|
||||
RemainingUnits *big.Int
|
||||
}
|
||||
|
||||
// Trade represents an executed trade
|
||||
@@ -75,6 +87,17 @@ type Trade struct {
|
||||
IsMaker bool
|
||||
MatchType string // Type of match (e.g., "normal", "liquidation")
|
||||
TakerSide Side // Side that took liquidity
|
||||
|
||||
// Exact-integer execution truth (CLOB / consensus path). Price/Size above are
|
||||
// the float view; BaseUnits and QuoteUnits are the AUTHORITATIVE atomic-unit
|
||||
// amounts this fill moved, computed with big.Int only (no float, see
|
||||
// settlement_units.go: QuoteUnits = BaseUnits*priceInt/PriceMultiplier). These
|
||||
// are what every settlement consumer (precompile fillsToDelta, dexvm
|
||||
// settleFromFills) derives the on-chain BalanceDelta from, so a fill is
|
||||
// value-conserving for 18-decimal magnitudes that float64 cannot represent.
|
||||
// Nil on the legacy float matcher path; set on SubmitMarketable fills.
|
||||
BaseUnits *big.Int
|
||||
QuoteUnits *big.Int
|
||||
}
|
||||
|
||||
// MarketDataUpdate represents market data updates
|
||||
|
||||
Reference in New Issue
Block a user