L1/L2 Node — Technical Design
Status: Design phase. This document describes the intended implementation. See L1/L2 Node for the problem statement and deployment modes.
RollupDriver Interface
The central abstraction. Generalizes what polygon/sync.Service does for Bor.
// rollup/driver.go
type Driver interface {
node.Lifecycle // Start() / Stop()
Type() DriverType // "optimistic", "zk", "based", "native", "consensus"
}
Each Driver owns its sub-services and drives L2 execution through the existing ExecutionClient interface:
Driver
├── DerivationPipeline — converts L1 data into L2 blocks (optimistic, zk, based)
├── Bridge — cross-chain message passing (deposits, withdrawals)
├── Sequencer — produces L2 blocks (sequencer mode only)
├── Prover — generates validity proofs (zk only)
├── L1DataSource — reads L1 state (direct in combined mode, RPC in l2-only)
└── L2Caplin — embedded CL instance for consensus rollups
The Ethereum struct in backend.go gains optional L2 fields while keeping all existing L1 fields unchanged:
type Ethereum struct {
// ... existing L1 fields unchanged ...
rollupDriver rollup.Driver // nil for pure L1
l1DataSource rollup.L1DataSource // direct or RPC-based
l2ChainDB kv.TemporalRwDB // separate L2 database
l2ExecModule *execmodule.ExecModule // separate L2 execution pipeline
}
Polygon/Bor as the First Driver
The existing Polygon integration wraps into the Driver interface without rewriting:
type BorDriver struct {
syncService *sync.Service
bridgeService *bridge.Service
heimdallService *heimdall.Service
}
func (d *BorDriver) Type() DriverType { return "bor" }
This proves the abstraction works and provides a migration path.
L1DataSource
The key optimization for combined mode. Abstracts L1 data access across node modes:
type L1DataSource interface {
BlockByNumber(ctx context.Context, num uint64) (*types.Block, error)
ReceiptsByBlock(ctx context.Context, hash common.Hash) (types.Receipts, error)
SubscribeFinalized(ctx context.Context) <-chan *types.Header
}
| Implementation | Mode | Mechanism |
|---|---|---|
DirectL1DataSource | Combined | Reads directly from L1's kv.TemporalRwDB — zero serialization, follows node/direct/ adapter pattern |
RPCL1DataSource | L2-only | JSON-RPC calls to external L1 |
L1 finality notifications trigger L2 derivation via the existing shards.Notifications mechanism (same as RPC daemon state-change notifications).
L2 Staged Sync Pipeline
A L2DefaultStages factory runs alongside the existing DefaultStages in execution/stagedsync/default_stages.go:
| Stage | Source | Notes |
|---|---|---|
| Snapshots | L2 snapshot files | Same mechanism as L1, different files |
| Derivation | L1 data via L1DataSource | New stage — rollup-type-specific; converts L1 data into L2 block headers and bodies |
| Headers | Derivation output | Reused stage, different data source |
| BlockHashes | — | Reused |
| Bodies | Derivation output | Reused stage, different data source |
| BridgeEvents | L2 state | New stage — processes deposit/withdrawal events; generalizes the existing Polygon bridge stage |
| Senders | — | Reused |
| Execution | L2 rules engine | Reused, different rules |
| StateCommit | L2 execution output | New stage — posts state roots/proofs to L1 settlement contract (sequencer mode only) |
| TxLookup | — | Reused |
| Finish | — | Reused |
New stage IDs added to execution/stagedsync/stages/:
Derivation SyncStage = "Derivation"
BridgeEvents SyncStage = "BridgeEvents"
StateCommit SyncStage = "StateCommit"
Chain Configuration Extension
Extend execution/chain/chain_config.go with an optional RollupConfig:
type Config struct {
// ... existing fields unchanged ...
Rollup *RollupConfig `json:"rollup,omitempty"` // nil for L1 chains
}
type RollupConfig struct {
Type string `json:"type"`
L1ChainID *big.Int `json:"l1ChainId"`
Portal common.Address `json:"portal"`
BatchInbox common.Address `json:"batchInbox"`
OutputOracle common.Address `json:"outputOracle"`
SequencerAddr common.Address `json:"sequencerAddr"`
L2BlockTime uint64 `json:"l2BlockTime"`
SeqWindowSize uint64 `json:"seqWindowSize"`
}
Native Rollups: Special Case
Native rollups are architecturally distinct from other types — they use the L1's own EVM to verify L2 state transitions via an EXECUTE precompile. There is no separate derivation pipeline, no separate state database, and no separate execution module.
The EXECUTE precompile is added to execution/vm/ and enabled via chain config when RollupConfig.Type == "native". A node running a native rollup looks identical to a standard L1 node.
RPC Multi-Chain Routing
In combined mode, the RPC server routes by chain ID:
- Requests include a
chainIdfield or HTTP header - Default chain ID = L1 (backwards compatible)
- L2 chain ID routes to the L2's
ExecModule, database, and tx pool - Single port, single server — no separate endpoints
Implementation Phases
| Phase | Scope |
|---|---|
| 1 | rollup.Driver, L1DataSource, Bridge, DerivationPipeline interfaces; wrap existing Polygon as BorDriver; generalize Ethereum.Start() dispatch |
| 2 | Extend datadir.Dirs with L2 paths; second kv.TemporalRwDB; second ExecModule; L2DefaultStages factory |
| 3 | DirectL1DataSource for combined mode; RPC-based L1DataSource for L2-only; L1→L2 notification; CLI flags; chain-ID routing in RPC |
| 4 | Based rollup driver — L1-sequenced derivation, bridge stage, based consensus rules; first concrete rollup targeting combined mode |
| 5 | Optimistic rollup driver — OP Stack compatible derivation, fraud proof integration, batch submission |
| 6 | Consensus rollup driver — second Caplin instance, configurable YAML, BLS aggregate signatures to L1 settlement contract, genesis tooling, embedded validator client |
| 6b | Liquid staking contracts (external Solidity — LST token, operator registry, delegation manager, reward distributor) |
| 7 | Native rollup driver — EXECUTE precompile in execution/vm/ |
| 8 | ZK rollup driver — prover integration interface, state commitment with validity proofs |
Key Files
| File | Role |
|---|---|
node/eth/backend.go:1553–1584 | Start() dispatch — generalize with Driver interface |
execution/stagedsync/default_stages.go | Add L2DefaultStages factory |
execution/chain/chain_config.go | Add RollupConfig |
polygon/sync/service.go | Reference implementation for Driver pattern |
node/direct/execution_client.go | Reference pattern for DirectL1DataSource |
node/ethconfig/config.go | Add RollupMode config |
db/datadir/dirs.go | Extend with L2 paths |
cl/clparams/config.go | All consensus timing parameters (consensus rollup) |
cmd/caplin/caplin1/run.go | Pattern for launching second Caplin instance |