feat: create v2-prep branch with comprehensive planning

Restructured project for V2 refactor:

**Structure Changes:**
- Moved all V1 code to orig/ folder (preserved with git mv)
- Created docs/planning/ directory
- Added orig/README_V1.md explaining V1 preservation

**Planning Documents:**
- 00_V2_MASTER_PLAN.md: Complete architecture overview
  - Executive summary of critical V1 issues
  - High-level component architecture diagrams
  - 5-phase implementation roadmap
  - Success metrics and risk mitigation

- 07_TASK_BREAKDOWN.md: Atomic task breakdown
  - 99+ hours of detailed tasks
  - Every task < 2 hours (atomic)
  - Clear dependencies and success criteria
  - Organized by implementation phase

**V2 Key Improvements:**
- Per-exchange parsers (factory pattern)
- Multi-layer strict validation
- Multi-index pool cache
- Background validation pipeline
- Comprehensive observability

**Critical Issues Addressed:**
- Zero address tokens (strict validation + cache enrichment)
- Parsing accuracy (protocol-specific parsers)
- No audit trail (background validation channel)
- Inefficient lookups (multi-index cache)
- Stats disconnection (event-driven metrics)

Next Steps:
1. Review planning documents
2. Begin Phase 1: Foundation (P1-001 through P1-010)
3. Implement parsers in Phase 2
4. Build cache system in Phase 3
5. Add validation pipeline in Phase 4
6. Migrate and test in Phase 5

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Administrator
2025-11-10 10:14:26 +01:00
parent 1773daffe7
commit 803de231ba
411 changed files with 20390 additions and 8680 deletions

View File

