mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
VMs now live in ONE place: chains/<name>/ owns the full implementation. Previously split across node/vms/<name>/ (impl) + chains/<name>/main.go (plugin wrapper). Now consolidated. New layout: chains/<name>vm/*.go — package <name>vm (VM code) chains/<name>vm/cmd/plugin/main.go — package main (plugin binary) chains/<name>vm/go.mod — module github.com/luxfi/chains/<name>vm Moved 11 VMs: aivm, bridgevm, dexvm, graphvm, identityvm, keyvm, oraclevm, quantumvm, relayvm, thresholdvm, zkvm. External callers now import github.com/luxfi/chains/<name> directly.
2179 lines
57 KiB
Go
2179 lines
57 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dexvm
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net/http"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/rpc/v2"
|
|
rpcjson "github.com/gorilla/rpc/v2/json"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/metric"
|
|
|
|
consensuscore "github.com/luxfi/consensus/core"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/database/versiondb"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/chains/dexvm/api"
|
|
"github.com/luxfi/chains/dexvm/config"
|
|
"github.com/luxfi/chains/dexvm/liquidity"
|
|
"github.com/luxfi/chains/dexvm/mev"
|
|
"github.com/luxfi/chains/dexvm/network"
|
|
"github.com/luxfi/chains/dexvm/orderbook"
|
|
"github.com/luxfi/chains/dexvm/perpetuals"
|
|
"github.com/luxfi/chains/dexvm/txs"
|
|
"github.com/luxfi/runtime"
|
|
"github.com/luxfi/timer/mockable"
|
|
"github.com/luxfi/version"
|
|
vmcore "github.com/luxfi/vm"
|
|
"github.com/luxfi/vm/chain"
|
|
"github.com/luxfi/warp"
|
|
)
|
|
|
|
var (
|
|
errUnknownState = errors.New("unknown state")
|
|
errNotBootstrapped = errors.New("VM not bootstrapped")
|
|
errShutdown = errors.New("VM is shutting down")
|
|
|
|
_ = errNotBootstrapped
|
|
_ = errShutdown
|
|
)
|
|
|
|
// BlockResult represents the deterministic result of processing a block.
|
|
// All state changes are captured here for verifiability.
|
|
type BlockResult struct {
|
|
// BlockHeight is the height of the processed block
|
|
BlockHeight uint64
|
|
|
|
// Timestamp is when this block was processed
|
|
Timestamp time.Time
|
|
|
|
// MatchedTrades from order matching in this block
|
|
MatchedTrades []orderbook.Trade
|
|
|
|
// FundingPayments processed in this block (if any)
|
|
FundingPayments []*perpetuals.FundingPayment
|
|
|
|
// Liquidations executed in this block (if any)
|
|
Liquidations []*perpetuals.LiquidationEvent
|
|
|
|
// StateRoot is the merkle root of state after this block
|
|
StateRoot ids.ID
|
|
}
|
|
|
|
// VM implements the DEX Virtual Machine using a pure functional architecture.
|
|
// Native decentralized exchange — Hyperliquid-class on-chain CLOB.
|
|
// All state transitions happen deterministically within block processing:
|
|
// - Central Limit Order Book (CLOB) — native, not EVM-based
|
|
// - Perpetual futures with auto-deleveraging
|
|
// - Cross-chain atomic swaps via Warp messaging
|
|
// - 1ms block times for ultra-low latency trading
|
|
//
|
|
// DESIGN: No background goroutines. All operations are block-driven and deterministic.
|
|
// This ensures:
|
|
// - Every node produces identical state from identical inputs
|
|
// - No race conditions or non-deterministic behavior
|
|
// - Easy to test and verify
|
|
// - Replay-safe for auditing
|
|
type VM struct {
|
|
config.Config
|
|
|
|
// Logger for this VM
|
|
log log.Logger
|
|
|
|
// Lock for thread safety (only for API access, not consensus)
|
|
lock sync.RWMutex
|
|
|
|
// Consensus context - provides chain identity and network info
|
|
consensusRuntime *runtime.Runtime
|
|
|
|
// Chain identity
|
|
chainID ids.ID
|
|
|
|
// Database management
|
|
baseDB database.Database
|
|
db *versiondb.Database
|
|
|
|
// Used to check local time
|
|
clock mockable.Clock
|
|
|
|
// Metrics
|
|
registerer metric.Registerer
|
|
|
|
// Network peers
|
|
connectedPeers map[ids.NodeID]*version.Application
|
|
|
|
// Application sender for gossip
|
|
appSender warp.Sender
|
|
|
|
// DEX components (all operations on these are deterministic)
|
|
orderbooks map[string]*orderbook.Orderbook // symbol -> orderbook
|
|
liquidityMgr *liquidity.Manager // AMM liquidity pools
|
|
perpetualsEng *perpetuals.Engine // Perpetual futures engine
|
|
commitmentStore *mev.CommitmentStore // MEV protection commit-reveal
|
|
adlEngine *perpetuals.AutoDeleveragingEngine // Auto-deleveraging
|
|
|
|
// Block state
|
|
currentBlockHeight uint64
|
|
lastBlockTime time.Time
|
|
lastFundingTime time.Time // Tracks when funding was last processed
|
|
|
|
// Lifecycle state
|
|
bootstrapped bool
|
|
isInitialized bool
|
|
shutdown bool
|
|
|
|
// Channel for sending messages to consensus engine
|
|
toEngine chan<- vmcore.Message
|
|
}
|
|
|
|
// NewVMForTest creates a new VM instance for testing purposes.
|
|
// This allows external test packages to create VM instances without
|
|
// needing to access internal fields directly.
|
|
func NewVMForTest(cfg config.Config, logger log.Logger) *VM {
|
|
return &VM{
|
|
Config: cfg,
|
|
log: logger,
|
|
}
|
|
}
|
|
|
|
// Initialize implements consensuscore.VM interface.
|
|
// It sets up the VM with the provided context, database, and genesis data.
|
|
func (vm *VM) Initialize(
|
|
ctx context.Context,
|
|
vmInit vmcore.Init,
|
|
) error {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
// Cast Runtime
|
|
vm.consensusRuntime = vmInit.Runtime
|
|
vm.chainID = vm.consensusRuntime.ChainID
|
|
|
|
// Initialize logger from Runtime
|
|
if vm.consensusRuntime != nil && vm.consensusRuntime.Log != nil {
|
|
if logger, ok := vm.consensusRuntime.Log.(log.Logger); ok && !logger.IsZero() {
|
|
vm.log = logger
|
|
} else {
|
|
vm.log = log.Noop()
|
|
}
|
|
} else {
|
|
vm.log = log.Noop()
|
|
}
|
|
|
|
// Setup database
|
|
vm.baseDB = vmInit.DB
|
|
vm.db = versiondb.New(vm.baseDB)
|
|
|
|
// Setup message channel
|
|
vm.toEngine = vmInit.ToEngine
|
|
|
|
// Setup app sender
|
|
if vmInit.Sender != nil {
|
|
vm.appSender = vmInit.Sender
|
|
}
|
|
|
|
// Initialize peer tracking
|
|
vm.connectedPeers = make(map[ids.NodeID]*version.Application)
|
|
|
|
// Initialize DEX components
|
|
vm.orderbooks = make(map[string]*orderbook.Orderbook)
|
|
vm.liquidityMgr = liquidity.NewManager()
|
|
vm.perpetualsEng = perpetuals.NewEngine()
|
|
vm.commitmentStore = mev.NewCommitmentStore()
|
|
vm.adlEngine = perpetuals.NewAutoDeleveragingEngine(perpetuals.DefaultADLConfig())
|
|
|
|
// Initialize block state
|
|
vm.currentBlockHeight = 0
|
|
vm.lastBlockTime = time.Time{}
|
|
vm.lastFundingTime = time.Time{}
|
|
|
|
// Parse genesis if provided
|
|
if len(vmInit.Genesis) > 0 {
|
|
if err := vm.parseGenesis(vmInit.Genesis); err != nil {
|
|
return fmt.Errorf("failed to parse genesis: %w", err)
|
|
}
|
|
}
|
|
|
|
// Parse config if provided
|
|
if len(vmInit.Config) > 0 {
|
|
if err := vm.parseConfig(vmInit.Config); err != nil {
|
|
return fmt.Errorf("failed to parse config: %w", err)
|
|
}
|
|
}
|
|
|
|
vm.isInitialized = true
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("DEX VM initialized (functional mode)",
|
|
"chainID", vm.chainID,
|
|
"blockInterval", vm.Config.BlockInterval,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Genesis represents the DEX VM genesis configuration.
|
|
type Genesis struct {
|
|
// TradingPairs defines the initial trading pairs (order book markets)
|
|
TradingPairs []GenesisTradingPair `json:"tradingPairs"`
|
|
|
|
// LiquidityPools defines the initial AMM liquidity pools
|
|
LiquidityPools []GenesisPool `json:"liquidityPools"`
|
|
|
|
// PerpetualMarkets defines the initial perpetual futures markets
|
|
PerpetualMarkets []GenesisPerpMarket `json:"perpetualMarkets"`
|
|
|
|
// FeeConfig contains global fee configuration
|
|
FeeConfig GenesisFeeConfig `json:"feeConfig"`
|
|
|
|
// TrustedChains are chain IDs trusted for cross-chain operations
|
|
TrustedChains []string `json:"trustedChains"`
|
|
|
|
// InitialBalances are pre-funded accounts (for testing/airdrops)
|
|
InitialBalances []GenesisBalance `json:"initialBalances,omitempty"`
|
|
}
|
|
|
|
// GenesisTradingPair defines a trading pair for the order book.
|
|
type GenesisTradingPair struct {
|
|
Symbol string `json:"symbol"` // e.g., "LUX/USDC"
|
|
BaseAsset string `json:"baseAsset"` // Base asset ID
|
|
QuoteAsset string `json:"quoteAsset"` // Quote asset ID
|
|
}
|
|
|
|
// GenesisPool defines an initial liquidity pool.
|
|
type GenesisPool struct {
|
|
Token0 string `json:"token0"`
|
|
Token1 string `json:"token1"`
|
|
InitialToken0 uint64 `json:"initialToken0"`
|
|
InitialToken1 uint64 `json:"initialToken1"`
|
|
PoolType uint8 `json:"poolType"` // 0=ConstantProduct, 1=StableSwap, 2=Concentrated
|
|
FeeBps uint16 `json:"feeBps"`
|
|
}
|
|
|
|
// GenesisPerpMarket defines an initial perpetual futures market.
|
|
type GenesisPerpMarket struct {
|
|
Symbol string `json:"symbol"` // e.g., "BTC-PERP"
|
|
BaseAsset string `json:"baseAsset"` // e.g., BTC
|
|
QuoteAsset string `json:"quoteAsset"` // e.g., USDC
|
|
MaxLeverage uint16 `json:"maxLeverage"`
|
|
MaintenanceMargin uint16 `json:"maintenanceMarginBps"` // in basis points
|
|
InitialMargin uint16 `json:"initialMarginBps"` // in basis points
|
|
MakerFee uint16 `json:"makerFeeBps"`
|
|
TakerFee uint16 `json:"takerFeeBps"`
|
|
}
|
|
|
|
// GenesisFeeConfig contains global fee configuration.
|
|
type GenesisFeeConfig struct {
|
|
DefaultSwapFeeBps uint16 `json:"defaultSwapFeeBps"`
|
|
ProtocolFeeBps uint16 `json:"protocolFeeBps"`
|
|
MaxSlippageBps uint16 `json:"maxSlippageBps"`
|
|
}
|
|
|
|
// GenesisBalance defines a pre-funded account balance.
|
|
type GenesisBalance struct {
|
|
Address string `json:"address"` // Hex-encoded address
|
|
Token string `json:"token"` // Token ID
|
|
Amount uint64 `json:"amount"`
|
|
}
|
|
|
|
// parseGenesis parses the genesis data and initializes initial state.
|
|
func (vm *VM) parseGenesis(genesisBytes []byte) error {
|
|
var genesis Genesis
|
|
if err := json.Unmarshal(genesisBytes, &genesis); err != nil {
|
|
return fmt.Errorf("failed to unmarshal genesis: %w", err)
|
|
}
|
|
|
|
// Initialize trading pairs (order books)
|
|
for _, pair := range genesis.TradingPairs {
|
|
ob := orderbook.New(pair.Symbol)
|
|
vm.orderbooks[pair.Symbol] = ob
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Initialized trading pair", "symbol", pair.Symbol)
|
|
}
|
|
}
|
|
|
|
// Initialize liquidity pools
|
|
for _, pool := range genesis.LiquidityPools {
|
|
token0, err := ids.FromString(pool.Token0)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid token0 ID %s: %w", pool.Token0, err)
|
|
}
|
|
token1, err := ids.FromString(pool.Token1)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid token1 ID %s: %w", pool.Token1, err)
|
|
}
|
|
|
|
_, err = vm.liquidityMgr.CreatePool(
|
|
token0, token1,
|
|
new(big.Int).SetUint64(pool.InitialToken0),
|
|
new(big.Int).SetUint64(pool.InitialToken1),
|
|
liquidity.PoolType(pool.PoolType),
|
|
pool.FeeBps,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create pool %s/%s: %w", pool.Token0, pool.Token1, err)
|
|
}
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Initialized liquidity pool", "token0", pool.Token0, "token1", pool.Token1)
|
|
}
|
|
}
|
|
|
|
// Initialize perpetual markets
|
|
for _, market := range genesis.PerpetualMarkets {
|
|
baseAsset, err := ids.FromString(market.BaseAsset)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid base asset ID %s: %w", market.BaseAsset, err)
|
|
}
|
|
quoteAsset, err := ids.FromString(market.QuoteAsset)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid quote asset ID %s: %w", market.QuoteAsset, err)
|
|
}
|
|
|
|
// Default initial price of 1e18 (1.0 scaled)
|
|
initialPrice := new(big.Int).Set(perpetuals.PrecisionFactor)
|
|
// Default min size of 1e15 (0.001 scaled)
|
|
minSize := new(big.Int).Div(perpetuals.PrecisionFactor, big.NewInt(1000))
|
|
// Default tick size of 1e12 (0.000001 scaled)
|
|
tickSize := new(big.Int).Div(perpetuals.PrecisionFactor, big.NewInt(1000000))
|
|
|
|
_, err = vm.perpetualsEng.CreateMarket(
|
|
market.Symbol,
|
|
baseAsset,
|
|
quoteAsset,
|
|
initialPrice,
|
|
market.MaxLeverage,
|
|
minSize,
|
|
tickSize,
|
|
market.MakerFee,
|
|
market.TakerFee,
|
|
market.MaintenanceMargin,
|
|
market.InitialMargin,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create perp market %s: %w", market.Symbol, err)
|
|
}
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Initialized perpetual market", "symbol", market.Symbol)
|
|
}
|
|
}
|
|
|
|
// Apply fee configuration
|
|
if genesis.FeeConfig.DefaultSwapFeeBps > 0 {
|
|
vm.Config.DefaultSwapFeeBps = genesis.FeeConfig.DefaultSwapFeeBps
|
|
}
|
|
if genesis.FeeConfig.ProtocolFeeBps > 0 {
|
|
vm.Config.ProtocolFeeBps = genesis.FeeConfig.ProtocolFeeBps
|
|
}
|
|
if genesis.FeeConfig.MaxSlippageBps > 0 {
|
|
vm.Config.MaxSlippageBps = genesis.FeeConfig.MaxSlippageBps
|
|
}
|
|
|
|
// Parse trusted chains for cross-chain operations
|
|
for _, chainIDStr := range genesis.TrustedChains {
|
|
chainID, err := ids.FromString(chainIDStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid trusted chain ID %s: %w", chainIDStr, err)
|
|
}
|
|
vm.Config.TrustedChains = append(vm.Config.TrustedChains, chainID)
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Added trusted chain", "chainID", chainID)
|
|
}
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Genesis parsed successfully",
|
|
"tradingPairs", len(genesis.TradingPairs),
|
|
"pools", len(genesis.LiquidityPools),
|
|
"perpMarkets", len(genesis.PerpetualMarkets),
|
|
"trustedChains", len(genesis.TrustedChains),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseConfig parses and applies runtime configuration.
|
|
func (vm *VM) parseConfig(configBytes []byte) error {
|
|
// Parse config into a temporary struct to avoid overwriting defaults
|
|
var cfg config.Config
|
|
if err := json.Unmarshal(configBytes, &cfg); err != nil {
|
|
return fmt.Errorf("failed to unmarshal config: %w", err)
|
|
}
|
|
|
|
// Only apply non-zero values to preserve defaults
|
|
if cfg.IndexAllowIncomplete {
|
|
vm.Config.IndexAllowIncomplete = cfg.IndexAllowIncomplete
|
|
}
|
|
if cfg.IndexTransactions {
|
|
vm.Config.IndexTransactions = cfg.IndexTransactions
|
|
}
|
|
if cfg.ChecksumsEnabled {
|
|
vm.Config.ChecksumsEnabled = cfg.ChecksumsEnabled
|
|
}
|
|
if cfg.DefaultSwapFeeBps > 0 {
|
|
vm.Config.DefaultSwapFeeBps = cfg.DefaultSwapFeeBps
|
|
}
|
|
if cfg.ProtocolFeeBps > 0 {
|
|
vm.Config.ProtocolFeeBps = cfg.ProtocolFeeBps
|
|
}
|
|
if cfg.MaxSlippageBps > 0 {
|
|
vm.Config.MaxSlippageBps = cfg.MaxSlippageBps
|
|
}
|
|
if cfg.MinLiquidity > 0 {
|
|
vm.Config.MinLiquidity = cfg.MinLiquidity
|
|
}
|
|
if cfg.MaxPoolsPerPair > 0 {
|
|
vm.Config.MaxPoolsPerPair = cfg.MaxPoolsPerPair
|
|
}
|
|
if cfg.MaxOrdersPerAccount > 0 {
|
|
vm.Config.MaxOrdersPerAccount = cfg.MaxOrdersPerAccount
|
|
}
|
|
if cfg.MaxOrderSize > 0 {
|
|
vm.Config.MaxOrderSize = cfg.MaxOrderSize
|
|
}
|
|
if cfg.MinOrderSize > 0 {
|
|
vm.Config.MinOrderSize = cfg.MinOrderSize
|
|
}
|
|
if cfg.OrderExpirationTime > 0 {
|
|
vm.Config.OrderExpirationTime = cfg.OrderExpirationTime
|
|
}
|
|
if cfg.WarpEnabled {
|
|
vm.Config.WarpEnabled = cfg.WarpEnabled
|
|
}
|
|
if cfg.TeleportEnabled {
|
|
vm.Config.TeleportEnabled = cfg.TeleportEnabled
|
|
}
|
|
if len(cfg.TrustedChains) > 0 {
|
|
vm.Config.TrustedChains = cfg.TrustedChains
|
|
}
|
|
if cfg.BlockInterval > 0 {
|
|
vm.Config.BlockInterval = cfg.BlockInterval
|
|
}
|
|
if cfg.MaxBlockSize > 0 {
|
|
vm.Config.MaxBlockSize = cfg.MaxBlockSize
|
|
}
|
|
if cfg.MaxTxsPerBlock > 0 {
|
|
vm.Config.MaxTxsPerBlock = cfg.MaxTxsPerBlock
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Config parsed successfully",
|
|
"blockInterval", vm.Config.BlockInterval,
|
|
"maxTxsPerBlock", vm.Config.MaxTxsPerBlock,
|
|
"warpEnabled", vm.Config.WarpEnabled,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetState implements consensuscore.VM interface.
|
|
// It transitions the VM between bootstrapping and normal operation states.
|
|
// NOTE: No background goroutines are started - all operations are block-driven.
|
|
func (vm *VM) SetState(ctx context.Context, stateNum uint32) error {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
switch vmcore.State(stateNum) {
|
|
case vmcore.Bootstrapping:
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("DEX VM entering bootstrap state")
|
|
}
|
|
vm.bootstrapped = false
|
|
return nil
|
|
case vmcore.Ready:
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("DEX VM entering ready state")
|
|
}
|
|
vm.bootstrapped = true
|
|
return nil
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ProcessBlock is the core function that processes all DEX operations deterministically.
|
|
// This is called by the consensus engine for each new block.
|
|
// All state changes happen here in a deterministic, reproducible manner.
|
|
//
|
|
// Operations performed per block:
|
|
// 1. Order matching for all orderbooks
|
|
// 2. Funding rate processing (every 8 hours)
|
|
// 3. Liquidation checks
|
|
// 4. State commitment
|
|
func (vm *VM) ProcessBlock(ctx context.Context, blockHeight uint64, blockTime time.Time, txs [][]byte) (*BlockResult, error) {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
if vm.shutdown {
|
|
return nil, errShutdown
|
|
}
|
|
|
|
result := &BlockResult{
|
|
BlockHeight: blockHeight,
|
|
Timestamp: blockTime,
|
|
MatchedTrades: make([]orderbook.Trade, 0),
|
|
FundingPayments: make([]*perpetuals.FundingPayment, 0),
|
|
Liquidations: make([]*perpetuals.LiquidationEvent, 0),
|
|
}
|
|
|
|
// 1. Process all transactions in the block
|
|
for _, tx := range txs {
|
|
if err := vm.processTx(tx, result); err != nil {
|
|
// Log but continue - individual tx failures don't fail the block
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Transaction failed", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Run order matching for all active orderbooks
|
|
result.MatchedTrades = vm.matchAllOrders()
|
|
|
|
// 3. Check if funding should be processed (every 8 hours)
|
|
if vm.shouldProcessFunding(blockTime) {
|
|
result.FundingPayments = vm.processFunding(blockTime)
|
|
vm.lastFundingTime = blockTime
|
|
}
|
|
|
|
// 4. Check and execute liquidations
|
|
result.Liquidations = vm.processLiquidations()
|
|
|
|
// 5. Update block state
|
|
vm.currentBlockHeight = blockHeight
|
|
vm.lastBlockTime = blockTime
|
|
|
|
// 6. Compute state root (merkle root of all state)
|
|
result.StateRoot = vm.computeStateRoot()
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Block processed",
|
|
"height", blockHeight,
|
|
"trades", len(result.MatchedTrades),
|
|
"funding", len(result.FundingPayments),
|
|
"liquidations", len(result.Liquidations),
|
|
)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// processTx processes a single transaction.
|
|
func (vm *VM) processTx(txBytes []byte, result *BlockResult) error {
|
|
if len(txBytes) < 1 {
|
|
return errors.New("empty transaction")
|
|
}
|
|
|
|
// Parse transaction using the TxParser
|
|
parser := &txs.TxParser{}
|
|
tx, err := parser.Parse(txBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse transaction: %w", err)
|
|
}
|
|
|
|
// Verify transaction
|
|
if err := tx.Verify(); err != nil {
|
|
return fmt.Errorf("transaction verification failed: %w", err)
|
|
}
|
|
|
|
// Dispatch based on transaction type
|
|
switch tx.Type() {
|
|
case txs.TxPlaceOrder:
|
|
return vm.executePlaceOrder(tx.(*txs.PlaceOrderTx), result)
|
|
case txs.TxCancelOrder:
|
|
return vm.executeCancelOrder(tx.(*txs.CancelOrderTx))
|
|
case txs.TxSwap:
|
|
return vm.executeSwap(tx.(*txs.SwapTx))
|
|
case txs.TxAddLiquidity:
|
|
return vm.executeAddLiquidity(tx.(*txs.AddLiquidityTx))
|
|
case txs.TxRemoveLiquidity:
|
|
return vm.executeRemoveLiquidity(tx.(*txs.RemoveLiquidityTx))
|
|
case txs.TxCreatePool:
|
|
return vm.executeCreatePool(tx.(*txs.CreatePoolTx))
|
|
case txs.TxCrossChainSwap:
|
|
return vm.executeCrossChainSwap(tx.(*txs.CrossChainSwapTx))
|
|
case txs.TxCrossChainTransfer:
|
|
return vm.executeCrossChainTransfer(tx.(*txs.CrossChainTransferTx))
|
|
case txs.TxCommitOrder:
|
|
return vm.executeCommitOrder(tx.(*txs.CommitOrderTx))
|
|
case txs.TxRevealOrder:
|
|
return vm.executeRevealOrder(tx.(*txs.RevealOrderTx), result)
|
|
default:
|
|
return fmt.Errorf("unknown transaction type: %d", tx.Type())
|
|
}
|
|
}
|
|
|
|
// executePlaceOrder executes a place order transaction.
|
|
func (vm *VM) executePlaceOrder(tx *txs.PlaceOrderTx, result *BlockResult) error {
|
|
ob, exists := vm.orderbooks[tx.Symbol]
|
|
if !exists {
|
|
return fmt.Errorf("orderbook not found for symbol: %s", tx.Symbol)
|
|
}
|
|
|
|
order := &orderbook.Order{
|
|
ID: tx.ID(),
|
|
Owner: tx.Sender(),
|
|
Symbol: tx.Symbol,
|
|
Side: orderbook.Side(tx.Side),
|
|
Type: orderbook.OrderType(tx.OrderType),
|
|
Price: tx.Price,
|
|
Quantity: tx.Quantity,
|
|
StopPrice: tx.StopPrice,
|
|
Status: orderbook.StatusOpen,
|
|
CreatedAt: tx.Timestamp(),
|
|
ExpiresAt: tx.ExpiresAt,
|
|
PostOnly: tx.PostOnly,
|
|
ReduceOnly: tx.ReduceOnly,
|
|
TimeInForce: tx.TimeInForce,
|
|
}
|
|
|
|
trades, err := ob.AddOrder(order)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add order: %w", err)
|
|
}
|
|
|
|
// Convert to orderbook.Trade slice for result
|
|
for _, trade := range trades {
|
|
result.MatchedTrades = append(result.MatchedTrades, *trade)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Order placed",
|
|
"orderID", order.ID,
|
|
"symbol", tx.Symbol,
|
|
"side", order.Side,
|
|
"price", tx.Price,
|
|
"quantity", tx.Quantity,
|
|
"trades", len(trades),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeCancelOrder executes a cancel order transaction.
|
|
func (vm *VM) executeCancelOrder(tx *txs.CancelOrderTx) error {
|
|
ob, exists := vm.orderbooks[tx.Symbol]
|
|
if !exists {
|
|
return fmt.Errorf("orderbook not found for symbol: %s", tx.Symbol)
|
|
}
|
|
|
|
// Verify the order belongs to the sender
|
|
order, err := ob.GetOrder(tx.OrderID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if order.Owner != tx.Sender() {
|
|
return errors.New("cannot cancel order owned by another account")
|
|
}
|
|
|
|
if err := ob.CancelOrder(tx.OrderID); err != nil {
|
|
return fmt.Errorf("failed to cancel order: %w", err)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Order cancelled", "orderID", tx.OrderID, "symbol", tx.Symbol)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeSwap executes an AMM swap transaction.
|
|
func (vm *VM) executeSwap(tx *txs.SwapTx) error {
|
|
// Check deadline
|
|
if tx.Deadline > 0 && time.Now().UnixNano() > tx.Deadline {
|
|
return errors.New("swap deadline exceeded")
|
|
}
|
|
|
|
_, err := vm.liquidityMgr.Swap(
|
|
tx.PoolID,
|
|
tx.TokenIn,
|
|
new(big.Int).SetUint64(tx.AmountIn),
|
|
new(big.Int).SetUint64(tx.MinAmountOut),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("swap failed: %w", err)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Swap executed",
|
|
"poolID", tx.PoolID,
|
|
"tokenIn", tx.TokenIn,
|
|
"amountIn", tx.AmountIn,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeAddLiquidity executes an add liquidity transaction.
|
|
func (vm *VM) executeAddLiquidity(tx *txs.AddLiquidityTx) error {
|
|
// Check deadline
|
|
if tx.Deadline > 0 && time.Now().UnixNano() > tx.Deadline {
|
|
return errors.New("add liquidity deadline exceeded")
|
|
}
|
|
|
|
liquidity, err := vm.liquidityMgr.AddLiquidity(
|
|
tx.PoolID,
|
|
new(big.Int).SetUint64(tx.Token0Amount),
|
|
new(big.Int).SetUint64(tx.Token1Amount),
|
|
new(big.Int).SetUint64(tx.MinLPTokens),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("add liquidity failed: %w", err)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Liquidity added",
|
|
"poolID", tx.PoolID,
|
|
"token0", tx.Token0Amount,
|
|
"token1", tx.Token1Amount,
|
|
"lpTokens", liquidity,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeRemoveLiquidity executes a remove liquidity transaction.
|
|
func (vm *VM) executeRemoveLiquidity(tx *txs.RemoveLiquidityTx) error {
|
|
// Check deadline
|
|
if tx.Deadline > 0 && time.Now().UnixNano() > tx.Deadline {
|
|
return errors.New("remove liquidity deadline exceeded")
|
|
}
|
|
|
|
amount0, amount1, err := vm.liquidityMgr.RemoveLiquidity(
|
|
tx.PoolID,
|
|
new(big.Int).SetUint64(tx.LPTokenAmount),
|
|
new(big.Int).SetUint64(tx.MinToken0),
|
|
new(big.Int).SetUint64(tx.MinToken1),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("remove liquidity failed: %w", err)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Liquidity removed",
|
|
"poolID", tx.PoolID,
|
|
"lpTokens", tx.LPTokenAmount,
|
|
"token0Out", amount0,
|
|
"token1Out", amount1,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeCreatePool executes a create pool transaction.
|
|
func (vm *VM) executeCreatePool(tx *txs.CreatePoolTx) error {
|
|
pool, err := vm.liquidityMgr.CreatePool(
|
|
tx.Token0,
|
|
tx.Token1,
|
|
new(big.Int).SetUint64(tx.InitialToken0),
|
|
new(big.Int).SetUint64(tx.InitialToken1),
|
|
liquidity.PoolType(tx.PoolType),
|
|
tx.SwapFeeBps,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("create pool failed: %w", err)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Pool created",
|
|
"poolID", pool.ID,
|
|
"token0", tx.Token0,
|
|
"token1", tx.Token1,
|
|
"type", tx.PoolType,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeCrossChainSwap executes a cross-chain swap via Warp.
|
|
func (vm *VM) executeCrossChainSwap(tx *txs.CrossChainSwapTx) error {
|
|
// Verify source chain is trusted
|
|
trusted := false
|
|
for _, chainID := range vm.Config.TrustedChains {
|
|
if chainID == tx.SourceChain {
|
|
trusted = true
|
|
break
|
|
}
|
|
}
|
|
if !trusted {
|
|
return errors.New("source chain not trusted for cross-chain swap")
|
|
}
|
|
|
|
// Check deadline
|
|
if tx.Deadline > 0 && time.Now().UnixNano() > tx.Deadline {
|
|
return errors.New("cross-chain swap deadline exceeded")
|
|
}
|
|
|
|
// The actual swap is executed when the Warp message is received
|
|
// Here we just validate and prepare
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Cross-chain swap initiated",
|
|
"sourceChain", tx.SourceChain,
|
|
"destChain", tx.DestChain,
|
|
"tokenIn", tx.TokenIn,
|
|
"amountIn", tx.AmountIn,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeCrossChainTransfer executes a cross-chain transfer via Warp.
|
|
func (vm *VM) executeCrossChainTransfer(tx *txs.CrossChainTransferTx) error {
|
|
// Verify source chain is trusted
|
|
trusted := false
|
|
for _, chainID := range vm.Config.TrustedChains {
|
|
if chainID == tx.SourceChain {
|
|
trusted = true
|
|
break
|
|
}
|
|
}
|
|
if !trusted {
|
|
return errors.New("source chain not trusted for cross-chain transfer")
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Cross-chain transfer initiated",
|
|
"sourceChain", tx.SourceChain,
|
|
"destChain", tx.DestChain,
|
|
"token", tx.Token,
|
|
"amount", tx.Amount,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeCommitOrder executes a commit phase for MEV-protected order placement.
|
|
func (vm *VM) executeCommitOrder(tx *txs.CommitOrderTx) error {
|
|
// Add commitment to the store with current block info
|
|
err := vm.commitmentStore.AddCommitment(
|
|
tx.CommitmentHash,
|
|
tx.Sender(),
|
|
vm.currentBlockHeight,
|
|
vm.lastBlockTime,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store commitment: %w", err)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Order commitment stored",
|
|
"hash", tx.CommitmentHash,
|
|
"sender", tx.Sender(),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeRevealOrder executes a reveal phase for MEV-protected order placement.
|
|
func (vm *VM) executeRevealOrder(tx *txs.RevealOrderTx, result *BlockResult) error {
|
|
// Use the Reveal method which handles verification and marks as revealed
|
|
orderBytes := mev.SerializeOrderForCommitment(
|
|
tx.Symbol,
|
|
tx.Side,
|
|
tx.OrderType,
|
|
tx.Price,
|
|
tx.Quantity,
|
|
tx.TimeInForce,
|
|
)
|
|
|
|
_, err := vm.commitmentStore.Reveal(
|
|
tx.CommitmentHash,
|
|
orderBytes,
|
|
tx.Salt,
|
|
tx.Sender(),
|
|
vm.lastBlockTime,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("commitment reveal failed: %w", err)
|
|
}
|
|
|
|
// Now execute as a regular place order
|
|
ob, exists := vm.orderbooks[tx.Symbol]
|
|
if !exists {
|
|
return fmt.Errorf("orderbook not found for symbol: %s", tx.Symbol)
|
|
}
|
|
|
|
order := &orderbook.Order{
|
|
ID: tx.ID(),
|
|
Owner: tx.Sender(),
|
|
Symbol: tx.Symbol,
|
|
Side: orderbook.Side(tx.Side),
|
|
Type: orderbook.OrderType(tx.OrderType),
|
|
Price: tx.Price,
|
|
Quantity: tx.Quantity,
|
|
StopPrice: tx.StopPrice,
|
|
Status: orderbook.StatusOpen,
|
|
CreatedAt: tx.Timestamp(),
|
|
ExpiresAt: tx.ExpiresAt,
|
|
PostOnly: tx.PostOnly,
|
|
ReduceOnly: tx.ReduceOnly,
|
|
TimeInForce: tx.TimeInForce,
|
|
}
|
|
|
|
trades, err := ob.AddOrder(order)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add revealed order: %w", err)
|
|
}
|
|
|
|
for _, trade := range trades {
|
|
result.MatchedTrades = append(result.MatchedTrades, *trade)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Order revealed and placed",
|
|
"orderID", order.ID,
|
|
"symbol", tx.Symbol,
|
|
"trades", len(trades),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
|
|
// matchAllOrders runs the matching engine for all orderbooks.
|
|
// This is deterministic - same orders always produce same matches.
|
|
func (vm *VM) matchAllOrders() []orderbook.Trade {
|
|
var allTrades []orderbook.Trade
|
|
|
|
for symbol, ob := range vm.orderbooks {
|
|
trades := ob.Match()
|
|
if len(trades) > 0 {
|
|
allTrades = append(allTrades, trades...)
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Matched trades", "symbol", symbol, "count", len(trades))
|
|
}
|
|
}
|
|
}
|
|
|
|
return allTrades
|
|
}
|
|
|
|
// shouldProcessFunding determines if funding should be processed.
|
|
// Funding happens every 8 hours (28800 seconds).
|
|
func (vm *VM) shouldProcessFunding(blockTime time.Time) bool {
|
|
if vm.lastFundingTime.IsZero() {
|
|
return true // First funding
|
|
}
|
|
|
|
fundingInterval := 8 * time.Hour
|
|
return blockTime.Sub(vm.lastFundingTime) >= fundingInterval
|
|
}
|
|
|
|
// processFunding processes funding payments for all perpetual markets.
|
|
// This is deterministic based on current positions and mark prices.
|
|
func (vm *VM) processFunding(blockTime time.Time) []*perpetuals.FundingPayment {
|
|
var allPayments []*perpetuals.FundingPayment
|
|
|
|
for _, m := range vm.perpetualsEng.GetAllMarkets() {
|
|
market, ok := m.(*perpetuals.Market)
|
|
if !ok {
|
|
continue
|
|
}
|
|
payments, err := vm.perpetualsEng.ProcessFunding(market.Symbol)
|
|
if err != nil {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Failed to process funding", "market", market.Symbol, "error", err)
|
|
}
|
|
continue
|
|
}
|
|
allPayments = append(allPayments, payments...)
|
|
}
|
|
|
|
return allPayments
|
|
}
|
|
|
|
// processLiquidations checks and executes liquidations for all markets.
|
|
// This is deterministic based on current prices and position health.
|
|
func (vm *VM) processLiquidations() []*perpetuals.LiquidationEvent {
|
|
var allLiquidations []*perpetuals.LiquidationEvent
|
|
|
|
for _, m := range vm.perpetualsEng.GetAllMarkets() {
|
|
market, ok := m.(*perpetuals.Market)
|
|
if !ok {
|
|
continue
|
|
}
|
|
liquidations, err := vm.perpetualsEng.CheckAndLiquidate(market.Symbol)
|
|
if err != nil {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Failed to check liquidations", "market", market.Symbol, "error", err)
|
|
}
|
|
continue
|
|
}
|
|
allLiquidations = append(allLiquidations, liquidations...)
|
|
}
|
|
|
|
return allLiquidations
|
|
}
|
|
|
|
// computeStateRoot computes the merkle root of all state.
|
|
// This ensures all nodes agree on state after processing a block.
|
|
// The merkle tree is computed over all state in deterministic order.
|
|
func (vm *VM) computeStateRoot() ids.ID {
|
|
// Collect all leaf hashes in deterministic order
|
|
var leaves [][]byte
|
|
|
|
// 1. Hash all orderbook state (sorted by symbol)
|
|
orderbookLeaves := vm.computeOrderbookHashes()
|
|
leaves = append(leaves, orderbookLeaves...)
|
|
|
|
// 2. Hash all liquidity pool state (sorted by pool ID)
|
|
poolLeaves := vm.computePoolHashes()
|
|
leaves = append(leaves, poolLeaves...)
|
|
|
|
// 3. Hash all perpetual market state (sorted by symbol)
|
|
perpLeaves := vm.computePerpetualHashes()
|
|
leaves = append(leaves, perpLeaves...)
|
|
|
|
// 4. Add block metadata
|
|
metaHash := vm.computeBlockMetaHash()
|
|
leaves = append(leaves, metaHash)
|
|
|
|
// If no state, return empty hash
|
|
if len(leaves) == 0 {
|
|
return ids.Empty
|
|
}
|
|
|
|
// Compute merkle root from leaves
|
|
return computeMerkleRoot(leaves)
|
|
}
|
|
|
|
// computeOrderbookHashes computes hashes for all orderbook state.
|
|
func (vm *VM) computeOrderbookHashes() [][]byte {
|
|
// Get sorted symbol list for deterministic ordering
|
|
symbols := make([]string, 0, len(vm.orderbooks))
|
|
for symbol := range vm.orderbooks {
|
|
symbols = append(symbols, symbol)
|
|
}
|
|
sort.Strings(symbols)
|
|
|
|
var hashes [][]byte
|
|
for _, symbol := range symbols {
|
|
ob := vm.orderbooks[symbol]
|
|
|
|
// Hash orderbook state: symbol + bestBid + bestAsk + spread
|
|
h := sha256.New()
|
|
h.Write([]byte(symbol))
|
|
|
|
bestBid := ob.GetBestBid()
|
|
bestAsk := ob.GetBestAsk()
|
|
|
|
bidBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(bidBuf, bestBid)
|
|
h.Write(bidBuf)
|
|
|
|
askBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(askBuf, bestAsk)
|
|
h.Write(askBuf)
|
|
|
|
// Add depth hash (top 10 levels each side)
|
|
bids, asks := ob.GetDepth(10)
|
|
for _, level := range bids {
|
|
priceBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(priceBuf, level.Price)
|
|
h.Write(priceBuf)
|
|
qtyBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(qtyBuf, level.Quantity)
|
|
h.Write(qtyBuf)
|
|
}
|
|
for _, level := range asks {
|
|
priceBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(priceBuf, level.Price)
|
|
h.Write(priceBuf)
|
|
qtyBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(qtyBuf, level.Quantity)
|
|
h.Write(qtyBuf)
|
|
}
|
|
|
|
hashes = append(hashes, h.Sum(nil))
|
|
}
|
|
|
|
return hashes
|
|
}
|
|
|
|
// computePoolHashes computes hashes for all liquidity pool state.
|
|
func (vm *VM) computePoolHashes() [][]byte {
|
|
pools := vm.liquidityMgr.GetAllPools()
|
|
|
|
// Sort by pool ID for deterministic ordering
|
|
sort.Slice(pools, func(i, j int) bool {
|
|
return pools[i].ID.Compare(pools[j].ID) < 0
|
|
})
|
|
|
|
var hashes [][]byte
|
|
for _, pool := range pools {
|
|
h := sha256.New()
|
|
|
|
// Pool ID
|
|
h.Write(pool.ID[:])
|
|
|
|
// Token pair
|
|
h.Write(pool.Token0[:])
|
|
h.Write(pool.Token1[:])
|
|
|
|
// Reserves (as big-endian bytes)
|
|
h.Write(pool.Reserve0.Bytes())
|
|
h.Write(pool.Reserve1.Bytes())
|
|
|
|
// Total supply
|
|
h.Write(pool.TotalSupply.Bytes())
|
|
|
|
// Fee
|
|
feeBuf := make([]byte, 2)
|
|
binary.BigEndian.PutUint16(feeBuf, pool.FeeBps)
|
|
h.Write(feeBuf)
|
|
|
|
hashes = append(hashes, h.Sum(nil))
|
|
}
|
|
|
|
return hashes
|
|
}
|
|
|
|
// computePerpetualHashes computes hashes for all perpetual market state.
|
|
func (vm *VM) computePerpetualHashes() [][]byte {
|
|
markets := vm.perpetualsEng.GetAllMarkets()
|
|
|
|
// Sort by symbol for deterministic ordering
|
|
sort.Slice(markets, func(i, j int) bool {
|
|
mi, ok1 := markets[i].(*perpetuals.Market)
|
|
mj, ok2 := markets[j].(*perpetuals.Market)
|
|
if !ok1 || !ok2 {
|
|
return false
|
|
}
|
|
return mi.Symbol < mj.Symbol
|
|
})
|
|
|
|
var hashes [][]byte
|
|
for _, m := range markets {
|
|
market, ok := m.(*perpetuals.Market)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
h := sha256.New()
|
|
|
|
// Symbol
|
|
h.Write([]byte(market.Symbol))
|
|
|
|
// Prices
|
|
if market.IndexPrice != nil {
|
|
h.Write(market.IndexPrice.Bytes())
|
|
}
|
|
if market.MarkPrice != nil {
|
|
h.Write(market.MarkPrice.Bytes())
|
|
}
|
|
if market.LastPrice != nil {
|
|
h.Write(market.LastPrice.Bytes())
|
|
}
|
|
|
|
// Open interest
|
|
if market.OpenInterestLong != nil {
|
|
h.Write(market.OpenInterestLong.Bytes())
|
|
}
|
|
if market.OpenInterestShort != nil {
|
|
h.Write(market.OpenInterestShort.Bytes())
|
|
}
|
|
|
|
// Funding rate
|
|
if market.FundingRate != nil {
|
|
h.Write(market.FundingRate.Bytes())
|
|
}
|
|
|
|
hashes = append(hashes, h.Sum(nil))
|
|
}
|
|
|
|
return hashes
|
|
}
|
|
|
|
// computeBlockMetaHash computes hash of block metadata.
|
|
func (vm *VM) computeBlockMetaHash() []byte {
|
|
h := sha256.New()
|
|
|
|
// Block height
|
|
heightBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(heightBuf, vm.currentBlockHeight)
|
|
h.Write(heightBuf)
|
|
|
|
// Last block time
|
|
timeBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(timeBuf, uint64(vm.lastBlockTime.UnixNano()))
|
|
h.Write(timeBuf)
|
|
|
|
// Last funding time
|
|
fundingBuf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(fundingBuf, uint64(vm.lastFundingTime.UnixNano()))
|
|
h.Write(fundingBuf)
|
|
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// computeMerkleRoot computes the merkle root from a list of leaf hashes.
|
|
// Uses standard binary merkle tree construction.
|
|
func computeMerkleRoot(leaves [][]byte) ids.ID {
|
|
if len(leaves) == 0 {
|
|
return ids.Empty
|
|
}
|
|
|
|
// Copy leaves to avoid modifying input
|
|
nodes := make([][]byte, len(leaves))
|
|
copy(nodes, leaves)
|
|
|
|
// Build tree bottom-up
|
|
for len(nodes) > 1 {
|
|
var nextLevel [][]byte
|
|
|
|
for i := 0; i < len(nodes); i += 2 {
|
|
h := sha256.New()
|
|
h.Write(nodes[i])
|
|
|
|
if i+1 < len(nodes) {
|
|
// Pair exists
|
|
h.Write(nodes[i+1])
|
|
} else {
|
|
// Odd node, duplicate
|
|
h.Write(nodes[i])
|
|
}
|
|
|
|
nextLevel = append(nextLevel, h.Sum(nil))
|
|
}
|
|
|
|
nodes = nextLevel
|
|
}
|
|
|
|
// Root is first (and only) node
|
|
var root ids.ID
|
|
copy(root[:], nodes[0])
|
|
return root
|
|
}
|
|
|
|
// Shutdown implements consensuscore.VM interface.
|
|
// It gracefully shuts down the VM.
|
|
// NOTE: No background tasks to wait for in functional mode.
|
|
func (vm *VM) Shutdown(ctx context.Context) error {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Shutting down DEX VM")
|
|
}
|
|
|
|
vm.shutdown = true
|
|
|
|
// Close database
|
|
if vm.db != nil {
|
|
if err := vm.db.Close(); err != nil {
|
|
return fmt.Errorf("failed to close database: %w", err)
|
|
}
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("DEX VM shutdown complete")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Version implements consensuscore.VM interface.
|
|
func (vm *VM) Version(ctx context.Context) (string, error) {
|
|
return "1.0.0", nil
|
|
}
|
|
|
|
// CreateHandlers implements consensuscore.VM interface.
|
|
// It creates HTTP handlers for the DEX API.
|
|
func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) {
|
|
server := rpc.NewServer()
|
|
server.RegisterCodec(rpcjson.NewCodec(), "application/json")
|
|
server.RegisterCodec(rpcjson.NewCodec(), "application/json;charset=UTF-8")
|
|
|
|
// Register DEX API service
|
|
service := api.NewService(vm)
|
|
if err := server.RegisterService(service, "dex"); err != nil {
|
|
return nil, fmt.Errorf("failed to register DEX service: %w", err)
|
|
}
|
|
|
|
return map[string]http.Handler{
|
|
"": server,
|
|
"/ws": vm.createWebSocketHandler(),
|
|
}, nil
|
|
}
|
|
|
|
// WebSocket upgrader with default options
|
|
var wsUpgrader = websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true // Allow all origins (configure appropriately in production)
|
|
},
|
|
}
|
|
|
|
// WSMessage represents a WebSocket message.
|
|
type WSMessage struct {
|
|
Type string `json:"type"`
|
|
Channel string `json:"channel"`
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
}
|
|
|
|
// WSSubscription represents a client subscription.
|
|
type WSSubscription struct {
|
|
Channel string `json:"channel"`
|
|
Symbol string `json:"symbol,omitempty"`
|
|
}
|
|
|
|
// wsClient represents a connected WebSocket client.
|
|
type wsClient struct {
|
|
conn *websocket.Conn
|
|
vm *VM
|
|
subscriptions map[string]bool
|
|
send chan []byte
|
|
done chan struct{}
|
|
}
|
|
|
|
// createWebSocketHandler creates a WebSocket handler for real-time updates.
|
|
func (vm *VM) createWebSocketHandler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Upgrade HTTP connection to WebSocket
|
|
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("WebSocket upgrade failed", "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
client := &wsClient{
|
|
conn: conn,
|
|
vm: vm,
|
|
subscriptions: make(map[string]bool),
|
|
send: make(chan []byte, 256),
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
// Start goroutines for reading and writing
|
|
go client.writePump()
|
|
go client.readPump()
|
|
})
|
|
}
|
|
|
|
// readPump pumps messages from the WebSocket connection to the hub.
|
|
func (c *wsClient) readPump() {
|
|
defer func() {
|
|
close(c.done)
|
|
c.conn.Close()
|
|
}()
|
|
|
|
c.conn.SetReadLimit(65536) // 64KB max message size
|
|
|
|
for {
|
|
_, message, err := c.conn.ReadMessage()
|
|
if err != nil {
|
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
if !c.vm.log.IsZero() {
|
|
c.vm.log.Warn("WebSocket read error", "error", err)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Parse incoming message
|
|
var msg WSMessage
|
|
if err := json.Unmarshal(message, &msg); err != nil {
|
|
c.sendError("Invalid JSON message")
|
|
continue
|
|
}
|
|
|
|
// Handle message based on type
|
|
switch msg.Type {
|
|
case "subscribe":
|
|
c.handleSubscribe(msg)
|
|
case "unsubscribe":
|
|
c.handleUnsubscribe(msg)
|
|
case "ping":
|
|
c.sendPong()
|
|
default:
|
|
c.sendError(fmt.Sprintf("Unknown message type: %s", msg.Type))
|
|
}
|
|
}
|
|
}
|
|
|
|
// writePump pumps messages from the hub to the WebSocket connection.
|
|
func (c *wsClient) writePump() {
|
|
ticker := time.NewTicker(30 * time.Second) // Ping interval
|
|
defer func() {
|
|
ticker.Stop()
|
|
c.conn.Close()
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case message, ok := <-c.send:
|
|
if !ok {
|
|
// Channel closed
|
|
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
|
return
|
|
}
|
|
|
|
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
|
|
return
|
|
}
|
|
|
|
case <-ticker.C:
|
|
// Send ping to keep connection alive
|
|
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
|
return
|
|
}
|
|
|
|
case <-c.done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleSubscribe handles subscription requests.
|
|
func (c *wsClient) handleSubscribe(msg WSMessage) {
|
|
var sub WSSubscription
|
|
if err := json.Unmarshal(msg.Data, &sub); err != nil {
|
|
c.sendError("Invalid subscription data")
|
|
return
|
|
}
|
|
|
|
channel := sub.Channel
|
|
if sub.Symbol != "" {
|
|
channel = fmt.Sprintf("%s:%s", sub.Channel, sub.Symbol)
|
|
}
|
|
|
|
c.subscriptions[channel] = true
|
|
|
|
// Send confirmation
|
|
c.sendJSON(WSMessage{
|
|
Type: "subscribed",
|
|
Channel: channel,
|
|
})
|
|
|
|
// Send initial snapshot based on channel type
|
|
switch sub.Channel {
|
|
case "orderbook":
|
|
c.sendOrderbookSnapshot(sub.Symbol)
|
|
case "trades":
|
|
// No initial snapshot for trades
|
|
case "ticker":
|
|
c.sendTickerSnapshot(sub.Symbol)
|
|
}
|
|
}
|
|
|
|
// handleUnsubscribe handles unsubscription requests.
|
|
func (c *wsClient) handleUnsubscribe(msg WSMessage) {
|
|
var sub WSSubscription
|
|
if err := json.Unmarshal(msg.Data, &sub); err != nil {
|
|
c.sendError("Invalid unsubscription data")
|
|
return
|
|
}
|
|
|
|
channel := sub.Channel
|
|
if sub.Symbol != "" {
|
|
channel = fmt.Sprintf("%s:%s", sub.Channel, sub.Symbol)
|
|
}
|
|
|
|
delete(c.subscriptions, channel)
|
|
|
|
c.sendJSON(WSMessage{
|
|
Type: "unsubscribed",
|
|
Channel: channel,
|
|
})
|
|
}
|
|
|
|
// sendOrderbookSnapshot sends the current orderbook state.
|
|
func (c *wsClient) sendOrderbookSnapshot(symbol string) {
|
|
ob, err := c.vm.GetOrderbook(symbol)
|
|
if err != nil {
|
|
c.sendError(fmt.Sprintf("Orderbook not found: %s", symbol))
|
|
return
|
|
}
|
|
|
|
bids, asks := ob.GetDepth(20)
|
|
|
|
type OrderbookSnapshot struct {
|
|
Symbol string `json:"symbol"`
|
|
Bids []*orderbook.PriceLevel `json:"bids"`
|
|
Asks []*orderbook.PriceLevel `json:"asks"`
|
|
Time int64 `json:"time"`
|
|
}
|
|
|
|
snapshot := OrderbookSnapshot{
|
|
Symbol: symbol,
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Time: time.Now().UnixNano(),
|
|
}
|
|
|
|
data, _ := json.Marshal(snapshot)
|
|
c.sendJSON(WSMessage{
|
|
Type: "snapshot",
|
|
Channel: fmt.Sprintf("orderbook:%s", symbol),
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// sendTickerSnapshot sends the current ticker state.
|
|
func (c *wsClient) sendTickerSnapshot(symbol string) {
|
|
ob, err := c.vm.GetOrderbook(symbol)
|
|
if err != nil {
|
|
c.sendError(fmt.Sprintf("Symbol not found: %s", symbol))
|
|
return
|
|
}
|
|
|
|
type Ticker struct {
|
|
Symbol string `json:"symbol"`
|
|
BestBid uint64 `json:"bestBid"`
|
|
BestAsk uint64 `json:"bestAsk"`
|
|
MidPrice uint64 `json:"midPrice"`
|
|
Spread uint64 `json:"spread"`
|
|
Time int64 `json:"time"`
|
|
}
|
|
|
|
ticker := Ticker{
|
|
Symbol: symbol,
|
|
BestBid: ob.GetBestBid(),
|
|
BestAsk: ob.GetBestAsk(),
|
|
MidPrice: ob.GetMidPrice(),
|
|
Spread: ob.GetSpread(),
|
|
Time: time.Now().UnixNano(),
|
|
}
|
|
|
|
data, _ := json.Marshal(ticker)
|
|
c.sendJSON(WSMessage{
|
|
Type: "snapshot",
|
|
Channel: fmt.Sprintf("ticker:%s", symbol),
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// sendPong sends a pong response.
|
|
func (c *wsClient) sendPong() {
|
|
c.sendJSON(WSMessage{Type: "pong"})
|
|
}
|
|
|
|
// sendError sends an error message.
|
|
func (c *wsClient) sendError(errMsg string) {
|
|
data, _ := json.Marshal(map[string]string{"message": errMsg})
|
|
c.sendJSON(WSMessage{
|
|
Type: "error",
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// sendJSON sends a JSON message to the client.
|
|
func (c *wsClient) sendJSON(msg WSMessage) {
|
|
data, err := json.Marshal(msg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case c.send <- data:
|
|
default:
|
|
// Channel full, drop message
|
|
}
|
|
}
|
|
|
|
// HealthCheck implements consensuscore.VM interface.
|
|
func (vm *VM) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
|
|
vm.lock.RLock()
|
|
defer vm.lock.RUnlock()
|
|
|
|
return chain.HealthResult{
|
|
Healthy: vm.isInitialized && vm.bootstrapped,
|
|
Details: map[string]string{
|
|
"bootstrapped": fmt.Sprintf("%v", vm.bootstrapped),
|
|
"orderbooks": fmt.Sprintf("%d", len(vm.orderbooks)),
|
|
"pools": fmt.Sprintf("%d", len(vm.liquidityMgr.GetAllPools())),
|
|
"perpMarkets": fmt.Sprintf("%d", len(vm.perpetualsEng.GetAllMarkets())),
|
|
"blockHeight": fmt.Sprintf("%d", vm.currentBlockHeight),
|
|
"mode": "functional",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Connected implements consensuscore.VM interface.
|
|
func (vm *VM) Connected(ctx context.Context, nodeID ids.NodeID, v *version.Application) error {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
vm.connectedPeers[nodeID] = v
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Peer connected", "nodeID", nodeID, "version", v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Disconnected implements consensuscore.VM interface.
|
|
func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
delete(vm.connectedPeers, nodeID)
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Peer disconnected", "nodeID", nodeID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetOrderbook returns the orderbook for a symbol.
|
|
func (vm *VM) GetOrderbook(symbol string) (*orderbook.Orderbook, error) {
|
|
vm.lock.RLock()
|
|
defer vm.lock.RUnlock()
|
|
|
|
ob, exists := vm.orderbooks[symbol]
|
|
if !exists {
|
|
return nil, fmt.Errorf("orderbook not found for symbol: %s", symbol)
|
|
}
|
|
return ob, nil
|
|
}
|
|
|
|
// GetOrCreateOrderbook returns or creates an orderbook for a symbol.
|
|
func (vm *VM) GetOrCreateOrderbook(symbol string) *orderbook.Orderbook {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
ob, exists := vm.orderbooks[symbol]
|
|
if !exists {
|
|
ob = orderbook.New(symbol)
|
|
vm.orderbooks[symbol] = ob
|
|
}
|
|
return ob
|
|
}
|
|
|
|
// GetLiquidityManager returns the liquidity pool manager.
|
|
func (vm *VM) GetLiquidityManager() *liquidity.Manager {
|
|
return vm.liquidityMgr
|
|
}
|
|
|
|
// GetPerpetualsEngine returns the perpetual futures engine.
|
|
func (vm *VM) GetPerpetualsEngine() api.PerpetualsEngine {
|
|
return vm.perpetualsEng
|
|
}
|
|
|
|
// GetCommitmentStore returns the MEV protection commitment store.
|
|
func (vm *VM) GetCommitmentStore() api.CommitmentStore {
|
|
return vm.commitmentStore
|
|
}
|
|
|
|
// GetADLEngine returns the auto-deleveraging engine.
|
|
func (vm *VM) GetADLEngine() api.ADLEngine {
|
|
return vm.adlEngine
|
|
}
|
|
|
|
// IsBootstrapped returns true if the VM is fully bootstrapped.
|
|
func (vm *VM) IsBootstrapped() bool {
|
|
vm.lock.RLock()
|
|
defer vm.lock.RUnlock()
|
|
return vm.bootstrapped
|
|
}
|
|
|
|
// GetBlockHeight returns the current block height.
|
|
func (vm *VM) GetBlockHeight() uint64 {
|
|
vm.lock.RLock()
|
|
defer vm.lock.RUnlock()
|
|
return vm.currentBlockHeight
|
|
}
|
|
|
|
// GetLastBlockTime returns the timestamp of the last processed block.
|
|
func (vm *VM) GetLastBlockTime() time.Time {
|
|
vm.lock.RLock()
|
|
defer vm.lock.RUnlock()
|
|
return vm.lastBlockTime
|
|
}
|
|
|
|
// Gossip implements consensuscore.VM interface.
|
|
// It handles gossiped messages from peers (orders and trades).
|
|
func (vm *VM) Gossip(ctx context.Context, nodeID ids.NodeID, msg []byte) error {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
if vm.shutdown {
|
|
return errShutdown
|
|
}
|
|
|
|
// Decode the network message
|
|
netMsg, err := network.DecodeMessage(msg)
|
|
if err != nil {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Failed to decode gossip message", "nodeID", nodeID, "error", err)
|
|
}
|
|
return nil // Don't fail on invalid messages from peers
|
|
}
|
|
|
|
switch netMsg.Type {
|
|
case network.MsgOrderGossip:
|
|
return vm.handleGossipedOrder(nodeID, netMsg)
|
|
case network.MsgTradeGossip:
|
|
return vm.handleGossipedTrade(nodeID, netMsg)
|
|
default:
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Unknown gossip message type", "type", netMsg.Type, "nodeID", nodeID)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleGossipedOrder handles an order gossiped from a peer.
|
|
func (vm *VM) handleGossipedOrder(nodeID ids.NodeID, msg *network.Message) error {
|
|
// Parse the order from the payload
|
|
parser := &txs.TxParser{}
|
|
tx, err := parser.Parse(msg.Payload)
|
|
if err != nil {
|
|
return nil // Ignore malformed orders
|
|
}
|
|
|
|
// Only handle place order transactions from gossip
|
|
placeOrderTx, ok := tx.(*txs.PlaceOrderTx)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Verify the transaction
|
|
if err := placeOrderTx.Verify(); err != nil {
|
|
return nil // Ignore invalid orders
|
|
}
|
|
|
|
ob, exists := vm.orderbooks[placeOrderTx.Symbol]
|
|
if !exists {
|
|
return nil // Unknown symbol
|
|
}
|
|
|
|
// Create order from gossip (will be confirmed in next block)
|
|
order := &orderbook.Order{
|
|
ID: placeOrderTx.ID(),
|
|
Owner: placeOrderTx.Sender(),
|
|
Symbol: placeOrderTx.Symbol,
|
|
Side: orderbook.Side(placeOrderTx.Side),
|
|
Type: orderbook.OrderType(placeOrderTx.OrderType),
|
|
Price: placeOrderTx.Price,
|
|
Quantity: placeOrderTx.Quantity,
|
|
StopPrice: placeOrderTx.StopPrice,
|
|
Status: orderbook.StatusOpen,
|
|
CreatedAt: placeOrderTx.Timestamp(),
|
|
ExpiresAt: placeOrderTx.ExpiresAt,
|
|
PostOnly: placeOrderTx.PostOnly,
|
|
ReduceOnly: placeOrderTx.ReduceOnly,
|
|
TimeInForce: placeOrderTx.TimeInForce,
|
|
}
|
|
|
|
// Add to orderbook (trades happen in block processing)
|
|
if _, err := ob.AddOrder(order); err != nil {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Failed to add gossiped order", "orderID", order.ID, "error", err)
|
|
}
|
|
} else if !vm.log.IsZero() {
|
|
vm.log.Debug("Received gossiped order",
|
|
"orderID", order.ID,
|
|
"symbol", order.Symbol,
|
|
"nodeID", nodeID,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleGossipedTrade handles a trade notification gossiped from a peer.
|
|
func (vm *VM) handleGossipedTrade(nodeID ids.NodeID, msg *network.Message) error {
|
|
// Trade gossip is informational only - actual trades are determined by block processing
|
|
// This can be used for real-time UI updates before block confirmation
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Received gossiped trade notification", "nodeID", nodeID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Request implements consensuscore.VM interface.
|
|
// It handles direct requests from peers (e.g., orderbook sync).
|
|
func (vm *VM) Request(
|
|
ctx context.Context,
|
|
nodeID ids.NodeID,
|
|
requestID uint32,
|
|
deadline time.Time,
|
|
request []byte,
|
|
) error {
|
|
vm.lock.RLock()
|
|
defer vm.lock.RUnlock()
|
|
|
|
if vm.shutdown {
|
|
return errShutdown
|
|
}
|
|
|
|
// Check deadline
|
|
if time.Now().After(deadline) {
|
|
return errors.New("request deadline exceeded")
|
|
}
|
|
|
|
// Decode the network message
|
|
netMsg, err := network.DecodeMessage(request)
|
|
if err != nil {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Failed to decode request", "nodeID", nodeID, "error", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
switch netMsg.Type {
|
|
case network.MsgOrderbookSync:
|
|
return vm.handleOrderbookSyncRequest(ctx, nodeID, requestID, netMsg)
|
|
case network.MsgPoolSync:
|
|
return vm.handlePoolSyncRequest(ctx, nodeID, requestID, netMsg)
|
|
default:
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Unknown request type", "type", netMsg.Type, "nodeID", nodeID)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleOrderbookSyncRequest handles an orderbook sync request from a peer.
|
|
func (vm *VM) handleOrderbookSyncRequest(
|
|
ctx context.Context,
|
|
nodeID ids.NodeID,
|
|
requestID uint32,
|
|
msg *network.Message,
|
|
) error {
|
|
// Extract symbol from payload (simple format: just the symbol string)
|
|
symbol := string(msg.Payload)
|
|
if symbol == "" {
|
|
return errors.New("missing symbol in orderbook sync request")
|
|
}
|
|
|
|
ob, exists := vm.orderbooks[symbol]
|
|
if !exists {
|
|
return fmt.Errorf("orderbook not found: %s", symbol)
|
|
}
|
|
|
|
// Get orderbook depth
|
|
bids, asks := ob.GetDepth(100) // Send top 100 levels
|
|
|
|
// Create response with orderbook state
|
|
type OrderbookSyncResponse struct {
|
|
Symbol string `json:"symbol"`
|
|
Bids []*orderbook.PriceLevel `json:"bids"`
|
|
Asks []*orderbook.PriceLevel `json:"asks"`
|
|
BestBid uint64 `json:"bestBid"`
|
|
BestAsk uint64 `json:"bestAsk"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
response := OrderbookSyncResponse{
|
|
Symbol: symbol,
|
|
Bids: bids,
|
|
Asks: asks,
|
|
BestBid: ob.GetBestBid(),
|
|
BestAsk: ob.GetBestAsk(),
|
|
Timestamp: time.Now().UnixNano(),
|
|
}
|
|
|
|
responseData, err := json.Marshal(response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create network message for response
|
|
responseMsg := &network.Message{
|
|
Type: network.MsgOrderbookSync,
|
|
RequestID: requestID,
|
|
ChainID: vm.chainID,
|
|
Payload: responseData,
|
|
Timestamp: time.Now().UnixNano(),
|
|
}
|
|
|
|
// Send response via appSender
|
|
if vm.appSender != nil {
|
|
// Response is sent automatically by the consensus layer
|
|
// Log for debugging
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Sent orderbook sync response",
|
|
"symbol", symbol,
|
|
"nodeID", nodeID,
|
|
"bids", len(bids),
|
|
"asks", len(asks),
|
|
)
|
|
}
|
|
_ = responseMsg // Response will be sent by caller
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handlePoolSyncRequest handles a liquidity pool sync request from a peer.
|
|
func (vm *VM) handlePoolSyncRequest(
|
|
ctx context.Context,
|
|
nodeID ids.NodeID,
|
|
requestID uint32,
|
|
msg *network.Message,
|
|
) error {
|
|
// Get all pools
|
|
pools := vm.liquidityMgr.GetAllPools()
|
|
|
|
// Create response with pool state
|
|
type PoolSyncResponse struct {
|
|
Pools []*liquidity.Pool `json:"pools"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
response := PoolSyncResponse{
|
|
Pools: pools,
|
|
Timestamp: time.Now().UnixNano(),
|
|
}
|
|
|
|
responseData, err := json.Marshal(response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Log response
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Sent pool sync response",
|
|
"nodeID", nodeID,
|
|
"pools", len(pools),
|
|
)
|
|
}
|
|
|
|
_ = responseData // Response will be sent by caller
|
|
return nil
|
|
}
|
|
|
|
// RequestFailed implements consensuscore.VM interface.
|
|
func (vm *VM) RequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32, appErr *consensuscore.AppError) error {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Request failed", "nodeID", nodeID, "requestID", requestID, "error", appErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Response implements consensuscore.VM interface.
|
|
func (vm *VM) Response(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Received response", "nodeID", nodeID, "requestID", requestID, "size", len(response))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CrossChainRequest implements consensuscore.VM interface.
|
|
// It handles cross-chain requests via Warp messaging.
|
|
func (vm *VM) CrossChainRequest(
|
|
ctx context.Context,
|
|
sourceChainID ids.ID,
|
|
requestID uint32,
|
|
deadline time.Time,
|
|
request []byte,
|
|
) error {
|
|
vm.lock.Lock()
|
|
defer vm.lock.Unlock()
|
|
|
|
if vm.shutdown {
|
|
return errShutdown
|
|
}
|
|
|
|
// Check deadline
|
|
if time.Now().After(deadline) {
|
|
return errors.New("cross-chain request deadline exceeded")
|
|
}
|
|
|
|
// Verify source chain is trusted
|
|
trusted := false
|
|
for _, chainID := range vm.Config.TrustedChains {
|
|
if chainID == sourceChainID {
|
|
trusted = true
|
|
break
|
|
}
|
|
}
|
|
if !trusted {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Cross-chain request from untrusted chain", "chainID", sourceChainID)
|
|
}
|
|
return errors.New("source chain not trusted")
|
|
}
|
|
|
|
// Decode the network message
|
|
netMsg, err := network.DecodeMessage(request)
|
|
if err != nil {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Failed to decode cross-chain request", "chainID", sourceChainID, "error", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Received cross-chain request",
|
|
"sourceChain", sourceChainID,
|
|
"type", netMsg.Type,
|
|
"requestID", requestID,
|
|
)
|
|
}
|
|
|
|
switch netMsg.Type {
|
|
case network.MsgCrossChainSwap:
|
|
return vm.handleCrossChainSwapRequest(ctx, sourceChainID, requestID, netMsg)
|
|
case network.MsgCrossChainTransfer:
|
|
return vm.handleCrossChainTransferRequest(ctx, sourceChainID, requestID, netMsg)
|
|
case network.MsgWarpMessage:
|
|
return vm.handleWarpMessage(ctx, sourceChainID, requestID, netMsg)
|
|
default:
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Unknown cross-chain request type", "type", netMsg.Type)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleCrossChainSwapRequest handles a cross-chain swap request.
|
|
func (vm *VM) handleCrossChainSwapRequest(
|
|
ctx context.Context,
|
|
sourceChainID ids.ID,
|
|
requestID uint32,
|
|
msg *network.Message,
|
|
) error {
|
|
// Parse the cross-chain swap transaction from payload
|
|
parser := &txs.TxParser{}
|
|
tx, err := parser.Parse(msg.Payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse cross-chain swap: %w", err)
|
|
}
|
|
|
|
swapTx, ok := tx.(*txs.CrossChainSwapTx)
|
|
if !ok {
|
|
return errors.New("invalid cross-chain swap transaction")
|
|
}
|
|
|
|
// Verify the swap
|
|
if err := swapTx.Verify(); err != nil {
|
|
return fmt.Errorf("cross-chain swap verification failed: %w", err)
|
|
}
|
|
|
|
// Check deadline
|
|
if swapTx.Deadline > 0 && time.Now().UnixNano() > swapTx.Deadline {
|
|
return errors.New("cross-chain swap deadline exceeded")
|
|
}
|
|
|
|
// Execute the swap on this chain
|
|
// Find the best pool for the token pair
|
|
pools := vm.liquidityMgr.GetPoolsByTokenPair(swapTx.TokenIn, swapTx.TokenOut)
|
|
if len(pools) == 0 {
|
|
return errors.New("no liquidity pool found for token pair")
|
|
}
|
|
|
|
// Execute swap on the first available pool
|
|
result, err := vm.liquidityMgr.Swap(
|
|
pools[0].ID,
|
|
swapTx.TokenIn,
|
|
new(big.Int).SetUint64(swapTx.AmountIn),
|
|
new(big.Int).SetUint64(swapTx.MinAmountOut),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("cross-chain swap execution failed: %w", err)
|
|
}
|
|
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Cross-chain swap executed",
|
|
"sourceChain", sourceChainID,
|
|
"tokenIn", swapTx.TokenIn,
|
|
"amountIn", swapTx.AmountIn,
|
|
"amountOut", result.AmountOut,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleCrossChainTransferRequest handles a cross-chain transfer request.
|
|
func (vm *VM) handleCrossChainTransferRequest(
|
|
ctx context.Context,
|
|
sourceChainID ids.ID,
|
|
requestID uint32,
|
|
msg *network.Message,
|
|
) error {
|
|
// Parse the cross-chain transfer transaction from payload
|
|
parser := &txs.TxParser{}
|
|
tx, err := parser.Parse(msg.Payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse cross-chain transfer: %w", err)
|
|
}
|
|
|
|
transferTx, ok := tx.(*txs.CrossChainTransferTx)
|
|
if !ok {
|
|
return errors.New("invalid cross-chain transfer transaction")
|
|
}
|
|
|
|
// Verify the transfer
|
|
if err := transferTx.Verify(); err != nil {
|
|
return fmt.Errorf("cross-chain transfer verification failed: %w", err)
|
|
}
|
|
|
|
// The actual transfer is completed when the Warp message is signed by validators
|
|
// Here we acknowledge receipt and prepare for settlement
|
|
if !vm.log.IsZero() {
|
|
vm.log.Info("Cross-chain transfer received",
|
|
"sourceChain", sourceChainID,
|
|
"token", transferTx.Token,
|
|
"amount", transferTx.Amount,
|
|
"recipient", transferTx.Recipient,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleWarpMessage handles a generic Warp message.
|
|
func (vm *VM) handleWarpMessage(
|
|
ctx context.Context,
|
|
sourceChainID ids.ID,
|
|
requestID uint32,
|
|
msg *network.Message,
|
|
) error {
|
|
// Generic Warp message handling
|
|
// Can be used for custom cross-chain operations
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Received Warp message",
|
|
"sourceChain", sourceChainID,
|
|
"payloadSize", len(msg.Payload),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CrossChainRequestFailed implements consensuscore.VM interface.
|
|
func (vm *VM) CrossChainRequestFailed(ctx context.Context, chainID ids.ID, requestID uint32, appErr *consensuscore.AppError) error {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Warn("Cross-chain request failed", "chainID", chainID, "requestID", requestID, "error", appErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CrossChainResponse implements consensuscore.VM interface.
|
|
func (vm *VM) CrossChainResponse(ctx context.Context, chainID ids.ID, requestID uint32, response []byte) error {
|
|
if !vm.log.IsZero() {
|
|
vm.log.Debug("Received cross-chain response", "chainID", chainID, "requestID", requestID, "size", len(response))
|
|
}
|
|
return nil
|
|
}
|