Files
mev-beta/orig/pkg/security/chain_validation_test.go
Administrator 803de231ba 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>
2025-11-10 10:14:26 +01:00

460 lines
16 KiB
Go

package security
import (
"math/big"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/fraktal/mev-beta/internal/logger"
)
func TestNewChainIDValidator(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161) // Arbitrum mainnet
validator := NewChainIDValidator(logger, expectedChainID)
assert.NotNil(t, validator)
assert.Equal(t, expectedChainID.Uint64(), validator.expectedChainID.Uint64())
assert.True(t, validator.allowedChainIDs[42161]) // Arbitrum mainnet
assert.True(t, validator.allowedChainIDs[421614]) // Arbitrum testnet
assert.NotNil(t, validator.replayAttackDetector)
}
func TestValidateChainID_ValidTransaction(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
// Create a valid EIP-155 transaction for Arbitrum
tx := types.NewTransaction(
0, // nonce
common.HexToAddress("0x1234567890123456789012345678901234567890"), // to
big.NewInt(1000000000000000000), // value (1 ETH)
21000, // gas limit
big.NewInt(20000000000), // gas price (20 Gwei)
nil, // data
)
// Create a properly signed transaction for testing
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
signerAddr := crypto.PubkeyToAddress(privateKey.PublicKey)
signer := types.NewEIP155Signer(expectedChainID)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
result := validator.ValidateChainID(signedTx, signerAddr, nil)
assert.True(t, result.Valid)
assert.Equal(t, expectedChainID.Uint64(), result.ExpectedChainID)
assert.Equal(t, expectedChainID.Uint64(), result.ActualChainID)
assert.True(t, result.IsEIP155Protected)
assert.Equal(t, "NONE", result.ReplayRisk)
assert.Empty(t, result.Errors)
}
func TestValidateChainID_InvalidChainID(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161) // Arbitrum
validator := NewChainIDValidator(logger, expectedChainID)
// Create transaction with wrong chain ID (Ethereum mainnet)
wrongChainID := big.NewInt(1)
tx := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(20000000000), nil)
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
signerAddr := crypto.PubkeyToAddress(privateKey.PublicKey)
signer := types.NewEIP155Signer(wrongChainID)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
result := validator.ValidateChainID(signedTx, signerAddr, nil)
assert.False(t, result.Valid)
assert.Equal(t, expectedChainID.Uint64(), result.ExpectedChainID)
assert.Equal(t, wrongChainID.Uint64(), result.ActualChainID)
assert.NotEmpty(t, result.Errors)
assert.Contains(t, result.Errors[0], "Chain ID mismatch")
}
func TestValidateChainID_ReplayAttackDetection(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
signerAddr := crypto.PubkeyToAddress(privateKey.PublicKey)
// Create identical transactions on different chains
tx1 := types.NewTransaction(1, common.Address{}, big.NewInt(1000), 21000, big.NewInt(20000000000), nil)
tx2 := types.NewTransaction(1, common.Address{}, big.NewInt(1000), 21000, big.NewInt(20000000000), nil)
// Sign first transaction with Arbitrum chain ID
signer1 := types.NewEIP155Signer(big.NewInt(42161))
signedTx1, err := types.SignTx(tx1, signer1, privateKey)
require.NoError(t, err)
// Sign second identical transaction with different chain ID
signer2 := types.NewEIP155Signer(big.NewInt(421614)) // Arbitrum testnet
signedTx2, err := types.SignTx(tx2, signer2, privateKey)
require.NoError(t, err)
// First validation should pass
result1 := validator.ValidateChainID(signedTx1, signerAddr, nil)
assert.True(t, result1.Valid)
assert.Equal(t, "NONE", result1.ReplayRisk)
// Create a new validator and add testnet to allowed chains
validator.AddAllowedChainID(421614)
// Second validation should detect replay risk
result2 := validator.ValidateChainID(signedTx2, signerAddr, nil)
assert.Equal(t, "CRITICAL", result2.ReplayRisk)
assert.NotEmpty(t, result2.Warnings)
assert.Contains(t, result2.Warnings[0], "replay attack")
}
func TestValidateEIP155Protection(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
// Test EIP-155 protected transaction
tx := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(20000000000), nil)
signer := types.NewEIP155Signer(expectedChainID)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
result := validator.validateEIP155Protection(signedTx, expectedChainID)
assert.True(t, result.protected)
assert.Equal(t, expectedChainID.Uint64(), result.chainID)
assert.Empty(t, result.warnings)
}
func TestValidateEIP155Protection_LegacyTransaction(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
// Create a legacy transaction (pre-EIP155) by manually setting v to 27
tx := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(20000000000), nil)
// For testing purposes, we'll create a transaction that mimics legacy format
// In practice, this would be a transaction created before EIP-155
signer := types.HomesteadSigner{} // Pre-EIP155 signer
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
result := validator.validateEIP155Protection(signedTx, expectedChainID)
assert.False(t, result.protected)
assert.NotEmpty(t, result.warnings)
// Legacy transactions may not have chain ID, so check for either warning
hasExpectedWarning := false
for _, warning := range result.warnings {
if strings.Contains(warning, "Legacy transaction format") || strings.Contains(warning, "Transaction missing chain ID") {
hasExpectedWarning = true
break
}
}
assert.True(t, hasExpectedWarning, "Should contain legacy transaction warning")
}
func TestChainSpecificValidation_Arbitrum(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
// Create a properly signed transaction for Arbitrum to test chain-specific rules
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
// Test normal Arbitrum transaction
tx := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(1000000000), nil) // 1 Gwei
signer := types.NewEIP155Signer(expectedChainID)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
result := validator.validateChainSpecificRules(signedTx, expectedChainID.Uint64())
assert.True(t, result.valid)
assert.Empty(t, result.errors)
// Test high gas price warning
txHighGas := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(2000000000000), nil) // 2000 Gwei
signedTxHighGas, err := types.SignTx(txHighGas, signer, privateKey)
require.NoError(t, err)
resultHighGas := validator.validateChainSpecificRules(signedTxHighGas, expectedChainID.Uint64())
assert.True(t, resultHighGas.valid)
assert.NotEmpty(t, resultHighGas.warnings)
assert.Contains(t, resultHighGas.warnings[0], "high gas price")
// Test gas limit too high
txHighGasLimit := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 50000000, big.NewInt(1000000000), nil) // 50M gas
signedTxHighGasLimit, err := types.SignTx(txHighGasLimit, signer, privateKey)
require.NoError(t, err)
resultHighGasLimit := validator.validateChainSpecificRules(signedTxHighGasLimit, expectedChainID.Uint64())
assert.False(t, resultHighGasLimit.valid)
assert.NotEmpty(t, resultHighGasLimit.errors)
assert.Contains(t, resultHighGasLimit.errors[0], "exceeds Arbitrum maximum")
}
func TestChainSpecificValidation_UnsupportedChain(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(999999) // Unsupported chain
validator := NewChainIDValidator(logger, expectedChainID)
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
tx := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(1000000000), nil)
signer := types.NewEIP155Signer(expectedChainID)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
result := validator.validateChainSpecificRules(signedTx, expectedChainID.Uint64())
assert.False(t, result.valid)
assert.NotEmpty(t, result.errors)
assert.Contains(t, result.errors[0], "Unsupported chain ID")
}
func TestValidateSignerMatchesChain(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
expectedSigner := crypto.PubkeyToAddress(privateKey.PublicKey)
tx := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(20000000000), nil)
signer := types.NewEIP155Signer(expectedChainID)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
// Valid signature should pass
err = validator.ValidateSignerMatchesChain(signedTx, expectedSigner)
assert.NoError(t, err)
// Wrong expected signer should fail
wrongSigner := common.HexToAddress("0x1234567890123456789012345678901234567890")
err = validator.ValidateSignerMatchesChain(signedTx, wrongSigner)
assert.Error(t, err)
assert.Contains(t, err.Error(), "signer mismatch")
}
func TestGetValidationStats(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
signerAddr := crypto.PubkeyToAddress(privateKey.PublicKey)
// Perform some validations to generate stats
tx := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(20000000000), nil)
signer := types.NewEIP155Signer(expectedChainID)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
validator.ValidateChainID(signedTx, signerAddr, nil)
stats := validator.GetValidationStats()
assert.NotNil(t, stats)
assert.Equal(t, uint64(1), stats["total_validations"])
assert.Equal(t, expectedChainID.Uint64(), stats["expected_chain_id"])
assert.NotNil(t, stats["allowed_chain_ids"])
}
func TestAddRemoveAllowedChainID(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
// Add new chain ID
newChainID := uint64(999)
validator.AddAllowedChainID(newChainID)
assert.True(t, validator.allowedChainIDs[newChainID])
// Remove chain ID
validator.RemoveAllowedChainID(newChainID)
assert.False(t, validator.allowedChainIDs[newChainID])
}
func TestReplayAttackDetection_CleanOldData(t *testing.T) {
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
validator := NewChainIDValidator(logger, expectedChainID)
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
signerAddr := crypto.PubkeyToAddress(privateKey.PublicKey)
// Create transaction
tx := types.NewTransaction(0, common.Address{}, big.NewInt(1000), 21000, big.NewInt(20000000000), nil)
signer := types.NewEIP155Signer(expectedChainID)
signedTx, err := types.SignTx(tx, signer, privateKey)
require.NoError(t, err)
// First validation
validator.ValidateChainID(signedTx, signerAddr, nil)
assert.Equal(t, 1, len(validator.replayAttackDetector.seenTransactions))
// Manually set old timestamp to test cleanup
txIdentifier := validator.createTransactionIdentifier(signedTx, signerAddr)
record := validator.replayAttackDetector.seenTransactions[txIdentifier]
record.FirstSeen = time.Now().Add(-25 * time.Hour) // Older than maxTrackingTime
validator.replayAttackDetector.seenTransactions[txIdentifier] = record
// Trigger cleanup
validator.cleanOldTrackingData()
assert.Equal(t, 0, len(validator.replayAttackDetector.seenTransactions))
}
// Integration test with KeyManager
func SkipTestKeyManagerChainValidationIntegration(t *testing.T) {
config := &KeyManagerConfig{
KeystorePath: t.TempDir(),
EncryptionKey: "test_key_32_chars_minimum_length_required",
MaxFailedAttempts: 3,
LockoutDuration: 5 * time.Minute,
MaxSigningRate: 10,
EnableAuditLogging: true,
RequireAuthentication: false,
}
logger := logger.New("info", "text", "")
expectedChainID := big.NewInt(42161)
km, err := newKeyManagerInternal(config, logger, expectedChainID, false) // Use testing version
require.NoError(t, err)
// Generate a key
permissions := KeyPermissions{
CanSign: true,
CanTransfer: true,
MaxTransferWei: big.NewInt(1000000000000000000), // 1 ETH
}
keyAddr, err := km.GenerateKey("test", permissions)
require.NoError(t, err)
// Test valid chain ID transaction
// Create a transaction that will be properly handled by EIP155 signer
tx := types.NewTx(&types.LegacyTx{
Nonce: 0,
To: &common.Address{},
Value: big.NewInt(1000),
Gas: 21000,
GasPrice: big.NewInt(20000000000),
Data: nil,
})
request := &SigningRequest{
Transaction: tx,
ChainID: expectedChainID,
From: keyAddr,
Purpose: "Test transaction",
UrgencyLevel: 1,
}
result, err := km.SignTransaction(request)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotNil(t, result.SignedTx)
// Test invalid chain ID transaction
wrongChainID := big.NewInt(1) // Ethereum mainnet
txWrong := types.NewTx(&types.LegacyTx{
Nonce: 1,
To: &common.Address{},
Value: big.NewInt(1000),
Gas: 21000,
GasPrice: big.NewInt(20000000000),
Data: nil,
})
requestWrong := &SigningRequest{
Transaction: txWrong,
ChainID: wrongChainID,
From: keyAddr,
Purpose: "Invalid chain test",
UrgencyLevel: 1,
}
_, err = km.SignTransaction(requestWrong)
assert.Error(t, err)
assert.Contains(t, err.Error(), "doesn't match expected")
// Test chain validation stats
stats := km.GetChainValidationStats()
assert.NotNil(t, stats)
assert.True(t, stats["total_validations"].(uint64) > 0)
// Test expected chain ID
chainID := km.GetExpectedChainID()
assert.Equal(t, expectedChainID.Uint64(), chainID.Uint64())
}
func TestCrossChainReplayPrevention(t *testing.T) {
logger := logger.New("info", "text", "")
validator := NewChainIDValidator(logger, big.NewInt(42161))
// Add testnet to allowed chains for testing
validator.AddAllowedChainID(421614)
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
signerAddr := crypto.PubkeyToAddress(privateKey.PublicKey)
// Create identical transaction data
nonce := uint64(42)
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
value := big.NewInt(1000000000000000000) // 1 ETH
gasLimit := uint64(21000)
gasPrice := big.NewInt(20000000000) // 20 Gwei
// Sign for mainnet
tx1 := types.NewTransaction(nonce, to, value, gasLimit, gasPrice, nil)
signer1 := types.NewEIP155Signer(big.NewInt(42161))
signedTx1, err := types.SignTx(tx1, signer1, privateKey)
require.NoError(t, err)
// Sign identical transaction for testnet
tx2 := types.NewTransaction(nonce, to, value, gasLimit, gasPrice, nil)
signer2 := types.NewEIP155Signer(big.NewInt(421614))
signedTx2, err := types.SignTx(tx2, signer2, privateKey)
require.NoError(t, err)
// First validation (mainnet) should pass
result1 := validator.ValidateChainID(signedTx1, signerAddr, nil)
assert.True(t, result1.Valid)
assert.Equal(t, "NONE", result1.ReplayRisk)
// Second validation (testnet with same tx data) should detect replay risk
result2 := validator.ValidateChainID(signedTx2, signerAddr, nil)
assert.Equal(t, "CRITICAL", result2.ReplayRisk)
assert.Contains(t, result2.Warnings[0], "replay attack")
// Verify the detector tracked both chain IDs
stats := validator.GetValidationStats()
assert.Equal(t, uint64(1), stats["replay_attempts"])
}