@@ -0,0 +1,484 @@
package logger
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
pkgerrors "github.com/fraktal/mev-beta/pkg/errors"
)
// LogLevel represents different log levels
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
OPPORTUNITY // Special level for opportunities
)
var logLevelNames = map[LogLevel]string{
DEBUG: "DEBUG",
INFO: "INFO",
WARN: "WARN",
ERROR: "ERROR",
OPPORTUNITY: "OPPORTUNITY",
}
var suppressedWarningSubstrings = []string{
"extractTokensGeneric",
"extractTokensFromMulticall",
}
// Logger represents a multi-file logger with separation of concerns
type Logger struct {
// Main application logger
logger *log.Logger
level LogLevel
// Specialized loggers for different concerns
opportunityLogger *log.Logger // MEV opportunities and arbitrage attempts
errorLogger *log.Logger // Errors and warnings only
performanceLogger *log.Logger // Performance metrics and RPC calls
transactionLogger *log.Logger // Detailed transaction analysis
// Security filtering
secureFilter *SecureFilter
levelName string
}
// parseLogLevel converts string log level to LogLevel enum
func parseLogLevel(level string) LogLevel {
switch strings.ToLower(level) {
case "debug":
return DEBUG
case "info":
return INFO
case "warn", "warning":
return WARN
case "error":
return ERROR
default:
return INFO // Default to INFO level
}
}
// createLogFile creates a log file or returns stdout if it fails
func createLogFile(filename string) *os.File {
if filename == "" {
return os.Stdout
}
if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
log.Printf("Failed to create log directory for %s: %v, falling back to stdout", filename, err)
return os.Stdout
}
// Check and rotate log file if needed (100MB max size)
maxSize := int64(100 * 1024 * 1024) // 100 MB
if err := rotateLogFile(filename, maxSize); err != nil {
log.Printf("Failed to rotate log file %s: %v", filename, err)
// Continue anyway, rotation failure shouldn't stop logging
}
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
log.Printf("Failed to create log file %s: %v, falling back to stdout", filename, err)
return os.Stdout
}
return f
}
// New creates a new multi-file logger with separation of concerns
func New(level string, format string, file string) *Logger {
// Parse base filename for specialized logs
baseDir := "logs"
baseName := "mev_bot"
if file != "" {
// Extract directory and base filename
parts := strings.Split(file, "/")
if len(parts) > 1 {
baseDir = strings.Join(parts[:len(parts)-1], "/")
}
filename := parts[len(parts)-1]
if strings.Contains(filename, ".") {
baseName = strings.Split(filename, ".")[0]
}
}
// Create specialized log files
mainFile := createLogFile(file)
opportunityFile := createLogFile(fmt.Sprintf("%s/%s_opportunities.log", baseDir, baseName))
errorFile := createLogFile(fmt.Sprintf("%s/%s_errors.log", baseDir, baseName))
performanceFile := createLogFile(fmt.Sprintf("%s/%s_performance.log", baseDir, baseName))
transactionFile := createLogFile(fmt.Sprintf("%s/%s_transactions.log", baseDir, baseName))
// Create loggers with no prefixes (we format ourselves)
logLevel := parseLogLevel(level)
// Determine security level based on environment and log level
var securityLevel SecurityLevel
env := os.Getenv("GO_ENV")
switch {
case env == "production":
securityLevel = SecurityLevelProduction
case logLevel >= WARN:
securityLevel = SecurityLevelInfo
default:
securityLevel = SecurityLevelDebug
}
return &Logger{
logger: log.New(mainFile, "", 0),
opportunityLogger: log.New(opportunityFile, "", 0),
errorLogger: log.New(errorFile, "", 0),
performanceLogger: log.New(performanceFile, "", 0),
transactionLogger: log.New(transactionFile, "", 0),
level: logLevel,
secureFilter: NewSecureFilter(securityLevel),
levelName: level,
}
}
// shouldLog determines if a message should be logged based on level
func (l *Logger) shouldLog(level LogLevel) bool {
return level >= l.level
}
// formatMessage formats a log message with timestamp and level
func (l *Logger) formatMessage(level LogLevel, v ...interface{}) string {
timestamp := time.Now().Format("2006/01/02 15:04:05")
levelName := logLevelNames[level]
message := formatKVMessage(v...)
return fmt.Sprintf("%s [%s] %s", timestamp, levelName, message)
}
// formatKVMessage converts a variadic list of arguments into a structured log string.
// It treats consecutive key/value pairs (string key followed by any value) specially
// so that existing logger calls like logger.Error("msg", "key", value) render as
// `msg key=value`.
func formatKVMessage(args ...interface{}) string {
if len(args) == 0 {
return ""
}
var b strings.Builder
// Always print the first argument verbatim to preserve legacy formatting.
fmt.Fprintf(&b, "%v", args[0])
// Process subsequent arguments as key/value pairs where possible.
for i := 1; i < len(args); i++ {
key, ok := args[i].(string)
if !ok || i == len(args)-1 {
// Not a key/value pair, fall back to simple spacing.
fmt.Fprintf(&b, " %v", args[i])
continue
}
value := args[i+1]
fmt.Fprintf(&b, " %s=%v", key, value)
i++
}
return b.String()
}
// Debug logs a debug message
func (l *Logger) Debug(v ...interface{}) {
if l.shouldLog(DEBUG) {
l.logger.Println(l.formatMessage(DEBUG, v...))
}
}
// Info logs an info message
func (l *Logger) Info(v ...interface{}) {
if l.shouldLog(INFO) {
l.logger.Println(l.formatMessage(INFO, v...))
}
}
// Warn logs a warning message
func (l *Logger) Warn(v ...interface{}) {
if l.shouldLog(WARN) {
message := l.formatMessage(WARN, v...)
for _, substr := range suppressedWarningSubstrings {
if strings.Contains(message, substr) {
return
}
}
l.logger.Println(message)
l.errorLogger.Println(message) // Also log to error file
}
}
// Error logs an error message
func (l *Logger) Error(v ...interface{}) {
if l.shouldLog(ERROR) {
message := l.formatMessage(ERROR, v...)
l.logger.Println(message)
l.errorLogger.Println(message) // Also log to error file
}
}
// ErrorStructured logs a structured error with full context
func (l *Logger) ErrorStructured(err *pkgerrors.StructuredError) {
if !l.shouldLog(ERROR) {
return
}
// Log compact format to main log
compactMsg := fmt.Sprintf("%s [%s] %s",
time.Now().Format("2006/01/02 15:04:05"),
"ERROR",
err.FormatCompact())
l.logger.Println(compactMsg)
// Log full detailed format to error log
fullMsg := fmt.Sprintf("%s [%s] %s",
time.Now().Format("2006/01/02 15:04:05"),
"ERROR",
err.FormatForLogging())
l.errorLogger.Println(fullMsg)
}
// WarnStructured logs a structured warning with full context
func (l *Logger) WarnStructured(err *pkgerrors.StructuredError) {
if !l.shouldLog(WARN) {
return
}
// Log compact format to main log
compactMsg := fmt.Sprintf("%s [%s] %s",
time.Now().Format("2006/01/02 15:04:05"),
"WARN",
err.FormatCompact())
// Check if warning should be suppressed
for _, substr := range suppressedWarningSubstrings {
if strings.Contains(compactMsg, substr) {
return
}
}
l.logger.Println(compactMsg)
l.errorLogger.Println(compactMsg)
}
// Opportunity logs a found opportunity with detailed information
// This always logs regardless of level since opportunities are critical
func (l *Logger) Opportunity(txHash, from, to, method, protocol string, amountIn, amountOut, minOut, profitUSD float64, additionalData map[string]interface{}) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
// Create sanitized additional data for production
sanitizedData := l.secureFilter.SanitizeForProduction(additionalData)
message := fmt.Sprintf(`%s [OPPORTUNITY] 🎯 ARBITRAGE OPPORTUNITY DETECTED
├── Transaction: %s
├── From: %s → To: %s
├── Method: %s (%s)
├── Amount In: %.6f tokens
├── Amount Out: %.6f tokens
├── Min Out: %.6f tokens
├── Estimated Profit: $%.2f USD
└── Additional Data: %v`,
timestamp, txHash, from, to, method, protocol,
amountIn, amountOut, minOut, profitUSD, sanitizedData)
// Apply security filtering to the entire message
filteredMessage := l.secureFilter.FilterMessage(message)
l.logger.Println(filteredMessage)
l.opportunityLogger.Println(filteredMessage) // Dedicated opportunity log
}
// OpportunitySimple logs a simple opportunity message (for backwards compatibility)
func (l *Logger) OpportunitySimple(v ...interface{}) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
message := fmt.Sprintf("%s [OPPORTUNITY] %s", timestamp, fmt.Sprint(v...))
l.logger.Println(message)
l.opportunityLogger.Println(message) // Dedicated opportunity log
}
// Performance logs performance metrics for optimization analysis
func (l *Logger) Performance(component, operation string, duration time.Duration, metadata map[string]interface{}) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
// Add standard performance fields
data := map[string]interface{}{
"component": component,
"operation": operation,
"duration_ms": duration.Milliseconds(),
"duration_ns": duration.Nanoseconds(),
"timestamp": timestamp,
}
// Merge with provided metadata
for k, v := range metadata {
data[k] = v
}
message := fmt.Sprintf(`%s [PERFORMANCE] 📊 %s.%s completed in %v - %v`,
timestamp, component, operation, duration, data)
l.performanceLogger.Println(message) // Dedicated performance log only
}
// Metrics logs business metrics for analysis
func (l *Logger) Metrics(name string, value float64, unit string, tags map[string]string) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
message := fmt.Sprintf(`%s [METRICS] 📈 %s: %.6f %s %v`,
timestamp, name, value, unit, tags)
l.performanceLogger.Println(message) // Metrics go to performance log
}
// Transaction logs detailed transaction information for MEV analysis
func (l *Logger) Transaction(txHash, from, to, method, protocol string, gasUsed, gasPrice uint64, value float64, success bool, metadata map[string]interface{}) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
status := "FAILED"
if success {
status = "SUCCESS"
}
// Sanitize metadata for production
sanitizedMetadata := l.secureFilter.SanitizeForProduction(metadata)
message := fmt.Sprintf(`%s [TRANSACTION] 💳 %s
├── Hash: %s
├── From: %s → To: %s
├── Method: %s (%s)
├── Gas Used: %d (Price: %d wei)
├── Value: %.6f ETH
├── Status: %s
└── Metadata: %v`,
timestamp, status, txHash, from, to, method, protocol,
gasUsed, gasPrice, value, status, sanitizedMetadata)
// Apply security filtering to the entire message
filteredMessage := l.secureFilter.FilterMessage(message)
l.transactionLogger.Println(filteredMessage) // Dedicated transaction log only
}
// BlockProcessing logs block processing metrics for sequencer monitoring
func (l *Logger) BlockProcessing(blockNumber uint64, txCount, dexTxCount int, processingTime time.Duration) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
message := fmt.Sprintf(`%s [BLOCK_PROCESSING] 🧱 Block %d: %d txs (%d DEX) processed in %v`,
timestamp, blockNumber, txCount, dexTxCount, processingTime)
l.performanceLogger.Println(message) // Block processing metrics go to performance log
}
// ArbitrageAnalysis logs arbitrage opportunity analysis results
func (l *Logger) ArbitrageAnalysis(poolA, poolB, tokenPair string, priceA, priceB, priceDiff, estimatedProfit float64, feasible bool) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
status := "REJECTED"
if feasible {
status = "VIABLE"
}
message := fmt.Sprintf(`%s [ARBITRAGE_ANALYSIS] 🔍 %s %s
├── Pool A: %s (Price: %.6f)
├── Pool B: %s (Price: %.6f)
├── Price Difference: %.4f%%
├── Estimated Profit: $%.2f
└── Status: %s`,
timestamp, status, tokenPair, poolA, priceA, poolB, priceB,
priceDiff*100, estimatedProfit, status)
// Apply security filtering to protect sensitive pricing data
filteredMessage := l.secureFilter.FilterMessage(message)
l.opportunityLogger.Println(filteredMessage) // Arbitrage analysis goes to opportunity log
}
// RPC logs RPC call metrics for endpoint optimization
func (l *Logger) RPC(endpoint, method string, duration time.Duration, success bool, errorMsg string) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
status := "SUCCESS"
if !success {
status = "FAILED"
}
message := fmt.Sprintf(`%s [RPC] 🌐 %s %s.%s in %v`,
timestamp, status, endpoint, method, duration)
if !success && errorMsg != "" {
message += fmt.Sprintf(" - Error: %s", errorMsg)
}
l.performanceLogger.Println(message) // RPC metrics go to performance log
}
// SwapAnalysis logs swap event analysis with security filtering
func (l *Logger) SwapAnalysis(tokenIn, tokenOut string, amountIn, amountOut float64, protocol, poolAddr string, metadata map[string]interface{}) {
timestamp := time.Now().Format("2006/01/02 15:04:05")
// Sanitize metadata for production
sanitizedMetadata := l.secureFilter.SanitizeForProduction(metadata)
message := fmt.Sprintf(`%s [SWAP_ANALYSIS] 🔄 %s → %s
├── Amount In: %.6f %s
├── Amount Out: %.6f %s
├── Protocol: %s
├── Pool: %s
└── Metadata: %v`,
timestamp, tokenIn, tokenOut, amountIn, tokenIn, amountOut, tokenOut,
protocol, poolAddr, sanitizedMetadata)
// Apply security filtering to the entire message
filteredMessage := l.secureFilter.FilterMessage(message)
l.transactionLogger.Println(filteredMessage) // Dedicated transaction log
}
// rotateLogFile rotates a log file when it exceeds the maximum size
func rotateLogFile(filename string, maxSize int64) error {
// Check if file exists
if _, err := os.Stat(filename); os.IsNotExist(err) {
return nil // File doesn't exist, nothing to rotate
}
// Get file info
fileInfo, err := os.Stat(filename)
if err != nil {
return fmt.Errorf("failed to get file info: %w", err)
}
// Check if file exceeds max size
if fileInfo.Size() < maxSize {
return nil // File is within size limits
}
// Create archive directory if it doesn't exist
archiveDir := "logs/archived"
if err := os.MkdirAll(archiveDir, 0755); err != nil {
return fmt.Errorf("failed to create archive directory: %w", err)
}
// Generate archive filename with timestamp
timestamp := time.Now().Format("20060102_150405")
baseName := filepath.Base(filename)
ext := filepath.Ext(baseName)
name := strings.TrimSuffix(baseName, ext)
archiveFilename := filepath.Join(archiveDir, fmt.Sprintf("%s_%s%s", name, timestamp, ext))
// Close current file handle and rename
if err := os.Rename(filename, archiveFilename); err != nil {
return fmt.Errorf("failed to rotate log file: %w", err)
}
return nil
}

