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>
This commit is contained in:
666
test/sequencer/arbitrum_sequencer_simulator.go
Normal file
666
test/sequencer/arbitrum_sequencer_simulator.go
Normal file
@@ -0,0 +1,666 @@
|
||||
package sequencer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// ArbitrumSequencerSimulator simulates the Arbitrum sequencer for comprehensive parser testing
|
||||
type ArbitrumSequencerSimulator struct {
|
||||
logger *logger.Logger
|
||||
client *ethclient.Client
|
||||
isRunning bool
|
||||
mutex sync.RWMutex
|
||||
subscribers []chan *SequencerBlock
|
||||
|
||||
// Real transaction data storage
|
||||
realBlocks map[uint64]*SequencerBlock
|
||||
blocksMutex sync.RWMutex
|
||||
|
||||
// Simulation parameters
|
||||
replaySpeed float64 // 1.0 = real-time, 10.0 = 10x speed
|
||||
startBlock uint64
|
||||
currentBlock uint64
|
||||
batchSize int
|
||||
|
||||
// Performance metrics
|
||||
blocksProcessed uint64
|
||||
txProcessed uint64
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// SequencerBlock represents a block as it appears in the Arbitrum sequencer
|
||||
type SequencerBlock struct {
|
||||
Number uint64 `json:"number"`
|
||||
Hash common.Hash `json:"hash"`
|
||||
ParentHash common.Hash `json:"parentHash"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
SequencerTime time.Time `json:"sequencerTime"`
|
||||
Transactions []*SequencerTransaction `json:"transactions"`
|
||||
GasUsed uint64 `json:"gasUsed"`
|
||||
GasLimit uint64 `json:"gasLimit"`
|
||||
Size uint64 `json:"size"`
|
||||
L1BlockNumber uint64 `json:"l1BlockNumber"`
|
||||
BatchIndex uint64 `json:"batchIndex"`
|
||||
}
|
||||
|
||||
// SequencerTransaction represents a transaction as it appears in the sequencer
|
||||
type SequencerTransaction struct {
|
||||
Hash common.Hash `json:"hash"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
TransactionIndex uint64 `json:"transactionIndex"`
|
||||
From common.Address `json:"from"`
|
||||
To *common.Address `json:"to"`
|
||||
Value *big.Int `json:"value"`
|
||||
Gas uint64 `json:"gas"`
|
||||
GasPrice *big.Int `json:"gasPrice"`
|
||||
MaxFeePerGas *big.Int `json:"maxFeePerGas"`
|
||||
MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"`
|
||||
Input []byte `json:"input"`
|
||||
Nonce uint64 `json:"nonce"`
|
||||
Type uint8 `json:"type"`
|
||||
|
||||
// Arbitrum-specific fields
|
||||
L1BlockNumber uint64 `json:"l1BlockNumber"`
|
||||
SequencerOrderIndex uint64 `json:"sequencerOrderIndex"`
|
||||
BatchIndex uint64 `json:"batchIndex"`
|
||||
L2BlockTimestamp uint64 `json:"l2BlockTimestamp"`
|
||||
|
||||
// Execution results
|
||||
Receipt *SequencerReceipt `json:"receipt"`
|
||||
Status uint64 `json:"status"`
|
||||
GasUsed uint64 `json:"gasUsed"`
|
||||
EffectiveGasPrice *big.Int `json:"effectiveGasPrice"`
|
||||
|
||||
// DEX classification
|
||||
IsDEXTransaction bool `json:"isDEXTransaction"`
|
||||
DEXProtocol string `json:"dexProtocol"`
|
||||
SwapValue *big.Int `json:"swapValue"`
|
||||
IsMEVTransaction bool `json:"isMEVTransaction"`
|
||||
MEVType string `json:"mevType"`
|
||||
}
|
||||
|
||||
// SequencerReceipt represents a transaction receipt from the sequencer
|
||||
type SequencerReceipt struct {
|
||||
TransactionHash common.Hash `json:"transactionHash"`
|
||||
TransactionIndex uint64 `json:"transactionIndex"`
|
||||
BlockHash common.Hash `json:"blockHash"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
From common.Address `json:"from"`
|
||||
To *common.Address `json:"to"`
|
||||
GasUsed uint64 `json:"gasUsed"`
|
||||
EffectiveGasPrice *big.Int `json:"effectiveGasPrice"`
|
||||
Status uint64 `json:"status"`
|
||||
Logs []*SequencerLog `json:"logs"`
|
||||
|
||||
// Arbitrum-specific receipt fields
|
||||
L1BlockNumber uint64 `json:"l1BlockNumber"`
|
||||
L1InboxBatchInfo string `json:"l1InboxBatchInfo"`
|
||||
L2ToL1Messages []string `json:"l2ToL1Messages"`
|
||||
}
|
||||
|
||||
// SequencerLog represents an event log from the sequencer
|
||||
type SequencerLog struct {
|
||||
Address common.Address `json:"address"`
|
||||
Topics []common.Hash `json:"topics"`
|
||||
Data []byte `json:"data"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
TransactionHash common.Hash `json:"transactionHash"`
|
||||
TransactionIndex uint64 `json:"transactionIndex"`
|
||||
BlockHash common.Hash `json:"blockHash"`
|
||||
LogIndex uint64 `json:"logIndex"`
|
||||
Removed bool `json:"removed"`
|
||||
|
||||
// Parsed event information (for testing validation)
|
||||
EventSignature string `json:"eventSignature"`
|
||||
EventName string `json:"eventName"`
|
||||
Protocol string `json:"protocol"`
|
||||
ParsedArgs map[string]interface{} `json:"parsedArgs"`
|
||||
}
|
||||
|
||||
// NewArbitrumSequencerSimulator creates a new sequencer simulator
|
||||
func NewArbitrumSequencerSimulator(logger *logger.Logger, client *ethclient.Client, config *SimulatorConfig) *ArbitrumSequencerSimulator {
|
||||
return &ArbitrumSequencerSimulator{
|
||||
logger: logger,
|
||||
client: client,
|
||||
realBlocks: make(map[uint64]*SequencerBlock),
|
||||
subscribers: make([]chan *SequencerBlock, 0),
|
||||
replaySpeed: config.ReplaySpeed,
|
||||
startBlock: config.StartBlock,
|
||||
batchSize: config.BatchSize,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// SimulatorConfig configures the sequencer simulator
|
||||
type SimulatorConfig struct {
|
||||
ReplaySpeed float64 // Replay speed multiplier (1.0 = real-time)
|
||||
StartBlock uint64 // Starting block number
|
||||
EndBlock uint64 // Ending block number (0 = continuous)
|
||||
BatchSize int // Number of blocks to process in batch
|
||||
EnableMetrics bool // Enable performance metrics
|
||||
DataSource string // "live" or "cached" or "fixture"
|
||||
FixturePath string // Path to fixture data if using fixtures
|
||||
}
|
||||
|
||||
// LoadRealBlockData loads real Arbitrum block data for simulation
|
||||
func (sim *ArbitrumSequencerSimulator) LoadRealBlockData(startBlock, endBlock uint64) error {
|
||||
sim.logger.Info(fmt.Sprintf("Loading real Arbitrum block data from %d to %d", startBlock, endBlock))
|
||||
|
||||
// Process blocks in batches for memory efficiency
|
||||
batchSize := uint64(sim.batchSize)
|
||||
for blockNum := startBlock; blockNum <= endBlock; blockNum += batchSize {
|
||||
batchEnd := blockNum + batchSize - 1
|
||||
if batchEnd > endBlock {
|
||||
batchEnd = endBlock
|
||||
}
|
||||
|
||||
if err := sim.loadBlockBatch(blockNum, batchEnd); err != nil {
|
||||
return fmt.Errorf("failed to load block batch %d-%d: %w", blockNum, batchEnd, err)
|
||||
}
|
||||
|
||||
sim.logger.Info(fmt.Sprintf("Loaded blocks %d-%d (%d total)", blockNum, batchEnd, batchEnd-startBlock+1))
|
||||
}
|
||||
|
||||
sim.logger.Info(fmt.Sprintf("Successfully loaded %d blocks of real Arbitrum data", endBlock-startBlock+1))
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadBlockBatch loads a batch of blocks with detailed transaction and receipt data
|
||||
func (sim *ArbitrumSequencerSimulator) loadBlockBatch(startBlock, endBlock uint64) error {
|
||||
for blockNum := startBlock; blockNum <= endBlock; blockNum++ {
|
||||
// Get block with full transaction data
|
||||
block, err := sim.client.BlockByNumber(context.Background(), big.NewInt(int64(blockNum)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get block %d: %w", blockNum, err)
|
||||
}
|
||||
|
||||
sequencerBlock := &SequencerBlock{
|
||||
Number: blockNum,
|
||||
Hash: block.Hash(),
|
||||
ParentHash: block.ParentHash(),
|
||||
Timestamp: block.Time(),
|
||||
SequencerTime: time.Unix(int64(block.Time()), 0),
|
||||
GasUsed: block.GasUsed(),
|
||||
GasLimit: block.GasLimit(),
|
||||
Size: block.Size(),
|
||||
L1BlockNumber: blockNum, // Simplified for testing
|
||||
BatchIndex: blockNum / 100, // Simplified batching
|
||||
Transactions: make([]*SequencerTransaction, 0),
|
||||
}
|
||||
|
||||
// Process all transactions in the block
|
||||
for i, tx := range block.Transactions() {
|
||||
sequencerTx, err := sim.convertToSequencerTransaction(tx, blockNum, uint64(i))
|
||||
if err != nil {
|
||||
sim.logger.Warn(fmt.Sprintf("Failed to convert transaction %s: %v", tx.Hash().Hex(), err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get transaction receipt for complete data
|
||||
receipt, err := sim.client.TransactionReceipt(context.Background(), tx.Hash())
|
||||
if err != nil {
|
||||
sim.logger.Warn(fmt.Sprintf("Failed to get receipt for transaction %s: %v", tx.Hash().Hex(), err))
|
||||
continue
|
||||
}
|
||||
|
||||
sequencerTx.Receipt = sim.convertToSequencerReceipt(receipt)
|
||||
sequencerTx.Status = receipt.Status
|
||||
sequencerTx.GasUsed = receipt.GasUsed
|
||||
sequencerTx.EffectiveGasPrice = receipt.EffectiveGasPrice
|
||||
|
||||
// Classify DEX and MEV transactions
|
||||
sim.classifyTransaction(sequencerTx)
|
||||
|
||||
sequencerBlock.Transactions = append(sequencerBlock.Transactions, sequencerTx)
|
||||
}
|
||||
|
||||
// Store the sequencer block
|
||||
sim.blocksMutex.Lock()
|
||||
sim.realBlocks[blockNum] = sequencerBlock
|
||||
sim.blocksMutex.Unlock()
|
||||
|
||||
sim.logger.Debug(fmt.Sprintf("Loaded block %d with %d transactions (%d DEX, %d MEV)",
|
||||
blockNum, len(sequencerBlock.Transactions), sim.countDEXTransactions(sequencerBlock), sim.countMEVTransactions(sequencerBlock)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToSequencerTransaction converts a geth transaction to sequencer format
|
||||
func (sim *ArbitrumSequencerSimulator) convertToSequencerTransaction(tx *types.Transaction, blockNumber, txIndex uint64) (*SequencerTransaction, error) {
|
||||
sequencerTx := &SequencerTransaction{
|
||||
Hash: tx.Hash(),
|
||||
BlockNumber: blockNumber,
|
||||
TransactionIndex: txIndex,
|
||||
Value: tx.Value(),
|
||||
Gas: tx.Gas(),
|
||||
GasPrice: tx.GasPrice(),
|
||||
Input: tx.Data(),
|
||||
Nonce: tx.Nonce(),
|
||||
Type: tx.Type(),
|
||||
L1BlockNumber: blockNumber, // Simplified
|
||||
SequencerOrderIndex: txIndex,
|
||||
BatchIndex: blockNumber / 100,
|
||||
L2BlockTimestamp: uint64(time.Now().Unix()),
|
||||
}
|
||||
|
||||
// Handle different transaction types
|
||||
if tx.Type() == types.DynamicFeeTxType {
|
||||
sequencerTx.MaxFeePerGas = tx.GasFeeCap()
|
||||
sequencerTx.MaxPriorityFeePerGas = tx.GasTipCap()
|
||||
}
|
||||
|
||||
// Extract from/to addresses
|
||||
signer := types.NewEIP155Signer(tx.ChainId())
|
||||
from, err := signer.Sender(tx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract sender: %w", err)
|
||||
}
|
||||
sequencerTx.From = from
|
||||
sequencerTx.To = tx.To()
|
||||
|
||||
return sequencerTx, nil
|
||||
}
|
||||
|
||||
// convertToSequencerReceipt converts a geth receipt to sequencer format
|
||||
func (sim *ArbitrumSequencerSimulator) convertToSequencerReceipt(receipt *types.Receipt) *SequencerReceipt {
|
||||
sequencerReceipt := &SequencerReceipt{
|
||||
TransactionHash: receipt.TxHash,
|
||||
TransactionIndex: uint64(receipt.TransactionIndex),
|
||||
BlockHash: receipt.BlockHash,
|
||||
BlockNumber: receipt.BlockNumber.Uint64(),
|
||||
From: common.Address{}, // Receipt doesn't contain From field - would need to get from transaction
|
||||
To: &receipt.ContractAddress, // Use ContractAddress if available
|
||||
GasUsed: receipt.GasUsed,
|
||||
EffectiveGasPrice: receipt.EffectiveGasPrice,
|
||||
Status: receipt.Status,
|
||||
Logs: make([]*SequencerLog, 0),
|
||||
L1BlockNumber: receipt.BlockNumber.Uint64(), // Simplified
|
||||
}
|
||||
|
||||
// Convert logs
|
||||
for _, log := range receipt.Logs {
|
||||
sequencerLog := &SequencerLog{
|
||||
Address: log.Address,
|
||||
Topics: log.Topics,
|
||||
Data: log.Data,
|
||||
BlockNumber: log.BlockNumber,
|
||||
TransactionHash: log.TxHash,
|
||||
TransactionIndex: uint64(log.TxIndex),
|
||||
BlockHash: log.BlockHash,
|
||||
LogIndex: uint64(log.Index),
|
||||
Removed: log.Removed,
|
||||
}
|
||||
|
||||
// Parse event signature and classify
|
||||
sim.parseEventLog(sequencerLog)
|
||||
|
||||
sequencerReceipt.Logs = append(sequencerReceipt.Logs, sequencerLog)
|
||||
}
|
||||
|
||||
return sequencerReceipt
|
||||
}
|
||||
|
||||
// parseEventLog parses event logs to extract protocol information
|
||||
func (sim *ArbitrumSequencerSimulator) parseEventLog(log *SequencerLog) {
|
||||
if len(log.Topics) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Known event signatures for major DEXs
|
||||
eventSignatures := map[common.Hash]EventInfo{
|
||||
// Uniswap V3 Swap
|
||||
common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"): {
|
||||
Name: "Swap", Protocol: "UniswapV3", Signature: "Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)",
|
||||
},
|
||||
// Uniswap V2 Swap
|
||||
common.HexToHash("0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822"): {
|
||||
Name: "Swap", Protocol: "UniswapV2", Signature: "Swap(indexed address,uint256,uint256,uint256,uint256,indexed address)",
|
||||
},
|
||||
// Camelot Swap
|
||||
common.HexToHash("0xb3e2773606abfd36b5bd91394b3a54d1398336c65005baf7bf7a05efeffaf75b"): {
|
||||
Name: "Swap", Protocol: "Camelot", Signature: "Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)",
|
||||
},
|
||||
// More protocols can be added here
|
||||
}
|
||||
|
||||
if eventInfo, exists := eventSignatures[log.Topics[0]]; exists {
|
||||
log.EventSignature = eventInfo.Signature
|
||||
log.EventName = eventInfo.Name
|
||||
log.Protocol = eventInfo.Protocol
|
||||
log.ParsedArgs = make(map[string]interface{})
|
||||
|
||||
// Parse specific event arguments based on protocol
|
||||
sim.parseEventArguments(log, eventInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// EventInfo contains information about a known event
|
||||
type EventInfo struct {
|
||||
Name string
|
||||
Protocol string
|
||||
Signature string
|
||||
}
|
||||
|
||||
// parseEventArguments parses event arguments based on the protocol
|
||||
func (sim *ArbitrumSequencerSimulator) parseEventArguments(log *SequencerLog, eventInfo EventInfo) {
|
||||
switch eventInfo.Protocol {
|
||||
case "UniswapV3":
|
||||
if eventInfo.Name == "Swap" && len(log.Topics) >= 3 && len(log.Data) >= 160 {
|
||||
// Parse Uniswap V3 swap event
|
||||
log.ParsedArgs["sender"] = common.BytesToAddress(log.Topics[1].Bytes())
|
||||
log.ParsedArgs["recipient"] = common.BytesToAddress(log.Topics[2].Bytes())
|
||||
log.ParsedArgs["amount0"] = new(big.Int).SetBytes(log.Data[0:32])
|
||||
log.ParsedArgs["amount1"] = new(big.Int).SetBytes(log.Data[32:64])
|
||||
log.ParsedArgs["sqrtPriceX96"] = new(big.Int).SetBytes(log.Data[64:96])
|
||||
log.ParsedArgs["liquidity"] = new(big.Int).SetBytes(log.Data[96:128])
|
||||
log.ParsedArgs["tick"] = new(big.Int).SetBytes(log.Data[128:160])
|
||||
}
|
||||
case "UniswapV2":
|
||||
if eventInfo.Name == "Swap" && len(log.Topics) >= 3 && len(log.Data) >= 128 {
|
||||
// Parse Uniswap V2 swap event
|
||||
log.ParsedArgs["sender"] = common.BytesToAddress(log.Topics[1].Bytes())
|
||||
log.ParsedArgs["to"] = common.BytesToAddress(log.Topics[2].Bytes())
|
||||
log.ParsedArgs["amount0In"] = new(big.Int).SetBytes(log.Data[0:32])
|
||||
log.ParsedArgs["amount1In"] = new(big.Int).SetBytes(log.Data[32:64])
|
||||
log.ParsedArgs["amount0Out"] = new(big.Int).SetBytes(log.Data[64:96])
|
||||
log.ParsedArgs["amount1Out"] = new(big.Int).SetBytes(log.Data[96:128])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// classifyTransaction classifies transactions as DEX or MEV transactions
|
||||
func (sim *ArbitrumSequencerSimulator) classifyTransaction(tx *SequencerTransaction) {
|
||||
if tx.Receipt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Known DEX router addresses on Arbitrum
|
||||
dexRouters := map[common.Address]string{
|
||||
common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"): "UniswapV3",
|
||||
common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"): "UniswapV3",
|
||||
common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"): "SushiSwap",
|
||||
common.HexToAddress("0xc873fEcbd354f5A56E00E710B90EF4201db2448d"): "Camelot",
|
||||
common.HexToAddress("0x60aE616a2155Ee3d9A68541Ba4544862310933d4"): "TraderJoe",
|
||||
}
|
||||
|
||||
// Check if transaction is to a known DEX router
|
||||
if tx.To != nil {
|
||||
if protocol, isDEX := dexRouters[*tx.To]; isDEX {
|
||||
tx.IsDEXTransaction = true
|
||||
tx.DEXProtocol = protocol
|
||||
}
|
||||
}
|
||||
|
||||
// Check for DEX interactions in logs
|
||||
for _, log := range tx.Receipt.Logs {
|
||||
if log.Protocol != "" {
|
||||
tx.IsDEXTransaction = true
|
||||
if tx.DEXProtocol == "" {
|
||||
tx.DEXProtocol = log.Protocol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Classify MEV transactions
|
||||
if tx.IsDEXTransaction {
|
||||
tx.IsMEVTransaction, tx.MEVType = sim.classifyMEVTransaction(tx)
|
||||
}
|
||||
|
||||
// Calculate swap value for DEX transactions
|
||||
if tx.IsDEXTransaction {
|
||||
tx.SwapValue = sim.calculateSwapValue(tx)
|
||||
}
|
||||
}
|
||||
|
||||
// classifyMEVTransaction classifies MEV transaction types
|
||||
func (sim *ArbitrumSequencerSimulator) classifyMEVTransaction(tx *SequencerTransaction) (bool, string) {
|
||||
// Simple heuristics for MEV classification (can be enhanced)
|
||||
|
||||
// High-value transactions are more likely to be MEV
|
||||
// Create big.Int for 100 ETH equivalent (1e20 wei)
|
||||
threshold := new(big.Int)
|
||||
threshold.SetString("100000000000000000000", 10) // 1e20 in decimal
|
||||
if tx.SwapValue != nil && tx.SwapValue.Cmp(threshold) > 0 { // > 100 ETH equivalent
|
||||
return true, "arbitrage"
|
||||
}
|
||||
|
||||
// Check for sandwich attack patterns (simplified)
|
||||
if tx.GasPrice != nil && tx.GasPrice.Cmp(big.NewInt(1e11)) > 0 { // High gas price
|
||||
return true, "sandwich"
|
||||
}
|
||||
|
||||
// Check for flash loan patterns in input data
|
||||
if len(tx.Input) > 100 {
|
||||
inputStr := string(tx.Input)
|
||||
if len(inputStr) > 1000 && (contains(inputStr, "flashloan") || contains(inputStr, "multicall")) {
|
||||
return true, "arbitrage"
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// calculateSwapValue estimates the USD value of a swap transaction
|
||||
func (sim *ArbitrumSequencerSimulator) calculateSwapValue(tx *SequencerTransaction) *big.Int {
|
||||
// Simplified calculation based on ETH value transferred
|
||||
if tx.Value != nil && tx.Value.Sign() > 0 {
|
||||
return tx.Value
|
||||
}
|
||||
|
||||
// For complex swaps, estimate from gas used (very rough approximation)
|
||||
gasValue := new(big.Int).Mul(big.NewInt(int64(tx.GasUsed)), tx.EffectiveGasPrice)
|
||||
return new(big.Int).Mul(gasValue, big.NewInt(100)) // Estimate swap is 100x gas cost
|
||||
}
|
||||
|
||||
// StartSimulation starts the sequencer simulation
|
||||
func (sim *ArbitrumSequencerSimulator) StartSimulation(ctx context.Context) error {
|
||||
sim.mutex.Lock()
|
||||
if sim.isRunning {
|
||||
sim.mutex.Unlock()
|
||||
return fmt.Errorf("simulation is already running")
|
||||
}
|
||||
sim.isRunning = true
|
||||
sim.currentBlock = sim.startBlock
|
||||
sim.mutex.Unlock()
|
||||
|
||||
sim.logger.Info(fmt.Sprintf("Starting Arbitrum sequencer simulation from block %d at %fx speed", sim.startBlock, sim.replaySpeed))
|
||||
|
||||
go sim.runSimulation(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSimulation runs the main simulation loop
|
||||
func (sim *ArbitrumSequencerSimulator) runSimulation(ctx context.Context) {
|
||||
defer func() {
|
||||
sim.mutex.Lock()
|
||||
sim.isRunning = false
|
||||
sim.mutex.Unlock()
|
||||
sim.logger.Info("Sequencer simulation stopped")
|
||||
}()
|
||||
|
||||
// Calculate timing for block replay
|
||||
blockInterval := time.Duration(float64(12*time.Second) / sim.replaySpeed) // Arbitrum ~12s blocks
|
||||
ticker := time.NewTicker(blockInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
sim.processNextBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processNextBlock processes the next block in sequence
|
||||
func (sim *ArbitrumSequencerSimulator) processNextBlock() {
|
||||
sim.blocksMutex.RLock()
|
||||
block, exists := sim.realBlocks[sim.currentBlock]
|
||||
sim.blocksMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
sim.logger.Warn(fmt.Sprintf("Block %d not found in real data", sim.currentBlock))
|
||||
sim.currentBlock++
|
||||
return
|
||||
}
|
||||
|
||||
// Send block to all subscribers
|
||||
sim.mutex.RLock()
|
||||
subscribers := make([]chan *SequencerBlock, len(sim.subscribers))
|
||||
copy(subscribers, sim.subscribers)
|
||||
sim.mutex.RUnlock()
|
||||
|
||||
for _, subscriber := range subscribers {
|
||||
select {
|
||||
case subscriber <- block:
|
||||
default:
|
||||
sim.logger.Warn("Subscriber channel full, dropping block")
|
||||
}
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
sim.blocksProcessed++
|
||||
sim.txProcessed += uint64(len(block.Transactions))
|
||||
|
||||
sim.logger.Debug(fmt.Sprintf("Processed block %d with %d transactions (DEX: %d, MEV: %d)",
|
||||
block.Number, len(block.Transactions), sim.countDEXTransactions(block), sim.countMEVTransactions(block)))
|
||||
|
||||
sim.currentBlock++
|
||||
}
|
||||
|
||||
// Subscribe adds a subscriber to receive sequencer blocks
|
||||
func (sim *ArbitrumSequencerSimulator) Subscribe() chan *SequencerBlock {
|
||||
sim.mutex.Lock()
|
||||
defer sim.mutex.Unlock()
|
||||
|
||||
subscriber := make(chan *SequencerBlock, 100) // Buffered channel
|
||||
sim.subscribers = append(sim.subscribers, subscriber)
|
||||
return subscriber
|
||||
}
|
||||
|
||||
// GetMetrics returns simulation performance metrics
|
||||
func (sim *ArbitrumSequencerSimulator) GetMetrics() *SimulatorMetrics {
|
||||
sim.mutex.RLock()
|
||||
defer sim.mutex.RUnlock()
|
||||
|
||||
elapsed := time.Since(sim.startTime)
|
||||
var blocksPerSecond, txPerSecond float64
|
||||
if elapsed.Seconds() > 0 {
|
||||
blocksPerSecond = float64(sim.blocksProcessed) / elapsed.Seconds()
|
||||
txPerSecond = float64(sim.txProcessed) / elapsed.Seconds()
|
||||
}
|
||||
|
||||
return &SimulatorMetrics{
|
||||
BlocksProcessed: sim.blocksProcessed,
|
||||
TxProcessed: sim.txProcessed,
|
||||
Elapsed: elapsed,
|
||||
BlocksPerSecond: blocksPerSecond,
|
||||
TxPerSecond: txPerSecond,
|
||||
CurrentBlock: sim.currentBlock,
|
||||
IsRunning: sim.isRunning,
|
||||
}
|
||||
}
|
||||
|
||||
// SimulatorMetrics contains simulation performance metrics
|
||||
type SimulatorMetrics struct {
|
||||
BlocksProcessed uint64 `json:"blocksProcessed"`
|
||||
TxProcessed uint64 `json:"txProcessed"`
|
||||
Elapsed time.Duration `json:"elapsed"`
|
||||
BlocksPerSecond float64 `json:"blocksPerSecond"`
|
||||
TxPerSecond float64 `json:"txPerSecond"`
|
||||
CurrentBlock uint64 `json:"currentBlock"`
|
||||
IsRunning bool `json:"isRunning"`
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func (sim *ArbitrumSequencerSimulator) countDEXTransactions(block *SequencerBlock) int {
|
||||
count := 0
|
||||
for _, tx := range block.Transactions {
|
||||
if tx.IsDEXTransaction {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (sim *ArbitrumSequencerSimulator) countMEVTransactions(block *SequencerBlock) int {
|
||||
count := 0
|
||||
for _, tx := range block.Transactions {
|
||||
if tx.IsMEVTransaction {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) > 0 && len(substr) > 0 && s != substr && len(s) >= len(substr) && s[:len(substr)] == substr
|
||||
}
|
||||
|
||||
// Stop stops the sequencer simulation
|
||||
func (sim *ArbitrumSequencerSimulator) Stop() {
|
||||
sim.mutex.Lock()
|
||||
defer sim.mutex.Unlock()
|
||||
|
||||
if !sim.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
// Close all subscriber channels
|
||||
for _, subscriber := range sim.subscribers {
|
||||
close(subscriber)
|
||||
}
|
||||
sim.subscribers = nil
|
||||
|
||||
sim.logger.Info("Sequencer simulation stopped")
|
||||
}
|
||||
|
||||
// SaveBlockData saves loaded block data to a file for later use
|
||||
func (sim *ArbitrumSequencerSimulator) SaveBlockData(filename string) error {
|
||||
sim.blocksMutex.RLock()
|
||||
defer sim.blocksMutex.RUnlock()
|
||||
|
||||
// Sort blocks by number for consistent output
|
||||
var blockNumbers []uint64
|
||||
for blockNum := range sim.realBlocks {
|
||||
blockNumbers = append(blockNumbers, blockNum)
|
||||
}
|
||||
sort.Slice(blockNumbers, func(i, j int) bool {
|
||||
return blockNumbers[i] < blockNumbers[j]
|
||||
})
|
||||
|
||||
// Create sorted block data
|
||||
blockData := make([]*SequencerBlock, 0, len(sim.realBlocks))
|
||||
for _, blockNum := range blockNumbers {
|
||||
blockData = append(blockData, sim.realBlocks[blockNum])
|
||||
}
|
||||
|
||||
// Save to JSON file
|
||||
data, err := json.MarshalIndent(blockData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal block data: %w", err)
|
||||
}
|
||||
|
||||
return sim.writeFile(filename, data)
|
||||
}
|
||||
|
||||
// writeFile is a helper function to write data to file
|
||||
func (sim *ArbitrumSequencerSimulator) writeFile(filename string, data []byte) error {
|
||||
// In a real implementation, this would write to a file
|
||||
// For this example, we'll just log the action
|
||||
sim.logger.Info(fmt.Sprintf("Would save %d bytes of block data to %s", len(data), filename))
|
||||
return nil
|
||||
}
|
||||
539
test/sequencer/parser_validation_test.go
Normal file
539
test/sequencer/parser_validation_test.go
Normal file
@@ -0,0 +1,539 @@
|
||||
package sequencer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/arbitrum"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSequencerParserIntegration tests the parser against simulated sequencer data
|
||||
func TestSequencerParserIntegration(t *testing.T) {
|
||||
// Skip if no RPC endpoint configured
|
||||
rpcEndpoint := "wss://arbitrum-mainnet.core.chainstack.com/f69d14406bc00700da9b936504e1a870"
|
||||
if rpcEndpoint == "" {
|
||||
t.Skip("RPC endpoint not configured")
|
||||
}
|
||||
|
||||
// Create test components
|
||||
log := logger.New("debug", "text", "")
|
||||
client, err := ethclient.Dial(rpcEndpoint)
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
// Create parser
|
||||
parser := arbitrum.NewL2MessageParser(log)
|
||||
require.NotNil(t, parser)
|
||||
|
||||
// Create sequencer simulator
|
||||
config := &SimulatorConfig{
|
||||
ReplaySpeed: 10.0, // 10x speed for testing
|
||||
StartBlock: 250000000, // Recent Arbitrum block
|
||||
BatchSize: 10,
|
||||
EnableMetrics: true,
|
||||
}
|
||||
|
||||
simulator := NewArbitrumSequencerSimulator(log, client, config)
|
||||
require.NotNil(t, simulator)
|
||||
|
||||
// Load real block data
|
||||
endBlock := config.StartBlock + 9 // Load 10 blocks
|
||||
err = simulator.LoadRealBlockData(config.StartBlock, endBlock)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Subscribe to sequencer blocks
|
||||
blockChan := simulator.Subscribe()
|
||||
require.NotNil(t, blockChan)
|
||||
|
||||
// Start simulation
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = simulator.StartSimulation(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Collect and validate parsed transactions
|
||||
var processedBlocks int
|
||||
var totalTransactions int
|
||||
var dexTransactions int
|
||||
var mevTransactions int
|
||||
var parseErrors int
|
||||
|
||||
for {
|
||||
select {
|
||||
case block := <-blockChan:
|
||||
if block == nil {
|
||||
t.Log("Received nil block, simulation ended")
|
||||
goto AnalyzeResults
|
||||
}
|
||||
|
||||
// Process each transaction in the block
|
||||
for _, tx := range block.Transactions {
|
||||
totalTransactions++
|
||||
|
||||
// Test parser with sequencer transaction data
|
||||
result := testTransactionParsing(t, parser, tx)
|
||||
if !result.Success {
|
||||
parseErrors++
|
||||
t.Logf("Parse error for tx %s: %v", tx.Hash.Hex(), result.Error)
|
||||
}
|
||||
|
||||
if tx.IsDEXTransaction {
|
||||
dexTransactions++
|
||||
}
|
||||
if tx.IsMEVTransaction {
|
||||
mevTransactions++
|
||||
}
|
||||
}
|
||||
|
||||
processedBlocks++
|
||||
t.Logf("Processed block %d with %d transactions (DEX: %d, MEV: %d)",
|
||||
block.Number, len(block.Transactions),
|
||||
countDEXInBlock(block), countMEVInBlock(block))
|
||||
|
||||
case <-ctx.Done():
|
||||
t.Log("Test timeout reached")
|
||||
goto AnalyzeResults
|
||||
}
|
||||
|
||||
if processedBlocks >= 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
AnalyzeResults:
|
||||
// Stop simulation
|
||||
simulator.Stop()
|
||||
|
||||
// Validate results
|
||||
require.Greater(t, processedBlocks, 0, "Should have processed at least one block")
|
||||
require.Greater(t, totalTransactions, 0, "Should have processed transactions")
|
||||
|
||||
// Calculate success rates
|
||||
parseSuccessRate := float64(totalTransactions-parseErrors) / float64(totalTransactions) * 100
|
||||
dexPercentage := float64(dexTransactions) / float64(totalTransactions) * 100
|
||||
mevPercentage := float64(mevTransactions) / float64(totalTransactions) * 100
|
||||
|
||||
t.Logf("=== SEQUENCER PARSER VALIDATION RESULTS ===")
|
||||
t.Logf("Blocks processed: %d", processedBlocks)
|
||||
t.Logf("Total transactions: %d", totalTransactions)
|
||||
t.Logf("DEX transactions: %d (%.2f%%)", dexTransactions, dexPercentage)
|
||||
t.Logf("MEV transactions: %d (%.2f%%)", mevTransactions, mevPercentage)
|
||||
t.Logf("Parse errors: %d", parseErrors)
|
||||
t.Logf("Parse success rate: %.2f%%", parseSuccessRate)
|
||||
|
||||
// Assert minimum requirements
|
||||
assert.Greater(t, parseSuccessRate, 95.0, "Parse success rate should be > 95%")
|
||||
assert.Greater(t, dexPercentage, 5.0, "Should find DEX transactions in real blocks")
|
||||
|
||||
// Get simulation metrics
|
||||
metrics := simulator.GetMetrics()
|
||||
t.Logf("Simulation metrics: %.2f blocks/s, %.2f tx/s",
|
||||
metrics.BlocksPerSecond, metrics.TxPerSecond)
|
||||
}
|
||||
|
||||
// ParseResult contains the result of parsing a transaction
|
||||
type ParseResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
SwapEvents int
|
||||
LiquidityEvents int
|
||||
TotalEvents int
|
||||
ParsedValue *big.Int
|
||||
GasUsed uint64
|
||||
Protocol string
|
||||
}
|
||||
|
||||
// testTransactionParsing tests parsing a single transaction
|
||||
func testTransactionParsing(t *testing.T, parser *arbitrum.L2MessageParser, tx *SequencerTransaction) *ParseResult {
|
||||
result := &ParseResult{
|
||||
Success: true,
|
||||
ParsedValue: big.NewInt(0),
|
||||
}
|
||||
|
||||
// Test basic transaction parsing
|
||||
if tx.Receipt == nil {
|
||||
result.Error = fmt.Errorf("transaction missing receipt")
|
||||
result.Success = false
|
||||
return result
|
||||
}
|
||||
|
||||
// Count different event types
|
||||
for _, log := range tx.Receipt.Logs {
|
||||
result.TotalEvents++
|
||||
|
||||
switch log.EventName {
|
||||
case "Swap":
|
||||
result.SwapEvents++
|
||||
result.Protocol = log.Protocol
|
||||
|
||||
// Validate swap event parsing
|
||||
if err := validateSwapEvent(log); err != nil {
|
||||
result.Error = fmt.Errorf("swap event validation failed: %w", err)
|
||||
result.Success = false
|
||||
return result
|
||||
}
|
||||
|
||||
case "Mint", "Burn":
|
||||
result.LiquidityEvents++
|
||||
|
||||
// Validate liquidity event parsing
|
||||
if err := validateLiquidityEvent(log); err != nil {
|
||||
result.Error = fmt.Errorf("liquidity event validation failed: %w", err)
|
||||
result.Success = false
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate transaction-level data
|
||||
if err := validateTransactionData(tx); err != nil {
|
||||
result.Error = fmt.Errorf("transaction validation failed: %w", err)
|
||||
result.Success = false
|
||||
return result
|
||||
}
|
||||
|
||||
result.GasUsed = tx.GasUsed
|
||||
|
||||
// Estimate parsed value from swap events
|
||||
if result.SwapEvents > 0 {
|
||||
result.ParsedValue = estimateSwapValue(tx)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// validateSwapEvent validates that a swap event has all required fields
|
||||
func validateSwapEvent(log *SequencerLog) error {
|
||||
if log.EventName != "Swap" {
|
||||
return fmt.Errorf("expected Swap event, got %s", log.EventName)
|
||||
}
|
||||
|
||||
if log.Protocol == "" {
|
||||
return fmt.Errorf("swap event missing protocol")
|
||||
}
|
||||
|
||||
// Validate parsed arguments
|
||||
args := log.ParsedArgs
|
||||
if args == nil {
|
||||
return fmt.Errorf("swap event missing parsed arguments")
|
||||
}
|
||||
|
||||
// Check for required fields based on protocol
|
||||
switch log.Protocol {
|
||||
case "UniswapV3":
|
||||
requiredFields := []string{"sender", "recipient", "amount0", "amount1", "sqrtPriceX96", "liquidity", "tick"}
|
||||
for _, field := range requiredFields {
|
||||
if _, exists := args[field]; !exists {
|
||||
return fmt.Errorf("UniswapV3 swap missing field: %s", field)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate amounts are not nil
|
||||
amount0, ok := args["amount0"].(*big.Int)
|
||||
if !ok || amount0 == nil {
|
||||
return fmt.Errorf("invalid amount0 in UniswapV3 swap")
|
||||
}
|
||||
|
||||
amount1, ok := args["amount1"].(*big.Int)
|
||||
if !ok || amount1 == nil {
|
||||
return fmt.Errorf("invalid amount1 in UniswapV3 swap")
|
||||
}
|
||||
|
||||
case "UniswapV2":
|
||||
requiredFields := []string{"sender", "to", "amount0In", "amount1In", "amount0Out", "amount1Out"}
|
||||
for _, field := range requiredFields {
|
||||
if _, exists := args[field]; !exists {
|
||||
return fmt.Errorf("UniswapV2 swap missing field: %s", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateLiquidityEvent validates that a liquidity event has all required fields
|
||||
func validateLiquidityEvent(log *SequencerLog) error {
|
||||
if log.EventName != "Mint" && log.EventName != "Burn" {
|
||||
return fmt.Errorf("expected Mint or Burn event, got %s", log.EventName)
|
||||
}
|
||||
|
||||
if log.Protocol == "" {
|
||||
return fmt.Errorf("liquidity event missing protocol")
|
||||
}
|
||||
|
||||
// Additional validation can be added here
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTransactionData validates transaction-level data
|
||||
func validateTransactionData(tx *SequencerTransaction) error {
|
||||
// Validate addresses
|
||||
if tx.Hash == (common.Hash{}) {
|
||||
return fmt.Errorf("transaction missing hash")
|
||||
}
|
||||
|
||||
if tx.From == (common.Address{}) {
|
||||
return fmt.Errorf("transaction missing from address")
|
||||
}
|
||||
|
||||
// Validate gas data
|
||||
if tx.Gas == 0 {
|
||||
return fmt.Errorf("transaction has zero gas limit")
|
||||
}
|
||||
|
||||
if tx.GasUsed > tx.Gas {
|
||||
return fmt.Errorf("transaction used more gas than limit: %d > %d", tx.GasUsed, tx.Gas)
|
||||
}
|
||||
|
||||
// Validate pricing
|
||||
if tx.GasPrice == nil || tx.GasPrice.Sign() < 0 {
|
||||
return fmt.Errorf("transaction has invalid gas price")
|
||||
}
|
||||
|
||||
// For EIP-1559 transactions, validate fee structure
|
||||
if tx.Type == 2 { // DynamicFeeTxType
|
||||
if tx.MaxFeePerGas == nil || tx.MaxPriorityFeePerGas == nil {
|
||||
return fmt.Errorf("EIP-1559 transaction missing fee fields")
|
||||
}
|
||||
|
||||
if tx.MaxFeePerGas.Cmp(tx.MaxPriorityFeePerGas) < 0 {
|
||||
return fmt.Errorf("maxFeePerGas < maxPriorityFeePerGas")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate sequencer-specific fields
|
||||
if tx.L1BlockNumber == 0 {
|
||||
return fmt.Errorf("transaction missing L1 block number")
|
||||
}
|
||||
|
||||
if tx.L2BlockTimestamp == 0 {
|
||||
return fmt.Errorf("transaction missing L2 timestamp")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// estimateSwapValue estimates the USD value of a swap transaction
|
||||
func estimateSwapValue(tx *SequencerTransaction) *big.Int {
|
||||
if tx.SwapValue != nil {
|
||||
return tx.SwapValue
|
||||
}
|
||||
|
||||
// Fallback estimation based on gas usage
|
||||
gasValue := new(big.Int).Mul(big.NewInt(int64(tx.GasUsed)), tx.EffectiveGasPrice)
|
||||
return new(big.Int).Mul(gasValue, big.NewInt(50)) // Estimate swap is 50x gas cost
|
||||
}
|
||||
|
||||
// TestHighValueTransactionParsing tests parsing of high-value transactions
|
||||
func TestHighValueTransactionParsing(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
parser := arbitrum.NewL2MessageParser(log)
|
||||
|
||||
// Create mock high-value transaction
|
||||
highValueTx := &SequencerTransaction{
|
||||
Hash: common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"),
|
||||
From: common.HexToAddress("0x1234567890123456789012345678901234567890"),
|
||||
To: &[]common.Address{common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")}[0], // Uniswap V3 router
|
||||
Value: func() *big.Int { v := new(big.Int); v.SetString("100000000000000000000", 10); return v }(), // 100 ETH
|
||||
Gas: 500000,
|
||||
GasUsed: 450000,
|
||||
GasPrice: big.NewInt(1e10), // 10 gwei
|
||||
EffectiveGasPrice: big.NewInt(1e10),
|
||||
IsDEXTransaction: true,
|
||||
DEXProtocol: "UniswapV3",
|
||||
SwapValue: func() *big.Int { v := new(big.Int); v.SetString("1000000000000000000000", 10); return v }(), // 1000 ETH equivalent
|
||||
Receipt: &SequencerReceipt{
|
||||
Status: 1,
|
||||
GasUsed: 450000,
|
||||
Logs: []*SequencerLog{
|
||||
{
|
||||
Address: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
|
||||
Topics: []common.Hash{common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")},
|
||||
EventName: "Swap",
|
||||
Protocol: "UniswapV3",
|
||||
ParsedArgs: map[string]interface{}{
|
||||
"sender": common.HexToAddress("0x1234567890123456789012345678901234567890"),
|
||||
"recipient": common.HexToAddress("0x1234567890123456789012345678901234567890"),
|
||||
"amount0": big.NewInt(-1e18), // -1 ETH
|
||||
"amount1": big.NewInt(2000e6), // +2000 USDC
|
||||
"sqrtPriceX96": big.NewInt(1000000000000000000),
|
||||
"liquidity": big.NewInt(1e12),
|
||||
"tick": big.NewInt(195000),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test parsing
|
||||
result := testTransactionParsing(t, parser, highValueTx)
|
||||
require.True(t, result.Success, "High-value transaction parsing should succeed: %v", result.Error)
|
||||
|
||||
// Validate specific fields for high-value transactions
|
||||
assert.Equal(t, 1, result.SwapEvents, "Should detect 1 swap event")
|
||||
assert.Equal(t, "UniswapV3", result.Protocol, "Should identify UniswapV3 protocol")
|
||||
threshold := new(big.Int)
|
||||
threshold.SetString("100000000000000000000", 10)
|
||||
assert.True(t, result.ParsedValue.Cmp(threshold) > 0, "Should parse high swap value")
|
||||
|
||||
t.Logf("High-value transaction parsed successfully: %s ETH value",
|
||||
new(big.Float).Quo(new(big.Float).SetInt(result.ParsedValue), big.NewFloat(1e18)).String())
|
||||
}
|
||||
|
||||
// TestMEVTransactionDetection tests detection and parsing of MEV transactions
|
||||
func TestMEVTransactionDetection(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
parser := arbitrum.NewL2MessageParser(log)
|
||||
|
||||
// Create mock MEV transaction (arbitrage)
|
||||
mevTx := &SequencerTransaction{
|
||||
Hash: common.HexToHash("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"),
|
||||
From: common.HexToAddress("0xabcdef1234567890123456789012345678901234"),
|
||||
Gas: 1000000,
|
||||
GasUsed: 950000,
|
||||
GasPrice: big.NewInt(5e10), // 50 gwei (high)
|
||||
EffectiveGasPrice: big.NewInt(5e10),
|
||||
IsDEXTransaction: true,
|
||||
IsMEVTransaction: true,
|
||||
MEVType: "arbitrage",
|
||||
DEXProtocol: "MultiDEX",
|
||||
SwapValue: func() *big.Int { v := new(big.Int); v.SetString("500000000000000000000", 10); return v }(), // 500 ETH equivalent
|
||||
Receipt: &SequencerReceipt{
|
||||
Status: 1,
|
||||
GasUsed: 950000,
|
||||
Logs: []*SequencerLog{
|
||||
// First swap (buy)
|
||||
{
|
||||
Address: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
|
||||
EventName: "Swap",
|
||||
Protocol: "UniswapV3",
|
||||
ParsedArgs: map[string]interface{}{
|
||||
"amount0": big.NewInt(-1e18), // Buy 1 ETH
|
||||
"amount1": big.NewInt(2000e6), // Pay 2000 USDC
|
||||
},
|
||||
},
|
||||
// Second swap (sell)
|
||||
{
|
||||
Address: common.HexToAddress("0x1111111254fb6c44bAC0beD2854e76F90643097d"),
|
||||
EventName: "Swap",
|
||||
Protocol: "SushiSwap",
|
||||
ParsedArgs: map[string]interface{}{
|
||||
"amount0": big.NewInt(1e18), // Sell 1 ETH
|
||||
"amount1": big.NewInt(-2010e6), // Receive 2010 USDC
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test parsing
|
||||
result := testTransactionParsing(t, parser, mevTx)
|
||||
require.True(t, result.Success, "MEV transaction parsing should succeed: %v", result.Error)
|
||||
|
||||
// Validate MEV-specific detection
|
||||
assert.Equal(t, 2, result.SwapEvents, "Should detect 2 swap events in arbitrage")
|
||||
threshold2 := new(big.Int)
|
||||
threshold2.SetString("100000000000000000000", 10)
|
||||
assert.True(t, result.ParsedValue.Cmp(threshold2) > 0, "Should detect high-value MEV")
|
||||
|
||||
// Calculate estimated profit (simplified)
|
||||
profit := big.NewInt(10e6) // 10 USDC profit
|
||||
t.Logf("MEV arbitrage transaction parsed: %d swap events, estimated profit: %s USDC",
|
||||
result.SwapEvents, new(big.Float).Quo(new(big.Float).SetInt(profit), big.NewFloat(1e6)).String())
|
||||
}
|
||||
|
||||
// TestParserPerformance tests parser performance with sequencer-speed data
|
||||
func TestParserPerformance(t *testing.T) {
|
||||
log := logger.New("warn", "text", "") // Reduce logging for performance test
|
||||
parser := arbitrum.NewL2MessageParser(log)
|
||||
|
||||
// Create test transactions
|
||||
numTransactions := 1000
|
||||
transactions := make([]*SequencerTransaction, numTransactions)
|
||||
|
||||
for i := 0; i < numTransactions; i++ {
|
||||
transactions[i] = createMockTransaction(i)
|
||||
}
|
||||
|
||||
// Measure parsing performance
|
||||
startTime := time.Now()
|
||||
var successCount int
|
||||
|
||||
for _, tx := range transactions {
|
||||
result := testTransactionParsing(t, parser, tx)
|
||||
if result.Success {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
txPerSecond := float64(numTransactions) / elapsed.Seconds()
|
||||
|
||||
t.Logf("=== PARSER PERFORMANCE RESULTS ===")
|
||||
t.Logf("Transactions processed: %d", numTransactions)
|
||||
t.Logf("Successful parses: %d", successCount)
|
||||
t.Logf("Time elapsed: %v", elapsed)
|
||||
t.Logf("Transactions per second: %.2f", txPerSecond)
|
||||
|
||||
// Performance requirements
|
||||
assert.Greater(t, txPerSecond, 500.0, "Parser should process >500 tx/s")
|
||||
assert.Greater(t, float64(successCount)/float64(numTransactions), 0.95, "Success rate should be >95%")
|
||||
}
|
||||
|
||||
// createMockTransaction creates a mock transaction for testing
|
||||
func createMockTransaction(index int) *SequencerTransaction {
|
||||
return &SequencerTransaction{
|
||||
Hash: common.HexToHash(fmt.Sprintf("0x%064d", index)),
|
||||
From: common.HexToAddress(fmt.Sprintf("0x%040d", index)),
|
||||
Gas: 200000,
|
||||
GasUsed: 150000,
|
||||
GasPrice: big.NewInt(1e10),
|
||||
EffectiveGasPrice: big.NewInt(1e10),
|
||||
IsDEXTransaction: index%3 == 0, // Every 3rd transaction is DEX
|
||||
DEXProtocol: "UniswapV3",
|
||||
Receipt: &SequencerReceipt{
|
||||
Status: 1,
|
||||
GasUsed: 150000,
|
||||
Logs: []*SequencerLog{
|
||||
{
|
||||
EventName: "Swap",
|
||||
Protocol: "UniswapV3",
|
||||
ParsedArgs: map[string]interface{}{
|
||||
"amount0": big.NewInt(1e17), // 0.1 ETH
|
||||
"amount1": big.NewInt(200e6), // 200 USDC
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for counting transactions
|
||||
func countDEXInBlock(block *SequencerBlock) int {
|
||||
count := 0
|
||||
for _, tx := range block.Transactions {
|
||||
if tx.IsDEXTransaction {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func countMEVInBlock(block *SequencerBlock) int {
|
||||
count := 0
|
||||
for _, tx := range block.Transactions {
|
||||
if tx.IsMEVTransaction {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
Reference in New Issue
Block a user