ev-node Architecture Explainer
ev-node is a sovereign rollup framework that allows building rollups on any Data Availability (DA) layer. It follows a modular architecture where components can be swapped.
Reference files:
- block-architecture.md - Block package deep dive
- da-sequencing.md - DA and sequencing deep dive
Core Principles
- Zero-dependency core -
core/contains only interfaces, no external deps - Modular components - Executor, Sequencer, DA are pluggable
- Two operating modes - Aggregator (produces blocks) and Sync-only (follows chain)
- Separation of concerns - Block production, syncing, and DA submission are independent
Package Overview
| Package | Responsibility |
|---|---|
core/ | Interfaces only (Executor, Sequencer) |
types/ | Data structures (Header, Data, State, SignedHeader) |
block/ | Block lifecycle management |
execution/ | Execution layer implementations (EVM, ABCI) |
node/ | Node initialization and orchestration |
pkg/p2p/ | libp2p-based networking |
pkg/store/ | Persistent storage |
pkg/da/ | DA layer abstraction |
Block Package Deep Dive
The block package is the most complex part of ev-node. See block-architecture.md for the complete breakdown.
Component Summary
Components struct:
βββ Executor - Block production (Aggregator only)
βββ Reaper - Transaction scraping (Aggregator only)
βββ Syncer - Block synchronization
βββ Submitter - DA submission and inclusion
βββ Cache - Unified state caching
Entry Points
NewAggregatorComponents()- Full node that produces and syncs blocksNewSyncComponents()- Non-aggregator that only syncs
Key Data Types
Header - Block metadata (height, time, hashes, proposer) Data - Transaction list with metadata SignedHeader - Header with proposer signature State - Chain state (last block, app hash, DA height)
Block Production Flow (Aggregator)
Sequencer.GetNextBatch()
β
βΌ
Executor.ExecuteTxs()
β
ββββΊ SignedHeader + Data
β
ββββΊ P2P Broadcast
β
ββββΊ Submitter Queue
β
βΌ
DA Layer
Block Sync Flow (Non-Aggregator)
βββββββββββββββββββββββββββββββββββββββ
β Syncer β
βββββββββββββββ¬ββββββββββββββ¬ββββββββββ€
β DA Worker β P2P Worker β Forced β
β β β Incl. β
ββββββββ¬βββββββ΄βββββββ¬βββββββ΄βββββ¬βββββ
β β β
βββββββββββββββ΄ββββββββββββ
β
βΌ
processHeightEvent()
β
βΌ
ExecuteTxs β Update State
Data Availability Layer
The DA layer abstracts blob storage. ev-node uses Celestia but the interface is pluggable. See da-sequencing.md for full details.
Namespaces
DA uses 29-byte namespaces (1 byte version + 28 byte ID). Three namespaces are used:
| Namespace | Purpose |
|---|---|
| Header | Block headers |
| Data | Transaction data (optional, can share with header) |
| Forced Inclusion | User-submitted txs for censorship resistance |
DA Client Interface
type Client interface { Submit(ctx, data [][]byte, gasPrice, namespace, options) ResultSubmit Retrieve(ctx, height uint64, namespace) ResultRetrieve Get(ctx, ids []ID, namespace) ([]Blob, error) }
Key Files
| File | Purpose |
|---|---|
pkg/da/types/types.go | Core types (Blob, ID, Commitment) |
pkg/da/types/namespace.go | Namespace handling |
block/internal/da/client.go | DA client wrapper |
block/internal/da/forced_inclusion_retriever.go | Forced tx retrieval |
Sequencing
Sequencers order transactions for block production. See da-sequencing.md for full details.
Two Modes
| Mode | Mempool | Forced Inclusion | Use Case |
|---|---|---|---|
| Single | Yes | Yes | Traditional rollup |
| Based | No | Only source | High liveness guarantee |
Sequencer Interface
type Sequencer interface { SubmitBatchTxs(ctx, req) (*SubmitBatchTxsResponse, error) GetNextBatch(ctx, req) (*GetNextBatchResponse, error) VerifyBatch(ctx, req) (*VerifyBatchResponse, error) SetDAHeight(height uint64) GetDAHeight() uint64 }
ForceIncludedMask
Batches include a mask distinguishing tx sources:
type Batch struct { Transactions [][]byte ForceIncludedMask []bool // true = from DA (must validate) }
This allows the execution layer to skip validation for already-validated mempool txs.
Key Files
| File | Purpose |
|---|---|
core/sequencer/sequencing.go | Core interface |
pkg/sequencers/single/sequencer.go | Hybrid sequencer |
pkg/sequencers/based/sequencer.go | Pure DA sequencer |
pkg/sequencers/common/checkpoint.go | Shared checkpoint logic |
Forced Inclusion
Forced inclusion prevents sequencer censorship:
- User submits tx directly to DA layer
- Syncer detects tx in forced-inclusion namespace
- Grace period starts (adjusts based on block fullness)
- If not included by sequencer within grace period β sequencer marked malicious
- Tx gets included regardless
Key Files
| File | Purpose |
|---|---|
block/public.go | Exported types and factories |
block/components.go | Component creation |
block/internal/executing/executor.go | Block production |
block/internal/syncing/syncer.go | Sync orchestration |
block/internal/submitting/submitter.go | DA submission |
block/internal/cache/manager.go | Unified cache |
Common Questions
How does block production work?
The Executor runs executionLoop():
- Wait for block time or new transactions
- Get batch from sequencer
- Execute via execution layer
- Create SignedHeader + Data
- Broadcast to P2P
- Queue for DA submission
How does syncing work?
The Syncer coordinates three workers:
- DA Worker - Fetches confirmed blocks from DA
- P2P Worker - Receives gossiped blocks
- Forced Inclusion - Monitors for censored txs
All feed into processHeightEvent() which validates and executes.
What happens if DA submission fails?
Submitter has retry logic with exponential backoff. Status codes:
TooBig- Splits blob into chunksAlreadyInMempool- Skips (duplicate)NotIncludedInBlock- Retries with backoffContextCanceled- Request canceled
How is state recovered after crash?
The Replayer syncs execution layer from disk:
- Load last committed height from store
- Check execution layer height
- Replay any missing blocks
- Ensure consistency before starting
Architecture Diagrams
For detailed component diagrams and state machines, see block-architecture.md.