View File

@@ -0,0 +1,243 @@
package logger
import (
"bytes"
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewLogger(t *testing.T) {
// Test creating a logger with stdout
logger := New("info", "text", "")
assert.NotNil(t, logger)
assert.NotNil(t, logger.logger)
assert.Equal(t, "info", logger.levelName)
}
func TestNewLoggerWithFile(t *testing.T) {
// Create a temporary file for testing
tmpFile, err := os.CreateTemp("", "logger_test_*.log")
assert.NoError(t, err)
defer os.Remove(tmpFile.Name())
err = tmpFile.Close()
assert.NoError(t, err)
// Test creating a logger with a file
logger := New("info", "text", tmpFile.Name())
assert.NotNil(t, logger)
assert.Equal(t, "info", logger.levelName)
}
func TestDebug(t *testing.T) {
// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create logger with debug level
logger := New("debug", "text", "")
// Log a debug message
logger.Debug("test debug message")
// Restore stdout
w.Close()
os.Stdout = old
// Read the captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Check that the log message contains the expected content with brackets
assert.Contains(t, output, "[DEBUG] test debug message")
}
func TestDebugWithInfoLevel(t *testing.T) {
// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create logger with info level (should not log debug messages)
logger := New("info", "text", "")
// Log a debug message
logger.Debug("test debug message")
// Restore stdout
w.Close()
os.Stdout = old
// Read the captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify the output does not contain the debug message
assert.NotContains(t, output, "DEBUG:")
assert.NotContains(t, output, "test debug message")
}
func TestInfo(t *testing.T) {
// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create logger with info level
logger := New("info", "text", "")
// Log an info message
logger.Info("test info message")
// Restore stdout
w.Close()
os.Stdout = old
// Read the captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify the output contains the info message
assert.Contains(t, output, "[INFO]")
assert.Contains(t, output, "test info message")
}
func TestInfoWithDebugLevel(t *testing.T) {
// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create logger with debug level
logger := New("debug", "text", "")
// Log an info message
logger.Info("test info message")
// Restore stdout
w.Close()
os.Stdout = old
// Read the captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify the output contains the info message
assert.Contains(t, output, "[INFO]")
assert.Contains(t, output, "test info message")
}
func TestWarn(t *testing.T) {
// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create logger with warn level
logger := New("warn", "text", "")
// Log a warning message
logger.Warn("test warn message")
// Restore stdout
w.Close()
os.Stdout = old
// Read the captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify the output contains the warning message
assert.Contains(t, output, "[WARN]")
assert.Contains(t, output, "test warn message")
}
func TestWarnWithInfoLevel(t *testing.T) {
// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create logger with info level (should log warnings)
logger := New("info", "text", "")
// Log a warning message
logger.Warn("test warn message")
// Restore stdout
w.Close()
os.Stdout = old
// Read the captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify the output contains the warning message
assert.Contains(t, output, "[WARN]")
assert.Contains(t, output, "test warn message")
}
func TestError(t *testing.T) {
// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create logger
logger := New("error", "text", "")
// Log an error message
logger.Error("test error message")
// Restore stdout
w.Close()
os.Stdout = old
// Read the captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify the output contains the error message
assert.Contains(t, output, "[ERROR]")
assert.Contains(t, output, "test error message")
}
func TestErrorWithAllLevels(t *testing.T) {
// Test that error messages are logged at all levels
levels := []string{"debug", "info", "warn", "error"}
for _, level := range levels {
// Capture stdout
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create logger with current level
logger := New(level, "text", "")
// Log an error message
logger.Error("test error message")
// Restore stdout
w.Close()
os.Stdout = old
// Read the captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify the output contains the error message
assert.Contains(t, output, "[ERROR]")
assert.Contains(t, output, "test error message")
}
}

View File

@@ -0,0 +1,241 @@
package logger
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"time"
)
// FilterMessageEnhanced provides comprehensive filtering with audit logging
func (sf *SecureFilter) FilterMessageEnhanced(message string, context map[string]interface{}) string {
originalMessage := message
filtered := sf.FilterMessage(message)
// Audit sensitive data detection if enabled
if sf.auditEnabled {
auditData := sf.detectSensitiveData(originalMessage, context)
if len(auditData) > 0 {
sf.logAuditEvent(auditData)
}
}
return filtered
}
// detectSensitiveData identifies and catalogs sensitive data found in messages
func (sf *SecureFilter) detectSensitiveData(message string, context map[string]interface{}) map[string]interface{} {
detected := make(map[string]interface{})
detected["timestamp"] = time.Now().UTC().Format(time.RFC3339)
detected["security_level"] = sf.level
if context != nil {
detected["context"] = context
}
// Check for different types of sensitive data
sensitiveTypes := []string{}
// Check for private keys (CRITICAL)
for _, pattern := range sf.privateKeyPatterns {
if pattern.MatchString(message) {
sensitiveTypes = append(sensitiveTypes, "private_key")
detected["severity"] = "CRITICAL"
break
}
}
// Check for transaction hashes BEFORE addresses (64 chars vs 40 chars)
for _, pattern := range sf.hashPatterns {
if pattern.MatchString(message) {
sensitiveTypes = append(sensitiveTypes, "transaction_hash")
if detected["severity"] == nil {
detected["severity"] = "LOW"
}
break
}
}
// Check for addresses AFTER hashes
for _, pattern := range sf.addressPatterns {
if pattern.MatchString(message) {
sensitiveTypes = append(sensitiveTypes, "address")
if detected["severity"] == nil {
detected["severity"] = "MEDIUM"
}
break
}
}
// Check for amounts/values
for _, pattern := range sf.amountPatterns {
if pattern.MatchString(message) {
sensitiveTypes = append(sensitiveTypes, "amount")
if detected["severity"] == nil {
detected["severity"] = "LOW"
}
break
}
}
if len(sensitiveTypes) > 0 {
detected["types"] = sensitiveTypes
detected["message_length"] = len(message)
detected["filtered_length"] = len(sf.FilterMessage(message))
return detected
}
return nil
}
// logAuditEvent logs sensitive data detection events
func (sf *SecureFilter) logAuditEvent(auditData map[string]interface{}) {
// Create audit log entry
auditEntry := map[string]interface{}{
"event_type": "sensitive_data_detected",
"timestamp": auditData["timestamp"],
"severity": auditData["severity"],
"types": auditData["types"],
"message_stats": map[string]interface{}{
"original_length": auditData["message_length"],
"filtered_length": auditData["filtered_length"],
},
}
if auditData["context"] != nil {
auditEntry["context"] = auditData["context"]
}
// Encrypt audit data if encryption is enabled
if sf.auditEncryption && len(sf.encryptionKey) > 0 {
encrypted, err := sf.encryptAuditData(auditEntry)
if err == nil {
auditEntry = map[string]interface{}{
"encrypted": true,
"data": encrypted,
"timestamp": auditData["timestamp"],
}
}
}
// Log to audit trail (this would typically go to a separate audit log file)
// For now, we'll add it to a structured format that can be easily extracted
auditJSON, _ := json.Marshal(auditEntry)
fmt.Printf("AUDIT_LOG: %s\n", string(auditJSON))
}
// encryptAuditData encrypts sensitive audit data
func (sf *SecureFilter) encryptAuditData(data map[string]interface{}) (string, error) {
if len(sf.encryptionKey) == 0 {
return "", fmt.Errorf("no encryption key available")
}
// Serialize data to JSON
jsonData, err := json.Marshal(data)
if err != nil {
return "", fmt.Errorf("failed to marshal audit data: %w", err)
}
// Create AES-GCM cipher (AEAD - authenticated encryption)
key := sha256.Sum256(sf.encryptionKey)
block, err := aes.NewCipher(key[:])
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}
// Create GCM instance
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("failed to create GCM: %w", err)
}
// Generate random nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("failed to generate nonce: %w", err)
}
// Encrypt and authenticate data
encrypted := gcm.Seal(nonce, nonce, jsonData, nil)
return hex.EncodeToString(encrypted), nil
}
// DecryptAuditData decrypts audit data (for authorized access)
func (sf *SecureFilter) DecryptAuditData(encryptedHex string) (map[string]interface{}, error) {
if len(sf.encryptionKey) == 0 {
return nil, fmt.Errorf("no encryption key available")
}
// Decode hex
encryptedData, err := hex.DecodeString(encryptedHex)
if err != nil {
return nil, fmt.Errorf("failed to decode hex: %w", err)
}
// Create AES-GCM cipher (AEAD - authenticated encryption)
key := sha256.Sum256(sf.encryptionKey)
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
// Create GCM instance
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
// Check minimum length (nonce + encrypted data + tag)
if len(encryptedData) < gcm.NonceSize() {
return nil, fmt.Errorf("encrypted data too short")
}
// Extract nonce and encrypted data
nonce := encryptedData[:gcm.NonceSize()]
ciphertext := encryptedData[gcm.NonceSize():]
// Decrypt and authenticate data
decrypted, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt data: %w", err)
}
// Unmarshal JSON
var result map[string]interface{}
err = json.Unmarshal(decrypted, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal decrypted data: %w", err)
}
return result, nil
}
// EnableAuditLogging enables audit logging with optional encryption
func (sf *SecureFilter) EnableAuditLogging(encryptionKey []byte) {
sf.auditEnabled = true
if len(encryptionKey) > 0 {
sf.encryptionKey = encryptionKey
sf.auditEncryption = true
}
}
// DisableAuditLogging disables audit logging
func (sf *SecureFilter) DisableAuditLogging() {
sf.auditEnabled = false
sf.auditEncryption = false
}
// SetSecurityLevel changes the security level dynamically
func (sf *SecureFilter) SetSecurityLevel(level SecurityLevel) {
sf.level = level
}
// GetSecurityLevel returns the current security level
func (sf *SecureFilter) GetSecurityLevel() SecurityLevel {
return sf.level
}

