Skip to main content

L1/L2 Node — Technical Design

note

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
}
ImplementationModeMechanism
DirectL1DataSourceCombinedReads directly from L1's kv.TemporalRwDB — zero serialization, follows node/direct/ adapter pattern
RPCL1DataSourceL2-onlyJSON-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:

StageSourceNotes
SnapshotsL2 snapshot filesSame mechanism as L1, different files
DerivationL1 data via L1DataSourceNew stage — rollup-type-specific; converts L1 data into L2 block headers and bodies
HeadersDerivation outputReused stage, different data source
BlockHashesReused
BodiesDerivation outputReused stage, different data source
BridgeEventsL2 stateNew stage — processes deposit/withdrawal events; generalizes the existing Polygon bridge stage
SendersReused
ExecutionL2 rules engineReused, different rules
StateCommitL2 execution outputNew stage — posts state roots/proofs to L1 settlement contract (sequencer mode only)
TxLookupReused
FinishReused

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 chainId field 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

PhaseScope
1rollup.Driver, L1DataSource, Bridge, DerivationPipeline interfaces; wrap existing Polygon as BorDriver; generalize Ethereum.Start() dispatch
2Extend datadir.Dirs with L2 paths; second kv.TemporalRwDB; second ExecModule; L2DefaultStages factory
3DirectL1DataSource for combined mode; RPC-based L1DataSource for L2-only; L1→L2 notification; CLI flags; chain-ID routing in RPC
4Based rollup driver — L1-sequenced derivation, bridge stage, based consensus rules; first concrete rollup targeting combined mode
5Optimistic rollup driver — OP Stack compatible derivation, fraud proof integration, batch submission
6Consensus rollup driver — second Caplin instance, configurable YAML, BLS aggregate signatures to L1 settlement contract, genesis tooling, embedded validator client
6bLiquid staking contracts (external Solidity — LST token, operator registry, delegation manager, reward distributor)
7Native rollup driver — EXECUTE precompile in execution/vm/
8ZK rollup driver — prover integration interface, state commitment with validity proofs

Key Files

FileRole
node/eth/backend.go:1553–1584Start() dispatch — generalize with Driver interface
execution/stagedsync/default_stages.goAdd L2DefaultStages factory
execution/chain/chain_config.goAdd RollupConfig
polygon/sync/service.goReference implementation for Driver pattern
node/direct/execution_client.goReference pattern for DirectL1DataSource
node/ethconfig/config.goAdd RollupMode config
db/datadir/dirs.goExtend with L2 paths
cl/clparams/config.goAll consensus timing parameters (consensus rollup)
cmd/caplin/caplin1/run.goPattern for launching second Caplin instance