mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
Single-binary GraphQL query layer replacing The Graph's Graph Node. SQLite WAL + hanzoai/replicate S3 streaming + luxfi/age PQ encryption. - 18 schemas: AMM V2/V3/V4, DEX, FHE, Platform, XChain, Bridge, Privacy, Quantum, Key, AI, Identity, Oracle, Relay, ServiceNode, Precompile, ERC20, ERC721 - WASM subgraph loader (uni-v2/v3/v4 build/ dirs) - EVM indexer: eth_blockNumber + eth_getLogs + V2/V3/V4 event decoding - Production hardened: rate limiter, query depth/field caps, MaxBytesReader, structured logging, /ready, /metrics - 32 e2e tests, 13 benchmarks, 775 qps @ 100 clients on SQLite - Dockerfile (multi-stage CGO, non-root, healthcheck)
433 lines
12 KiB
Go
433 lines
12 KiB
Go
//go:build e2e
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/luxfi/graph/engine"
|
|
"github.com/luxfi/graph/storage"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Seed helpers — realistic data volumes.
|
|
// 100 tokens, 500 pools, 10 000 swaps, 1 000 mints, 1 000 burns.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// seedBenchStore populates a store with production-scale data.
|
|
// Addresses are deterministic hex strings so benchmarks are reproducible.
|
|
func seedBenchStore(b *testing.B) *storage.Store {
|
|
b.Helper()
|
|
|
|
store, err := storage.New(b.TempDir())
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if err := store.Init(context.Background()); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
// Factory + bundle
|
|
store.SeedFactory("1", &storage.SeedFactoryData{
|
|
PoolCount: 500,
|
|
TxCount: 12000,
|
|
TotalVolumeUSD: "87654321.12",
|
|
TotalValueLockedUSD: "23456789.01",
|
|
})
|
|
store.SeedBundle("1", &storage.SeedBundleData{
|
|
EthPriceUSD: "2.53",
|
|
})
|
|
|
|
// 100 tokens
|
|
for i := 0; i < 100; i++ {
|
|
addr := fmt.Sprintf("0x%040x", i+1)
|
|
store.SeedToken(addr, &storage.SeedTokenData{
|
|
Symbol: fmt.Sprintf("TK%d", i),
|
|
Name: fmt.Sprintf("Token %d", i),
|
|
Decimals: 18,
|
|
VolumeUSD: fmt.Sprintf("%d.00", (i+1)*50000),
|
|
TotalValueLockedUSD: fmt.Sprintf("%d.00", (i+1)*30000),
|
|
DerivedETH: fmt.Sprintf("0.%04d", i+1),
|
|
TxCount: int64((i + 1) * 10),
|
|
})
|
|
}
|
|
|
|
// 500 pools — each references two of the 100 tokens
|
|
for i := 0; i < 500; i++ {
|
|
id := fmt.Sprintf("0xpool%06d", i)
|
|
t0 := fmt.Sprintf("0x%040x", (i%100)+1)
|
|
t1 := fmt.Sprintf("0x%040x", ((i+1)%100)+1)
|
|
fee := []int64{500, 3000, 10000}[i%3]
|
|
store.SeedPool(id, &storage.SeedPoolData{
|
|
Token0: t0,
|
|
Token1: t1,
|
|
FeeTier: fee,
|
|
TotalValueLockedUSD: fmt.Sprintf("%d.00", (i+1)*4000),
|
|
VolumeUSD: fmt.Sprintf("%d.00", (i+1)*1000),
|
|
Token0Price: "2.53",
|
|
Token1Price: "0.395",
|
|
TxCount: int64((i + 1) * 2),
|
|
})
|
|
}
|
|
|
|
// 10 000 swaps
|
|
baseTS := int64(1711929600)
|
|
for i := 0; i < 10000; i++ {
|
|
id := fmt.Sprintf("0xtx%08d#%d", i/4, i%4)
|
|
pool := fmt.Sprintf("0xpool%06d", i%500)
|
|
store.SeedSwap(id, &storage.SeedSwapData{
|
|
Timestamp: baseTS - int64(i),
|
|
Pool: pool,
|
|
Amount0: fmt.Sprintf("%d.%02d", i%10000, i%100),
|
|
Amount1: fmt.Sprintf("-%d.%02d", (i*2)%10000, (i*3)%100),
|
|
AmountUSD: fmt.Sprintf("%d.%02d", (i+1)*25, (i*7)%100),
|
|
Sender: fmt.Sprintf("0x%040x", i%200+1000),
|
|
})
|
|
}
|
|
|
|
// 1 000 mints — stored as generic entities (mint storage returns empty slices)
|
|
for i := 0; i < 1000; i++ {
|
|
store.SetEntity("Mint", fmt.Sprintf("mint-%d", i), map[string]interface{}{
|
|
"id": fmt.Sprintf("mint-%d", i),
|
|
"transaction": fmt.Sprintf("0x%064x", i),
|
|
"timestamp": baseTS - int64(i*3),
|
|
"pool": fmt.Sprintf("0xpool%06d", i%500),
|
|
"amount0": fmt.Sprintf("%d.00", i*100),
|
|
"amount1": fmt.Sprintf("%d.00", i*50),
|
|
"amountUSD": fmt.Sprintf("%d.00", i*75),
|
|
})
|
|
}
|
|
|
|
// 1 000 burns
|
|
for i := 0; i < 1000; i++ {
|
|
store.SetEntity("Burn", fmt.Sprintf("burn-%d", i), map[string]interface{}{
|
|
"id": fmt.Sprintf("burn-%d", i),
|
|
"transaction": fmt.Sprintf("0x%064x", i+10000),
|
|
"timestamp": baseTS - int64(i*5),
|
|
"pool": fmt.Sprintf("0xpool%06d", i%500),
|
|
"amount0": fmt.Sprintf("%d.00", i*80),
|
|
"amount1": fmt.Sprintf("%d.00", i*40),
|
|
"amountUSD": fmt.Sprintf("%d.00", i*60),
|
|
})
|
|
}
|
|
|
|
return store
|
|
}
|
|
|
|
// newBenchEngine builds an engine with AMM + all chain schemas loaded.
|
|
func newBenchEngine(b *testing.B, store *storage.Store) *engine.Engine {
|
|
b.Helper()
|
|
eng := engine.New(store, nil)
|
|
if err := eng.LoadBuiltin("all"); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
return eng
|
|
}
|
|
|
|
// runQuery is a helper that executes a query and fails the benchmark on error.
|
|
func runQuery(b *testing.B, eng *engine.Engine, query string) {
|
|
b.Helper()
|
|
resp := eng.Execute(context.Background(), &engine.Request{Query: query})
|
|
if len(resp.Errors) > 0 {
|
|
b.Fatalf("query error: %s", resp.Errors[0].Message)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// A) Query latency benchmarks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func BenchmarkQuery_Factory(b *testing.B) {
|
|
store := seedBenchStore(b)
|
|
eng := newBenchEngine(b, store)
|
|
q := `{ factory(id: "1") { id poolCount txCount totalVolumeUSD totalValueLockedUSD } }`
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
runQuery(b, eng, q)
|
|
}
|
|
}
|
|
|
|
func BenchmarkQuery_Tokens50(b *testing.B) {
|
|
store := seedBenchStore(b)
|
|
eng := newBenchEngine(b, store)
|
|
q := `{ tokens(first: 50) { id symbol name decimals volumeUSD totalValueLockedUSD derivedETH txCount } }`
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
runQuery(b, eng, q)
|
|
}
|
|
}
|
|
|
|
func BenchmarkQuery_Pools50(b *testing.B) {
|
|
store := seedBenchStore(b)
|
|
eng := newBenchEngine(b, store)
|
|
q := `{ pools(first: 50) { id token0 { id symbol name decimals } token1 { id symbol name decimals } feeTier totalValueLockedUSD volumeUSD token0Price token1Price txCount } }`
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
runQuery(b, eng, q)
|
|
}
|
|
}
|
|
|
|
func BenchmarkQuery_Swaps200(b *testing.B) {
|
|
store := seedBenchStore(b)
|
|
eng := newBenchEngine(b, store)
|
|
q := `{ swaps(first: 200, orderBy: timestamp, orderDirection: desc) { id timestamp pool amount0 amount1 amountUSD sender } }`
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
runQuery(b, eng, q)
|
|
}
|
|
}
|
|
|
|
func BenchmarkQuery_Dashboard(b *testing.B) {
|
|
store := seedBenchStore(b)
|
|
eng := newBenchEngine(b, store)
|
|
q := `{
|
|
bundle(id: "1") { ethPriceUSD }
|
|
factories(first: 1) { id poolCount txCount totalVolumeUSD totalValueLockedUSD }
|
|
tokens(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc) {
|
|
id symbol name decimals totalValueLockedUSD derivedETH
|
|
}
|
|
pools(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc) {
|
|
id token0 { id symbol } token1 { id symbol }
|
|
feeTier totalValueLockedUSD token0Price token1Price
|
|
}
|
|
}`
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
runQuery(b, eng, q)
|
|
}
|
|
}
|
|
|
|
func BenchmarkQuery_AllSchemas(b *testing.B) {
|
|
store := seedBenchStore(b)
|
|
// Seed chain-specific entities
|
|
store.SetEntity("Validator", "NodeID-bench", map[string]interface{}{
|
|
"id": "NodeID-bench", "stake": "1000000", "uptime": "99.9",
|
|
})
|
|
store.SetEntity("Order", "order-bench", map[string]interface{}{
|
|
"id": "order-bench", "market": "LUX/USD", "side": "buy", "price": "2.50", "amount": "100",
|
|
})
|
|
store.SetEntity("DKGCeremony", "dkg-bench", map[string]interface{}{
|
|
"id": "dkg-bench", "participants": 5, "threshold": 3, "status": "complete",
|
|
})
|
|
store.SetEntity("Asset", "LUX-bench", map[string]interface{}{
|
|
"id": "LUX-bench", "symbol": "LUX", "name": "Lux", "denomination": 6,
|
|
})
|
|
store.SetEntity("BridgeTransfer", "bt-bench", map[string]interface{}{
|
|
"id": "bt-bench", "sourceChain": "C", "destChain": "ethereum", "amount": "1000",
|
|
})
|
|
store.SetEntity("ShieldedTransfer", "st-bench", map[string]interface{}{
|
|
"id": "st-bench", "nullifier": "0xabc", "commitment": "0xdef",
|
|
})
|
|
|
|
eng := newBenchEngine(b, store)
|
|
|
|
// Query every chain type in sequence per iteration.
|
|
queries := []string{
|
|
`{ factories(first: 1) { id } }`,
|
|
`{ tokens(first: 10) { id symbol } }`,
|
|
`{ pools(first: 10) { id } }`,
|
|
`{ swaps(first: 10) { id } }`,
|
|
`{ orders(first: 10) { id } }`,
|
|
`{ validators(first: 10) { id } }`,
|
|
`{ dkgCeremonies(first: 10) { id } }`,
|
|
`{ assets(first: 10) { id } }`,
|
|
`{ bridgeTransfers(first: 10) { id } }`,
|
|
`{ shieldedTransfers(first: 10) { id } }`,
|
|
}
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
for _, q := range queries {
|
|
runQuery(b, eng, q)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// B) Throughput benchmarks — concurrent clients
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// benchThroughput fires N goroutines each running queries in a loop.
|
|
func benchThroughput(b *testing.B, clients int) {
|
|
b.Helper()
|
|
|
|
store := seedBenchStore(b)
|
|
eng := newBenchEngine(b, store)
|
|
|
|
// Mix of realistic queries weighted by exchange usage patterns.
|
|
queries := []string{
|
|
`{ bundle(id: "1") { ethPriceUSD } }`,
|
|
`{ factories(first: 1) { poolCount txCount totalVolumeUSD totalValueLockedUSD } }`,
|
|
`{ tokens(first: 50) { id symbol volumeUSD totalValueLockedUSD derivedETH } }`,
|
|
`{ pools(first: 50) { id token0 { id symbol } token1 { id symbol } feeTier totalValueLockedUSD } }`,
|
|
`{ swaps(first: 200, orderBy: timestamp, orderDirection: desc) { id timestamp amountUSD sender } }`,
|
|
}
|
|
|
|
var ops atomic.Int64
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
|
|
var wg sync.WaitGroup
|
|
perClient := b.N / clients
|
|
if perClient < 1 {
|
|
perClient = 1
|
|
}
|
|
|
|
for c := 0; c < clients; c++ {
|
|
wg.Add(1)
|
|
go func(clientID int) {
|
|
defer wg.Done()
|
|
for i := 0; i < perClient; i++ {
|
|
q := queries[(clientID+i)%len(queries)]
|
|
resp := eng.Execute(context.Background(), &engine.Request{Query: q})
|
|
if len(resp.Errors) > 0 {
|
|
b.Errorf("client %d query error: %s", clientID, resp.Errors[0].Message)
|
|
return
|
|
}
|
|
ops.Add(1)
|
|
}
|
|
}(c)
|
|
}
|
|
wg.Wait()
|
|
|
|
b.ReportMetric(float64(ops.Load())/b.Elapsed().Seconds(), "queries/sec")
|
|
}
|
|
|
|
func BenchmarkThroughput_1Client(b *testing.B) {
|
|
benchThroughput(b, 1)
|
|
}
|
|
|
|
func BenchmarkThroughput_10Clients(b *testing.B) {
|
|
benchThroughput(b, 10)
|
|
}
|
|
|
|
func BenchmarkThroughput_100Clients(b *testing.B) {
|
|
benchThroughput(b, 100)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C) Indexing simulation — entity writes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func BenchmarkIndex_SwapEvent(b *testing.B) {
|
|
store, err := storage.New(b.TempDir())
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
store.Init(context.Background())
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
id := fmt.Sprintf("0xtx%08d#0", i)
|
|
store.SeedSwap(id, &storage.SeedSwapData{
|
|
Timestamp: 1711929600 + int64(i),
|
|
Pool: "0xpool000001",
|
|
Amount0: "123.456",
|
|
Amount1: "-308.640",
|
|
AmountUSD: "308.64",
|
|
Sender: "0x000000000000000000000000000000000000abcd",
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkIndex_BatchSwaps1000(b *testing.B) {
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
b.StopTimer()
|
|
store, _ := storage.New(b.TempDir())
|
|
store.Init(context.Background())
|
|
b.StartTimer()
|
|
|
|
for j := 0; j < 1000; j++ {
|
|
id := fmt.Sprintf("0xtx%08d#%d", i*1000+j/4, j%4)
|
|
store.SeedSwap(id, &storage.SeedSwapData{
|
|
Timestamp: 1711929600 + int64(j),
|
|
Pool: fmt.Sprintf("0xpool%06d", j%50),
|
|
Amount0: fmt.Sprintf("%d.%02d", j%10000, j%100),
|
|
Amount1: fmt.Sprintf("-%d.%02d", (j*2)%10000, (j*3)%100),
|
|
AmountUSD: fmt.Sprintf("%d.%02d", (j+1)*25, (j*7)%100),
|
|
Sender: fmt.Sprintf("0x%040x", j%200+1000),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkIndex_GenericEntity measures SetEntity for chain-specific resolvers.
|
|
func BenchmarkIndex_GenericEntity(b *testing.B) {
|
|
store, err := storage.New(b.TempDir())
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
store.Init(context.Background())
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
store.SetEntity("Swap", fmt.Sprintf("swap-%d", i), map[string]interface{}{
|
|
"id": fmt.Sprintf("swap-%d", i),
|
|
"timestamp": 1711929600 + i,
|
|
"pool": "0xpool000001",
|
|
"amount0": "123.456",
|
|
"amount1": "-308.640",
|
|
"amountUSD": "308.64",
|
|
"sender": "0x000000000000000000000000000000000000abcd",
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkIndex_ConcurrentWrites measures write contention under load.
|
|
func BenchmarkIndex_ConcurrentWrites(b *testing.B) {
|
|
store, err := storage.New(b.TempDir())
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
store.Init(context.Background())
|
|
|
|
writers := 8
|
|
perWriter := b.N / writers
|
|
if perWriter < 1 {
|
|
perWriter = 1
|
|
}
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
|
|
var wg sync.WaitGroup
|
|
for w := 0; w < writers; w++ {
|
|
wg.Add(1)
|
|
go func(wID int) {
|
|
defer wg.Done()
|
|
for i := 0; i < perWriter; i++ {
|
|
id := fmt.Sprintf("0xtx-w%d-%08d#0", wID, i)
|
|
store.SeedSwap(id, &storage.SeedSwapData{
|
|
Timestamp: 1711929600 + int64(i),
|
|
Pool: fmt.Sprintf("0xpool%06d", i%50),
|
|
Amount0: "100.00",
|
|
Amount1: "-250.00",
|
|
AmountUSD: "250.00",
|
|
Sender: fmt.Sprintf("0x%040x", wID*10000+i),
|
|
})
|
|
}
|
|
}(w)
|
|
}
|
|
wg.Wait()
|
|
}
|