View File

@@ -0,0 +1,301 @@
package logger
import (
"math/big"
"regexp"
"strings"
"github.com/ethereum/go-ethereum/common"
)
// SecurityLevel defines the logging security level
type SecurityLevel int
const (
SecurityLevelDebug SecurityLevel = iota // Log everything (development only)
SecurityLevelInfo // Log basic info, filter amounts
SecurityLevelProduction // Log minimal info, filter sensitive data
)
// SecureFilter filters sensitive data from log messages
type SecureFilter struct {
level SecurityLevel
// Patterns to detect sensitive data
amountPatterns []*regexp.Regexp
addressPatterns []*regexp.Regexp
valuePatterns []*regexp.Regexp
hashPatterns []*regexp.Regexp
privateKeyPatterns []*regexp.Regexp
encryptionKey []byte
auditEnabled bool
auditEncryption bool
}
// SecureFilterConfig contains configuration for the secure filter
type SecureFilterConfig struct {
Level SecurityLevel
EncryptionKey []byte
AuditEnabled bool
AuditEncryption bool
}
// NewSecureFilter creates a new secure filter with enhanced configuration
func NewSecureFilter(level SecurityLevel) *SecureFilter {
return NewSecureFilterWithConfig(&SecureFilterConfig{
Level: level,
AuditEnabled: false,
AuditEncryption: false,
})
}
// NewSecureFilterWithConfig creates a new secure filter with full configuration
func NewSecureFilterWithConfig(config *SecureFilterConfig) *SecureFilter {
return &SecureFilter{
level: config.Level,
encryptionKey: config.EncryptionKey,
auditEnabled: config.AuditEnabled,
auditEncryption: config.AuditEncryption,
amountPatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)amount[^=]*=\s*[0-9]+`),
regexp.MustCompile(`(?i)value[^=]*=\s*[0-9]+`),
regexp.MustCompile(`\$[0-9]+\.?[0-9]*`),
regexp.MustCompile(`[0-9]+\.[0-9]+ USD`),
regexp.MustCompile(`(?i)amountIn=[0-9]+`),
regexp.MustCompile(`(?i)amountOut=[0-9]+`),
regexp.MustCompile(`(?i)balance[^=]*=\s*[0-9]+`),
regexp.MustCompile(`(?i)profit[^=]*=\s*[0-9]+`),
regexp.MustCompile(`(?i)gas[Pp]rice[^=]*=\s*[0-9]+`),
regexp.MustCompile(`\b[0-9]{15,}\b`), // Very large numbers likely to be wei amounts (but not hex addresses)
},
addressPatterns: []*regexp.Regexp{
regexp.MustCompile(`0x[a-fA-F0-9]{40}`),
regexp.MustCompile(`(?i)address[^=]*=\s*0x[a-fA-F0-9]{40}`),
regexp.MustCompile(`(?i)contract[^=]*=\s*0x[a-fA-F0-9]{40}`),
regexp.MustCompile(`(?i)token[^=]*=\s*0x[a-fA-F0-9]{40}`),
},
valuePatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)value:\s*\$[0-9]+\.?[0-9]*`),
regexp.MustCompile(`(?i)profit[^=]*=\s*\$?[0-9]+\.?[0-9]*`),
regexp.MustCompile(`(?i)total:\s*\$[0-9]+\.?[0-9]*`),
regexp.MustCompile(`(?i)revenue[^=]*=\s*\$?[0-9]+\.?[0-9]*`),
regexp.MustCompile(`(?i)fee[^=]*=\s*\$?[0-9]+\.?[0-9]*`),
},
hashPatterns: []*regexp.Regexp{
regexp.MustCompile(`0x[a-fA-F0-9]{64}`), // Transaction hashes
regexp.MustCompile(`(?i)txHash[^=]*=\s*0x[a-fA-F0-9]{64}`),
regexp.MustCompile(`(?i)blockHash[^=]*=\s*0x[a-fA-F0-9]{64}`),
},
privateKeyPatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)private[_\s]*key[^=]*=\s*[a-fA-F0-9]{64}`),
regexp.MustCompile(`(?i)secret[^=]*=\s*[a-fA-F0-9]{64}`),
regexp.MustCompile(`(?i)mnemonic[^=]*=\s*\"[^\"]*\"`),
regexp.MustCompile(`(?i)seed[^=]*=\s*\"[^\"]*\"`),
},
}
}
// FilterMessage filters sensitive data from a log message with enhanced input sanitization
func (sf *SecureFilter) FilterMessage(message string) string {
if sf.level == SecurityLevelDebug {
return sf.sanitizeInput(message) // Still sanitize for security even in debug mode
}
filtered := sf.sanitizeInput(message)
// Filter private keys FIRST (highest security priority)
for _, pattern := range sf.privateKeyPatterns {
filtered = pattern.ReplaceAllString(filtered, "[PRIVATE_KEY_FILTERED]")
}
// Filter transaction hashes
if sf.level >= SecurityLevelInfo {
for _, pattern := range sf.hashPatterns {
filtered = pattern.ReplaceAllStringFunc(filtered, func(hash string) string {
if len(hash) == 66 { // Full transaction hash
return hash[:10] + "..." + hash[62:] // Show first 8 and last 4 chars
}
return "[HASH_FILTERED]"
})
}
}
// Filter addresses NEXT (before amounts) to prevent addresses from being treated as numbers
if sf.level >= SecurityLevelProduction {
for _, pattern := range sf.addressPatterns {
filtered = pattern.ReplaceAllStringFunc(filtered, func(addr string) string {
if len(addr) == 42 { // Full Ethereum address
return addr[:6] + "..." + addr[38:] // Show first 4 and last 4 chars
}
return "[ADDR_FILTERED]"
})
}
}
// Filter amounts LAST
if sf.level >= SecurityLevelInfo {
for _, pattern := range sf.amountPatterns {
filtered = pattern.ReplaceAllString(filtered, "[AMOUNT_FILTERED]")
}
for _, pattern := range sf.valuePatterns {
filtered = pattern.ReplaceAllString(filtered, "[VALUE_FILTERED]")
}
}
return filtered
}
// sanitizeInput performs comprehensive input sanitization for log messages
func (sf *SecureFilter) sanitizeInput(input string) string {
// Remove null bytes and other control characters that could cause issues
sanitized := strings.ReplaceAll(input, "\x00", "")
sanitized = strings.ReplaceAll(sanitized, "\r", "")
// Remove potential log injection patterns
sanitized = strings.ReplaceAll(sanitized, "\n", " ") // Replace newlines with spaces
sanitized = strings.ReplaceAll(sanitized, "\t", " ") // Replace tabs with spaces
// Remove ANSI escape codes that could interfere with log parsing
ansiPattern := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
sanitized = ansiPattern.ReplaceAllString(sanitized, "")
// Remove other control characters (keep only printable ASCII and common Unicode)
controlPattern := regexp.MustCompile(`[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]`)
sanitized = controlPattern.ReplaceAllString(sanitized, "")
// Prevent log injection by escaping special characters
sanitized = strings.ReplaceAll(sanitized, "%", "%%") // Escape printf format specifiers
// Limit message length to prevent DoS via large log messages
const maxLogMessageLength = 4096
if len(sanitized) > maxLogMessageLength {
sanitized = sanitized[:maxLogMessageLength-3] + "..."
}
return sanitized
}
// FilterTransactionData provides enhanced filtering for transaction data logging
func (sf *SecureFilter) FilterTransactionData(txHash, from, to string, value, gasPrice *big.Int, data []byte) map[string]interface{} {
result := map[string]interface{}{}
// Always include sanitized transaction hash
result["tx_hash"] = sf.sanitizeInput(txHash)
switch sf.level {
case SecurityLevelDebug:
result["from"] = sf.sanitizeInput(from)
result["to"] = sf.sanitizeInput(to)
if value != nil {
result["value"] = value.String()
}
if gasPrice != nil {
result["gas_price"] = gasPrice.String()
}
result["data_size"] = len(data)
case SecurityLevelInfo:
result["from"] = sf.shortenAddress(common.HexToAddress(from))
result["to"] = sf.shortenAddress(common.HexToAddress(to))
if value != nil {
result["value_range"] = sf.getAmountRange(value)
}
if gasPrice != nil {
result["gas_price_range"] = sf.getAmountRange(gasPrice)
}
result["data_size"] = len(data)
case SecurityLevelProduction:
result["has_from"] = from != ""
result["has_to"] = to != ""
result["has_value"] = value != nil && value.Sign() > 0
result["data_size"] = len(data)
}
return result
}
// FilterSwapData creates a safe representation of swap data for logging
func (sf *SecureFilter) FilterSwapData(tokenIn, tokenOut common.Address, amountIn, amountOut *big.Int, protocol string) map[string]interface{} {
data := map[string]interface{}{
"protocol": protocol,
}
switch sf.level {
case SecurityLevelDebug:
data["tokenIn"] = tokenIn.Hex()
data["tokenOut"] = tokenOut.Hex()
data["amountIn"] = amountIn.String()
data["amountOut"] = amountOut.String()
case SecurityLevelInfo:
data["tokenInShort"] = sf.shortenAddress(tokenIn)
data["tokenOutShort"] = sf.shortenAddress(tokenOut)
data["amountRange"] = sf.getAmountRange(amountIn)
data["amountOutRange"] = sf.getAmountRange(amountOut)
case SecurityLevelProduction:
data["tokenCount"] = 2
data["hasAmounts"] = amountIn != nil && amountOut != nil
}
return data
}
// shortenAddress returns a shortened version of an address
func (sf *SecureFilter) shortenAddress(addr common.Address) string {
hex := addr.Hex()
if len(hex) >= 10 {
return hex[:6] + "..." + hex[len(hex)-4:]
}
return "[ADDR]"
}
// getAmountRange returns a range category for an amount
func (sf *SecureFilter) getAmountRange(amount *big.Int) string {
if amount == nil {
return "nil"
}
// Convert to rough USD equivalent (assuming 18 decimals)
usdValue := new(big.Float).Quo(new(big.Float).SetInt(amount), big.NewFloat(1e18))
usdFloat, _ := usdValue.Float64()
switch {
case usdFloat < 1:
return "micro"
case usdFloat < 100:
return "small"
case usdFloat < 10000:
return "medium"
case usdFloat < 1000000:
return "large"
default:
return "whale"
}
}
// SanitizeForProduction removes all sensitive data for production logging
func (sf *SecureFilter) SanitizeForProduction(data map[string]interface{}) map[string]interface{} {
sanitized := make(map[string]interface{})
for key, value := range data {
switch strings.ToLower(key) {
case "amount", "amountin", "amountout", "value", "profit", "usd", "price":
sanitized[key] = "[FILTERED]"
case "address", "token", "tokenin", "tokenout", "pool", "contract":
if addr, ok := value.(common.Address); ok {
sanitized[key] = sf.shortenAddress(addr)
} else if str, ok := value.(string); ok && strings.HasPrefix(str, "0x") && len(str) == 42 {
sanitized[key] = str[:6] + "..." + str[38:]
} else {
sanitized[key] = value
}
default:
sanitized[key] = value
}
}
return sanitized
}

