Files
mev-beta/test/sequencer_simulation.go
Krypto Kajun 3f69aeafcf fix: resolve all compilation issues across transport and lifecycle packages
- Fixed duplicate type declarations in transport package
- Removed unused variables in lifecycle and dependency injection
- Fixed big.Int arithmetic operations in uniswap contracts
- Added missing methods to MetricsCollector (IncrementCounter, RecordLatency, etc.)
- Fixed jitter calculation in TCP transport retry logic
- Updated ComponentHealth field access to use transport type
- Ensured all core packages build successfully

All major compilation errors resolved:
 Transport package builds clean
 Lifecycle package builds clean
 Main MEV bot application builds clean
 Fixed method signature mismatches
 Resolved type conflicts and duplications

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 17:23:14 -05:00

595 lines
19 KiB
Go

package test
import (
"context"
"encoding/hex"
"fmt"
"math/big"
"os"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/arbitrum"
"github.com/fraktal/mev-beta/pkg/oracle"
)
// ArbitrumSequencerSimulator simulates the Arbitrum sequencer for testing
type ArbitrumSequencerSimulator struct {
config *SequencerConfig
logger *logger.Logger
realDataCollector *RealDataCollector
mockService *MockSequencerService
validator *ParserValidator
storage *TransactionStorage
benchmark *PerformanceBenchmark
mu sync.RWMutex
isRunning bool
}
// SequencerConfig contains configuration for the sequencer simulator
type SequencerConfig struct {
// Data collection
RPCEndpoint string `json:"rpc_endpoint"`
WSEndpoint string `json:"ws_endpoint"`
DataDir string `json:"data_dir"`
// Block range for data collection
StartBlock uint64 `json:"start_block"`
EndBlock uint64 `json:"end_block"`
MaxBlocksPerBatch int `json:"max_blocks_per_batch"`
// Filtering criteria
MinSwapValueUSD float64 `json:"min_swap_value_usd"`
TargetProtocols []string `json:"target_protocols"`
// Simulation parameters
SequencerTiming time.Duration `json:"sequencer_timing"`
BatchSize int `json:"batch_size"`
CompressionLevel int `json:"compression_level"`
// Performance testing
MaxConcurrentOps int `json:"max_concurrent_ops"`
TestDuration time.Duration `json:"test_duration"`
// Validation
ValidateResults bool `json:"validate_results"`
StrictMode bool `json:"strict_mode"`
ExpectedAccuracy float64 `json:"expected_accuracy"`
}
// RealTransactionData represents real Arbitrum transaction data
type RealTransactionData struct {
Hash common.Hash `json:"hash"`
BlockNumber uint64 `json:"block_number"`
BlockHash common.Hash `json:"block_hash"`
TransactionIndex uint `json:"transaction_index"`
From common.Address `json:"from"`
To *common.Address `json:"to"`
Value *big.Int `json:"value"`
GasLimit uint64 `json:"gas_limit"`
GasUsed uint64 `json:"gas_used"`
GasPrice *big.Int `json:"gas_price"`
GasTipCap *big.Int `json:"gas_tip_cap,omitempty"`
GasFeeCap *big.Int `json:"gas_fee_cap,omitempty"`
Data []byte `json:"data"`
Logs []*types.Log `json:"logs"`
Status uint64 `json:"status"`
SequencerTimestamp time.Time `json:"sequencer_timestamp"`
L1BlockNumber uint64 `json:"l1_block_number,omitempty"`
// L2-specific fields
ArbTxType int `json:"arb_tx_type,omitempty"`
RequestId *big.Int `json:"request_id,omitempty"`
SequenceNumber uint64 `json:"sequence_number,omitempty"`
// Parsed information
ParsedDEX *arbitrum.DEXTransaction `json:"parsed_dex,omitempty"`
SwapDetails *SequencerSwapDetails `json:"swap_details,omitempty"`
MEVClassification string `json:"mev_classification,omitempty"`
EstimatedValueUSD float64 `json:"estimated_value_usd,omitempty"`
}
// SequencerSwapDetails contains detailed swap information from sequencer perspective
type SequencerSwapDetails struct {
Protocol string `json:"protocol"`
TokenIn string `json:"token_in"`
TokenOut string `json:"token_out"`
AmountIn *big.Int `json:"amount_in"`
AmountOut *big.Int `json:"amount_out"`
AmountMin *big.Int `json:"amount_min"`
Fee uint32 `json:"fee,omitempty"`
Slippage float64 `json:"slippage"`
PriceImpact float64 `json:"price_impact"`
PoolAddress string `json:"pool_address,omitempty"`
Recipient string `json:"recipient"`
Deadline uint64 `json:"deadline"`
// Sequencer-specific metrics
SequencerLatency time.Duration `json:"sequencer_latency"`
BatchPosition int `json:"batch_position"`
CompressionRatio float64 `json:"compression_ratio"`
}
// RealDataCollector fetches and processes real Arbitrum transaction data
type RealDataCollector struct {
config *SequencerConfig
logger *logger.Logger
ethClient *ethclient.Client
rpcClient *rpc.Client
l2Parser *arbitrum.ArbitrumL2Parser
oracle *oracle.PriceOracle
storage *TransactionStorage
}
// TransactionStorage manages storage and indexing of transaction data
type TransactionStorage struct {
config *SequencerConfig
logger *logger.Logger
dataDir string
indexFile string
mu sync.RWMutex
index map[string]*TransactionIndex
}
// TransactionIndex provides fast lookup for stored transactions
type TransactionIndex struct {
Hash string `json:"hash"`
BlockNumber uint64 `json:"block_number"`
Protocol string `json:"protocol"`
ValueUSD float64 `json:"value_usd"`
MEVType string `json:"mev_type"`
FilePath string `json:"file_path"`
StoredAt time.Time `json:"stored_at"`
DataSize int64 `json:"data_size"`
CompressionMeta map[string]interface{} `json:"compression_meta,omitempty"`
}
// NewArbitrumSequencerSimulator creates a new sequencer simulator
func NewArbitrumSequencerSimulator(config *SequencerConfig, logger *logger.Logger) (*ArbitrumSequencerSimulator, error) {
if config == nil {
config = DefaultSequencerConfig()
}
// Create data directory
if err := os.MkdirAll(config.DataDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create data directory: %w", err)
}
// Initialize storage
storage, err := NewTransactionStorage(config, logger)
if err != nil {
return nil, fmt.Errorf("failed to initialize storage: %w", err)
}
// Initialize real data collector
collector, err := NewRealDataCollector(config, logger, storage)
if err != nil {
return nil, fmt.Errorf("failed to initialize data collector: %w", err)
}
// Initialize mock sequencer service
mockService := NewMockSequencerService(config, logger, storage)
// Initialize parser validator
validator := NewParserValidator(config, logger, storage)
// Initialize performance benchmark
benchmark := NewPerformanceBenchmark(config, logger)
return &ArbitrumSequencerSimulator{
config: config,
logger: logger,
realDataCollector: collector,
mockService: mockService,
validator: validator,
storage: storage,
benchmark: benchmark,
}, nil
}
// DefaultSequencerConfig returns default configuration
func DefaultSequencerConfig() *SequencerConfig {
return &SequencerConfig{
RPCEndpoint: "https://arb1.arbitrum.io/rpc",
WSEndpoint: "wss://arb1.arbitrum.io/ws",
DataDir: "./test_data/sequencer_simulation",
StartBlock: 0, // Will be set to recent block
EndBlock: 0, // Will be set to latest
MaxBlocksPerBatch: 10,
MinSwapValueUSD: 1000.0, // Only collect swaps > $1k
TargetProtocols: []string{"UniswapV2", "UniswapV3", "SushiSwap", "Camelot", "TraderJoe", "1Inch"},
SequencerTiming: 250 * time.Millisecond, // ~4 blocks per second
BatchSize: 100,
CompressionLevel: 6,
MaxConcurrentOps: 10,
TestDuration: 5 * time.Minute,
ValidateResults: true,
StrictMode: false,
ExpectedAccuracy: 0.95, // 95% accuracy required
}
}
// NewRealDataCollector creates a new real data collector
func NewRealDataCollector(config *SequencerConfig, logger *logger.Logger, storage *TransactionStorage) (*RealDataCollector, error) {
// Connect to Arbitrum
ethClient, err := ethclient.Dial(config.RPCEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to connect to Arbitrum: %w", err)
}
rpcClient, err := rpc.Dial(config.RPCEndpoint)
if err != nil {
ethClient.Close()
return nil, fmt.Errorf("failed to connect to Arbitrum RPC: %w", err)
}
// Create price oracle
oracle := oracle.NewPriceOracle(ethClient, logger)
// Create L2 parser
l2Parser, err := arbitrum.NewArbitrumL2Parser(config.RPCEndpoint, logger, oracle)
if err != nil {
ethClient.Close()
rpcClient.Close()
return nil, fmt.Errorf("failed to create L2 parser: %w", err)
}
return &RealDataCollector{
config: config,
logger: logger,
ethClient: ethClient,
rpcClient: rpcClient,
l2Parser: l2Parser,
oracle: oracle,
storage: storage,
}, nil
}
// CollectRealData fetches real transaction data from Arbitrum
func (rdc *RealDataCollector) CollectRealData(ctx context.Context) error {
rdc.logger.Info("Starting real data collection from Arbitrum...")
// Get latest block if end block is not set
if rdc.config.EndBlock == 0 {
header, err := rdc.ethClient.HeaderByNumber(ctx, nil)
if err != nil {
return fmt.Errorf("failed to get latest block: %w", err)
}
rdc.config.EndBlock = header.Number.Uint64()
}
// Set start block if not set (collect recent data)
if rdc.config.StartBlock == 0 {
rdc.config.StartBlock = rdc.config.EndBlock - 1000 // Last 1000 blocks
}
rdc.logger.Info(fmt.Sprintf("Collecting data from blocks %d to %d", rdc.config.StartBlock, rdc.config.EndBlock))
// Process blocks in batches
for blockNum := rdc.config.StartBlock; blockNum <= rdc.config.EndBlock; blockNum += uint64(rdc.config.MaxBlocksPerBatch) {
endBlock := blockNum + uint64(rdc.config.MaxBlocksPerBatch) - 1
if endBlock > rdc.config.EndBlock {
endBlock = rdc.config.EndBlock
}
if err := rdc.processBlockBatch(ctx, blockNum, endBlock); err != nil {
rdc.logger.Error(fmt.Sprintf("Failed to process block batch %d-%d: %v", blockNum, endBlock, err))
continue
}
// Check context cancellation
select {
case <-ctx.Done():
return ctx.Err()
default:
}
}
return nil
}
// processBlockBatch processes a batch of blocks
func (rdc *RealDataCollector) processBlockBatch(ctx context.Context, startBlock, endBlock uint64) error {
rdc.logger.Debug(fmt.Sprintf("Processing block batch %d-%d", startBlock, endBlock))
var collectedTxs []*RealTransactionData
for blockNum := startBlock; blockNum <= endBlock; blockNum++ {
block, err := rdc.ethClient.BlockByNumber(ctx, big.NewInt(int64(blockNum)))
if err != nil {
rdc.logger.Warn(fmt.Sprintf("Failed to get block %d: %v", blockNum, err))
continue
}
// Get block receipts for logs
receipts, err := rdc.getBlockReceipts(ctx, block.Hash())
if err != nil {
rdc.logger.Warn(fmt.Sprintf("Failed to get receipts for block %d: %v", blockNum, err))
continue
}
// Process transactions
blockTxs := rdc.processBlockTransactions(ctx, block, receipts)
collectedTxs = append(collectedTxs, blockTxs...)
rdc.logger.Debug(fmt.Sprintf("Block %d: collected %d DEX transactions", blockNum, len(blockTxs)))
}
// Store collected transactions
if len(collectedTxs) > 0 {
if err := rdc.storage.StoreBatch(collectedTxs); err != nil {
return fmt.Errorf("failed to store transaction batch: %w", err)
}
rdc.logger.Info(fmt.Sprintf("Stored %d transactions from blocks %d-%d", len(collectedTxs), startBlock, endBlock))
}
return nil
}
// processBlockTransactions processes transactions in a block
func (rdc *RealDataCollector) processBlockTransactions(ctx context.Context, block *types.Block, receipts []*types.Receipt) []*RealTransactionData {
var dexTxs []*RealTransactionData
for i, tx := range block.Transactions() {
// Skip non-DEX transactions quickly
if !rdc.isLikelyDEXTransaction(tx) {
continue
}
// Get receipt
var receipt *types.Receipt
if i < len(receipts) {
receipt = receipts[i]
} else {
var err error
receipt, err = rdc.ethClient.TransactionReceipt(ctx, tx.Hash())
if err != nil {
continue
}
}
// Skip failed transactions
if receipt.Status != 1 {
continue
}
// Parse DEX transaction
dexTx := rdc.parseRealTransaction(ctx, block, tx, receipt)
if dexTx != nil && rdc.meetsCriteria(dexTx) {
dexTxs = append(dexTxs, dexTx)
}
}
return dexTxs
}
// isLikelyDEXTransaction performs quick filtering for DEX transactions
func (rdc *RealDataCollector) isLikelyDEXTransaction(tx *types.Transaction) bool {
// Must have recipient
if tx.To() == nil {
return false
}
// Must have data
if len(tx.Data()) < 4 {
return false
}
// Check function signature for known DEX functions
funcSig := hex.EncodeToString(tx.Data()[:4])
dexFunctions := map[string]bool{
"38ed1739": true, // swapExactTokensForTokens
"8803dbee": true, // swapTokensForExactTokens
"7ff36ab5": true, // swapExactETHForTokens
"414bf389": true, // exactInputSingle
"c04b8d59": true, // exactInput
"db3e2198": true, // exactOutputSingle
"ac9650d8": true, // multicall
"5ae401dc": true, // multicall with deadline
"7c025200": true, // 1inch swap
"3593564c": true, // universal router execute
}
return dexFunctions[funcSig]
}
// parseRealTransaction parses a real transaction into structured data
func (rdc *RealDataCollector) parseRealTransaction(ctx context.Context, block *types.Block, tx *types.Transaction, receipt *types.Receipt) *RealTransactionData {
// Create base transaction data
realTx := &RealTransactionData{
Hash: tx.Hash(),
BlockNumber: block.NumberU64(),
BlockHash: block.Hash(),
TransactionIndex: receipt.TransactionIndex,
From: receipt.From,
To: tx.To(),
Value: tx.Value(),
GasLimit: tx.Gas(),
GasUsed: receipt.GasUsed,
GasPrice: tx.GasPrice(),
Data: tx.Data(),
Logs: receipt.Logs,
Status: receipt.Status,
SequencerTimestamp: time.Unix(int64(block.Time()), 0),
}
// Add L2-specific fields for dynamic fee transactions
if tx.Type() == types.DynamicFeeTxType {
realTx.GasTipCap = tx.GasTipCap()
realTx.GasFeeCap = tx.GasFeeCap()
}
// Parse using L2 parser to get DEX details
rawTx := convertToRawL2Transaction(tx, receipt)
if parsedDEX := rdc.l2Parser.parseDEXTransaction(rawTx); parsedDEX != nil {
realTx.ParsedDEX = parsedDEX
// Extract detailed swap information
if parsedDEX.SwapDetails != nil && parsedDEX.SwapDetails.IsValid {
realTx.SwapDetails = &SequencerSwapDetails{
Protocol: parsedDEX.Protocol,
TokenIn: parsedDEX.SwapDetails.TokenIn,
TokenOut: parsedDEX.SwapDetails.TokenOut,
AmountIn: parsedDEX.SwapDetails.AmountIn,
AmountOut: parsedDEX.SwapDetails.AmountOut,
AmountMin: parsedDEX.SwapDetails.AmountMin,
Fee: parsedDEX.SwapDetails.Fee,
Recipient: parsedDEX.SwapDetails.Recipient,
Deadline: parsedDEX.SwapDetails.Deadline,
BatchPosition: int(receipt.TransactionIndex),
}
// Calculate price impact and slippage if possible
if realTx.SwapDetails.AmountIn != nil && realTx.SwapDetails.AmountOut != nil &&
realTx.SwapDetails.AmountIn.Sign() > 0 && realTx.SwapDetails.AmountOut.Sign() > 0 {
// Simplified slippage calculation
if realTx.SwapDetails.AmountMin != nil && realTx.SwapDetails.AmountMin.Sign() > 0 {
minFloat := new(big.Float).SetInt(realTx.SwapDetails.AmountMin)
outFloat := new(big.Float).SetInt(realTx.SwapDetails.AmountOut)
if minFloat.Sign() > 0 {
slippage := new(big.Float).Quo(
new(big.Float).Sub(outFloat, minFloat),
outFloat,
)
slippageFloat, _ := slippage.Float64()
realTx.SwapDetails.Slippage = slippageFloat * 100 // Convert to percentage
}
}
}
}
// Classify MEV type based on transaction characteristics
realTx.MEVClassification = rdc.classifyMEVTransaction(realTx)
// Estimate USD value (simplified)
realTx.EstimatedValueUSD = rdc.estimateTransactionValueUSD(realTx)
}
return realTx
}
// convertToRawL2Transaction converts standard transaction to raw L2 format
func convertToRawL2Transaction(tx *types.Transaction, receipt *types.Receipt) arbitrum.RawL2Transaction {
var to string
if tx.To() != nil {
to = tx.To().Hex()
}
return arbitrum.RawL2Transaction{
Hash: tx.Hash().Hex(),
From: receipt.From.Hex(),
To: to,
Value: fmt.Sprintf("0x%x", tx.Value()),
Gas: fmt.Sprintf("0x%x", tx.Gas()),
GasPrice: fmt.Sprintf("0x%x", tx.GasPrice()),
Input: hex.EncodeToString(tx.Data()),
Nonce: fmt.Sprintf("0x%x", tx.Nonce()),
TransactionIndex: fmt.Sprintf("0x%x", receipt.TransactionIndex),
Type: fmt.Sprintf("0x%x", tx.Type()),
}
}
// getBlockReceipts fetches all receipts for a block
func (rdc *RealDataCollector) getBlockReceipts(ctx context.Context, blockHash common.Hash) ([]*types.Receipt, error) {
var receipts []*types.Receipt
err := rdc.rpcClient.CallContext(ctx, &receipts, "eth_getBlockReceipts", blockHash.Hex())
return receipts, err
}
// meetsCriteria checks if transaction meets collection criteria
func (rdc *RealDataCollector) meetsCriteria(tx *RealTransactionData) bool {
// Must have valid DEX parsing
if tx.ParsedDEX == nil {
return false
}
// Check protocol filter
if len(rdc.config.TargetProtocols) > 0 {
found := false
for _, protocol := range rdc.config.TargetProtocols {
if strings.EqualFold(tx.ParsedDEX.Protocol, protocol) {
found = true
break
}
}
if !found {
return false
}
}
// Check minimum value
if rdc.config.MinSwapValueUSD > 0 && tx.EstimatedValueUSD < rdc.config.MinSwapValueUSD {
return false
}
return true
}
// classifyMEVTransaction classifies the MEV type of a transaction
func (rdc *RealDataCollector) classifyMEVTransaction(tx *RealTransactionData) string {
if tx.ParsedDEX == nil {
return "unknown"
}
// Analyze transaction characteristics
funcName := strings.ToLower(tx.ParsedDEX.FunctionName)
// Arbitrage indicators
if strings.Contains(funcName, "multicall") {
return "potential_arbitrage"
}
// Large swap indicators
if tx.EstimatedValueUSD > 100000 { // > $100k
return "large_swap"
}
// Sandwich attack indicators (would need more context analysis)
if tx.SwapDetails != nil && tx.SwapDetails.Slippage > 5.0 { // > 5% slippage
return "high_slippage"
}
// Regular swap
return "regular_swap"
}
// estimateTransactionValueUSD estimates the USD value of a transaction
func (rdc *RealDataCollector) estimateTransactionValueUSD(tx *RealTransactionData) float64 {
if tx.SwapDetails == nil || tx.SwapDetails.AmountIn == nil {
return 0.0
}
// Simplified estimation - in practice, use price oracle
// Convert to ETH equivalent (assuming 18 decimals)
amountFloat := new(big.Float).Quo(
new(big.Float).SetInt(tx.SwapDetails.AmountIn),
big.NewFloat(1e18),
)
amountEth, _ := amountFloat.Float64()
// Rough ETH price estimation (would use real oracle in production)
ethPriceUSD := 2000.0
return amountEth * ethPriceUSD
}
// Close closes the data collector connections
func (rdc *RealDataCollector) Close() {
if rdc.ethClient != nil {
rdc.ethClient.Close()
}
if rdc.rpcClient != nil {
rdc.rpcClient.Close()
}
if rdc.l2Parser != nil {
rdc.l2Parser.Close()
}
}