mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
7.5 KiB
7.5 KiB
Lux EVM Architecture Guidelines
This document outlines the architectural principles and implementation guidelines for the Lux EVM codebase.
1. Module Architecture
Core Modules
evm/
├── core/ # State, consensus, block/tx formats (no external deps)
├── interfaces/ # Minimal interface definitions
├── common/ # Shared types and utilities
├── plugin/ # Pluggable components
│ ├── db/ # Database backends
│ ├── sync/ # State sync implementations
│ └── validator/ # Block validators
├── rpc/ # RPC layer
├── node/ # Node orchestration
└── cli/ # Command-line interface
Dependency Rules
- Core → interfaces only
- Plugin → core, interfaces
- Node/RPC/CLI → core, interfaces, plugin
- No upward imports: Core must never import from plugin, node, rpc, or cli
2. Interface Design
Example: ChainReader Interface
// interfaces/blockchain.go
package interfaces
type ChainReader interface {
Config() ChainConfig
CurrentHeader() *types.Header
GetHeader(hash common.Hash, number uint64) *types.Header
}
type ChainConfig interface {
GetChainID() *big.Int
IsActive(fork ForkID, blockNum *big.Int, timestamp uint64) bool
}
Adapter Pattern
// adapters/geth_adapter.go
package adapters
type GethChainConfigAdapter struct {
*gethparams.ChainConfig
}
func (g *GethChainConfigAdapter) IsActive(fork ForkID, blockNum *big.Int, timestamp uint64) bool {
switch fork {
case ForkShanghai:
return g.IsShanghai(blockNum, timestamp)
// ... other forks
}
}
3. Fork Management
Centralized Fork Registry
// common/forks/registry.go
package forks
type ForkID string
const (
ForkHomestead ForkID = "homestead"
ForkByzantium ForkID = "byzantium"
ForkShanghai ForkID = "shanghai"
ForkCancun ForkID = "cancun"
// Lux-specific
ForkEVM ForkID = "evm"
ForkDurango ForkID = "durango"
ForkEtna ForkID = "etna"
ForkFortuna ForkID = "fortuna"
ForkGranite ForkID = "granite"
)
type Fork struct {
ID ForkID
Block *big.Int // nil for time-based
Timestamp *uint64 // nil for block-based
AlwaysActive bool // true for historical forks
}
var DefaultForks = []Fork{
// All historical forks enabled from genesis
{ID: ForkHomestead, AlwaysActive: true},
{ID: ForkByzantium, AlwaysActive: true},
{ID: ForkShanghai, AlwaysActive: true},
{ID: ForkCancun, AlwaysActive: true},
{ID: ForkEVM, AlwaysActive: true},
{ID: ForkDurango, AlwaysActive: true},
{ID: ForkEtna, AlwaysActive: true},
{ID: ForkFortuna, AlwaysActive: true},
{ID: ForkGranite, AlwaysActive: true},
}
// Usage throughout codebase:
// if cfg.IsActive(forks.ForkCancun, blockNum, timestamp) { ... }
4. Database Abstraction
Clean DB Interface
// interfaces/database.go
package interfaces
type Database interface {
Get(key []byte) ([]byte, error)
Put(key []byte, value []byte) error
Delete(key []byte) error
NewBatch() Batch
Close() error
}
type Batch interface {
Put(key []byte, value []byte) error
Delete(key []byte) error
Write() error
Reset()
}
Plugin Registration
// plugin/db/registry.go
package db
var drivers = make(map[string]func(config.DB) (interfaces.Database, error))
func Register(name string, driver func(config.DB) (interfaces.Database, error)) {
drivers[name] = driver
}
func Open(name string, config config.DB) (interfaces.Database, error) {
driver, ok := drivers[name]
if !ok {
return nil, fmt.Errorf("unknown database driver: %s", name)
}
return driver(config)
}
5. Precompile Architecture
Clean Precompile Interface
// interfaces/precompile.go
package interfaces
type PrecompileContract interface {
Address() common.Address
RequiredGas(input []byte) uint64
Run(input []byte, caller common.Address, state StateDB, readOnly bool) ([]byte, error)
}
type PrecompileRegistry interface {
Register(contract PrecompileContract)
Get(addr common.Address) (PrecompileContract, bool)
IsActive(addr common.Address, rules ChainRules) bool
}
6. Build Configuration
Single Build Command
# Makefile
.DEFAULT_GOAL := build
# Single canonical build
build:
go build -tags="sqlite,rocksdb" -ldflags "-X main.version=$(VERSION)" ./...
# Test with coverage
test:
go test -v -cover ./...
# Lint
lint:
golangci-lint run
# All CI checks
ci: lint test build
CI Configuration
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
go-version: [1.21.x, 1.22.x]
db-backend: [sqlite, rocksdb, leveldb]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- run: make ci DB_BACKEND=${{ matrix.db-backend }}
7. Code Generation
Interface Mocks
//go:generate moq -out mocks/chain_reader_mock.go -pkg mocks . ChainReader
//go:generate moq -out mocks/state_db_mock.go -pkg mocks . StateDB
Delegate Pattern
//go:generate delegate -type ChainConfig -from github.com/luxfi/geth/params.ChainConfig -to wrapped
8. Metrics & Observability
Standardized Metrics
// common/metrics/registry.go
package metrics
type Counter interface {
Inc(delta int64)
Get() int64
}
type Timer interface {
UpdateSince(start time.Time)
}
var (
blockImportTime = NewTimer("block_import_time")
txProcessed = NewCounter("tx_processed_total")
stateReads = NewCounter("state_reads_total")
)
9. Migration Path
Phase 1: Structure (Week 1-2)
- Create interfaces/ package with minimal types
- Move shared types to common/
- Create plugin/ directory structure
Phase 2: Decouple Core (Week 3-4)
- Remove precompile imports from core
- Move fork logic to centralized registry
- Create adapter layer for geth types
Phase 3: Simplify Build (Week 5)
- Remove unused build tags
- Standardize on single build command
- Update CI configuration
Phase 4: Enhance (Week 6+)
- Add comprehensive metrics
- Implement code generation
- Write architecture tests
10. Architecture Tests
Enforce Module Boundaries
// architecture_test.go
package evm_test
import (
"go/build"
"testing"
)
func TestNoCoreImportsFromPlugins(t *testing.T) {
pkg, err := build.Import("github.com/luxfi/evm/core", "", 0)
require.NoError(t, err)
for _, imp := range pkg.Imports {
assert.NotContains(t, imp, "plugin/")
assert.NotContains(t, imp, "node/")
assert.NotContains(t, imp, "rpc/")
}
}
Best Practices
- Interface Segregation: Keep interfaces small and focused
- Dependency Injection: Pass interfaces, not concrete types
- Feature Flags: Use config, not code, for toggles
- Documentation: Every package must have a README
- Benchmarks: Track performance of critical paths
- Error Handling: Wrap errors with context
- Logging: Structured logging with context
Conclusion
By following these guidelines, we achieve:
- Clean module boundaries
- No import cycles
- Easy testing and mocking
- Simple build process
- Clear upgrade path
- Better performance visibility
This architecture supports rapid iteration while maintaining code quality and performance.