View File

@@ -0,0 +1,226 @@
package logger
import (
"crypto/rand"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common"
)
func TestSecureFilterEnhanced(t *testing.T) {
tests := []struct {
name string
level SecurityLevel
message string
expectFiltered bool
expectedLevel string
}{
{
name: "Private key detection",
level: SecurityLevelProduction,
message: "private_key=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
expectFiltered: true,
expectedLevel: "CRITICAL",
},
{
name: "Address detection",
level: SecurityLevelProduction,
message: "Swapping on address 0x1234567890123456789012345678901234567890",
expectFiltered: true,
expectedLevel: "MEDIUM",
},
{
name: "Amount detection",
level: SecurityLevelInfo,
message: "Profit amount=1000000 wei detected",
expectFiltered: true,
expectedLevel: "LOW",
},
{
name: "Transaction hash detection",
level: SecurityLevelInfo,
message: "txHash=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
expectFiltered: true,
expectedLevel: "LOW",
},
{
name: "No sensitive data",
level: SecurityLevelProduction,
message: "Normal log message with no sensitive data",
expectFiltered: false,
expectedLevel: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &SecureFilterConfig{
Level: tt.level,
AuditEnabled: true,
AuditEncryption: false,
}
filter := NewSecureFilterWithConfig(config)
// Test detection without filtering yet
auditData := filter.detectSensitiveData(tt.message, nil)
if tt.expectFiltered {
if auditData == nil {
t.Errorf("Expected sensitive data detection for: %s", tt.message)
return
}
if auditData["severity"] != tt.expectedLevel {
t.Errorf("Expected severity %s, got %v", tt.expectedLevel, auditData["severity"])
}
} else {
if auditData != nil {
t.Errorf("Unexpected sensitive data detection for: %s", tt.message)
}
}
// Test the actual filtering
filtered := filter.FilterMessage(tt.message)
if tt.expectFiltered && filtered == tt.message {
t.Errorf("Expected message to be filtered, but it wasn't: %s", tt.message)
}
})
}
}
func TestSecureFilterWithEncryption(t *testing.T) {
// Generate a random encryption key
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
t.Fatalf("Failed to generate encryption key: %v", err)
}
config := &SecureFilterConfig{
Level: SecurityLevelProduction,
EncryptionKey: key,
AuditEnabled: true,
AuditEncryption: true,
}
filter := NewSecureFilterWithConfig(config)
testData := map[string]interface{}{
"test_field": "test_value",
"number": 123,
"nested": map[string]interface{}{
"inner": "value",
},
}
// Test encryption and decryption
encrypted, err := filter.encryptAuditData(testData)
if err != nil {
t.Fatalf("Failed to encrypt audit data: %v", err)
}
if encrypted == "" {
t.Fatal("Encrypted data should not be empty")
}
// Test decryption
decrypted, err := filter.DecryptAuditData(encrypted)
if err != nil {
t.Fatalf("Failed to decrypt audit data: %v", err)
}
// Verify data integrity
if decrypted["test_field"] != testData["test_field"] {
t.Errorf("Decrypted data doesn't match original")
}
}
func TestSecureFilterAddressFiltering(t *testing.T) {
filter := NewSecureFilter(SecurityLevelProduction)
address := common.HexToAddress("0x1234567890123456789012345678901234567890")
testMessage := "Processing transaction for address " + address.Hex()
filtered := filter.FilterMessage(testMessage)
// Should contain shortened address
if !strings.Contains(filtered, "0x1234...7890") {
t.Errorf("Expected shortened address in filtered message, got: %s", filtered)
}
}
func TestSecureFilterAmountFiltering(t *testing.T) {
filter := NewSecureFilter(SecurityLevelInfo)
testCases := []struct {
message string
contains string
}{
{"amount=1000000", "[AMOUNT_FILTERED]"},
{"Profit $123.45 detected", "[AMOUNT_FILTERED]"},
{"balance=999999999999999999", "[AMOUNT_FILTERED]"},
{"gasPrice=20000000000", "[AMOUNT_FILTERED]"},
}
for _, tc := range testCases {
filtered := filter.FilterMessage(tc.message)
if !strings.Contains(filtered, tc.contains) {
t.Errorf("Expected %s in filtered message for input '%s', got: %s", tc.contains, tc.message, filtered)
}
}
}
func TestSecureFilterConfiguration(t *testing.T) {
filter := NewSecureFilter(SecurityLevelDebug)
// Test initial level
if filter.GetSecurityLevel() != SecurityLevelDebug {
t.Errorf("Expected initial level to be Debug, got: %v", filter.GetSecurityLevel())
}
// Test level change
filter.SetSecurityLevel(SecurityLevelProduction)
if filter.GetSecurityLevel() != SecurityLevelProduction {
t.Errorf("Expected level to be Production, got: %v", filter.GetSecurityLevel())
}
// Test audit enabling
filter.EnableAuditLogging([]byte("test-key"))
if !filter.auditEnabled {
t.Error("Expected audit to be enabled")
}
if !filter.auditEncryption {
t.Error("Expected audit encryption to be enabled")
}
// Test audit disabling
filter.DisableAuditLogging()
if filter.auditEnabled {
t.Error("Expected audit to be disabled")
}
}
func BenchmarkSecureFilter(b *testing.B) {
filter := NewSecureFilter(SecurityLevelProduction)
testMessage := "Processing transaction 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef for address 0x1111111111111111111111111111111111111111 with amount=1000000"
b.ResetTimer()
for i := 0; i < b.N; i++ {
filter.FilterMessage(testMessage)
}
}
func BenchmarkSecureFilterEnhanced(b *testing.B) {
config := &SecureFilterConfig{
Level: SecurityLevelProduction,
AuditEnabled: true,
AuditEncryption: false,
}
filter := NewSecureFilterWithConfig(config)
testMessage := "Processing transaction 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef for address 0x1111111111111111111111111111111111111111 with amount=1000000"
b.ResetTimer()
for i := 0; i < b.N; i++ {
filter.FilterMessageEnhanced(testMessage, nil)
}
}

View File

@@ -0,0 +1,439 @@
package logger
import (
"math/big"
"regexp"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
)
func TestNewSecureFilter(t *testing.T) {
tests := []struct {
name string
level SecurityLevel
}{
{"Debug level", SecurityLevelDebug},
{"Info level", SecurityLevelInfo},
{"Production level", SecurityLevelProduction},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filter := NewSecureFilter(tt.level)
assert.NotNil(t, filter)
assert.Equal(t, tt.level, filter.level)
assert.NotNil(t, filter.amountPatterns)
assert.NotNil(t, filter.addressPatterns)
assert.NotNil(t, filter.valuePatterns)
})
}
}
func TestFilterMessage_DebugLevel(t *testing.T) {
filter := NewSecureFilter(SecurityLevelDebug)
tests := []struct {
name string
input string
expected string
}{
{
name: "Debug shows everything",
input: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789, Value: $5000.00",
expected: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789, Value: $5000.00",
},
{
name: "Large amounts shown in debug",
input: "Profit: 999999.123456789 USDC",
expected: "Profit: 999999.123456789 USDC",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filter.FilterMessage(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestFilterMessage_InfoLevel(t *testing.T) {
filter := NewSecureFilter(SecurityLevelInfo)
tests := []struct {
name string
input string
expected string
}{
{
name: "Info filters amounts but shows full addresses",
input: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789",
expected: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789",
},
{
name: "USD values filtered",
input: "Profit: $5000.00 USD",
expected: "Profit: [AMOUNT_FILTERED] USD",
},
{
name: "Multiple amounts filtered",
input: "Swap 100.0 USDC for 0.05 ETH",
expected: "Swap [AMOUNT_FILTERED]C for 0.05 ETH",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filter.FilterMessage(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestFilterMessage_ProductionLevel(t *testing.T) {
filter := NewSecureFilter(SecurityLevelProduction)
tests := []struct {
name string
input string
expected string
}{
{
name: "Production filters everything sensitive",
input: "Amount: 1000.5 ETH, Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789, Value: $5000.00",
expected: "Amount: 1000.5 ETH, Address: 0x742d...6789, Value: [AMOUNT_FILTERED]",
},
{
name: "Complex transaction filtered",
input: "Swap 1500.789 USDC from 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD to 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 for $1500.00 profit",
expected: "Swap [AMOUNT_FILTERED]C from 0xA0b8...7ACD to 0xaf88...5831 for [AMOUNT_FILTERED] profit",
},
{
name: "Multiple addresses and amounts",
input: "Transfer 500 tokens from 0x1234567890123456789012345678901234567890 to 0x0987654321098765432109876543210987654321 worth $1000",
expected: "Transfer 500 tokens from 0x1234...7890 to 0x0987...4321 worth [AMOUNT_FILTERED]",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filter.FilterMessage(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestShortenAddress(t *testing.T) {
filter := NewSecureFilter(SecurityLevelInfo)
tests := []struct {
name string
input common.Address
expected string
}{
{
name: "Standard address",
input: common.HexToAddress("0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789"),
expected: "0x742D...6789",
},
{
name: "Zero address",
input: common.HexToAddress("0x0000000000000000000000000000000000000000"),
expected: "0x0000...0000",
},
{
name: "Max address",
input: common.HexToAddress("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"),
expected: "0xFFfF...FFfF",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filter.shortenAddress(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestCategorizeAmount(t *testing.T) {
_ = NewSecureFilter(SecurityLevelInfo) // Reference to avoid unused variable warning
tests := []struct {
name string
input *big.Int
expected string
}{
{
name: "Nil amount",
input: nil,
expected: "nil",
},
{
name: "Micro amount (< $1)",
input: big.NewInt(500000000000000000), // 0.5 ETH assuming 18 decimals
expected: "micro",
},
{
name: "Small amount ($1-$100)",
input: func() *big.Int { val, _ := new(big.Int).SetString("50000000000000000000", 10); return val }(), // 50 ETH
expected: "small",
},
{
name: "Medium amount ($100-$10k)",
input: func() *big.Int { val, _ := new(big.Int).SetString("5000000000000000000000", 10); return val }(), // 5000 ETH
expected: "medium",
},
{
name: "Large amount ($10k-$1M)",
input: func() *big.Int { val, _ := new(big.Int).SetString("500000000000000000000000", 10); return val }(), // 500k ETH
expected: "large",
},
{
name: "Whale amount (>$1M)",
input: func() *big.Int { val, _ := new(big.Int).SetString("2000000000000000000000000", 10); return val }(), // 2M ETH
expected: "whale",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Note: categorizeAmount is private, so we can't test it directly
// This test would need to be adapted to test the public API that uses it
_ = tt.input // Reference to avoid unused variable warning
_ = tt.expected // Reference to avoid unused variable warning
// Just pass the test since we can't test private methods directly
assert.True(t, true, "categorizeAmount is private - testing would need public wrapper")
})
}
}
func TestSanitizeForProduction(t *testing.T) {
filter := NewSecureFilter(SecurityLevelProduction)
tests := []struct {
name string
input map[string]interface{}
expected map[string]interface{}
}{
{
name: "Amounts filtered",
input: map[string]interface{}{
"amount": 1000.5,
"amountIn": 500,
"amountOut": 750,
"value": 999.99,
"other": "should remain",
},
expected: map[string]interface{}{
"amount": "[FILTERED]",
"amountIn": "[FILTERED]",
"amountOut": "[FILTERED]",
"value": "[FILTERED]",
"other": "should remain",
},
},
{
name: "Addresses filtered and shortened",
input: map[string]interface{}{
"address": common.HexToAddress("0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789"),
"token": "0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD",
"pool": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"other": "should remain",
},
expected: map[string]interface{}{
"address": "0x742D...6789",
"token": "0xA0b8...7ACD",
"pool": "0xaf88...5831",
"other": "should remain",
},
},
{
name: "Mixed data types",
input: map[string]interface{}{
"profit": 1000.0,
"tokenOut": common.HexToAddress("0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789"),
"fee": 30,
"protocol": "UniswapV3",
"timestamp": 1640995200,
},
expected: map[string]interface{}{
"profit": "[FILTERED]",
"tokenOut": "0x742D...6789",
"fee": 30,
"protocol": "UniswapV3",
"timestamp": 1640995200,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filter.SanitizeForProduction(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestFilterMessage_ComplexScenarios(t *testing.T) {
productionFilter := NewSecureFilter(SecurityLevelProduction)
infoFilter := NewSecureFilter(SecurityLevelInfo)
tests := []struct {
name string
input string
production string
info string
}{
{
name: "MEV opportunity log",
input: "🎯 ARBITRAGE OPPORTUNITY: Swap 1500.789 USDC via 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD for profit $250.50",
production: "🎯 ARBITRAGE OPPORTUNITY: Swap [AMOUNT_FILTERED]C via 0xA0b8...7ACD for profit [AMOUNT_FILTERED]",
info: "🎯 ARBITRAGE OPPORTUNITY: Swap [AMOUNT_FILTERED]C via 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD for profit [AMOUNT_FILTERED]",
},
{
name: "Transaction log with multiple sensitive data",
input: "TX: 0x123...abc Amount: 999.123456 ETH → 1500000.5 USDC, Gas: 150000, Value: $2500000.75",
production: "TX: 0x123...abc Amount: 999.123456 ETH → [AMOUNT_FILTERED]C, Gas: 150000, Value: [AMOUNT_FILTERED]",
info: "TX: 0x123...abc Amount: 999.123456 ETH → [AMOUNT_FILTERED]C, Gas: 150000, Value: [AMOUNT_FILTERED]",
},
{
name: "Pool creation event",
input: "Pool created: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789 with 1000000.0 liquidity worth $5000000",
production: "Pool created: 0x742d...6789 with 1000000.0 liquidity worth [AMOUNT_FILTERED]",
info: "Pool created: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789 with 1000000.0 liquidity worth [AMOUNT_FILTERED]",
},
}
for _, tt := range tests {
t.Run(tt.name+" - Production", func(t *testing.T) {
result := productionFilter.FilterMessage(tt.input)
assert.Equal(t, tt.production, result)
})
t.Run(tt.name+" - Info", func(t *testing.T) {
result := infoFilter.FilterMessage(tt.input)
assert.Equal(t, tt.info, result)
})
}
}
func TestFilterMessage_EdgeCases(t *testing.T) {
filter := NewSecureFilter(SecurityLevelProduction)
tests := []struct {
name string
input string
expected string
}{
{
name: "Empty string",
input: "",
expected: "",
},
{
name: "No sensitive data",
input: "Simple log message with no sensitive information",
expected: "Simple log message with no sensitive information",
},
{
name: "Only numbers (not amounts)",
input: "Block number: 12345, Gas limit: 8000000",
expected: "Block number: 12345, Gas limit: 8000000",
},
{
name: "Scientific notation",
input: "Amount: 1.5e18 wei",
expected: "Amount: 1.5e18 wei",
},
{
name: "Multiple decimal places",
input: "Price: 1234.567890123456 tokens",
expected: "Price: 1234.567890123456 tokens",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filter.FilterMessage(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Benchmark tests
func BenchmarkFilterMessage_Production(b *testing.B) {
filter := NewSecureFilter(SecurityLevelProduction)
input := "🎯 ARBITRAGE OPPORTUNITY: Swap 1500.789 USDC via 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD for profit $250.50"
b.ResetTimer()
for i := 0; i < b.N; i++ {
filter.FilterMessage(input)
}
}
func BenchmarkFilterMessage_Info(b *testing.B) {
filter := NewSecureFilter(SecurityLevelInfo)
input := "Transaction: 1000.5 ETH from 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789 to 0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD"
b.ResetTimer()
for i := 0; i < b.N; i++ {
filter.FilterMessage(input)
}
}
func BenchmarkSanitizeForProduction(b *testing.B) {
filter := NewSecureFilter(SecurityLevelProduction)
data := map[string]interface{}{
"amount": 1000.5,
"address": common.HexToAddress("0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789"),
"profit": 250.75,
"protocol": "UniswapV3",
"token": "0xA0b86a33E6441f43E2e4A96439abFA2A69067ACD",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
filter.SanitizeForProduction(data)
}
}
func TestSecurityLevelConstants(t *testing.T) {
// Verify security level constants are defined correctly
assert.Equal(t, SecurityLevel(0), SecurityLevelDebug)
assert.Equal(t, SecurityLevel(1), SecurityLevelInfo)
assert.Equal(t, SecurityLevel(2), SecurityLevelProduction)
}
func TestRegexPatterns(t *testing.T) {
filter := NewSecureFilter(SecurityLevelProduction)
// Test that patterns are properly compiled
assert.True(t, len(filter.amountPatterns) > 0, "Should have amount patterns")
assert.True(t, len(filter.addressPatterns) > 0, "Should have address patterns")
assert.True(t, len(filter.valuePatterns) > 0, "Should have value patterns")
// Test pattern matching
testCases := []struct {
patterns []*regexp.Regexp
input string
should string
}{
{filter.amountPatterns, "amount=123", "match amount patterns"},
{filter.addressPatterns, "Address: 0x742d35Cc6AaB8f5d6649c8C4F7C6b2d123456789", "match address patterns"},
{filter.valuePatterns, "profit=$1234.56", "match value patterns"},
}
for _, tc := range testCases {
found := false
for _, pattern := range tc.patterns {
if pattern.MatchString(tc.input) {
found = true
break
}
}
assert.True(t, found, tc.should)
}
}