Repository: ethereum-optimism/optimism
Pull request #6897 (2023-08-18 01:28:44)

chore: technical-documents rename
**Description**

Renames `technical-documents` to `docs` to use as a more general document directory for monorepo-specific implementation details.

This can include things like detailed contributing rules, reviewer standards, specs, architecture docs, etc etc.



Patch:
diff --git a/.github/mergify.yml b/.github/mergify.yml
index f1ac5475d..8963ea05f 100644
--- a/.github/mergify.yml
+++ b/.github/mergify.yml
@@ -300,7 +300,7 @@ pull_request_rules:
           - A-proxyd
   - name: Add M-docs label
     conditions:
-      - 'files~=^(technical-documents|specs)\/'
+      - 'files~=^(docs|specs)\/'
       - '#label<5'
     actions:
       label:
diff --git a/README.md b/README.md
index 86b00904d..dab53b675 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-
 <div align="center">
   <br />
   <br />
@@ -48,13 +47,7 @@ Refer to the Directory Structure section below to understand which packages are
 ## Directory Structure
 
 <pre>
-~~ Production ~~
-├── <a href="./packages">packages</a>
-│   ├── <a href="./packages/common-ts">common-ts</a>: Common tools for building apps in TypeScript
-│   ├── <a href="./packages/contracts-bedrock">contracts-bedrock</a>: Bedrock smart contracts.
-│   ├── <a href="./packages/core-utils">core-utils</a>: Low-level utilities that make building Optimism easier
-│   ├── <a href="./packages/chain-mon">chain-mon</a>: Chain monitoring services
-│   └── <a href="./packages/sdk">sdk</a>: provides a set of tools for interacting with Optimism
+├── <a href="./docs">docs</a>: A collection of documents including audits and post-mortems
 ├── <a href="./op-bindings">op-bindings</a>: Go bindings for Bedrock smart contracts.
 ├── <a href="./op-batcher">op-batcher</a>: L2-Batch Submitter, submits bundles of batches to L1
 ├── <a href="./op-bootnode">op-bootnode</a>: Standalone op-node discovery bootnode
@@ -70,19 +63,15 @@ Refer to the Directory Structure section below to understand which packages are
 ├── <a href="./op-signer">op-signer</a>: Client signer
 ├── <a href="./op-wheel">op-wheel</a>: Database utilities
 ├── <a href="./ops-bedrock">ops-bedrock</a>: Bedrock devnet work
-├── <a href="./proxyd">proxyd</a>: Configurable RPC request router and proxy
-└── <a href="./specs">specs</a>: Specs of the rollup starting at the Bedrock upgrade
-
-~~ Pre-BEDROCK ~~
 ├── <a href="./packages">packages</a>
+│   ├── <a href="./packages/chain-mon">chain-mon</a>: Chain monitoring services
 │   ├── <a href="./packages/common-ts">common-ts</a>: Common tools for building apps in TypeScript
+│   ├── <a href="./packages/contracts-ts">contracts-ts</a>: ABI and Address constants
+│   ├── <a href="./packages/contracts-bedrock">contracts-bedrock</a>: Bedrock smart contracts
 │   ├── <a href="./packages/core-utils">core-utils</a>: Low-level utilities that make building Optimism easier
-│   ├── <a href="./packages/chain-mon">chain-mon</a>: Chain monitoring services
 │   └── <a href="./packages/sdk">sdk</a>: provides a set of tools for interacting with Optimism
-├── <a href="./indexer">indexer</a>: indexes and syncs transactions
-├── <a href="./op-exporter">op-exporter</a>: A prometheus exporter to collect/serve metrics from an Optimism node
 ├── <a href="./proxyd">proxyd</a>: Configurable RPC request router and proxy
-└── <a href="./technical-documents">technical-documents</a>: audits and post-mortem documents
+└── <a href="./specs">specs</a>: Specs of the rollup starting at the Bedrock upgrade
 </pre>
 
 ## Branching Model
diff --git a/indexer/cli/cli.go b/indexer/cli/cli.go
index 5bfa81f6c..d97a6d402 100644
--- a/indexer/cli/cli.go
+++ b/indexer/cli/cli.go
@@ -40,7 +40,7 @@ func runIndexer(ctx *cli.Context) error {
 		return err
 	}
 
-	indexer, err := indexer.NewIndexer(cfg.Chain, cfg.RPCs, db, logger)
+	indexer, err := indexer.NewIndexer(logger, cfg.Chain, cfg.RPCs, db)
 	if err != nil {
 		return err
 	}
diff --git a/indexer/config/config.go b/indexer/config/config.go
index 6c3969d56..c2332f5fb 100644
--- a/indexer/config/config.go
+++ b/indexer/config/config.go
@@ -3,7 +3,6 @@ package config
 import (
 	"fmt"
 	"os"
-	"reflect"
 
 	"github.com/BurntSushi/toml"
 	"github.com/ethereum/go-ethereum/common"
@@ -23,31 +22,18 @@ type Config struct {
 
 // fetch this via onchain config from RPCsConfig and remove from config in future
 type L1Contracts struct {
-	OptimismPortal         common.Address `toml:"optimism-portal"`
-	L2OutputOracle         common.Address `toml:"l2-output-oracle"`
-	L1CrossDomainMessenger common.Address `toml:"l1-cross-domain-messenger"`
-	L1StandardBridge       common.Address `toml:"l1-standard-bridge"`
-	L1ERC721Bridge         common.Address `toml:"l1-erc721-bridge"`
+	OptimismPortalProxy         common.Address `toml:"optimism-portal"`
+	L2OutputOracleProxy         common.Address `toml:"l2-output-oracle"`
+	L1CrossDomainMessengerProxy common.Address `toml:"l1-cross-domain-messenger"`
+	L1StandardBridgeProxy       common.Address `toml:"l1-standard-bridge"`
 
-	// Some more contracts -- ProxyAdmin, SystemConfig, etcc
+	// Some more contracts -- L1ERC721Bridge, ProxyAdmin, SystemConfig, etc
 	// Ignore the auxiliary contracts?
 
 	// Legacy contracts? We'll add this in to index the legacy chain.
 	// Remove afterwards?
 }
 
-func (c L1Contracts) ToSlice() []common.Address {
-	fields := reflect.VisibleFields(reflect.TypeOf(c))
-	v := reflect.ValueOf(c)
-
-	contracts := make([]common.Address, len(fields))
-	for i, field := range fields {
-		contracts[i] = (v.FieldByName(field.Name).Interface()).(common.Address)
-	}
-
-	return contracts
-}
-
 // ChainConfig configures of the chain being indexed
 type ChainConfig struct {
 	// Configure known chains with the l2 chain id
diff --git a/indexer/config/presets.go b/indexer/config/presets.go
index 5d00415d1..4fb1ee263 100644
--- a/indexer/config/presets.go
+++ b/indexer/config/presets.go
@@ -10,54 +10,44 @@ import (
 var presetL1Contracts = map[int]L1Contracts{
 	// OP Mainnet
 	10: {
-		OptimismPortal:         common.HexToAddress("0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"),
-		L2OutputOracle:         common.HexToAddress("0xdfe97868233d1aa22e815a266982f2cf17685a27"),
-		L1CrossDomainMessenger: common.HexToAddress("0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1"),
-		L1StandardBridge:       common.HexToAddress("0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1"),
-		L1ERC721Bridge:         common.HexToAddress("0x5a7749f83b81B301cAb5f48EB8516B986DAef23D"),
+		OptimismPortalProxy:         common.HexToAddress("0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"),
+		L2OutputOracleProxy:         common.HexToAddress("0xdfe97868233d1aa22e815a266982f2cf17685a27"),
+		L1CrossDomainMessengerProxy: common.HexToAddress("0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1"),
+		L1StandardBridgeProxy:       common.HexToAddress("0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1"),
 	},
 	// OP Goerli
 	420: {
-		OptimismPortal:         common.HexToAddress("0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383"),
-		L2OutputOracle:         common.HexToAddress("0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0"),
-		L1CrossDomainMessenger: common.HexToAddress("0x5086d1eEF304eb5284A0f6720f79403b4e9bE294"),
-		L1StandardBridge:       common.HexToAddress("0x636Af16bf2f682dD3109e60102b8E1A089FedAa8"),
-		L1ERC721Bridge:         common.HexToAddress("0x8DD330DdE8D9898d43b4dc840Da27A07dF91b3c9"),
+		OptimismPortalProxy:         common.HexToAddress("0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383"),
+		L2OutputOracleProxy:         common.HexToAddress("0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0"),
+		L1CrossDomainMessengerProxy: common.HexToAddress("0x5086d1eEF304eb5284A0f6720f79403b4e9bE294"),
+		L1StandardBridgeProxy:       common.HexToAddress("0x636Af16bf2f682dD3109e60102b8E1A089FedAa8"),
 	},
 	// Base Mainnet
 	8453: {
-		OptimismPortal:         common.HexToAddress("0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"),
-		L2OutputOracle:         common.HexToAddress("0x56315b90c40730925ec5485cf004d835058518A0"),
-		L1CrossDomainMessenger: common.HexToAddress("0x866E82a600A1414e583f7F13623F1aC5d58b0Afa"),
-		L1StandardBridge:       common.HexToAddress("0x3154Cf16ccdb4C6d922629664174b904d80F2C35"),
-		// FIXME update this to the correct address
-		L1ERC721Bridge: common.HexToAddress("0x0000000000000000000000000000000000000000"),
+		OptimismPortalProxy:         common.HexToAddress("0x49048044D57e1C92A77f79988d21Fa8fAF74E97e"),
+		L2OutputOracleProxy:         common.HexToAddress("0x56315b90c40730925ec5485cf004d835058518A0"),
+		L1CrossDomainMessengerProxy: common.HexToAddress("0x866E82a600A1414e583f7F13623F1aC5d58b0Afa"),
+		L1StandardBridgeProxy:       common.HexToAddress("0x3154Cf16ccdb4C6d922629664174b904d80F2C35"),
 	},
 	// Base Goerli
 	84531: {
-		OptimismPortal:         common.HexToAddress("0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA"),
-		L2OutputOracle:         common.HexToAddress("0x2A35891ff30313CcFa6CE88dcf3858bb075A2298"),
-		L1CrossDomainMessenger: common.HexToAddress("0x8e5693140eA606bcEB98761d9beB1BC87383706D"),
-		L1StandardBridge:       common.HexToAddress("0xfA6D8Ee5BE770F84FC001D098C4bD604Fe01284a"),
-		// FIXME update this to the correct address
-		L1ERC721Bridge: common.HexToAddress("0x0000000000000000000000000000000000000000"),
+		OptimismPortalProxy:         common.HexToAddress("0xe93c8cD0D409341205A592f8c4Ac1A5fe5585cfA"),
+		L2OutputOracleProxy:         common.HexToAddress("0x2A35891ff30313CcFa6CE88dcf3858bb075A2298"),
+		L1CrossDomainMessengerProxy: common.HexToAddress("0x8e5693140eA606bcEB98761d9beB1BC87383706D"),
+		L1StandardBridgeProxy:       common.HexToAddress("0xfA6D8Ee5BE770F84FC001D098C4bD604Fe01284a"),
 	},
 	// Zora mainnet
 	7777777: {
-		OptimismPortal:         common.HexToAddress("0x1a0ad011913A150f69f6A19DF447A0CfD9551054"),
-		L2OutputOracle:         common.HexToAddress("0x9E6204F750cD866b299594e2aC9eA824E2e5f95c"),
-		L1CrossDomainMessenger: common.HexToAddress("0xdC40a14d9abd6F410226f1E6de71aE03441ca506"),
-		L1StandardBridge:       common.HexToAddress("0x3e2Ea9B92B7E48A52296fD261dc26fd995284631"),
-		// FIXME update this to the correct address
-		L1ERC721Bridge: common.HexToAddress("0x0000000000000000000000000000000000000000"),
+		OptimismPortalProxy:         common.HexToAddress("0x1a0ad011913A150f69f6A19DF447A0CfD9551054"),
+		L2OutputOracleProxy:         common.HexToAddress("0x9E6204F750cD866b299594e2aC9eA824E2e5f95c"),
+		L1CrossDomainMessengerProxy: common.HexToAddress("0xdC40a14d9abd6F410226f1E6de71aE03441ca506"),
+		L1StandardBridgeProxy:       common.HexToAddress("0x3e2Ea9B92B7E48A52296fD261dc26fd995284631"),
 	},
 	// Zora goerli
 	999: {
-		OptimismPortal:         common.HexToAddress("0xDb9F51790365e7dc196e7D072728df39Be958ACe"),
-		L2OutputOracle:         common.HexToAddress("0xdD292C9eEd00f6A32Ff5245d0BCd7f2a15f24e00"),
-		L1CrossDomainMessenger: common.HexToAddress("0xD87342e16352D33170557A7dA1e5fB966a60FafC"),
-		L1StandardBridge:       common.HexToAddress("0x7CC09AC2452D6555d5e0C213Ab9E2d44eFbFc956"),
-		// FIXME update this to the correct address
-		L1ERC721Bridge: common.HexToAddress("0x0000000000000000000000000000000000000000"),
+		OptimismPortalProxy:         common.HexToAddress("0xDb9F51790365e7dc196e7D072728df39Be958ACe"),
+		L2OutputOracleProxy:         common.HexToAddress("0xdD292C9eEd00f6A32Ff5245d0BCd7f2a15f24e00"),
+		L1CrossDomainMessengerProxy: common.HexToAddress("0xD87342e16352D33170557A7dA1e5fB966a60FafC"),
+		L1StandardBridgeProxy:       common.HexToAddress("0x7CC09AC2452D6555d5e0C213Ab9E2d44eFbFc956"),
 	},
 }
diff --git a/indexer/database/blocks.go b/indexer/database/blocks.go
index 33d3c065d..89cffb40d 100644
--- a/indexer/database/blocks.go
+++ b/indexer/database/blocks.go
@@ -38,11 +38,11 @@ func BlockHeaderFromHeader(header *types.Header) BlockHeader {
 }
 
 type L1BlockHeader struct {
-	BlockHeader
+	BlockHeader `gorm:"embedded"`
 }
 
 type L2BlockHeader struct {
-	BlockHeader
+	BlockHeader `gorm:"embedded"`
 }
 
 type LegacyStateBatch struct {
@@ -65,24 +65,28 @@ type OutputProposal struct {
 }
 
 type BlocksView interface {
-	L1BlockHeader(*big.Int) (*L1BlockHeader, error)
-	LatestL1BlockHeader() (*L1BlockHeader, error)
+	L1BlockHeader(common.Hash) (*L1BlockHeader, error)
+	L1BlockHeaderWithFilter(BlockHeader) (*L1BlockHeader, error)
+	L1LatestBlockHeader() (*L1BlockHeader, error)
+
+	L2BlockHeader(common.Hash) (*L2BlockHeader, error)
+	L2BlockHeaderWithFilter(BlockHeader) (*L2BlockHeader, error)
+	L2LatestBlockHeader() (*L2BlockHeader, error)
 
 	LatestCheckpointedOutput() (*OutputProposal, error)
 	OutputProposal(index *big.Int) (*OutputProposal, error)
 
-	L2BlockHeader(*big.Int) (*L2BlockHeader, error)
-	LatestL2BlockHeader() (*L2BlockHeader, error)
+	LatestEpoch() (*Epoch, error)
 }
 
 type BlocksDB interface {
 	BlocksView
 
-	StoreL1BlockHeaders([]*L1BlockHeader) error
-	StoreL2BlockHeaders([]*L2BlockHeader) error
+	StoreL1BlockHeaders([]L1BlockHeader) error
+	StoreL2BlockHeaders([]L2BlockHeader) error
 
-	StoreLegacyStateBatches([]*LegacyStateBatch) error
-	StoreOutputProposals([]*OutputProposal) error
+	StoreLegacyStateBatches([]LegacyStateBatch) error
+	StoreOutputProposals([]OutputProposal) error
 }
 
 /**
@@ -99,36 +103,39 @@ func newBlocksDB(db *gorm.DB) BlocksDB {
 
 // L1
 
-func (db *blocksDB) StoreL1BlockHeaders(headers []*L1BlockHeader) error {
+func (db *blocksDB) StoreL1BlockHeaders(headers []L1BlockHeader) error {
 	result := db.gorm.Create(&headers)
 	return result.Error
 }
 
-func (db *blocksDB) StoreLegacyStateBatches(stateBatches []*LegacyStateBatch) error {
+func (db *blocksDB) StoreLegacyStateBatches(stateBatches []LegacyStateBatch) error {
 	result := db.gorm.Create(stateBatches)
 	return result.Error
 }
 
-func (db *blocksDB) StoreOutputProposals(outputs []*OutputProposal) error {
+func (db *blocksDB) StoreOutputProposals(outputs []OutputProposal) error {
 	result := db.gorm.Create(outputs)
 	return result.Error
 }
 
-func (db *blocksDB) L1BlockHeader(height *big.Int) (*L1BlockHeader, error) {
+func (db *blocksDB) L1BlockHeader(hash common.Hash) (*L1BlockHeader, error) {
+	return db.L1BlockHeaderWithFilter(BlockHeader{Hash: hash})
+}
+
+func (db *blocksDB) L1BlockHeaderWithFilter(filter BlockHeader) (*L1BlockHeader, error) {
 	var l1Header L1BlockHeader
-	result := db.gorm.Where(&BlockHeader{Number: U256{Int: height}}).Take(&l1Header)
+	result := db.gorm.Where(&filter).Take(&l1Header)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, nil
 		}
-
 		return nil, result.Error
 	}
 
 	return &l1Header, nil
 }
 
-func (db *blocksDB) LatestL1BlockHeader() (*L1BlockHeader, error) {
+func (db *blocksDB) L1LatestBlockHeader() (*L1BlockHeader, error) {
 	var l1Header L1BlockHeader
 	result := db.gorm.Order("number DESC").Take(&l1Header)
 	if result.Error != nil {
@@ -172,36 +179,71 @@ func (db *blocksDB) OutputProposal(index *big.Int) (*OutputProposal, error) {
 
 // L2
 
-func (db *blocksDB) StoreL2BlockHeaders(headers []*L2BlockHeader) error {
+func (db *blocksDB) StoreL2BlockHeaders(headers []L2BlockHeader) error {
 	result := db.gorm.Create(&headers)
 	return result.Error
 }
 
-func (db *blocksDB) L2BlockHeader(height *big.Int) (*L2BlockHeader, error) {
+func (db *blocksDB) L2BlockHeader(hash common.Hash) (*L2BlockHeader, error) {
+	return db.L2BlockHeaderWithFilter(BlockHeader{Hash: hash})
+}
+
+func (db *blocksDB) L2BlockHeaderWithFilter(filter BlockHeader) (*L2BlockHeader, error) {
 	var l2Header L2BlockHeader
-	result := db.gorm.Where(&BlockHeader{Number: U256{Int: height}}).Take(&l2Header)
+	result := db.gorm.Where(&filter).Take(&l2Header)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, nil
 		}
-
 		return nil, result.Error
 	}
 
 	return &l2Header, nil
 }
 
-func (db *blocksDB) LatestL2BlockHeader() (*L2BlockHeader, error) {
+func (db *blocksDB) L2LatestBlockHeader() (*L2BlockHeader, error) {
 	var l2Header L2BlockHeader
 	result := db.gorm.Order("number DESC").Take(&l2Header)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, nil
 		}
-
 		return nil, result.Error
 	}
 
 	result.Logger.Info(context.Background(), "number ", l2Header.Number)
 	return &l2Header, nil
 }
+
+// Auxiliary Methods on both L1 & L2
+
+type Epoch struct {
+	L1BlockHeader L1BlockHeader `gorm:"embedded"`
+	L2BlockHeader L2BlockHeader `gorm:"embedded"`
+}
+
+// LatestEpoch return the latest epoch, seen on  L1 & L2. In other words
+// this returns the latest indexed L1 block that has a corresponding
+// indexed L2 block with a matching L1Origin (equal timestamps).
+//
+// For more, see the protocol spec:
+//   - https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md
+func (db *blocksDB) LatestEpoch() (*Epoch, error) {
+	// Since L1 blocks occur less frequently than L2, we do a INNER JOIN from L1 on
+	// L2 for a faster query. Per the protocol, the L2 block that starts a new epoch
+	// will have a matching timestamp with the L1 origin.
+	query := db.gorm.Table("l1_block_headers").Order("l1_block_headers.timestamp DESC")
+	query = query.Joins("INNER JOIN l2_block_headers ON l1_block_headers.timestamp = l2_block_headers.timestamp")
+	query = query.Select("*")
+
+	var epoch Epoch
+	result := query.Take(&epoch)
+	if result.Error != nil {
+		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+			return nil, nil
+		}
+		return nil, result.Error
+	}
+
+	return &epoch, nil
+}
diff --git a/indexer/database/bridge_messages.go b/indexer/database/bridge_messages.go
index a5504b423..99f61f906 100644
--- a/indexer/database/bridge_messages.go
+++ b/indexer/database/bridge_messages.go
@@ -47,10 +47,10 @@ type BridgeMessagesView interface {
 type BridgeMessagesDB interface {
 	BridgeMessagesView
 
-	StoreL1BridgeMessages([]*L1BridgeMessage) error
+	StoreL1BridgeMessages([]L1BridgeMessage) error
 	MarkRelayedL1BridgeMessage(common.Hash, uuid.UUID) error
 
-	StoreL2BridgeMessages([]*L2BridgeMessage) error
+	StoreL2BridgeMessages([]L2BridgeMessage) error
 	MarkRelayedL2BridgeMessage(common.Hash, uuid.UUID) error
 }
 
@@ -70,7 +70,7 @@ func newBridgeMessagesDB(db *gorm.DB) BridgeMessagesDB {
  * Arbitrary Messages Sent from L1
  */
 
-func (db bridgeMessagesDB) StoreL1BridgeMessages(messages []*L1BridgeMessage) error {
+func (db bridgeMessagesDB) StoreL1BridgeMessages(messages []L1BridgeMessage) error {
 	result := db.gorm.Create(&messages)
 	return result.Error
 }
@@ -109,7 +109,7 @@ func (db bridgeMessagesDB) MarkRelayedL1BridgeMessage(messageHash common.Hash, r
  * Arbitrary Messages Sent from L2
  */
 
-func (db bridgeMessagesDB) StoreL2BridgeMessages(messages []*L2BridgeMessage) error {
+func (db bridgeMessagesDB) StoreL2BridgeMessages(messages []L2BridgeMessage) error {
 	result := db.gorm.Create(&messages)
 	return result.Error
 }
diff --git a/indexer/database/bridge_transactions.go b/indexer/database/bridge_transactions.go
index b4fd4c14d..b01b6cc45 100644
--- a/indexer/database/bridge_transactions.go
+++ b/indexer/database/bridge_transactions.go
@@ -53,9 +53,9 @@ type BridgeTransactionsView interface {
 type BridgeTransactionsDB interface {
 	BridgeTransactionsView
 
-	StoreL1TransactionDeposits([]*L1TransactionDeposit) error
+	StoreL1TransactionDeposits([]L1TransactionDeposit) error
 
-	StoreL2TransactionWithdrawals([]*L2TransactionWithdrawal) error
+	StoreL2TransactionWithdrawals([]L2TransactionWithdrawal) error
 	MarkL2TransactionWithdrawalProvenEvent(common.Hash, uuid.UUID) error
 	MarkL2TransactionWithdrawalFinalizedEvent(common.Hash, uuid.UUID, bool) error
 }
@@ -76,7 +76,7 @@ func newBridgeTransactionsDB(db *gorm.DB) BridgeTransactionsDB {
  * Transactions deposited from L1
  */
 
-func (db *bridgeTransactionsDB) StoreL1TransactionDeposits(deposits []*L1TransactionDeposit) error {
+func (db *bridgeTransactionsDB) StoreL1TransactionDeposits(deposits []L1TransactionDeposit) error {
 	result := db.gorm.Create(&deposits)
 	return result.Error
 }
@@ -98,7 +98,7 @@ func (db *bridgeTransactionsDB) L1TransactionDeposit(sourceHash common.Hash) (*L
  * Transactions withdrawn from L2
  */
 
-func (db *bridgeTransactionsDB) StoreL2TransactionWithdrawals(withdrawals []*L2TransactionWithdrawal) error {
+func (db *bridgeTransactionsDB) StoreL2TransactionWithdrawals(withdrawals []L2TransactionWithdrawal) error {
 	result := db.gorm.Create(&withdrawals)
 	return result.Error
 }
diff --git a/indexer/database/bridge_transfers.go b/indexer/database/bridge_transfers.go
index a5ded0aae..04d0e7778 100644
--- a/indexer/database/bridge_transfers.go
+++ b/indexer/database/bridge_transfers.go
@@ -31,8 +31,7 @@ type BridgeTransfer struct {
 }
 
 type L1BridgeDeposit struct {
-	BridgeTransfer `gorm:"embedded"`
-
+	BridgeTransfer        `gorm:"embedded"`
 	TransactionSourceHash common.Hash `gorm:"primaryKey;serializer:json"`
 }
 
@@ -44,8 +43,7 @@ type L1BridgeDepositWithTransactionHashes struct {
 }
 
 type L2BridgeWithdrawal struct {
-	BridgeTransfer `gorm:"embedded"`
-
+	BridgeTransfer            `gorm:"embedded"`
 	TransactionWithdrawalHash common.Hash `gorm:"primaryKey;serializer:json"`
 }
 
@@ -70,8 +68,8 @@ type BridgeTransfersView interface {
 type BridgeTransfersDB interface {
 	BridgeTransfersView
 
-	StoreL1BridgeDeposits([]*L1BridgeDeposit) error
-	StoreL2BridgeWithdrawals([]*L2BridgeWithdrawal) error
+	StoreL1BridgeDeposits([]L1BridgeDeposit) error
+	StoreL2BridgeWithdrawals([]L2BridgeWithdrawal) error
 }
 
 /**
@@ -90,7 +88,7 @@ func newBridgeTransfersDB(db *gorm.DB) BridgeTransfersDB {
  * Tokens Bridged (Deposited) from L1
  */
 
-func (db *bridgeTransfersDB) StoreL1BridgeDeposits(deposits []*L1BridgeDeposit) error {
+func (db *bridgeTransfersDB) StoreL1BridgeDeposits(deposits []L1BridgeDeposit) error {
 	result := db.gorm.Create(&deposits)
 	return result.Error
 }
@@ -124,7 +122,7 @@ func (db *bridgeTransfersDB) L1BridgeDepositWithFilter(filter BridgeTransfer) (*
 }
 
 type L1BridgeDepositsResponse struct {
-	Deposits    []*L1BridgeDepositWithTransactionHashes
+	Deposits    []L1BridgeDepositWithTransactionHashes
 	Cursor      string
 	HasNextPage bool
 }
@@ -152,7 +150,7 @@ l1_transaction_deposits.l2_transaction_hash`)
 
 	filteredQuery := depositsQuery.Where(&Transaction{FromAddress: address}).Order("l1_bridge_deposits.transaction_source_hash DESC").Limit(limit + 1)
 
-	deposits := []*L1BridgeDepositWithTransactionHashes{}
+	deposits := []L1BridgeDepositWithTransactionHashes{}
 	result := filteredQuery.Scan(&deposits)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -185,7 +183,7 @@ l1_transaction_deposits.l2_transaction_hash`)
  * Tokens Bridged (Withdrawn) from L2
  */
 
-func (db *bridgeTransfersDB) StoreL2BridgeWithdrawals(withdrawals []*L2BridgeWithdrawal) error {
+func (db *bridgeTransfersDB) StoreL2BridgeWithdrawals(withdrawals []L2BridgeWithdrawal) error {
 	result := db.gorm.Create(&withdrawals)
 	return result.Error
 }
@@ -219,7 +217,7 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalWithFilter(filter BridgeTransfer)
 }
 
 type L2BridgeWithdrawalsResponse struct {
-	Withdrawals []*L2BridgeWithdrawalWithTransactionHashes
+	Withdrawals []L2BridgeWithdrawalWithTransactionHashes
 	Cursor      string
 	HasNextPage bool
 }
@@ -249,7 +247,7 @@ finalized_l1_contract_events.transaction_hash AS finalized_l1_transaction_hash`)
 
 	filteredQuery := withdrawalsQuery.Where(&Transaction{FromAddress: address}).Order("l2_bridge_withdrawals.timestamp DESC").Limit(limit + 1)
 
-	withdrawals := []*L2BridgeWithdrawalWithTransactionHashes{}
+	withdrawals := []L2BridgeWithdrawalWithTransactionHashes{}
 	result := filteredQuery.Scan(&withdrawals)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
diff --git a/indexer/database/contract_events.go b/indexer/database/contract_events.go
index 265961dc2..091ce4f94 100644
--- a/indexer/database/contract_events.go
+++ b/indexer/database/contract_events.go
@@ -2,6 +2,7 @@ package database
 
 import (
 	"errors"
+	"fmt"
 	"math/big"
 
 	"gorm.io/gorm"
@@ -74,19 +75,23 @@ type L2ContractEvent struct {
 
 type ContractEventsView interface {
 	L1ContractEvent(uuid.UUID) (*L1ContractEvent, error)
-	L1ContractEventByTxLogIndex(common.Hash, uint64) (*L1ContractEvent, error)
+	L1ContractEventWithFilter(ContractEvent) (*L1ContractEvent, error)
 	L1ContractEventsWithFilter(ContractEvent, *big.Int, *big.Int) ([]L1ContractEvent, error)
+	L1LatestContractEventWithFilter(ContractEvent) (*L1ContractEvent, error)
 
 	L2ContractEvent(uuid.UUID) (*L2ContractEvent, error)
-	L2ContractEventByTxLogIndex(common.Hash, uint64) (*L2ContractEvent, error)
+	L2ContractEventWithFilter(ContractEvent) (*L2ContractEvent, error)
 	L2ContractEventsWithFilter(ContractEvent, *big.Int, *big.Int) ([]L2ContractEvent, error)
+	L2LatestContractEventWithFilter(ContractEvent) (*L2ContractEvent, error)
+
+	ContractEventsWithFilter(ContractEvent, string, *big.Int, *big.Int) ([]ContractEvent, error)
 }
 
 type ContractEventsDB interface {
 	ContractEventsView
 
-	StoreL1ContractEvents([]*L1ContractEvent) error
-	StoreL2ContractEvents([]*L2ContractEvent) error
+	StoreL1ContractEvents([]L1ContractEvent) error
+	StoreL2ContractEvents([]L2ContractEvent) error
 }
 
 /**
@@ -103,33 +108,22 @@ func newContractEventsDB(db *gorm.DB) ContractEventsDB {
 
 // L1
 
-func (db *contractEventsDB) StoreL1ContractEvents(events []*L1ContractEvent) error {
+func (db *contractEventsDB) StoreL1ContractEvents(events []L1ContractEvent) error {
 	result := db.gorm.Create(&events)
 	return result.Error
 }
 
 func (db *contractEventsDB) L1ContractEvent(uuid uuid.UUID) (*L1ContractEvent, error) {
-	var l1ContractEvent L1ContractEvent
-	result := db.gorm.Where(&ContractEvent{GUID: uuid}).Take(&l1ContractEvent)
-	if result.Error != nil {
-		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
-			return nil, nil
-		}
-
-		return nil, result.Error
-	}
-
-	return &l1ContractEvent, nil
+	return db.L1ContractEventWithFilter(ContractEvent{GUID: uuid})
 }
 
-func (db *contractEventsDB) L1ContractEventByTxLogIndex(txHash common.Hash, logIndex uint64) (*L1ContractEvent, error) {
+func (db *contractEventsDB) L1ContractEventWithFilter(filter ContractEvent) (*L1ContractEvent, error) {
 	var l1ContractEvent L1ContractEvent
-	result := db.gorm.Where(&ContractEvent{TransactionHash: txHash, LogIndex: logIndex}).Take(&l1ContractEvent)
+	result := db.gorm.Where(&filter).Take(&l1ContractEvent)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, nil
 		}
-
 		return nil, result.Error
 	}
 
@@ -140,6 +134,12 @@ func (db *contractEventsDB) L1ContractEventsWithFilter(filter ContractEvent, fro
 	if fromHeight == nil {
 		fromHeight = big.NewInt(0)
 	}
+	if toHeight == nil {
+		return nil, errors.New("end height unspecified")
+	}
+	if fromHeight.Cmp(toHeight) > 0 {
+		return nil, fmt.Errorf("fromHeight %d is greater than toHeight %d", fromHeight, toHeight)
+	}
 
 	query := db.gorm.Table("l1_contract_events").Where(&filter)
 	query = query.Joins("INNER JOIN l1_block_headers ON l1_contract_events.block_hash = l1_block_headers.hash")
@@ -160,35 +160,37 @@ func (db *contractEventsDB) L1ContractEventsWithFilter(filter ContractEvent, fro
 	return events, nil
 }
 
-// L2
-
-func (db *contractEventsDB) StoreL2ContractEvents(events []*L2ContractEvent) error {
-	result := db.gorm.Create(&events)
-	return result.Error
-}
-
-func (db *contractEventsDB) L2ContractEvent(uuid uuid.UUID) (*L2ContractEvent, error) {
-	var l2ContractEvent L2ContractEvent
-	result := db.gorm.Where(&ContractEvent{GUID: uuid}).Take(&l2ContractEvent)
+func (db *contractEventsDB) L1LatestContractEventWithFilter(filter ContractEvent) (*L1ContractEvent, error) {
+	var l1ContractEvent L1ContractEvent
+	result := db.gorm.Where(&filter).Order("timestamp DESC").Take(&l1ContractEvent)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, nil
 		}
-
 		return nil, result.Error
 	}
 
-	return &l2ContractEvent, nil
+	return &l1ContractEvent, nil
 }
 
-func (db *contractEventsDB) L2ContractEventByTxLogIndex(txHash common.Hash, logIndex uint64) (*L2ContractEvent, error) {
+// L2
+
+func (db *contractEventsDB) StoreL2ContractEvents(events []L2ContractEvent) error {
+	result := db.gorm.Create(&events)
+	return result.Error
+}
+
+func (db *contractEventsDB) L2ContractEvent(uuid uuid.UUID) (*L2ContractEvent, error) {
+	return db.L2ContractEventWithFilter(ContractEvent{GUID: uuid})
+}
+
+func (db *contractEventsDB) L2ContractEventWithFilter(filter ContractEvent) (*L2ContractEvent, error) {
 	var l2ContractEvent L2ContractEvent
-	result := db.gorm.Where(&ContractEvent{TransactionHash: txHash, LogIndex: logIndex}).Take(&l2ContractEvent)
+	result := db.gorm.Where(&filter).Take(&l2ContractEvent)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, nil
 		}
-
 		return nil, result.Error
 	}
 
@@ -199,6 +201,12 @@ func (db *contractEventsDB) L2ContractEventsWithFilter(filter ContractEvent, fro
 	if fromHeight == nil {
 		fromHeight = big.NewInt(0)
 	}
+	if toHeight == nil {
+		return nil, errors.New("end height unspecified")
+	}
+	if fromHeight.Cmp(toHeight) > 0 {
+		return nil, fmt.Errorf("fromHeight %d is greater than toHeight %d", fromHeight, toHeight)
+	}
 
 	query := db.gorm.Table("l2_contract_events").Where(&filter)
 	query = query.Joins("INNER JOIN l2_block_headers ON l2_contract_events.block_hash = l2_block_headers.hash")
@@ -218,3 +226,46 @@ func (db *contractEventsDB) L2ContractEventsWithFilter(filter ContractEvent, fro
 
 	return events, nil
 }
+
+func (db *contractEventsDB) L2LatestContractEventWithFilter(filter ContractEvent) (*L2ContractEvent, error) {
+	var l2ContractEvent L2ContractEvent
+	result := db.gorm.Where(&filter).Order("timestamp DESC").Take(&l2ContractEvent)
+	if result.Error != nil {
+		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+			return nil, nil
+		}
+		return nil, result.Error
+	}
+
+	return &l2ContractEvent, nil
+}
+
+// Auxiliary methods for both L1 and L2
+
+// ContractEventsWithFilter will retrieve contract events within the specified range according to the `chainSelector`.
+func (db *contractEventsDB) ContractEventsWithFilter(filter ContractEvent, chainSelector string, fromHeight, toHeight *big.Int) ([]ContractEvent, error) {
+	switch chainSelector {
+	case "l1":
+		l1Events, err := db.L1ContractEventsWithFilter(filter, fromHeight, toHeight)
+		if err != nil {
+			return nil, err
+		}
+		events := make([]ContractEvent, len(l1Events))
+		for i := range l1Events {
+			events[i] = l1Events[i].ContractEvent
+		}
+		return events, nil
+	case "l2":
+		l2Events, err := db.L2ContractEventsWithFilter(filter, fromHeight, toHeight)
+		if err != nil {
+			return nil, err
+		}
+		events := make([]ContractEvent, len(l2Events))
+		for i := range l2Events {
+			events[i] = l2Events[i].ContractEvent
+		}
+		return events, nil
+	default:
+		return nil, errors.New("expected 'l1' or 'l2' for chain selection")
+	}
+}
diff --git a/indexer/etl/etl.go b/indexer/etl/etl.go
new file mode 100644
index 000000000..2989d070d
--- /dev/null
+++ b/indexer/etl/etl.go
@@ -0,0 +1,112 @@
+package etl
+
+import (
+	"context"
+	"errors"
+	"time"
+
+	"github.com/ethereum-optimism/optimism/indexer/node"
+	"github.com/ethereum/go-ethereum"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/ethclient"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+const (
+	defaultLoopInterval     = 5 * time.Second
+	defaultHeaderBufferSize = 500
+)
+
+type ETL struct {
+	log log.Logger
+
+	headerTraversal *node.HeaderTraversal
+	ethClient       *ethclient.Client
+	contracts       []common.Address
+
+	etlBatches chan ETLBatch
+}
+
+type ETLBatch struct {
+	Logger log.Logger
+
+	Headers   []types.Header
+	HeaderMap map[common.Hash]*types.Header
+
+	Logs           []types.Log
+	HeadersWithLog map[common.Hash]bool
+}
+
+func (etl *ETL) Start(ctx context.Context) error {
+	done := ctx.Done()
+	pollTicker := time.NewTicker(defaultLoopInterval)
+	defer pollTicker.Stop()
+
+	etl.log.Info("starting etl...")
+	var headers []types.Header
+	for {
+		select {
+		case <-done:
+			etl.log.Info("stopping etl")
+			return nil
+
+		case <-pollTicker.C:
+			if len(headers) == 0 {
+				newHeaders, err := etl.headerTraversal.NextFinalizedHeaders(defaultHeaderBufferSize)
+				if err != nil {
+					etl.log.Error("error querying for headers", "err", err)
+					continue
+				}
+				if len(newHeaders) == 0 {
+					// Logged as an error since this loop should be operating at a longer interval than the provider
+					etl.log.Error("no new headers. processor unexpectedly at head...")
+					continue
+				}
+
+				headers = newHeaders
+			} else {
+				etl.log.Info("retrying previous batch")
+			}
+
+			firstHeader := headers[0]
+			lastHeader := headers[len(headers)-1]
+			batchLog := etl.log.New("batch_start_block_number", firstHeader.Number, "batch_end_block_number", lastHeader.Number)
+			batchLog.Info("extracting batch", "size", len(headers))
+
+			headerMap := make(map[common.Hash]*types.Header, len(headers))
+			for i := range headers {
+				headerMap[headers[i].Hash()] = &headers[i]
+			}
+
+			headersWithLog := make(map[common.Hash]bool, len(headers))
+			logFilter := ethereum.FilterQuery{FromBlock: firstHeader.Number, ToBlock: lastHeader.Number, Addresses: etl.contracts}
+			logs, err := etl.ethClient.FilterLogs(context.Background(), logFilter)
+			if err != nil {
+				batchLog.Info("unable to extract logs within batch", "err", err)
+				continue // spin and try again
+			}
+
+			for i := range logs {
+				if _, ok := headerMap[logs[i].BlockHash]; !ok {
+					// NOTE. Definitely an error state if the none of the headers were re-orged out in between
+					// the blocks and logs retreival operations. However, we need to gracefully handle reorgs
+					batchLog.Error("log found with block hash not in the batch", "block_hash", logs[i].BlockHash, "log_index", logs[i].Index)
+					return errors.New("parsed log with a block hash not in the fetched batch")
+				}
+				headersWithLog[logs[i].BlockHash] = true
+			}
+
+			if len(logs) > 0 {
+				batchLog.Info("detected logs", "size", len(logs))
+			}
+
+			// create a new reference such that subsequent changes to `headers` does not affect the reference
+			headersRef := headers
+			batch := ETLBatch{Logger: batchLog, Headers: headersRef, HeaderMap: headerMap, Logs: logs, HeadersWithLog: headersWithLog}
+
+			headers = nil
+			etl.etlBatches <- batch
+		}
+	}
+}
diff --git a/indexer/etl/l1_etl.go b/indexer/etl/l1_etl.go
new file mode 100644
index 000000000..5c0b90806
--- /dev/null
+++ b/indexer/etl/l1_etl.go
@@ -0,0 +1,129 @@
+package etl
+
+import (
+	"context"
+	"errors"
+	"reflect"
+
+	"github.com/ethereum-optimism/optimism/indexer/config"
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/indexer/node"
+	"github.com/ethereum-optimism/optimism/op-service/retry"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+type L1ETL struct {
+	ETL
+
+	db *database.DB
+}
+
+func NewL1ETL(log log.Logger, db *database.DB, client node.EthClient, contracts config.L1Contracts) (*L1ETL, error) {
+	log = log.New("etl", "l1")
+
+	contractValue := reflect.ValueOf(contracts)
+	fields := reflect.VisibleFields(reflect.TypeOf(contracts))
+	l1Contracts := make([]common.Address, len(fields))
+	for i, field := range fields {
+		// ruleid: unsafe-reflect-by-name
+		addr, ok := (contractValue.FieldByName(field.Name).Interface()).(common.Address)
+		if !ok {
+			log.Error("non-address found in L1Contracts", "name", field.Name)
+			return nil, errors.New("non-address found in L1Contracts")
+		}
+
+		log.Info("configured contract", "name", field.Name, "addr", addr)
+		l1Contracts[i] = addr
+	}
+
+	latestHeader, err := db.Blocks.L1LatestBlockHeader()
+	if err != nil {
+		return nil, err
+	}
+
+	var fromHeader *types.Header
+	if latestHeader != nil {
+		log.Info("detected last indexed block", "number", latestHeader.Number.Int, "hash", latestHeader.Hash)
+		fromHeader = latestHeader.RLPHeader.Header()
+	} else {
+		log.Info("no indexed state, starting from genesis")
+	}
+
+	etlBatches := make(chan ETLBatch)
+	etl := ETL{
+		log:             log,
+		headerTraversal: node.NewHeaderTraversal(client, fromHeader),
+		ethClient:       client.GethEthClient(),
+		contracts:       l1Contracts,
+		etlBatches:      etlBatches,
+	}
+
+	return &L1ETL{ETL: etl, db: db}, nil
+}
+
+func (l1Etl *L1ETL) Start(ctx context.Context) error {
+	errCh := make(chan error, 1)
+	go func() {
+		errCh <- l1Etl.ETL.Start(ctx)
+	}()
+
+	for {
+		select {
+		case err := <-errCh:
+			return err
+
+		// Index incoming batches
+		case batch := <-l1Etl.etlBatches:
+			// Pull out only L1 blocks that have emitted a log ( <= batch.Headers )
+			l1BlockHeaders := make([]database.L1BlockHeader, 0, len(batch.Headers))
+			for i := range batch.Headers {
+				if _, ok := batch.HeadersWithLog[batch.Headers[i].Hash()]; ok {
+					l1BlockHeaders = append(l1BlockHeaders, database.L1BlockHeader{BlockHeader: database.BlockHeaderFromHeader(&batch.Headers[i])})
+				}
+			}
+
+			if len(l1BlockHeaders) == 0 {
+				batch.Logger.Info("no l1 blocks with logs in batch")
+				continue
+			}
+
+			l1ContractEvents := make([]database.L1ContractEvent, len(batch.Logs))
+			for i := range batch.Logs {
+				timestamp := batch.HeaderMap[batch.Logs[i].BlockHash].Time
+				l1ContractEvents[i] = database.L1ContractEvent{ContractEvent: database.ContractEventFromLog(&batch.Logs[i], timestamp)}
+			}
+
+			// Continually try to persist this batch. If it fails after 10 attempts, we simply error out
+			retryStrategy := &retry.ExponentialStrategy{Min: 1000, Max: 20_000, MaxJitter: 250}
+			_, err := retry.Do[interface{}](ctx, 10, retryStrategy, func() (interface{}, error) {
+				err := l1Etl.db.Transaction(func(tx *database.DB) error {
+					if err := tx.Blocks.StoreL1BlockHeaders(l1BlockHeaders); err != nil {
+						return err
+					}
+
+					// we must have logs if we have l1 blocks
+					if err := tx.ContractEvents.StoreL1ContractEvents(l1ContractEvents); err != nil {
+						return err
+					}
+					return nil
+				})
+
+				if err != nil {
+					batch.Logger.Error("unable to persist batch", "err", err)
+					return nil, err
+				}
+
+				// a-ok! Can merge with the above block but being explicit
+				return nil, nil
+			})
+
+			if err != nil {
+				return err
+			}
+
+			batch.Logger.Info("indexed batch")
+		}
+	}
+}
diff --git a/indexer/etl/l2_etl.go b/indexer/etl/l2_etl.go
new file mode 100644
index 000000000..8790f4640
--- /dev/null
+++ b/indexer/etl/l2_etl.go
@@ -0,0 +1,112 @@
+package etl
+
+import (
+	"context"
+
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/indexer/node"
+	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
+	"github.com/ethereum-optimism/optimism/op-service/retry"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+type L2ETL struct {
+	ETL
+
+	db *database.DB
+}
+
+func NewL2ETL(log log.Logger, db *database.DB, client node.EthClient) (*L2ETL, error) {
+	log = log.New("etl", "l2")
+
+	// allow predeploys to be overridable
+	l2Contracts := []common.Address{}
+	for name, addr := range predeploys.Predeploys {
+		log.Info("configured contract", "name", name, "addr", addr)
+		l2Contracts = append(l2Contracts, *addr)
+	}
+
+	latestHeader, err := db.Blocks.L2LatestBlockHeader()
+	if err != nil {
+		return nil, err
+	}
+
+	var fromHeader *types.Header
+	if latestHeader != nil {
+		log.Info("detected last indexed block", "number", latestHeader.Number.Int, "hash", latestHeader.Hash)
+		fromHeader = latestHeader.RLPHeader.Header()
+	} else {
+		log.Info("no indexed state, starting from genesis")
+	}
+
+	etlBatches := make(chan ETLBatch)
+	etl := ETL{
+		log:             log,
+		headerTraversal: node.NewHeaderTraversal(client, fromHeader),
+		ethClient:       client.GethEthClient(),
+		contracts:       l2Contracts,
+		etlBatches:      etlBatches,
+	}
+
+	return &L2ETL{ETL: etl, db: db}, nil
+}
+
+func (l2Etl *L2ETL) Start(ctx context.Context) error {
+	errCh := make(chan error, 1)
+	go func() {
+		errCh <- l2Etl.ETL.Start(ctx)
+	}()
+
+	for {
+		select {
+		case err := <-errCh:
+			return err
+
+		// Index incoming batches
+		case batch := <-l2Etl.etlBatches:
+			// We're indexing every L2 block.
+			l2BlockHeaders := make([]database.L2BlockHeader, len(batch.Headers))
+			for i := range batch.Headers {
+				l2BlockHeaders[i] = database.L2BlockHeader{BlockHeader: database.BlockHeaderFromHeader(&batch.Headers[i])}
+			}
+
+			l2ContractEvents := make([]database.L2ContractEvent, len(batch.Logs))
+			for i := range batch.Logs {
+				timestamp := batch.HeaderMap[batch.Logs[i].BlockHash].Time
+				l2ContractEvents[i] = database.L2ContractEvent{ContractEvent: database.ContractEventFromLog(&batch.Logs[i], timestamp)}
+			}
+
+			// Continually try to persist this batch. If it fails after 5 attempts, we simply error out
+			retryStrategy := &retry.ExponentialStrategy{Min: 1000, Max: 20_000, MaxJitter: 250}
+			_, err := retry.Do[interface{}](ctx, 10, retryStrategy, func() (interface{}, error) {
+				err := l2Etl.db.Transaction(func(tx *database.DB) error {
+					if err := tx.Blocks.StoreL2BlockHeaders(l2BlockHeaders); err != nil {
+						return err
+					}
+					if len(l2ContractEvents) > 0 {
+						if err := tx.ContractEvents.StoreL2ContractEvents(l2ContractEvents); err != nil {
+							return err
+						}
+					}
+					return nil
+				})
+
+				if err != nil {
+					batch.Logger.Error("unable to persist batch", "err", err)
+					return nil, err
+				}
+
+				// a-ok! Can merge with the above block but being explicit
+				return nil, nil
+			})
+
+			if err != nil {
+				return err
+			}
+
+			batch.Logger.Info("indexed batch")
+		}
+	}
+}
diff --git a/indexer/indexer.go b/indexer/indexer.go
index cb3b994a2..6a88211c5 100644
--- a/indexer/indexer.go
+++ b/indexer/indexer.go
@@ -3,14 +3,16 @@ package indexer
 import (
 	"context"
 	"fmt"
+	"runtime/debug"
 	"sync"
 
 	"github.com/ethereum/go-ethereum/log"
 
 	"github.com/ethereum-optimism/optimism/indexer/config"
 	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/indexer/etl"
 	"github.com/ethereum-optimism/optimism/indexer/node"
-	"github.com/ethereum-optimism/optimism/indexer/processor"
+	"github.com/ethereum-optimism/optimism/indexer/processors"
 )
 
 // Indexer contains the necessary resources for
@@ -19,38 +21,47 @@ type Indexer struct {
 	db  *database.DB
 	log log.Logger
 
-	L1Processor *processor.L1Processor
-	L2Processor *processor.L2Processor
+	L1ETL *etl.L1ETL
+	L2ETL *etl.L2ETL
+
+	BridgeProcessor *processors.BridgeProcessor
 }
 
 // NewIndexer initializes an instance of the Indexer
-func NewIndexer(chainConfig config.ChainConfig, rpcsConfig config.RPCsConfig, db *database.DB, logger log.Logger) (*Indexer, error) {
-	l1Contracts := chainConfig.L1Contracts
+func NewIndexer(logger log.Logger, chainConfig config.ChainConfig, rpcsConfig config.RPCsConfig, db *database.DB) (*Indexer, error) {
 	l1EthClient, err := node.DialEthClient(rpcsConfig.L1RPC)
 	if err != nil {
 		return nil, err
 	}
-	l1Processor, err := processor.NewL1Processor(logger, l1EthClient, db, l1Contracts)
+
+	l1Etl, err := etl.NewL1ETL(logger, db, l1EthClient, chainConfig.L1Contracts)
 	if err != nil {
 		return nil, err
 	}
 
-	// L2Processor (predeploys). Although most likely the right setting, make this configurable?
-	l2Contracts := processor.L2ContractPredeploys()
 	l2EthClient, err := node.DialEthClient(rpcsConfig.L2RPC)
 	if err != nil {
 		return nil, err
 	}
-	l2Processor, err := processor.NewL2Processor(logger, l2EthClient, db, l2Contracts)
+
+	// Currently defaults to the predeploys
+	l2Etl, err := etl.NewL2ETL(logger, db, l2EthClient)
+	if err != nil {
+		return nil, err
+	}
+
+	bridgeProcessor, err := processors.NewBridgeProcessor(logger, db, chainConfig)
 	if err != nil {
 		return nil, err
 	}
 
 	indexer := &Indexer{
-		db:          db,
-		log:         logger,
-		L1Processor: l1Processor,
-		L2Processor: l2Processor,
+		db:  db,
+		log: logger,
+
+		L1ETL:           l1Etl,
+		L2ETL:           l2Etl,
+		BridgeProcessor: bridgeProcessor,
 	}
 
 	return indexer, nil
@@ -59,39 +70,41 @@ func NewIndexer(chainConfig config.ChainConfig, rpcsConfig config.RPCsConfig, db
 // Start starts the indexing service on L1 and L2 chains
 func (i *Indexer) Run(ctx context.Context) error {
 	var wg sync.WaitGroup
-	errCh := make(chan error, 1)
+	errCh := make(chan error, 3)
 
 	// If either processor errors out, we stop
-	processorCtx, cancel := context.WithCancel(ctx)
+	subCtx, cancel := context.WithCancel(ctx)
 	run := func(start func(ctx context.Context) error) {
 		wg.Add(1)
 		defer func() {
 			if err := recover(); err != nil {
 				i.log.Error("halting indexer on panic", "err", err)
+				debug.PrintStack()
 				errCh <- fmt.Errorf("panic: %v", err)
 			}
 
+			cancel()
 			wg.Done()
 		}()
 
-		err := start(processorCtx)
+		err := start(subCtx)
 		if err != nil {
 			i.log.Error("halting indexer on error", "err", err)
-			cancel()
 		}
 
-		// Send a value down regardless if we've received an error or halted
-		// via cancellation where err == nil
+		// Send a value down regardless if we've received an error
+		// or halted via cancellation where err == nil
 		errCh <- err
 	}
 
-	// Kick off the processors
-	go run(i.L1Processor.Start)
-	go run(i.L2Processor.Start)
+	// Kick off all the dependent routines
+	go run(i.L1ETL.Start)
+	go run(i.L2ETL.Start)
+	go run(i.BridgeProcessor.Start)
 	err := <-errCh
 
-	// ensure both processors have halted before returning
 	wg.Wait()
+	i.log.Info("indexer stopped")
 	return err
 }
 
diff --git a/indexer/migrations/20230523_create_schema.sql b/indexer/migrations/20230523_create_schema.sql
index f9ea5a0a4..2a2af8d83 100644
--- a/indexer/migrations/20230523_create_schema.sql
+++ b/indexer/migrations/20230523_create_schema.sql
@@ -191,13 +191,13 @@ CREATE TABLE IF NOT EXISTS l1_bridge_deposits (
     cross_domain_message_hash VARCHAR UNIQUE REFERENCES l1_bridge_messages(message_hash),
 
     -- Deposit information
-	from_address     VARCHAR NOT NULL,
-	to_address       VARCHAR NOT NULL,
-	local_token_address VARCHAR NOT NULL, -- REFERENCES l1_tokens(address), uncomment me in future pr
+	from_address         VARCHAR NOT NULL,
+	to_address           VARCHAR NOT NULL,
+	local_token_address  VARCHAR NOT NULL, -- REFERENCES l1_tokens(address), uncomment me in future pr
 	remote_token_address VARCHAR NOT NULL, -- REFERENCES l2_tokens(address), uncomment me in future pr
-	amount           UINT256 NOT NULL,
-	data             VARCHAR NOT NULL,
-    timestamp        INTEGER NOT NULL CHECK (timestamp > 0)
+	amount               UINT256 NOT NULL,
+	data                 VARCHAR NOT NULL,
+    timestamp            INTEGER NOT NULL CHECK (timestamp > 0)
 );
 CREATE TABLE IF NOT EXISTS l2_bridge_withdrawals (
     transaction_withdrawal_hash VARCHAR PRIMARY KEY REFERENCES l2_transaction_withdrawals(withdrawal_hash),
@@ -207,11 +207,11 @@ CREATE TABLE IF NOT EXISTS l2_bridge_withdrawals (
     cross_domain_message_hash VARCHAR UNIQUE REFERENCES l2_bridge_messages(message_hash),
 
     -- Withdrawal information
-	from_address     VARCHAR NOT NULL,
-	to_address       VARCHAR NOT NULL,
-	local_token_address VARCHAR NOT NULL, -- REFERENCES l2_tokens(address), uncomment me in future pr
+	from_address         VARCHAR NOT NULL,
+	to_address           VARCHAR NOT NULL,
+	local_token_address  VARCHAR NOT NULL, -- REFERENCES l2_tokens(address), uncomment me in future pr
 	remote_token_address VARCHAR NOT NULL, -- REFERENCES l1_tokens(address), uncomment me in future pr
-	amount           UINT256 NOT NULL,
-	data             VARCHAR NOT NULL,
-    timestamp        INTEGER NOT NULL CHECK (timestamp > 0)
+	amount               UINT256 NOT NULL,
+	data                 VARCHAR NOT NULL,
+    timestamp            INTEGER NOT NULL CHECK (timestamp > 0)
 );
diff --git a/indexer/node/client.go b/indexer/node/client.go
index fac8d1a9e..49a1fea72 100644
--- a/indexer/node/client.go
+++ b/indexer/node/client.go
@@ -27,12 +27,13 @@ const (
 type EthClient interface {
 	FinalizedBlockHeight() (*big.Int, error)
 
-	BlockHeadersByRange(*big.Int, *big.Int) ([]*types.Header, error)
+	BlockHeadersByRange(*big.Int, *big.Int) ([]types.Header, error)
 	BlockHeaderByHash(common.Hash) (*types.Header, error)
 
 	StorageHash(common.Address, *big.Int) (common.Hash, error)
 
-	RawRpcClient() *rpc.Client
+	GethRpcClient() *rpc.Client
+	GethEthClient() *ethclient.Client
 }
 
 type client struct {
@@ -56,10 +57,14 @@ func NewEthClient(rpcClient *rpc.Client) EthClient {
 	return &client{rpcClient}
 }
 
-func (c *client) RawRpcClient() *rpc.Client {
+func (c *client) GethRpcClient() *rpc.Client {
 	return c.rpcClient
 }
 
+func (c *client) GethEthClient() *ethclient.Client {
+	return ethclient.NewClient(c.GethRpcClient())
+}
+
 // FinalizedBlockHeight retrieves the latest block height in a finalized state
 func (c *client) FinalizedBlockHeight() (*big.Int, error) {
 	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
@@ -97,7 +102,7 @@ func (c *client) BlockHeaderByHash(hash common.Hash) (*types.Header, error) {
 // BlockHeadersByRange will retrieve block headers within the specified range -- includsive. No restrictions
 // are placed on the range such as blocks in the "latest", "safe" or "finalized" states. If the specified
 // range is too large, `endHeight > latest`, the resulting list is truncated to the available headers
-func (c *client) BlockHeadersByRange(startHeight, endHeight *big.Int) ([]*types.Header, error) {
+func (c *client) BlockHeadersByRange(startHeight, endHeight *big.Int) ([]types.Header, error) {
 	count := new(big.Int).Sub(endHeight, startHeight).Uint64() + 1
 	batchElems := make([]rpc.BatchElem, count)
 	for i := uint64(0); i < count; i++ {
@@ -121,7 +126,7 @@ func (c *client) BlockHeadersByRange(startHeight, endHeight *big.Int) ([]*types.
 	//  - Ensure integrity that they build on top of each other
 	//  - Truncate out headers that do not exist (endHeight > "latest")
 	size := 0
-	headers := make([]*types.Header, count)
+	headers := make([]types.Header, count)
 	for i, batchElem := range batchElems {
 		if batchElem.Error != nil {
 			return nil, batchElem.Error
@@ -129,17 +134,19 @@ func (c *client) BlockHeadersByRange(startHeight, endHeight *big.Int) ([]*types.
 			break
 		}
 
-		header := batchElem.Result.(*types.Header)
+		header, ok := batchElem.Result.(*types.Header)
+		if !ok {
+			return nil, fmt.Errorf("unable to transform rpc response %v into types.Header", batchElem.Result)
+		}
 		if i > 0 && header.ParentHash != headers[i-1].Hash() {
-			// Warn here that we got a bad (malicious?) response
-			break
+			return nil, fmt.Errorf("queried header %s does not follow parent %s", header.Hash(), headers[i-1].Hash())
 		}
 
-		headers[i] = header
+		headers[i] = *header
 		size = size + 1
 	}
-	headers = headers[:size]
 
+	headers = headers[:size]
 	return headers, nil
 }
 
diff --git a/indexer/node/header_traversal.go b/indexer/node/header_traversal.go
index 352d217a5..7ab0413af 100644
--- a/indexer/node/header_traversal.go
+++ b/indexer/node/header_traversal.go
@@ -26,7 +26,7 @@ func NewHeaderTraversal(ethClient EthClient, fromHeader *types.Header) *HeaderTr
 
 // NextFinalizedHeaders retrives the next set of headers that have been
 // marked as finalized by the connected client, bounded by the supplied size
-func (f *HeaderTraversal) NextFinalizedHeaders(maxSize uint64) ([]*types.Header, error) {
+func (f *HeaderTraversal) NextFinalizedHeaders(maxSize uint64) ([]types.Header, error) {
 	finalizedBlockHeight, err := f.ethClient.FinalizedBlockHeight()
 	if err != nil {
 		return nil, err
@@ -61,6 +61,6 @@ func (f *HeaderTraversal) NextFinalizedHeaders(maxSize uint64) ([]*types.Header,
 		return nil, ErrHeaderTraversalAndProviderMismatchedState
 	}
 
-	f.lastHeader = headers[numHeaders-1]
+	f.lastHeader = &headers[numHeaders-1]
 	return headers, nil
 }
diff --git a/indexer/processor/contract_events.go b/indexer/processor/contract_events.go
deleted file mode 100644
index 1ac70c99a..000000000
--- a/indexer/processor/contract_events.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package processor
-
-import (
-	"errors"
-	"fmt"
-
-	"github.com/ethereum-optimism/optimism/indexer/database"
-
-	"github.com/ethereum/go-ethereum/accounts/abi"
-	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/core/types"
-)
-
-type ProcessedContractEventLogIndexKey struct {
-	blockHash common.Hash
-	index     uint
-}
-
-type ProcessedContractEvents struct {
-	events            []*database.ContractEvent
-	eventsBySignature map[common.Hash][]*database.ContractEvent
-	eventByLogIndex   map[ProcessedContractEventLogIndexKey]*database.ContractEvent
-}
-
-func NewProcessedContractEvents() *ProcessedContractEvents {
-	return &ProcessedContractEvents{
-		events:            []*database.ContractEvent{},
-		eventsBySignature: make(map[common.Hash][]*database.ContractEvent),
-		eventByLogIndex:   make(map[ProcessedContractEventLogIndexKey]*database.ContractEvent),
-	}
-}
-
-func (p *ProcessedContractEvents) AddLog(log *types.Log, time uint64) *database.ContractEvent {
-	event := database.ContractEventFromLog(log, time)
-	emptyHash := common.Hash{}
-
-	p.events = append(p.events, &event)
-	p.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index}] = &event
-	if event.EventSignature != emptyHash {
-		p.eventsBySignature[event.EventSignature] = append(p.eventsBySignature[event.EventSignature], &event)
-	}
-
-	return &event
-}
-
-func UnpackLog(out interface{}, log *types.Log, name string, contractAbi *abi.ABI) error {
-	eventAbi, ok := contractAbi.Events[name]
-	if !ok {
-		return fmt.Errorf("event %s not present in supplied ABI", name)
-	} else if len(log.Topics) == 0 {
-		return errors.New("anonymous events are not supported")
-	} else if log.Topics[0] != eventAbi.ID {
-		return errors.New("event signature mismatch")
-	}
-
-	err := contractAbi.UnpackIntoInterface(out, name, log.Data)
-	if err != nil {
-		return err
-	}
-
-	// handle topics if present
-	if len(log.Topics) > 1 {
-		var indexedArgs abi.Arguments
-		for _, arg := range eventAbi.Inputs {
-			if arg.Indexed {
-				indexedArgs = append(indexedArgs, arg)
-			}
-		}
-
-		// The first topic (event signature) is omitted
-		err := abi.ParseTopics(out, indexedArgs, log.Topics[1:])
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
diff --git a/indexer/processor/cross_domain_messenger.go b/indexer/processor/cross_domain_messenger.go
deleted file mode 100644
index b99ca2292..000000000
--- a/indexer/processor/cross_domain_messenger.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package processor
-
-import (
-	"fmt"
-	"math/big"
-
-	"github.com/ethereum/go-ethereum/accounts/abi"
-	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/crypto"
-
-	"github.com/ethereum-optimism/optimism/indexer/database"
-	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
-)
-
-var (
-	// Standard ABI types copied from golang ABI tests
-	Uint256Type, _ = abi.NewType("uint256", "", nil)
-	BytesType, _   = abi.NewType("bytes", "", nil)
-	AddressType, _ = abi.NewType("address", "", nil)
-
-	LegacyCrossDomainMessengerRelayMessageMethod = abi.NewMethod(
-		"relayMessage",
-		"relayMessage",
-		abi.Function,
-		"external", // mutability
-		false,      // isConst
-		true,       // payable
-		abi.Arguments{ // inputs
-			{Name: "sender", Type: AddressType},
-			{Name: "target", Type: AddressType},
-			{Name: "data", Type: BytesType},
-			{Name: "nonce", Type: Uint256Type},
-		},
-		abi.Arguments{}, // outputs
-	)
-)
-
-type CrossDomainMessengerSentMessageEvent struct {
-	*bindings.CrossDomainMessengerSentMessage
-
-	Value       *big.Int
-	MessageHash common.Hash
-	Event       *database.ContractEvent
-}
-
-type CrossDomainMessengerRelayedMessageEvent struct {
-	*bindings.CrossDomainMessengerRelayedMessage
-	Event *database.ContractEvent
-}
-
-func CrossDomainMessengerSentMessageEvents(events *ProcessedContractEvents) ([]CrossDomainMessengerSentMessageEvent, error) {
-	crossDomainMessengerABI, err := bindings.CrossDomainMessengerMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	sentMessageEventAbi := crossDomainMessengerABI.Events["SentMessage"]
-	sentMessageEventExtensionAbi := crossDomainMessengerABI.Events["SentMessageExtension1"]
-
-	processedSentMessageEvents := events.eventsBySignature[sentMessageEventAbi.ID]
-	crossDomainMessageEvents := make([]CrossDomainMessengerSentMessageEvent, len(processedSentMessageEvents))
-	for i, sentMessageEvent := range processedSentMessageEvents {
-		log := sentMessageEvent.RLPLog
-
-		var sentMsgData bindings.CrossDomainMessengerSentMessage
-		sentMsgData.Raw = *log
-		err = UnpackLog(&sentMsgData, log, sentMessageEventAbi.Name, crossDomainMessengerABI)
-		if err != nil {
-			return nil, err
-		}
-
-		var sentMsgExtensionData bindings.CrossDomainMessengerSentMessageExtension1
-		extensionLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 1}].RLPLog
-		sentMsgExtensionData.Raw = *extensionLog
-		err = UnpackLog(&sentMsgExtensionData, extensionLog, sentMessageEventExtensionAbi.Name, crossDomainMessengerABI)
-		if err != nil {
-			return nil, err
-		}
-
-		msgHash, err := CrossDomainMessageHash(crossDomainMessengerABI, &sentMsgData, sentMsgExtensionData.Value)
-		if err != nil {
-			return nil, err
-		}
-
-		crossDomainMessageEvents[i] = CrossDomainMessengerSentMessageEvent{
-			CrossDomainMessengerSentMessage: &sentMsgData,
-			Value:                           sentMsgExtensionData.Value,
-			MessageHash:                     msgHash,
-			Event:                           sentMessageEvent,
-		}
-	}
-
-	return crossDomainMessageEvents, nil
-}
-
-func CrossDomainMessengerRelayedMessageEvents(events *ProcessedContractEvents) ([]CrossDomainMessengerRelayedMessageEvent, error) {
-	crossDomainMessengerABI, err := bindings.L1CrossDomainMessengerMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	relayedMessageEventAbi := crossDomainMessengerABI.Events["RelayedMessage"]
-	processedRelayedMessageEvents := events.eventsBySignature[relayedMessageEventAbi.ID]
-	crossDomainMessageEvents := make([]CrossDomainMessengerRelayedMessageEvent, len(processedRelayedMessageEvents))
-	for i, relayedMessageEvent := range processedRelayedMessageEvents {
-		log := relayedMessageEvent.RLPLog
-
-		var relayedMsgData bindings.CrossDomainMessengerRelayedMessage
-		relayedMsgData.Raw = *log
-		err = UnpackLog(&relayedMsgData, log, relayedMessageEventAbi.Name, crossDomainMessengerABI)
-		if err != nil {
-			return nil, err
-		}
-
-		crossDomainMessageEvents[i] = CrossDomainMessengerRelayedMessageEvent{
-			CrossDomainMessengerRelayedMessage: &relayedMsgData,
-			Event:                              relayedMessageEvent,
-		}
-	}
-
-	return crossDomainMessageEvents, nil
-}
-
-// Replica of `Hashing.sol#hashCrossDomainMessage` solidity implementation
-func CrossDomainMessageHash(abi *abi.ABI, sentMsg *bindings.CrossDomainMessengerSentMessage, value *big.Int) (common.Hash, error) {
-	version, _ := DecodeVersionedNonce(sentMsg.MessageNonce)
-	switch version {
-	case 0:
-		// Legacy Message
-		inputBytes, err := LegacyCrossDomainMessengerRelayMessageMethod.Inputs.Pack(sentMsg.Sender, sentMsg.Target, sentMsg.Message, sentMsg.MessageNonce)
-		if err != nil {
-			return common.Hash{}, err
-		}
-		msgBytes := append(LegacyCrossDomainMessengerRelayMessageMethod.ID, inputBytes...)
-		return crypto.Keccak256Hash(msgBytes), nil
-	case 1:
-		// Current Message
-		msgBytes, err := abi.Pack("relayMessage", sentMsg.MessageNonce, sentMsg.Sender, sentMsg.Target, value, sentMsg.GasLimit, sentMsg.Message)
-		if err != nil {
-			return common.Hash{}, err
-		}
-		return crypto.Keccak256Hash(msgBytes), nil
-	}
-
-	return common.Hash{}, fmt.Errorf("unsupported cross domain messenger version: %d", version)
-}
diff --git a/indexer/processor/encoding.go b/indexer/processor/encoding.go
deleted file mode 100644
index fe78cedc8..000000000
--- a/indexer/processor/encoding.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package processor
-
-import (
-	"encoding/binary"
-	"math/big"
-)
-
-// DecodeVersionNonce is an re-implementation of Encoding.sol#decodeVersionedNonce.
-// If the nonce is greater than 32 bytes (solidity uint256), bytes [32:] are ignored
-func DecodeVersionedNonce(nonce *big.Int) (uint16, *big.Int) {
-	nonceBytes := nonce.Bytes()
-	nonceByteLen := len(nonceBytes)
-	if nonceByteLen < 30 {
-		// version is 0x0000
-		return 0, nonce
-	} else if nonceByteLen == 31 {
-		// version is 0x00[01..ff]
-		return uint16(nonceBytes[0]), new(big.Int).SetBytes(nonceBytes[1:])
-	} else {
-		// fully specified
-		version := binary.BigEndian.Uint16(nonceBytes[:2])
-		return version, new(big.Int).SetBytes(nonceBytes[2:])
-	}
-}
diff --git a/indexer/processor/l1_processor.go b/indexer/processor/l1_processor.go
deleted file mode 100644
index 25ab7b378..000000000
--- a/indexer/processor/l1_processor.go
+++ /dev/null
@@ -1,514 +0,0 @@
-package processor
-
-import (
-	"bytes"
-	"context"
-	"errors"
-	"fmt"
-	"math/big"
-
-	"github.com/ethereum-optimism/optimism/indexer/config"
-	"github.com/ethereum-optimism/optimism/indexer/database"
-	"github.com/ethereum-optimism/optimism/indexer/node"
-	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
-	legacy_bindings "github.com/ethereum-optimism/optimism/op-bindings/legacy-bindings"
-	"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
-
-	"github.com/ethereum/go-ethereum"
-	"github.com/ethereum/go-ethereum/accounts/abi"
-	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/core/types"
-	"github.com/ethereum/go-ethereum/ethclient"
-	"github.com/ethereum/go-ethereum/log"
-)
-
-type checkpointAbi struct {
-	l2OutputOracle             *abi.ABI
-	legacyStateCommitmentChain *abi.ABI
-}
-
-type L1Processor struct {
-	processor
-}
-
-func NewL1Processor(logger log.Logger, ethClient node.EthClient, db *database.DB, l1Contracts config.L1Contracts) (*L1Processor, error) {
-	l1ProcessLog := logger.New("processor", "l1")
-	l1ProcessLog.Info("initializing processor")
-
-	l2OutputOracleABI, err := bindings.L2OutputOracleMetaData.GetAbi()
-	if err != nil {
-		l1ProcessLog.Error("unable to generate L2OutputOracle ABI", "err", err)
-		return nil, err
-	}
-	legacyStateCommitmentChainABI, err := legacy_bindings.StateCommitmentChainMetaData.GetAbi()
-	if err != nil {
-		l1ProcessLog.Error("unable to generate legacy StateCommitmentChain ABI", "err", err)
-		return nil, err
-	}
-	checkpointAbi := checkpointAbi{l2OutputOracle: l2OutputOracleABI, legacyStateCommitmentChain: legacyStateCommitmentChainABI}
-
-	latestHeader, err := db.Blocks.LatestL1BlockHeader()
-	if err != nil {
-		return nil, err
-	}
-
-	var fromL1Header *types.Header
-	if latestHeader != nil {
-		l1ProcessLog.Info("detected last indexed block", "height", latestHeader.Number.Int, "hash", latestHeader.Hash)
-		l1Header, err := ethClient.BlockHeaderByHash(latestHeader.Hash)
-		if err != nil {
-			l1ProcessLog.Error("unable to fetch header for last indexed block", "hash", latestHeader.Hash, "err", err)
-			return nil, err
-		}
-
-		fromL1Header = l1Header
-	} else {
-		// we shouldn't start from genesis with l1. Need a "genesis" L1 height provided for the rollup
-		l1ProcessLog.Info("no indexed state, starting from genesis")
-		fromL1Header = nil
-	}
-
-	l1Processor := &L1Processor{
-		processor: processor{
-			headerTraversal: node.NewHeaderTraversal(ethClient, fromL1Header),
-			db:              db,
-			processFn:       l1ProcessFn(l1ProcessLog, ethClient, l1Contracts, checkpointAbi),
-			processLog:      l1ProcessLog,
-		},
-	}
-
-	return l1Processor, nil
-}
-
-func l1ProcessFn(processLog log.Logger, ethClient node.EthClient, l1Contracts config.L1Contracts, checkpointAbi checkpointAbi) ProcessFn {
-	rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())
-
-	contractAddrs := l1Contracts.ToSlice()
-	processLog.Info("processor configured with contracts", "contracts", l1Contracts)
-
-	outputProposedEventName := "OutputProposed"
-	outputProposedEventSig := checkpointAbi.l2OutputOracle.Events[outputProposedEventName].ID
-
-	legacyStateBatchAppendedEventName := "StateBatchAppended"
-	legacyStateBatchAppendedEventSig := checkpointAbi.legacyStateCommitmentChain.Events[legacyStateBatchAppendedEventName].ID
-
-	return func(db *database.DB, headers []*types.Header) error {
-		headerMap := make(map[common.Hash]*types.Header)
-		for _, header := range headers {
-			headerMap[header.Hash()] = header
-		}
-
-		/** Watch for all Optimism Contract Events **/
-
-		logFilter := ethereum.FilterQuery{FromBlock: headers[0].Number, ToBlock: headers[len(headers)-1].Number, Addresses: contractAddrs}
-		logs, err := rawEthClient.FilterLogs(context.Background(), logFilter) // []types.Log
-		if err != nil {
-			return err
-		}
-
-		// L2 checkpoints posted on L1
-		outputProposals := []*database.OutputProposal{}
-		legacyStateBatches := []*database.LegacyStateBatch{}
-
-		l1HeadersOfInterest := make(map[common.Hash]bool)
-		l1ContractEvents := make([]*database.L1ContractEvent, len(logs))
-
-		processedContractEvents := NewProcessedContractEvents()
-		for i := range logs {
-			log := &logs[i]
-			header, ok := headerMap[log.BlockHash]
-			if !ok {
-				processLog.Error("contract event found with associated header not in the batch", "header", log.BlockHash, "log_index", log.Index)
-				return errors.New("parsed log with a block hash not in this batch")
-			}
-
-			contractEvent := processedContractEvents.AddLog(log, header.Time)
-			l1HeadersOfInterest[log.BlockHash] = true
-			l1ContractEvents[i] = &database.L1ContractEvent{ContractEvent: *contractEvent}
-
-			// Track Checkpoint Events for L2
-			switch contractEvent.EventSignature {
-			case outputProposedEventSig:
-				var outputProposed bindings.L2OutputOracleOutputProposed
-				err := UnpackLog(&outputProposed, log, outputProposedEventName, checkpointAbi.l2OutputOracle)
-				if err != nil {
-					return err
-				}
-
-				outputProposals = append(outputProposals, &database.OutputProposal{
-					OutputRoot:          outputProposed.OutputRoot,
-					L2OutputIndex:       database.U256{Int: outputProposed.L2OutputIndex},
-					L2BlockNumber:       database.U256{Int: outputProposed.L2BlockNumber},
-					L1ContractEventGUID: contractEvent.GUID,
-				})
-
-			case legacyStateBatchAppendedEventSig:
-				var stateBatchAppended legacy_bindings.StateCommitmentChainStateBatchAppended
-				err := UnpackLog(&stateBatchAppended, log, legacyStateBatchAppendedEventName, checkpointAbi.legacyStateCommitmentChain)
-				if err != nil {
-					return err
-				}
-
-				legacyStateBatches = append(legacyStateBatches, &database.LegacyStateBatch{
-					Index:               stateBatchAppended.BatchIndex.Uint64(),
-					Root:                stateBatchAppended.BatchRoot,
-					Size:                stateBatchAppended.BatchSize.Uint64(),
-					PrevTotal:           stateBatchAppended.PrevTotalElements.Uint64(),
-					L1ContractEventGUID: contractEvent.GUID,
-				})
-			}
-		}
-
-		/** Aggregate applicable L1 Blocks **/
-
-		// we iterate on the original array to maintain ordering. probably can find a more efficient
-		// way to iterate over the `l1HeadersOfInterest` map while maintaining ordering
-		indexedL1Headers := []*database.L1BlockHeader{}
-		for _, header := range headers {
-			_, hasLogs := l1HeadersOfInterest[header.Hash()]
-			if !hasLogs {
-				continue
-			}
-
-			indexedL1Headers = append(indexedL1Headers, &database.L1BlockHeader{BlockHeader: database.BlockHeaderFromHeader(header)})
-		}
-
-		/** Update Database **/
-
-		numIndexedL1Headers := len(indexedL1Headers)
-		if numIndexedL1Headers > 0 {
-			processLog.Info("saving l1 blocks with optimism logs", "size", numIndexedL1Headers, "batch_size", len(headers))
-			err = db.Blocks.StoreL1BlockHeaders(indexedL1Headers)
-			if err != nil {
-				return err
-			}
-
-			// Since the headers to index are derived from the existence of logs, we know in this branch `numLogs > 0`
-			processLog.Info("detected contract logs", "size", len(l1ContractEvents))
-			err = db.ContractEvents.StoreL1ContractEvents(l1ContractEvents)
-			if err != nil {
-				return err
-			}
-
-			// Mark L2 checkpoints that have been recorded on L1 (L2OutputProposal & StateBatchAppended events)
-			numLegacyStateBatches := len(legacyStateBatches)
-			if numLegacyStateBatches > 0 {
-				latestBatch := legacyStateBatches[numLegacyStateBatches-1]
-				latestL2Height := latestBatch.PrevTotal + latestBatch.Size - 1
-				processLog.Info("detected legacy state batches", "size", numLegacyStateBatches, "latest_l2_block_number", latestL2Height)
-			}
-
-			numOutputProposals := len(outputProposals)
-			if numOutputProposals > 0 {
-				latestL2Height := outputProposals[numOutputProposals-1].L2BlockNumber.Int
-				processLog.Info("detected output proposals", "size", numOutputProposals, "latest_l2_block_number", latestL2Height)
-				err := db.Blocks.StoreOutputProposals(outputProposals)
-				if err != nil {
-					return err
-				}
-			}
-
-			// forward along contract events to bridge txs processor
-			err = l1ProcessContractEventsBridgeTransactions(processLog, db, l1Contracts, processedContractEvents)
-			if err != nil {
-				return err
-			}
-
-			// forward along contract events to bridge messages processor
-			err = l1ProcessContractEventsBridgeCrossDomainMessages(processLog, db, processedContractEvents)
-			if err != nil {
-				return err
-			}
-
-			// forward along contract events to standard bridge processor
-			err = l1ProcessContractEventsStandardBridge(processLog, db, processedContractEvents)
-			if err != nil {
-				return err
-			}
-		} else {
-			processLog.Info("no l1 blocks of interest within batch")
-		}
-
-		// a-ok!
-		return nil
-	}
-}
-
-func l1ProcessContractEventsBridgeTransactions(processLog log.Logger, db *database.DB, l1Contracts config.L1Contracts, events *ProcessedContractEvents) error {
-	// (1) Process New Deposits
-	portalDeposits, err := OptimismPortalTransactionDepositEvents(events)
-	if err != nil {
-		return err
-	}
-
-	ethDeposits := []*database.L1BridgeDeposit{}
-	transactionDeposits := make([]*database.L1TransactionDeposit, len(portalDeposits))
-	for i, depositEvent := range portalDeposits {
-		depositTx := depositEvent.DepositTx
-		transactionDeposits[i] = &database.L1TransactionDeposit{
-			SourceHash:           depositTx.SourceHash,
-			L2TransactionHash:    types.NewTx(depositTx).Hash(),
-			InitiatedL1EventGUID: depositEvent.Event.GUID,
-			GasLimit:             database.U256{Int: new(big.Int).SetUint64(depositTx.Gas)},
-			Tx: database.Transaction{
-				FromAddress: depositTx.From,
-				ToAddress:   depositTx.From,
-				Amount:      database.U256{Int: depositTx.Value},
-				Data:        depositTx.Data,
-				Timestamp:   depositEvent.Event.Timestamp,
-			},
-		}
-
-		// catch ETH transfers to the portal contract.
-		if len(depositTx.Data) == 0 && depositTx.Value.BitLen() > 0 {
-			ethDeposits = append(ethDeposits, &database.L1BridgeDeposit{
-				TransactionSourceHash: depositTx.SourceHash,
-				BridgeTransfer: database.BridgeTransfer{
-					Tx: transactionDeposits[i].Tx,
-					// TODO index eth token if it doesn't exist
-					TokenPair: database.ETHTokenPair,
-				},
-			})
-		}
-	}
-
-	if len(transactionDeposits) > 0 {
-		processLog.Info("detected transaction deposits", "size", len(transactionDeposits))
-		err := db.BridgeTransactions.StoreL1TransactionDeposits(transactionDeposits)
-		if err != nil {
-			return err
-		}
-
-		if len(ethDeposits) > 0 {
-			processLog.Info("detected portal ETH transfers", "size", len(ethDeposits))
-			err := db.BridgeTransfers.StoreL1BridgeDeposits(ethDeposits)
-			if err != nil {
-				return err
-			}
-		}
-	}
-
-	// (2) Process Proven Withdrawals
-	provenWithdrawals, err := OptimismPortalWithdrawalProvenEvents(events)
-	if err != nil {
-		return err
-	}
-
-	latestL2Header, err := db.Blocks.LatestL2BlockHeader()
-	if err != nil {
-		return nil
-	} else if len(provenWithdrawals) > 0 && latestL2Header == nil {
-		return errors.New("no indexed L2 headers to prove withdrawals. waiting for L2Processor to catch up")
-	}
-
-	for _, provenWithdrawal := range provenWithdrawals {
-		withdrawalHash := provenWithdrawal.WithdrawalHash
-		withdrawal, err := db.BridgeTransactions.L2TransactionWithdrawal(withdrawalHash)
-		if err != nil {
-			return err
-		}
-
-		if withdrawal == nil {
-			// We need to ensure we are in a caught up state before claiming a missing event. Since L2 timestamps
-			// are derived from L1, we can simply compare the timestamp of this event with the latest L2 header.
-			if provenWithdrawal.Event.Timestamp > latestL2Header.Timestamp {
-				processLog.Warn("behind on indexed L2 withdrawals")
-				return errors.New("waiting for L2Processor to catch up")
-			} else {
-				processLog.Crit("L2 withdrawal missing!", "withdrawal_hash", withdrawalHash)
-				return errors.New("withdrawal missing!")
-			}
-		}
-
-		err = db.BridgeTransactions.MarkL2TransactionWithdrawalProvenEvent(withdrawalHash, provenWithdrawal.Event.GUID)
-		if err != nil {
-			return err
-		}
-	}
-
-	if len(provenWithdrawals) > 0 {
-		processLog.Info("proven transaction withdrawals", "size", len(provenWithdrawals))
-	}
-
-	// (2) Process Withdrawal Finalization
-	finalizedWithdrawals, err := OptimismPortalWithdrawalFinalizedEvents(events)
-	if err != nil {
-		return err
-	}
-
-	for _, finalizedWithdrawal := range finalizedWithdrawals {
-		withdrawalHash := finalizedWithdrawal.WithdrawalHash
-		withdrawal, err := db.BridgeTransactions.L2TransactionWithdrawal(withdrawalHash)
-		if err != nil {
-			return err
-		} else if withdrawal == nil {
-			// since withdrawals must be proven first, we don't have to check on the L2Processor
-			processLog.Crit("withdrawal missing!", "hash", withdrawalHash)
-			return errors.New("withdrawal missing!")
-		}
-
-		err = db.BridgeTransactions.MarkL2TransactionWithdrawalFinalizedEvent(withdrawalHash, finalizedWithdrawal.Event.GUID, finalizedWithdrawal.Success)
-		if err != nil {
-			return err
-		}
-	}
-
-	if len(finalizedWithdrawals) > 0 {
-		processLog.Info("finalized transaction withdrawals", "size", len(finalizedWithdrawals))
-	}
-
-	// a-ok
-	return nil
-}
-
-func l1ProcessContractEventsBridgeCrossDomainMessages(processLog log.Logger, db *database.DB, events *ProcessedContractEvents) error {
-	// (1) Process New Messages
-	sentMessageEvents, err := CrossDomainMessengerSentMessageEvents(events)
-	if err != nil {
-		return err
-	}
-
-	sentMessages := make([]*database.L1BridgeMessage, len(sentMessageEvents))
-	for i, sentMessageEvent := range sentMessageEvents {
-		log := sentMessageEvent.Event.RLPLog
-
-		// extract the deposit hash from the previous TransactionDepositedEvent
-		transactionDepositedLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index - 1}].RLPLog
-		depositTx, err := derive.UnmarshalDepositLogEvent(transactionDepositedLog)
-		if err != nil {
-			return err
-		}
-
-		sentMessages[i] = &database.L1BridgeMessage{
-			TransactionSourceHash: depositTx.SourceHash,
-			BridgeMessage: database.BridgeMessage{
-				MessageHash:          sentMessageEvent.MessageHash,
-				Nonce:                database.U256{Int: sentMessageEvent.MessageNonce},
-				SentMessageEventGUID: sentMessageEvent.Event.GUID,
-				GasLimit:             database.U256{Int: sentMessageEvent.GasLimit},
-				Tx: database.Transaction{
-					FromAddress: sentMessageEvent.Sender,
-					ToAddress:   sentMessageEvent.Target,
-					Amount:      database.U256{Int: sentMessageEvent.Value},
-					Data:        sentMessageEvent.Message,
-					Timestamp:   sentMessageEvent.Event.Timestamp,
-				},
-			},
-		}
-	}
-
-	if len(sentMessages) > 0 {
-		processLog.Info("detected L1CrossDomainMessenger messages", "size", len(sentMessages))
-		err := db.BridgeMessages.StoreL1BridgeMessages(sentMessages)
-		if err != nil {
-			return err
-		}
-	}
-
-	// (2) Process Relayed Messages.
-	//
-	// NOTE: Should we care about failed messages? A failed message can be
-	// inferred via a finalized withdrawal that has not been marked as relayed.
-	relayedMessageEvents, err := CrossDomainMessengerRelayedMessageEvents(events)
-	if err != nil {
-		return err
-	}
-
-	for _, relayedMessage := range relayedMessageEvents {
-		message, err := db.BridgeMessages.L2BridgeMessage(relayedMessage.MsgHash)
-		if err != nil {
-			return err
-		} else if message == nil {
-			// Since L2 withdrawals must be proven before being relayed, the transaction processor
-			// ensures that we are in indexed state on L2 if we've seen this finalization event
-			processLog.Crit("missing indexed L2CrossDomainMessenger sent message", "message_hash", relayedMessage.MsgHash)
-			return fmt.Errorf("missing indexed L2CrossDomainMessager mesesage: 0x%x", relayedMessage.MsgHash)
-		}
-
-		err = db.BridgeMessages.MarkRelayedL2BridgeMessage(relayedMessage.MsgHash, relayedMessage.Event.GUID)
-		if err != nil {
-			return err
-		}
-	}
-
-	if len(relayedMessageEvents) > 0 {
-		processLog.Info("relayed L2CrossDomainMessenger messages", "size", len(relayedMessageEvents))
-	}
-
-	// a-ok!
-	return nil
-}
-
-func l1ProcessContractEventsStandardBridge(processLog log.Logger, db *database.DB, events *ProcessedContractEvents) error {
-	// (1) Process New Deposits
-	initiatedDepositEvents, err := StandardBridgeInitiatedEvents(events)
-	if err != nil {
-		return err
-	}
-
-	deposits := make([]*database.L1BridgeDeposit, len(initiatedDepositEvents))
-	for i, initiatedBridgeEvent := range initiatedDepositEvents {
-		log := initiatedBridgeEvent.Event.RLPLog
-
-		// extract the deposit hash from the following TransactionDeposited event. The `BlockHash` and `LogIndex`
-		// fields are filled in for `RLPLog` which is required for `DepositTx#SourceHash` to be computed correctly
-		transactionDepositedRLPLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 1}].RLPLog
-		depositTx, err := derive.UnmarshalDepositLogEvent(transactionDepositedRLPLog)
-		if err != nil {
-			return err
-		}
-
-		deposits[i] = &database.L1BridgeDeposit{
-			TransactionSourceHash: depositTx.SourceHash,
-			BridgeTransfer: database.BridgeTransfer{
-				CrossDomainMessageHash: &initiatedBridgeEvent.CrossDomainMessageHash,
-				// TODO index the tokens pairs if they don't exist
-				TokenPair: database.TokenPair{LocalTokenAddress: initiatedBridgeEvent.LocalToken, RemoteTokenAddress: initiatedBridgeEvent.RemoteToken},
-				Tx: database.Transaction{
-					FromAddress: initiatedBridgeEvent.From,
-					ToAddress:   initiatedBridgeEvent.To,
-					Amount:      database.U256{Int: initiatedBridgeEvent.Amount},
-					Data:        initiatedBridgeEvent.ExtraData,
-					Timestamp:   initiatedBridgeEvent.Event.Timestamp,
-				},
-			},
-		}
-	}
-
-	if len(deposits) > 0 {
-		processLog.Info("detected L1StandardBridge deposits", "size", len(deposits))
-		err := db.BridgeTransfers.StoreL1BridgeDeposits(deposits)
-		if err != nil {
-			return err
-		}
-	}
-
-	// (2) Process Finalized Withdrawals
-	//  - We dont need do anything actionable on the database here as this is layered on top of the
-	// bridge transaction & messages that have a tracked lifecyle. We simply walk through and ensure
-	// that the corresponding initiated withdrawals exist and match as an integrity check
-	finalizedWithdrawalEvents, err := StandardBridgeFinalizedEvents(events)
-	if err != nil {
-		return err
-	}
-
-	for _, finalizedWithdrawalEvent := range finalizedWithdrawalEvents {
-		withdrawal, err := db.BridgeTransfers.L2BridgeWithdrawalWithFilter(database.BridgeTransfer{CrossDomainMessageHash: &finalizedWithdrawalEvent.CrossDomainMessageHash})
-		if err != nil {
-			return err
-		} else if withdrawal == nil {
-			processLog.Error("missing indexed L2StandardBridge withdrawal for finalization", "cross_domain_message_hash", finalizedWithdrawalEvent.CrossDomainMessageHash)
-			return errors.New("missing indexed L2StandardBridge withdrawal for finalization event")
-		}
-
-		// sanity check on the bridge fields
-		if finalizedWithdrawalEvent.From != withdrawal.Tx.FromAddress || finalizedWithdrawalEvent.To != withdrawal.Tx.ToAddress ||
-			finalizedWithdrawalEvent.Amount.Cmp(withdrawal.Tx.Amount.Int) != 0 || !bytes.Equal(finalizedWithdrawalEvent.ExtraData, withdrawal.Tx.Data) ||
-			finalizedWithdrawalEvent.LocalToken != withdrawal.TokenPair.LocalTokenAddress || finalizedWithdrawalEvent.RemoteToken != withdrawal.TokenPair.RemoteTokenAddress {
-			processLog.Crit("bridge finalization fields mismatch with initiated fields!", "tx_withdrawal_hash", withdrawal.TransactionWithdrawalHash, "cross_domain_message_hash", withdrawal.CrossDomainMessageHash)
-		}
-	}
-
-	// a-ok!
-	return nil
-}
diff --git a/indexer/processor/l2_processor.go b/indexer/processor/l2_processor.go
deleted file mode 100644
index a1bcb4fb6..000000000
--- a/indexer/processor/l2_processor.go
+++ /dev/null
@@ -1,409 +0,0 @@
-package processor
-
-import (
-	"bytes"
-	"context"
-	"errors"
-	"fmt"
-	"reflect"
-
-	"github.com/ethereum-optimism/optimism/indexer/database"
-	"github.com/ethereum-optimism/optimism/indexer/node"
-	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
-
-	"github.com/ethereum/go-ethereum"
-	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/core/types"
-	"github.com/ethereum/go-ethereum/ethclient"
-	"github.com/ethereum/go-ethereum/log"
-)
-
-type L2Contracts struct {
-	L2CrossDomainMessenger common.Address
-	L2StandardBridge       common.Address
-	L2ERC721Bridge         common.Address
-	L2ToL1MessagePasser    common.Address
-
-	// Some more contracts -- ProxyAdmin, SystemConfig, etcc
-	// Ignore the auxiliary contracts?
-
-	// Legacy Contracts? We'll add this in to index the legacy chain.
-	// Remove afterwards?
-}
-
-func L2ContractPredeploys() L2Contracts {
-	return L2Contracts{
-		L2CrossDomainMessenger: common.HexToAddress("0x4200000000000000000000000000000000000007"),
-		L2StandardBridge:       common.HexToAddress("0x4200000000000000000000000000000000000010"),
-		L2ERC721Bridge:         common.HexToAddress("0x4200000000000000000000000000000000000014"),
-		L2ToL1MessagePasser:    common.HexToAddress("0x4200000000000000000000000000000000000016"),
-	}
-}
-
-func (c L2Contracts) ToSlice() []common.Address {
-	fields := reflect.VisibleFields(reflect.TypeOf(c))
-	v := reflect.ValueOf(c)
-
-	contracts := make([]common.Address, len(fields))
-	for i, field := range fields {
-		contracts[i] = (v.FieldByName(field.Name).Interface()).(common.Address)
-	}
-
-	return contracts
-}
-
-type L2Processor struct {
-	processor
-}
-
-func NewL2Processor(logger log.Logger, ethClient node.EthClient, db *database.DB, l2Contracts L2Contracts) (*L2Processor, error) {
-	l2ProcessLog := logger.New("processor", "l2")
-	l2ProcessLog.Info("initializing processor")
-
-	latestHeader, err := db.Blocks.LatestL2BlockHeader()
-	if err != nil {
-		return nil, err
-	}
-
-	var fromL2Header *types.Header
-	if latestHeader != nil {
-		l2ProcessLog.Info("detected last indexed block", "height", latestHeader.Number.Int, "hash", latestHeader.Hash)
-		l2Header, err := ethClient.BlockHeaderByHash(latestHeader.Hash)
-		if err != nil {
-			l2ProcessLog.Error("unable to fetch header for last indexed block", "hash", latestHeader.Hash, "err", err)
-			return nil, err
-		}
-
-		fromL2Header = l2Header
-	} else {
-		l2ProcessLog.Info("no indexed state, starting from genesis")
-		fromL2Header = nil
-	}
-
-	l2Processor := &L2Processor{
-		processor: processor{
-			headerTraversal: node.NewHeaderTraversal(ethClient, fromL2Header),
-			db:              db,
-			processFn:       l2ProcessFn(l2ProcessLog, ethClient, l2Contracts),
-			processLog:      l2ProcessLog,
-		},
-	}
-
-	return l2Processor, nil
-}
-
-func l2ProcessFn(processLog log.Logger, ethClient node.EthClient, l2Contracts L2Contracts) ProcessFn {
-	rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())
-
-	contractAddrs := l2Contracts.ToSlice()
-	processLog.Info("processor configured with contracts", "contracts", l2Contracts)
-	return func(db *database.DB, headers []*types.Header) error {
-		numHeaders := len(headers)
-
-		/** Index all L2 blocks **/
-
-		l2Headers := make([]*database.L2BlockHeader, len(headers))
-		l2HeaderMap := make(map[common.Hash]*types.Header)
-		for i, header := range headers {
-			l2Headers[i] = &database.L2BlockHeader{BlockHeader: database.BlockHeaderFromHeader(header)}
-			l2HeaderMap[l2Headers[i].Hash] = header
-		}
-
-		/** Watch for Contract Events **/
-
-		logFilter := ethereum.FilterQuery{FromBlock: headers[0].Number, ToBlock: headers[numHeaders-1].Number, Addresses: contractAddrs}
-		logs, err := rawEthClient.FilterLogs(context.Background(), logFilter)
-		if err != nil {
-			return err
-		}
-
-		l2ContractEvents := make([]*database.L2ContractEvent, len(logs))
-		processedContractEvents := NewProcessedContractEvents()
-		for i := range logs {
-			log := &logs[i]
-			header, ok := l2HeaderMap[log.BlockHash]
-			if !ok {
-				processLog.Error("contract event found with associated header not in the batch", "header", header, "log_index", log.Index)
-				return errors.New("parsed log with a block hash not in this batch")
-			}
-
-			contractEvent := processedContractEvents.AddLog(log, header.Time)
-			l2ContractEvents[i] = &database.L2ContractEvent{ContractEvent: *contractEvent}
-		}
-
-		/** Update Database **/
-
-		processLog.Info("saving l2 blocks", "size", numHeaders)
-		err = db.Blocks.StoreL2BlockHeaders(l2Headers)
-		if err != nil {
-			return err
-		}
-
-		numLogs := len(l2ContractEvents)
-		if numLogs > 0 {
-			processLog.Info("detected contract logs", "size", numLogs)
-			err = db.ContractEvents.StoreL2ContractEvents(l2ContractEvents)
-			if err != nil {
-				return err
-			}
-
-			// forward along contract events to bridge txs processor
-			err = l2ProcessContractEventsBridgeTransactions(processLog, db, processedContractEvents)
-			if err != nil {
-				return err
-			}
-
-			err = l2ProcessContractEventsBridgeCrossDomainMessages(processLog, db, processedContractEvents)
-			if err != nil {
-				return err
-			}
-
-			// forward along contract events to standard bridge processor
-			err = l2ProcessContractEventsStandardBridge(processLog, db, processedContractEvents)
-			if err != nil {
-				return err
-			}
-		}
-
-		// a-ok!
-		return nil
-	}
-}
-
-func l2ProcessContractEventsBridgeTransactions(processLog log.Logger, db *database.DB, events *ProcessedContractEvents) error {
-	// (1) Process New Withdrawals
-	messagesPassed, err := L2ToL1MessagePasserMessagesPassed(events)
-	if err != nil {
-		return err
-	}
-
-	ethWithdrawals := []*database.L2BridgeWithdrawal{}
-	transactionWithdrawals := make([]*database.L2TransactionWithdrawal, len(messagesPassed))
-	for i, withdrawalEvent := range messagesPassed {
-		transactionWithdrawals[i] = &database.L2TransactionWithdrawal{
-			WithdrawalHash:       withdrawalEvent.WithdrawalHash,
-			InitiatedL2EventGUID: withdrawalEvent.Event.GUID,
-			Nonce:                database.U256{Int: withdrawalEvent.Nonce},
-			GasLimit:             database.U256{Int: withdrawalEvent.GasLimit},
-			Tx: database.Transaction{
-				FromAddress: withdrawalEvent.Sender,
-				ToAddress:   withdrawalEvent.Target,
-				Amount:      database.U256{Int: withdrawalEvent.Value},
-				Data:        withdrawalEvent.Data,
-				Timestamp:   withdrawalEvent.Event.Timestamp,
-			},
-		}
-
-		if len(withdrawalEvent.Data) == 0 && withdrawalEvent.Value.BitLen() > 0 {
-			ethWithdrawals = append(ethWithdrawals, &database.L2BridgeWithdrawal{
-				TransactionWithdrawalHash: withdrawalEvent.WithdrawalHash,
-				BridgeTransfer: database.BridgeTransfer{
-					Tx:        transactionWithdrawals[i].Tx,
-					TokenPair: database.ETHTokenPair,
-				},
-			})
-		}
-	}
-
-	if len(transactionWithdrawals) > 0 {
-		processLog.Info("detected transaction withdrawals", "size", len(transactionWithdrawals))
-		err := db.BridgeTransactions.StoreL2TransactionWithdrawals(transactionWithdrawals)
-		if err != nil {
-			return err
-		}
-
-		if len(ethWithdrawals) > 0 {
-			processLog.Info("detected L2ToL1MessagePasser ETH transfers", "size", len(ethWithdrawals))
-			err := db.BridgeTransfers.StoreL2BridgeWithdrawals(ethWithdrawals)
-			if err != nil {
-				return err
-			}
-		}
-	}
-
-	// (2) Process Deposit Finalization
-	//  - Since L2 deposits are apart of the block derivation processes, we dont track finalization as it's too tricky
-	// to do so purely from the L2-side since there is not a way to easily identify deposit transactions on L2 without walking
-	// the transaction list of every L2 epoch.
-
-	// a-ok!
-	return nil
-}
-
-func l2ProcessContractEventsBridgeCrossDomainMessages(processLog log.Logger, db *database.DB, events *ProcessedContractEvents) error {
-	l2ToL1MessagePasserABI, err := bindings.NewL2ToL1MessagePasser(common.Address{}, nil)
-	if err != nil {
-		return err
-	}
-
-	// (2) Process New Messages
-	sentMessageEvents, err := CrossDomainMessengerSentMessageEvents(events)
-	if err != nil {
-		return err
-	}
-
-	sentMessages := make([]*database.L2BridgeMessage, len(sentMessageEvents))
-	for i, sentMessageEvent := range sentMessageEvents {
-		log := sentMessageEvent.Event.RLPLog
-
-		// extract the withdrawal hash from the previous MessagePassed event
-		msgPassedLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index - 1}].RLPLog
-		msgPassedEvent, err := l2ToL1MessagePasserABI.ParseMessagePassed(*msgPassedLog)
-		if err != nil {
-			return err
-		}
-
-		sentMessages[i] = &database.L2BridgeMessage{
-			TransactionWithdrawalHash: msgPassedEvent.WithdrawalHash,
-			BridgeMessage: database.BridgeMessage{
-				MessageHash:          sentMessageEvent.MessageHash,
-				Nonce:                database.U256{Int: sentMessageEvent.MessageNonce},
-				SentMessageEventGUID: sentMessageEvent.Event.GUID,
-				GasLimit:             database.U256{Int: sentMessageEvent.GasLimit},
-				Tx: database.Transaction{
-					FromAddress: sentMessageEvent.Sender,
-					ToAddress:   sentMessageEvent.Target,
-					Amount:      database.U256{Int: sentMessageEvent.Value},
-					Data:        sentMessageEvent.Message,
-					Timestamp:   sentMessageEvent.Event.Timestamp,
-				},
-			},
-		}
-	}
-
-	if len(sentMessages) > 0 {
-		processLog.Info("detected L2CrossDomainMessenger messages", "size", len(sentMessages))
-		err := db.BridgeMessages.StoreL2BridgeMessages(sentMessages)
-		if err != nil {
-			return err
-		}
-	}
-
-	// (2) Process Relayed Messages.
-	//
-	// NOTE: Should we care about failed messages? A failed message can be
-	// inferred via an included deposit on L2 that has not been marked as relayed.
-	relayedMessageEvents, err := CrossDomainMessengerRelayedMessageEvents(events)
-	if err != nil {
-		return err
-	}
-
-	latestL1Header, err := db.Blocks.LatestL1BlockHeader()
-	if err != nil {
-		return err
-	} else if len(relayedMessageEvents) > 0 && latestL1Header == nil {
-		return errors.New("no indexed L1 headers to relay messages. waiting for L1Processor to catch up")
-	}
-
-	for _, relayedMessage := range relayedMessageEvents {
-		message, err := db.BridgeMessages.L1BridgeMessage(relayedMessage.MsgHash)
-		if err != nil {
-			return err
-		}
-
-		if message == nil {
-			// Since the transaction processor running prior does not ensure the deposit inclusion, we need to
-			// ensure we are in a caught up state before claiming a missing event. Since L2 timestamps are derived
-			// from L1, we can simply compare the timestamp of this event with the latest L1 header.
-			if latestL1Header == nil || relayedMessage.Event.Timestamp > latestL1Header.Timestamp {
-				processLog.Warn("waiting for L1Processor to catch up on L1CrossDomainMessages")
-				return errors.New("waiting for L1Processor to catch up")
-			} else {
-				processLog.Crit("missing indexed L1CrossDomainMessenger message", "message_hash", relayedMessage.MsgHash)
-				return fmt.Errorf("missing indexed L1CrossDomainMessager mesesage: 0x%x", relayedMessage.MsgHash)
-			}
-		}
-
-		err = db.BridgeMessages.MarkRelayedL1BridgeMessage(relayedMessage.MsgHash, relayedMessage.Event.GUID)
-		if err != nil {
-			return err
-		}
-	}
-
-	if len(relayedMessageEvents) > 0 {
-		processLog.Info("relayed L1CrossDomainMessenger messages", "size", len(relayedMessageEvents))
-	}
-
-	// a-ok!
-	return nil
-}
-
-func l2ProcessContractEventsStandardBridge(processLog log.Logger, db *database.DB, events *ProcessedContractEvents) error {
-	l2ToL1MessagePasserABI, err := bindings.NewL2ToL1MessagePasser(common.Address{}, nil)
-	if err != nil {
-		return err
-	}
-
-	// (1) Process New Withdrawals
-	initiatedWithdrawalEvents, err := StandardBridgeInitiatedEvents(events)
-	if err != nil {
-		return err
-	}
-
-	withdrawals := make([]*database.L2BridgeWithdrawal, len(initiatedWithdrawalEvents))
-	for i, initiatedBridgeEvent := range initiatedWithdrawalEvents {
-		log := initiatedBridgeEvent.Event.RLPLog
-
-		// extract the withdrawal hash from the following MessagePassed event
-		msgPassedLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 1}].RLPLog
-		msgPassedEvent, err := l2ToL1MessagePasserABI.ParseMessagePassed(*msgPassedLog)
-		if err != nil {
-			return err
-		}
-
-		withdrawals[i] = &database.L2BridgeWithdrawal{
-			TransactionWithdrawalHash: msgPassedEvent.WithdrawalHash,
-			BridgeTransfer: database.BridgeTransfer{
-				CrossDomainMessageHash: &initiatedBridgeEvent.CrossDomainMessageHash,
-				TokenPair:              database.TokenPair{LocalTokenAddress: initiatedBridgeEvent.LocalToken, RemoteTokenAddress: initiatedBridgeEvent.RemoteToken},
-				Tx: database.Transaction{
-					FromAddress: initiatedBridgeEvent.From,
-					ToAddress:   initiatedBridgeEvent.To,
-					Amount:      database.U256{Int: initiatedBridgeEvent.Amount},
-					Data:        initiatedBridgeEvent.ExtraData,
-					Timestamp:   initiatedBridgeEvent.Event.Timestamp,
-				},
-			},
-		}
-	}
-
-	if len(withdrawals) > 0 {
-		processLog.Info("detected L2StandardBridge withdrawals", "num", len(withdrawals))
-		err := db.BridgeTransfers.StoreL2BridgeWithdrawals(withdrawals)
-		if err != nil {
-			return err
-		}
-	}
-
-	// (2) Process Finalized Deposits
-	//  - We dont need do anything actionable on the database here as this is layered on top of the
-	// bridge transaction & messages that have a tracked lifecyle. We simply walk through and ensure
-	// that the corresponding initiated deposits exist as an integrity check
-
-	finalizedDepositEvents, err := StandardBridgeFinalizedEvents(events)
-	if err != nil {
-		return err
-	}
-
-	for _, finalizedDepositEvent := range finalizedDepositEvents {
-		deposit, err := db.BridgeTransfers.L1BridgeDepositWithFilter(database.BridgeTransfer{CrossDomainMessageHash: &finalizedDepositEvent.CrossDomainMessageHash})
-		if err != nil {
-			return err
-		} else if deposit == nil {
-			// Indexed CrossDomainMessenger messages ensure we're in a caught up state here
-			processLog.Error("missing indexed L1StandardBridge deposit on finalization", "cross_domain_massage_hash", finalizedDepositEvent.CrossDomainMessageHash)
-			return errors.New("missing indexed L1StandardBridge deposit on finalization")
-		}
-
-		// sanity check on the bridge fields
-		if finalizedDepositEvent.From != deposit.Tx.FromAddress || finalizedDepositEvent.To != deposit.Tx.ToAddress ||
-			finalizedDepositEvent.Amount.Cmp(deposit.Tx.Amount.Int) != 0 || !bytes.Equal(finalizedDepositEvent.ExtraData, deposit.Tx.Data) ||
-			finalizedDepositEvent.LocalToken != deposit.TokenPair.LocalTokenAddress || finalizedDepositEvent.RemoteToken != deposit.TokenPair.RemoteTokenAddress {
-			processLog.Error("bridge finalization fields mismatch with initiated fields!", "tx_source_hash", deposit.TransactionSourceHash, "cross_domain_message_hash", deposit.CrossDomainMessageHash)
-			return errors.New("bridge tx mismatch")
-		}
-	}
-
-	// a-ok!
-	return nil
-}
diff --git a/indexer/processor/l2_to_l1_message_passer.go b/indexer/processor/l2_to_l1_message_passer.go
deleted file mode 100644
index 196911dd0..000000000
--- a/indexer/processor/l2_to_l1_message_passer.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package processor
-
-import (
-	"github.com/ethereum-optimism/optimism/indexer/database"
-	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
-)
-
-type L2ToL1MessagePasserMessagePassed struct {
-	*bindings.L2ToL1MessagePasserMessagePassed
-	Event *database.ContractEvent
-}
-
-func L2ToL1MessagePasserMessagesPassed(events *ProcessedContractEvents) ([]L2ToL1MessagePasserMessagePassed, error) {
-	l2ToL1MessagePasserAbi, err := bindings.L2ToL1MessagePasserMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	eventName := "MessagePassed"
-	processedMessagePassedEvents := events.eventsBySignature[l2ToL1MessagePasserAbi.Events[eventName].ID]
-	messagesPassed := make([]L2ToL1MessagePasserMessagePassed, len(processedMessagePassedEvents))
-	for i, messagePassedEvent := range processedMessagePassedEvents {
-		log := messagePassedEvent.RLPLog
-
-		var messagePassed bindings.L2ToL1MessagePasserMessagePassed
-		messagePassed.Raw = *log
-		err := UnpackLog(&messagePassed, log, eventName, l2ToL1MessagePasserAbi)
-		if err != nil {
-			return nil, err
-		}
-
-		messagesPassed[i] = L2ToL1MessagePasserMessagePassed{
-			L2ToL1MessagePasserMessagePassed: &messagePassed,
-			Event:                            messagePassedEvent,
-		}
-	}
-
-	return messagesPassed, nil
-}
diff --git a/indexer/processor/optimism_portal.go b/indexer/processor/optimism_portal.go
deleted file mode 100644
index 65a1fdf71..000000000
--- a/indexer/processor/optimism_portal.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package processor
-
-import (
-	"errors"
-	"math/big"
-
-	"github.com/ethereum-optimism/optimism/indexer/database"
-	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
-	"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
-
-	"github.com/ethereum/go-ethereum/core/types"
-)
-
-type OptimismPortalTransactionDepositEvent struct {
-	*bindings.OptimismPortalTransactionDeposited
-	DepositTx *types.DepositTx
-	Event     *database.ContractEvent
-}
-
-type OptimismPortalWithdrawalProvenEvent struct {
-	*bindings.OptimismPortalWithdrawalProven
-	Event *database.ContractEvent
-}
-
-type OptimismPortalWithdrawalFinalizedEvent struct {
-	*bindings.OptimismPortalWithdrawalFinalized
-	Event *database.ContractEvent
-}
-
-type OptimismPortalProvenWithdrawal struct {
-	OutputRoot    [32]byte
-	Timestamp     *big.Int
-	L2OutputIndex *big.Int
-}
-
-func OptimismPortalTransactionDepositEvents(events *ProcessedContractEvents) ([]OptimismPortalTransactionDepositEvent, error) {
-	optimismPortalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	eventName := "TransactionDeposited"
-	if optimismPortalAbi.Events[eventName].ID != derive.DepositEventABIHash {
-		return nil, errors.New("op-node deposit event abi hash & optimism portal tx deposit mismatch")
-	}
-
-	processedTxDepositedEvents := events.eventsBySignature[derive.DepositEventABIHash]
-	txDeposits := make([]OptimismPortalTransactionDepositEvent, len(processedTxDepositedEvents))
-	for i, txDepositEvent := range processedTxDepositedEvents {
-		log := txDepositEvent.RLPLog
-
-		depositTx, err := derive.UnmarshalDepositLogEvent(log)
-		if err != nil {
-			return nil, err
-		}
-
-		var txDeposit bindings.OptimismPortalTransactionDeposited
-		txDeposit.Raw = *log
-		err = UnpackLog(&txDeposit, log, eventName, optimismPortalAbi)
-		if err != nil {
-			return nil, err
-		}
-
-		txDeposits[i] = OptimismPortalTransactionDepositEvent{
-			OptimismPortalTransactionDeposited: &txDeposit,
-			DepositTx:                          depositTx,
-			Event:                              txDepositEvent,
-		}
-	}
-
-	return txDeposits, nil
-}
-
-func OptimismPortalWithdrawalProvenEvents(events *ProcessedContractEvents) ([]OptimismPortalWithdrawalProvenEvent, error) {
-	optimismPortalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	eventName := "WithdrawalProven"
-	processedWithdrawalProvenEvents := events.eventsBySignature[optimismPortalAbi.Events[eventName].ID]
-	provenEvents := make([]OptimismPortalWithdrawalProvenEvent, len(processedWithdrawalProvenEvents))
-	for i, provenEvent := range processedWithdrawalProvenEvents {
-		log := provenEvent.RLPLog
-
-		var withdrawalProven bindings.OptimismPortalWithdrawalProven
-		withdrawalProven.Raw = *log
-		err := UnpackLog(&withdrawalProven, log, eventName, optimismPortalAbi)
-		if err != nil {
-			return nil, err
-		}
-
-		provenEvents[i] = OptimismPortalWithdrawalProvenEvent{
-			OptimismPortalWithdrawalProven: &withdrawalProven,
-			Event:                          provenEvent,
-		}
-	}
-
-	return provenEvents, nil
-}
-
-func OptimismPortalWithdrawalFinalizedEvents(events *ProcessedContractEvents) ([]OptimismPortalWithdrawalFinalizedEvent, error) {
-	optimismPortalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	eventName := "WithdrawalFinalized"
-	processedWithdrawalFinalizedEvents := events.eventsBySignature[optimismPortalAbi.Events[eventName].ID]
-	finalizedEvents := make([]OptimismPortalWithdrawalFinalizedEvent, len(processedWithdrawalFinalizedEvents))
-	for i, finalizedEvent := range processedWithdrawalFinalizedEvents {
-		log := finalizedEvent.RLPLog
-
-		var withdrawalFinalized bindings.OptimismPortalWithdrawalFinalized
-		err := UnpackLog(&withdrawalFinalized, log, eventName, optimismPortalAbi)
-		if err != nil {
-			return nil, err
-		}
-
-		finalizedEvents[i] = OptimismPortalWithdrawalFinalizedEvent{
-			OptimismPortalWithdrawalFinalized: &withdrawalFinalized,
-			Event:                             finalizedEvent,
-		}
-	}
-
-	return finalizedEvents, nil
-}
diff --git a/indexer/processor/processor.go b/indexer/processor/processor.go
deleted file mode 100644
index 6166ba1b2..000000000
--- a/indexer/processor/processor.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package processor
-
-import (
-	"context"
-	"time"
-
-	"github.com/ethereum-optimism/optimism/indexer/database"
-	"github.com/ethereum-optimism/optimism/indexer/node"
-
-	"github.com/ethereum/go-ethereum/core/types"
-	"github.com/ethereum/go-ethereum/log"
-)
-
-const (
-	defaultLoopInterval     = 5 * time.Second
-	defaultHeaderBufferSize = 500
-)
-
-// ProcessFn is the the entrypoint for processing a batch of headers.
-// In the event of failure, database operations are rolled back
-type ProcessFn func(*database.DB, []*types.Header) error
-
-type processor struct {
-	headerTraversal *node.HeaderTraversal
-
-	db         *database.DB
-	processFn  ProcessFn
-	processLog log.Logger
-
-	paused                bool
-	latestProcessedHeader *types.Header
-}
-
-// Start kicks off the processing loop. This is a block operation
-// unless the processor encountering an error, abrupting the loop,
-// or the supplied context is cancelled.
-func (p *processor) Start(ctx context.Context) error {
-	done := ctx.Done()
-	pollTicker := time.NewTicker(defaultLoopInterval)
-	defer pollTicker.Stop()
-
-	p.processLog.Info("starting processor...")
-	var unprocessedHeaders []*types.Header
-	for {
-		select {
-		case <-done:
-			p.processLog.Info("stopping processor")
-			return nil
-
-		case <-pollTicker.C:
-			if p.paused {
-				p.processLog.Warn("processor is paused...")
-				continue
-			}
-
-			if len(unprocessedHeaders) == 0 {
-				newHeaders, err := p.headerTraversal.NextFinalizedHeaders(defaultHeaderBufferSize)
-				if err != nil {
-					p.processLog.Error("error querying for headers", "err", err)
-					continue
-				} else if len(newHeaders) == 0 {
-					// Logged as an error since this loop should be operating at a longer interval than the provider
-					p.processLog.Error("no new headers. processor unexpectedly at head...")
-					continue
-				}
-
-				unprocessedHeaders = newHeaders
-			} else {
-				p.processLog.Info("retrying previous batch")
-			}
-
-			firstHeader := unprocessedHeaders[0]
-			lastHeader := unprocessedHeaders[len(unprocessedHeaders)-1]
-			batchLog := p.processLog.New("batch_start_block_number", firstHeader.Number, "batch_end_block_number", lastHeader.Number)
-			err := p.db.Transaction(func(db *database.DB) error {
-				batchLog.Info("processing batch")
-				return p.processFn(db, unprocessedHeaders)
-			})
-
-			// Eventually, we want to halt the processor on any error rather than rely
-			// on this loop for retry functionality.
-			if err != nil {
-				batchLog.Warn("error processing batch. no operations committed", "err", err)
-			} else {
-				batchLog.Info("fully committed batch")
-
-				unprocessedHeaders = nil
-				p.latestProcessedHeader = lastHeader
-			}
-		}
-	}
-}
-
-func (p processor) LatestProcessedHeader() *types.Header {
-	return p.latestProcessedHeader
-}
-
-// Useful ONLY for tests!
-
-func (p *processor) PauseForTest() {
-	p.paused = true
-}
-
-func (p *processor) ResumeForTest() {
-	p.paused = false
-}
diff --git a/indexer/processor/standard_bridge.go b/indexer/processor/standard_bridge.go
deleted file mode 100644
index b45b62c5a..000000000
--- a/indexer/processor/standard_bridge.go
+++ /dev/null
@@ -1,254 +0,0 @@
-package processor
-
-import (
-	"bytes"
-	"errors"
-
-	"github.com/ethereum-optimism/optimism/indexer/database"
-	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
-	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
-
-	"github.com/ethereum/go-ethereum/common"
-)
-
-type StandardBridgeInitiatedEvent struct {
-	// We hardcode to ERC20 since ETH can be pseudo-represented as an ERC20 utilizing
-	// the hardcoded ETH address
-	*bindings.StandardBridgeERC20BridgeInitiated
-
-	CrossDomainMessageHash common.Hash
-	Event                  *database.ContractEvent
-}
-
-type StandardBridgeFinalizedEvent struct {
-	// We hardcode to ERC20 since ETH can be pseudo-represented as an ERC20 utilizing
-	// the hardcoded ETH address
-	*bindings.StandardBridgeERC20BridgeFinalized
-
-	CrossDomainMessageHash common.Hash
-	Event                  *database.ContractEvent
-}
-
-// StandardBridgeInitiatedEvents extracts all initiated bridge events from the contracts that follow the StandardBridge ABI. The
-// correlated CrossDomainMessenger nonce is also parsed from the associated messenger events.
-func StandardBridgeInitiatedEvents(events *ProcessedContractEvents) ([]StandardBridgeInitiatedEvent, error) {
-	ethBridgeInitiatedEvents, err := _standardBridgeInitiatedEvents[bindings.StandardBridgeETHBridgeInitiated](events)
-	if err != nil {
-		return nil, err
-	}
-
-	erc20BridgeInitiatedEvents, err := _standardBridgeInitiatedEvents[bindings.StandardBridgeERC20BridgeInitiated](events)
-	if err != nil {
-		return nil, err
-	}
-
-	return append(ethBridgeInitiatedEvents, erc20BridgeInitiatedEvents...), nil
-}
-
-// StandardBridgeFinalizedEvents extracts all finalization bridge events from the contracts that follow the StandardBridge ABI. The
-// correlated CrossDomainMessenger nonce is also parsed by looking at the parameters of the corresponding relayMessage transaction data.
-func StandardBridgeFinalizedEvents(events *ProcessedContractEvents) ([]StandardBridgeFinalizedEvent, error) {
-	ethBridgeFinalizedEvents, err := _standardBridgeFinalizedEvents[bindings.StandardBridgeETHBridgeFinalized](events)
-	if err != nil {
-		return nil, err
-	}
-
-	erc20BridgeFinalizedEvents, err := _standardBridgeFinalizedEvents[bindings.StandardBridgeERC20BridgeFinalized](events)
-	if err != nil {
-		return nil, err
-	}
-
-	return append(ethBridgeFinalizedEvents, erc20BridgeFinalizedEvents...), nil
-}
-
-// parse out eth or erc20 bridge initiated events
-func _standardBridgeInitiatedEvents[BridgeEvent bindings.StandardBridgeETHBridgeInitiated | bindings.StandardBridgeERC20BridgeInitiated](
-	events *ProcessedContractEvents,
-) ([]StandardBridgeInitiatedEvent, error) {
-	standardBridgeABI, err := bindings.StandardBridgeMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	crossDomainMessengerABI, err := bindings.CrossDomainMessengerMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	sentMessageEventAbi := crossDomainMessengerABI.Events["SentMessage"]
-	sentMessageExtensionEventAbi := crossDomainMessengerABI.Events["SentMessageExtension1"]
-
-	var tmp BridgeEvent
-	var eventName string
-	var finalizeMethodName string
-	switch any(tmp).(type) {
-	case bindings.StandardBridgeETHBridgeInitiated:
-		eventName = "ETHBridgeInitiated"
-		finalizeMethodName = "finalizeBridgeETH"
-	case bindings.StandardBridgeERC20BridgeInitiated:
-		eventName = "ERC20BridgeInitiated"
-		finalizeMethodName = "finalizeBridgeERC20"
-	default:
-		panic("should not be here")
-	}
-
-	processedInitiatedBridgeEvents := events.eventsBySignature[standardBridgeABI.Events[eventName].ID]
-	initiatedBridgeEvents := make([]StandardBridgeInitiatedEvent, len(processedInitiatedBridgeEvents))
-	for i, bridgeInitiatedEvent := range processedInitiatedBridgeEvents {
-		log := bridgeInitiatedEvent.RLPLog
-
-		var bridgeData BridgeEvent
-		err := UnpackLog(&bridgeData, log, eventName, standardBridgeABI)
-		if err != nil {
-			return nil, err
-		}
-
-		// Look for the sent message event to compute the message hash of the relayed tx
-		//   - L1: BridgeInitiated -> Portal#DepositTransaction -> SentMessage ...
-		//   - L1: BridgeInitiated -> L2ToL1MessagePasser#MessagePassed -> SentMessage ...
-		var sentMsgData bindings.CrossDomainMessengerSentMessage
-		sentMsgLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 2}].RLPLog
-		if sentMsgLog.Topics[0] != sentMessageEventAbi.ID {
-			return nil, errors.New("unexpected bridge event ordering")
-		}
-		sentMsgData.Raw = *sentMsgLog
-		err = UnpackLog(&sentMsgData, sentMsgLog, sentMessageEventAbi.Name, crossDomainMessengerABI)
-		if err != nil {
-			return nil, err
-		}
-
-		var sentMsgExtensionData bindings.CrossDomainMessengerSentMessageExtension1
-		sentMsgExtensionLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 3}].RLPLog
-		if sentMsgExtensionLog.Topics[0] != sentMessageExtensionEventAbi.ID {
-			return nil, errors.New("unexpected bridge event ordering")
-		}
-		sentMsgData.Raw = *sentMsgLog
-		err = UnpackLog(&sentMsgExtensionData, sentMsgExtensionLog, sentMessageExtensionEventAbi.Name, crossDomainMessengerABI)
-		if err != nil {
-			return nil, err
-		}
-
-		msgHash, err := CrossDomainMessageHash(crossDomainMessengerABI, &sentMsgData, sentMsgExtensionData.Value)
-		if err != nil {
-			return nil, err
-		}
-
-		var erc20BridgeData *bindings.StandardBridgeERC20BridgeInitiated
-		var expectedCrossDomainMessage []byte
-		switch any(bridgeData).(type) {
-		case bindings.StandardBridgeETHBridgeInitiated:
-			ethBridgeData := any(bridgeData).(bindings.StandardBridgeETHBridgeInitiated)
-			expectedCrossDomainMessage, err = standardBridgeABI.Pack(finalizeMethodName, ethBridgeData.From, ethBridgeData.To, ethBridgeData.Amount, ethBridgeData.ExtraData)
-			if err != nil {
-				return nil, err
-			}
-
-			// represent eth bridge as an erc20
-			erc20BridgeData = &bindings.StandardBridgeERC20BridgeInitiated{
-				Raw: *log,
-				// Represent ETH using the hardcoded address
-				LocalToken: predeploys.LegacyERC20ETHAddr, RemoteToken: predeploys.LegacyERC20ETHAddr,
-				// Bridge data
-				From: ethBridgeData.From, To: ethBridgeData.To, Amount: ethBridgeData.Amount, ExtraData: ethBridgeData.ExtraData,
-			}
-
-		case bindings.StandardBridgeERC20BridgeInitiated:
-			_temp := any(bridgeData).(bindings.StandardBridgeERC20BridgeInitiated)
-			erc20BridgeData = &_temp
-			erc20BridgeData.Raw = *log
-			expectedCrossDomainMessage, err = standardBridgeABI.Pack(finalizeMethodName, erc20BridgeData.RemoteToken, erc20BridgeData.LocalToken, erc20BridgeData.From, erc20BridgeData.To, erc20BridgeData.Amount, erc20BridgeData.ExtraData)
-			if err != nil {
-				return nil, err
-			}
-		}
-
-		if !bytes.Equal(sentMsgData.Message, expectedCrossDomainMessage) {
-			return nil, errors.New("bridge cross domain message mismatch")
-		}
-
-		initiatedBridgeEvents[i] = StandardBridgeInitiatedEvent{
-			StandardBridgeERC20BridgeInitiated: erc20BridgeData,
-			CrossDomainMessageHash:             msgHash,
-			Event:                              bridgeInitiatedEvent,
-		}
-	}
-
-	return initiatedBridgeEvents, nil
-}
-
-// parse out eth or erc20 bridge finalization events
-func _standardBridgeFinalizedEvents[BridgeEvent bindings.StandardBridgeETHBridgeFinalized | bindings.StandardBridgeERC20BridgeFinalized](
-	events *ProcessedContractEvents,
-) ([]StandardBridgeFinalizedEvent, error) {
-	standardBridgeABI, err := bindings.StandardBridgeMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	crossDomainMessengerABI, err := bindings.CrossDomainMessengerMetaData.GetAbi()
-	if err != nil {
-		return nil, err
-	}
-
-	relayedMessageEventAbi := crossDomainMessengerABI.Events["RelayedMessage"]
-
-	var bridgeData BridgeEvent
-	var eventName string
-	switch any(bridgeData).(type) {
-	case bindings.StandardBridgeETHBridgeFinalized:
-		eventName = "ETHBridgeFinalized"
-	case bindings.StandardBridgeERC20BridgeFinalized:
-		eventName = "ERC20BridgeFinalized"
-	default:
-		panic("should not be here")
-	}
-
-	processedFinalizedBridgeEvents := events.eventsBySignature[standardBridgeABI.Events[eventName].ID]
-	finalizedBridgeEvents := make([]StandardBridgeFinalizedEvent, len(processedFinalizedBridgeEvents))
-	for i, bridgeFinalizedEvent := range processedFinalizedBridgeEvents {
-		log := bridgeFinalizedEvent.RLPLog
-
-		var bridgeData BridgeEvent
-		err := UnpackLog(&bridgeData, log, eventName, standardBridgeABI)
-		if err != nil {
-			return nil, err
-		}
-
-		// Look for the RelayedMessage event that follows right after the BridgeFinalized Event
-		var relayedMsgData bindings.CrossDomainMessengerRelayedMessage
-		relayedMsgLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 1}].RLPLog
-		if relayedMsgLog.Topics[0] != relayedMessageEventAbi.ID {
-			return nil, errors.New("unexpected bridge event ordering")
-		}
-		err = UnpackLog(&relayedMsgData, relayedMsgLog, relayedMessageEventAbi.Name, crossDomainMessengerABI)
-		if err != nil {
-			return nil, err
-		}
-
-		var erc20BridgeData *bindings.StandardBridgeERC20BridgeFinalized
-		switch any(bridgeData).(type) {
-		case bindings.StandardBridgeETHBridgeFinalized:
-			ethBridgeData := any(bridgeData).(bindings.StandardBridgeETHBridgeFinalized)
-			erc20BridgeData = &bindings.StandardBridgeERC20BridgeFinalized{
-				Raw: *log,
-				// Represent ETH using the hardcoded address
-				LocalToken: predeploys.LegacyERC20ETHAddr, RemoteToken: predeploys.LegacyERC20ETHAddr,
-				// Bridge data
-				From: ethBridgeData.From, To: ethBridgeData.To, Amount: ethBridgeData.Amount, ExtraData: ethBridgeData.ExtraData,
-			}
-
-		case bindings.StandardBridgeERC20BridgeFinalized:
-			_temp := any(bridgeData).(bindings.StandardBridgeERC20BridgeFinalized)
-			erc20BridgeData = &_temp
-			erc20BridgeData.Raw = *log
-		}
-
-		finalizedBridgeEvents[i] = StandardBridgeFinalizedEvent{
-			StandardBridgeERC20BridgeFinalized: erc20BridgeData,
-			CrossDomainMessageHash:             relayedMsgData.MsgHash,
-			Event:                              bridgeFinalizedEvent,
-		}
-	}
-
-	return finalizedBridgeEvents, nil
-}
diff --git a/indexer/processors/bridge.go b/indexer/processors/bridge.go
new file mode 100644
index 000000000..41712cd22
--- /dev/null
+++ b/indexer/processors/bridge.go
@@ -0,0 +1,154 @@
+package processors
+
+import (
+	"context"
+	"errors"
+	"math/big"
+	"time"
+
+	"github.com/ethereum-optimism/optimism/indexer/config"
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/indexer/processors/bridge"
+
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+type BridgeProcessor struct {
+	log         log.Logger
+	db          *database.DB
+	chainConfig config.ChainConfig
+
+	// NOTE: We'll need this processor to handle for reorgs events.
+
+	LatestL1Header *types.Header
+	LatestL2Header *types.Header
+}
+
+func NewBridgeProcessor(log log.Logger, db *database.DB, chainConfig config.ChainConfig) (*BridgeProcessor, error) {
+	log = log.New("processor", "bridge")
+
+	latestL1Header, err := bridge.L1LatestBridgeEventHeader(db, chainConfig)
+	if err != nil {
+		return nil, err
+	}
+	latestL2Header, err := bridge.L2LatestBridgeEventHeader(db)
+	if err != nil {
+		return nil, err
+	}
+
+	// Since the bridge processor indexes events based on epochs, there's
+	// no scenario in which we have indexed L2 data with no L1 data.
+	//
+	// NOTE: Technically there is an exception if our bridging contracts are
+	// used to bridges native from L2 and an op-chain happens to launch where
+	// only L2 native bridge events have occurred. This is a rare situation now
+	// and it's worth the assertion as an integrity check. We can revisit this
+	// as more chains launch with primarily L2-native activity.
+	if latestL1Header == nil && latestL2Header != nil {
+		log.Error("detected indexed L2 bridge activity with no indexed L1 state", "l2_block_number", latestL2Header.Number)
+		return nil, errors.New("detected indexed L2 bridge activity with no indexed L1 state")
+	}
+
+	if latestL1Header == nil && latestL2Header == nil {
+		log.Info("no indexed state, starting from genesis")
+	} else {
+		log.Info("detected the latest indexed state", "l1_block_number", latestL1Header.Number, "l2_block_number", latestL2Header.Number)
+	}
+
+	return &BridgeProcessor{log, db, chainConfig, latestL1Header, latestL2Header}, nil
+}
+
+func (b *BridgeProcessor) Start(ctx context.Context) error {
+	done := ctx.Done()
+
+	// NOTE: This should run on same iterval as L1 ETL rather than as finding the
+	// lasted epoch is constrained to how much L1 data we've indexed.
+	pollTicker := time.NewTicker(5 * time.Second)
+	defer pollTicker.Stop()
+
+	// In order to ensure all seen bridge finalization events correspond with seen
+	// bridge initiated events, we establish a shared marker between L1 and L2 when
+	// processing events.
+	//
+	// As L1 and L2 blocks are indexed, the highest indexed L2 block starting a new
+	// sequencing epoch and corresponding L1 origin that has also been indexed
+	// serves as this shared marker.
+
+	// TODOs:
+	// 	  1. Fix Logging. Should be clear if we're looking at L1 or L2 side of things
+
+	b.log.Info("starting bridge processor...")
+	for {
+		select {
+		case <-done:
+			b.log.Info("stopping bridge processor")
+			return nil
+
+		case <-pollTicker.C:
+			latestEpoch, err := b.db.Blocks.LatestEpoch()
+			if err != nil {
+				return err
+			}
+			if latestEpoch == nil {
+				if b.LatestL1Header != nil {
+					// Once we have some satte `latestEpoch` should never return nil.
+					b.log.Error("started with indexed bridge state, but no blocks epochs returned", "latest_bridge_l1_block_number", b.LatestL1Header.Number)
+					return errors.New("started with indexed bridge state, but no blocks epochs returned")
+				} else {
+					b.log.Warn("no indexed block state. waiting...")
+					continue
+				}
+			}
+
+			if b.LatestL1Header != nil && latestEpoch.L1BlockHeader.Hash == b.LatestL1Header.Hash() {
+				// Marked as a warning since the bridge should always be processing at least 1 new epoch
+				b.log.Warn("all available epochs indexed by the bridge", "latest_epoch_number", b.LatestL1Header.Number)
+				continue
+			}
+
+			toL1Height, toL2Height := latestEpoch.L1BlockHeader.Number.Int, latestEpoch.L2BlockHeader.Number.Int
+			fromL1Height, fromL2Height := big.NewInt(0), big.NewInt(0)
+			if b.LatestL1Header != nil {
+				// `NewBridgeProcessor` ensures that LatestL2Header must not be nil if LatestL1Header is set
+				fromL1Height = new(big.Int).Add(b.LatestL1Header.Number, big.NewInt(1))
+				fromL2Height = new(big.Int).Add(b.LatestL2Header.Number, big.NewInt(1))
+			}
+
+			batchLog := b.log.New("epoch_start_number", fromL1Height, "epoch_end_number", toL1Height)
+			batchLog.Info("scanning bridge events")
+			err = b.db.Transaction(func(tx *database.DB) error {
+				l1BridgeLog := b.log.New("from_l1_block_number", fromL1Height, "to_l1_block_number", toL1Height)
+				l2BridgeLog := b.log.New("from_l2_block_number", fromL2Height, "to_l2_block_number", toL2Height)
+
+				// First, find all possible initiated bridge events
+				if err := bridge.L1ProcessInitiatedBridgeEvents(l1BridgeLog, tx, b.chainConfig, fromL1Height, toL1Height); err != nil {
+					return err
+				}
+				if err := bridge.L2ProcessInitiatedBridgeEvents(l2BridgeLog, tx, fromL2Height, toL2Height); err != nil {
+					return err
+				}
+
+				// Now that all initiated events have been indexed, it is ensured that all finalization can find their counterpart.
+				if err := bridge.L1ProcessFinalizedBridgeEvents(l1BridgeLog, tx, b.chainConfig, fromL1Height, toL1Height); err != nil {
+					return err
+				}
+				if err := bridge.L2ProcessFinalizedBridgeEvents(l2BridgeLog, tx, fromL2Height, toL2Height); err != nil {
+					return err
+				}
+
+				// a-ok
+				return nil
+			})
+
+			if err != nil {
+				// Try again on a subsequent interval
+				batchLog.Error("unable to index new bridge events", "err", err)
+			} else {
+				batchLog.Info("done indexing new bridge events", "latest_l1_block_number", toL1Height, "latest_l2_block_number", toL2Height)
+				b.LatestL1Header = latestEpoch.L1BlockHeader.RLPHeader.Header()
+				b.LatestL2Header = latestEpoch.L2BlockHeader.RLPHeader.Header()
+			}
+		}
+	}
+}
diff --git a/indexer/processors/bridge/l1_bridge_processor.go b/indexer/processors/bridge/l1_bridge_processor.go
new file mode 100644
index 000000000..3e8a40fdb
--- /dev/null
+++ b/indexer/processors/bridge/l1_bridge_processor.go
@@ -0,0 +1,327 @@
+package bridge
+
+import (
+	"errors"
+	"fmt"
+	"math/big"
+
+	"github.com/ethereum-optimism/optimism/indexer/config"
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/indexer/processors/contracts"
+	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
+
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+// L1ProcessInitiatedBridgeEvents will query the database for new bridge events that have been initiated between
+// the specified block range. This covers every part of the multi-layered stack:
+//  1. OptimismPortal
+//  2. L1CrossDomainMessenger
+//  3. L1StandardBridge
+func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig config.ChainConfig, fromHeight *big.Int, toHeight *big.Int) error {
+	// (1) OptimismPortal
+	optimismPortalTxDeposits, err := contracts.OptimismPortalTransactionDepositEvents(chainConfig.L1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+
+	ethDeposits := []database.L1BridgeDeposit{}
+	portalDeposits := make(map[logKey]*contracts.OptimismPortalTransactionDepositEvent, len(optimismPortalTxDeposits))
+	transactionDeposits := make([]database.L1TransactionDeposit, len(optimismPortalTxDeposits))
+	for i := range optimismPortalTxDeposits {
+		depositTx := optimismPortalTxDeposits[i]
+		portalDeposits[logKey{depositTx.Event.BlockHash, depositTx.Event.LogIndex}] = &depositTx
+		transactionDeposits[i] = database.L1TransactionDeposit{
+			SourceHash:           depositTx.DepositTx.SourceHash,
+			L2TransactionHash:    types.NewTx(depositTx.DepositTx).Hash(),
+			InitiatedL1EventGUID: depositTx.Event.GUID,
+			GasLimit:             depositTx.GasLimit,
+			Tx:                   depositTx.Tx,
+		}
+
+		// catch ETH transfers to the portal contract.
+		if len(depositTx.DepositTx.Data) == 0 && depositTx.DepositTx.Value.BitLen() > 0 {
+			ethDeposits = append(ethDeposits, database.L1BridgeDeposit{
+				TransactionSourceHash: depositTx.DepositTx.SourceHash,
+				BridgeTransfer:        database.BridgeTransfer{Tx: transactionDeposits[i].Tx, TokenPair: database.ETHTokenPair},
+			})
+		}
+	}
+
+	if len(transactionDeposits) > 0 {
+		log.Info("detected transaction deposits", "size", len(transactionDeposits))
+		if err := db.BridgeTransactions.StoreL1TransactionDeposits(transactionDeposits); err != nil {
+			return err
+		}
+		if len(ethDeposits) > 0 {
+			log.Info("detected portal ETH transfers", "size", len(ethDeposits))
+			if err := db.BridgeTransfers.StoreL1BridgeDeposits(ethDeposits); err != nil {
+				return err
+			}
+		}
+	}
+
+	// (2) L1CrossDomainMessenger
+	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l1", chainConfig.L1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(crossDomainSentMessages) > len(transactionDeposits) {
+		return fmt.Errorf("missing transaction deposit for each cross-domain message. deposits: %d, messages: %d", len(transactionDeposits), len(crossDomainSentMessages))
+	}
+
+	sentMessages := make(map[logKey]*contracts.CrossDomainMessengerSentMessageEvent, len(crossDomainSentMessages))
+	l1BridgeMessages := make([]database.L1BridgeMessage, len(crossDomainSentMessages))
+	for i := range crossDomainSentMessages {
+		sentMessage := crossDomainSentMessages[i]
+		sentMessages[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex}] = &sentMessage
+
+		// extract the deposit hash from the previous TransactionDepositedEvent
+		portalDeposit, ok := portalDeposits[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex - 1}]
+		if !ok {
+			return fmt.Errorf("missing expected preceding TransactionDeposit for SentMessage. tx_hash = %s", sentMessage.Event.TransactionHash)
+		}
+
+		l1BridgeMessages[i] = database.L1BridgeMessage{TransactionSourceHash: portalDeposit.DepositTx.SourceHash, BridgeMessage: sentMessage.BridgeMessage}
+	}
+
+	if len(l1BridgeMessages) > 0 {
+		log.Info("detected L1CrossDomainMessenger messages", "size", len(l1BridgeMessages))
+		if err := db.BridgeMessages.StoreL1BridgeMessages(l1BridgeMessages); err != nil {
+			return err
+		}
+	}
+
+	// (3) L1StandardBridge
+	initiatedBridges, err := contracts.StandardBridgeInitiatedEvents("l1", chainConfig.L1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(initiatedBridges) > len(crossDomainSentMessages) {
+		return fmt.Errorf("missing cross-domain message for each initiated bridge event. messages: %d, bridges: %d", len(crossDomainSentMessages), len(initiatedBridges))
+	}
+
+	l1BridgeDeposits := make([]database.L1BridgeDeposit, len(initiatedBridges))
+	for i := range initiatedBridges {
+		initiatedBridge := initiatedBridges[i]
+
+		// extract the cross domain message hash & deposit source hash from the following events
+		portalDeposit, ok := portalDeposits[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 1}]
+		if !ok {
+			return fmt.Errorf("missing expected following TransactionDeposit for BridgeInitiated. tx_hash = %s", initiatedBridge.Event.TransactionHash)
+		}
+		sentMessage, ok := sentMessages[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 2}]
+		if !ok {
+			return fmt.Errorf("missing expected following SentMessage for BridgeInitiated. tx_hash = %s", initiatedBridge.Event.TransactionHash)
+		}
+
+		initiatedBridge.BridgeTransfer.CrossDomainMessageHash = &sentMessage.BridgeMessage.MessageHash
+		l1BridgeDeposits[i] = database.L1BridgeDeposit{
+			TransactionSourceHash: portalDeposit.DepositTx.SourceHash,
+			BridgeTransfer:        initiatedBridge.BridgeTransfer,
+		}
+	}
+
+	if len(l1BridgeDeposits) > 0 {
+		log.Info("detected L1StandardBridge deposits", "size", len(l1BridgeDeposits))
+		if err := db.BridgeTransfers.StoreL1BridgeDeposits(l1BridgeDeposits); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// L1ProcessFinalizedBridgeEvent will query the database for all the finalization markers for all initiated
+// bridge events. This covers every part of the multi-layered stack:
+//  1. OptimismPortal (Bedrock prove & finalize steps)
+//  2. L1CrossDomainMessenger (relayMessage marker)
+//  3. L1StandardBridge (no-op, since this is simply a wrapper over the L1CrossDomainMEssenger)
+func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig config.ChainConfig, fromHeight *big.Int, toHeight *big.Int) error {
+	// (1) OptimismPortal (proven withdrawals)
+	provenWithdrawals, err := contracts.OptimismPortalWithdrawalProvenEvents(chainConfig.L1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+
+	for i := range provenWithdrawals {
+		proven := provenWithdrawals[i]
+		withdrawal, err := db.BridgeTransactions.L2TransactionWithdrawal(proven.WithdrawalHash)
+		if err != nil {
+			return err
+		} else if withdrawal == nil {
+			log.Crit("missing indexed withdrawal on prove event!", "withdrawal_hash", proven.WithdrawalHash, "tx_hash", proven.Event.TransactionHash)
+			return errors.New("missing indexed withdrawal")
+		}
+
+		if err := db.BridgeTransactions.MarkL2TransactionWithdrawalProvenEvent(proven.WithdrawalHash, provenWithdrawals[i].Event.GUID); err != nil {
+			return err
+		}
+	}
+
+	if len(provenWithdrawals) > 0 {
+		log.Info("proven transaction withdrawals", "size", len(provenWithdrawals))
+	}
+
+	// (2) OptimismPortal (finalized withdrawals)
+	finalizedWithdrawals, err := contracts.OptimismPortalWithdrawalFinalizedEvents(chainConfig.L1Contracts.OptimismPortalProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+
+	for i := range finalizedWithdrawals {
+		finalized := finalizedWithdrawals[i]
+		withdrawal, err := db.BridgeTransactions.L2TransactionWithdrawal(finalized.WithdrawalHash)
+		if err != nil {
+			return err
+		} else if withdrawal == nil {
+			log.Crit("missing indexed withdrawal on finalization event!", "withdrawal_hash", finalized.WithdrawalHash, "tx_hash", finalized.Event.TransactionHash)
+			return errors.New("missing indexed withdrawal")
+		}
+
+		if err = db.BridgeTransactions.MarkL2TransactionWithdrawalFinalizedEvent(finalized.WithdrawalHash, finalized.Event.GUID, finalized.Success); err != nil {
+			return err
+		}
+	}
+
+	if len(finalizedWithdrawals) > 0 {
+		log.Info("finalized transaction withdrawals", "size", len(finalizedWithdrawals))
+	}
+
+	// (3) L1CrossDomainMessenger
+	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l1", chainConfig.L1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+
+	relayedMessages := make(map[logKey]*contracts.CrossDomainMessengerRelayedMessageEvent, len(crossDomainRelayedMessages))
+	for i := range crossDomainRelayedMessages {
+		relayed := crossDomainRelayedMessages[i]
+		relayedMessages[logKey{BlockHash: relayed.Event.BlockHash, LogIndex: relayed.Event.LogIndex}] = &relayed
+		message, err := db.BridgeMessages.L2BridgeMessage(relayed.MessageHash)
+		if err != nil {
+			return err
+		} else if message == nil {
+			log.Crit("missing indexed L2CrossDomainMessenger message", "message_hash", relayed.MessageHash, "tx_hash", relayed.Event.TransactionHash)
+			return fmt.Errorf("missing indexed L2CrossDomainMessager message")
+		}
+
+		if err := db.BridgeMessages.MarkRelayedL2BridgeMessage(relayed.MessageHash, relayed.Event.GUID); err != nil {
+			return err
+		}
+	}
+
+	if len(crossDomainRelayedMessages) > 0 {
+		log.Info("relayed L2CrossDomainMessenger messages", "size", len(crossDomainRelayedMessages))
+	}
+
+	// (4) L1StandardBridge
+	finalizedBridges, err := contracts.StandardBridgeFinalizedEvents("l1", chainConfig.L1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(finalizedBridges) > len(crossDomainRelayedMessages) {
+		return fmt.Errorf("missing cross-domain message for each finalized bridge event. messages: %d, bridges: %d", len(crossDomainRelayedMessages), len(finalizedBridges))
+	}
+
+	for i := range finalizedBridges {
+		// Nothing actionable on the database. However, we can treat the relayed message
+		// as an invariant by ensuring we can query for a deposit by the same hash
+		finalizedBridge := finalizedBridges[i]
+		relayedMessage, ok := relayedMessages[logKey{finalizedBridge.Event.BlockHash, finalizedBridge.Event.LogIndex + 1}]
+		if !ok {
+			return fmt.Errorf("missing following RelayedMessage for BridgeFinalized event. tx_hash = %s", finalizedBridge.Event.TransactionHash)
+		}
+
+		// Since the message hash is computed from the relayed message, this ensures the deposit fields must match. For good measure,
+		// we may choose to make sure `withdrawal.BridgeTransfer` matches with the finalized bridge
+		withdrawal, err := db.BridgeTransfers.L2BridgeWithdrawalWithFilter(database.BridgeTransfer{CrossDomainMessageHash: &relayedMessage.MessageHash})
+		if err != nil {
+			return err
+		} else if withdrawal == nil {
+			log.Crit("missing L2StandardBridge withdrawal on L1 finalization", "tx_hash", finalizedBridge.Event.TransactionHash)
+			return errors.New("missing L2StandardBridge withdrawal on L1 finalization")
+		}
+	}
+
+	// a-ok!
+	return nil
+}
+
+// L1LatestBridgeEventHeader returns the latest header for which and on-chain event
+// has been observed on L1 -- Both initiated L1 events and finalization markers on L2.
+func L1LatestBridgeEventHeader(db *database.DB, chainConfig config.ChainConfig) (*types.Header, error) {
+	portalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	depositEventID := portalAbi.Events["TransactionDeposited"].ID
+	provenEventID := portalAbi.Events["WithdrawalProven"].ID
+	finalizedEventID := portalAbi.Events["WithdrawalFinalized"].ID
+
+	// (1) Initiated L1 Events
+	// Since all initaited bridge events eventually reach the OptimismPortal to
+	// conduct the deposit, we can simply look for the last deposited transaction
+	// event on L2.
+	var latestDepositHeader *types.Header
+	contractEventFilter := database.ContractEvent{ContractAddress: chainConfig.L1Contracts.OptimismPortalProxy, EventSignature: depositEventID}
+	depositEvent, err := db.ContractEvents.L1LatestContractEventWithFilter(contractEventFilter)
+	if err != nil {
+		return nil, err
+	}
+	if depositEvent != nil {
+		l1BlockHeader, err := db.Blocks.L1BlockHeader(depositEvent.BlockHash)
+		if err != nil {
+			return nil, err
+		}
+		if l1BlockHeader != nil {
+			latestDepositHeader = l1BlockHeader.RLPHeader.Header()
+		}
+	}
+
+	// (2) Finalization markers for L2
+	// Like initiated L1 events, all withdrawals must flow through the OptimismPortal
+	// contract. We must look for both proven and finalized withdrawal events.
+	var latestWithdrawHeader *types.Header
+	contractEventFilter.EventSignature = finalizedEventID
+	withdrawEvent, err := db.ContractEvents.L1LatestContractEventWithFilter(contractEventFilter)
+	if err != nil {
+		return nil, err
+	}
+	if withdrawEvent != nil {
+		// Check if a have a later detected proven event
+		contractEventFilter.EventSignature = provenEventID
+		provenEvent, err := db.ContractEvents.L1LatestContractEventWithFilter(contractEventFilter)
+		if err != nil {
+			return nil, err
+		}
+		if provenEvent != nil && provenEvent.Timestamp > withdrawEvent.Timestamp {
+			withdrawEvent = provenEvent
+		}
+
+		l1BlockHeader, err := db.Blocks.L1BlockHeader(withdrawEvent.BlockHash)
+		if err != nil {
+			return nil, err
+		}
+		latestWithdrawHeader = l1BlockHeader.RLPHeader.Header()
+	}
+
+	if latestDepositHeader == nil {
+		// If there has been no seen deposits yet, there could have been no seen withdrawals
+		if latestWithdrawHeader != nil {
+			return nil, errors.New("detected an indexed withdrawal without any deposits")
+		}
+		return nil, nil
+	} else if latestWithdrawHeader == nil {
+		return latestDepositHeader, nil
+	} else {
+		// both deposits & withdrawals have occurred
+		if latestDepositHeader.Time > latestWithdrawHeader.Time {
+			return latestDepositHeader, nil
+		} else {
+			return latestWithdrawHeader, nil
+		}
+	}
+}
diff --git a/indexer/processors/bridge/l2_bridge_processor.go b/indexer/processors/bridge/l2_bridge_processor.go
new file mode 100644
index 000000000..e23da874d
--- /dev/null
+++ b/indexer/processors/bridge/l2_bridge_processor.go
@@ -0,0 +1,266 @@
+package bridge
+
+import (
+	"errors"
+	"fmt"
+	"math/big"
+
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/indexer/processors/contracts"
+	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
+	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
+
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+// L2ProcessInitiatedBridgeEvents will query the database for new bridge events that have been initiated between
+// the specified block range. This covers every part of the multi-layered stack:
+//  1. OptimismPortal
+//  2. L2CrossDomainMessenger
+//  3. L2StandardBridge
+func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight *big.Int, toHeight *big.Int) error {
+	// (1) L2ToL1MessagePasser
+	l2ToL1MPMessagesPassed, err := contracts.L2ToL1MessagePasserMessagePassedEvents(predeploys.L2ToL1MessagePasserAddr, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+
+	ethWithdrawals := []database.L2BridgeWithdrawal{}
+	messagesPassed := make(map[logKey]*contracts.L2ToL1MessagePasserMessagePassed, len(l2ToL1MPMessagesPassed))
+	transactionWithdrawals := make([]database.L2TransactionWithdrawal, len(l2ToL1MPMessagesPassed))
+	for i := range l2ToL1MPMessagesPassed {
+		messagePassed := l2ToL1MPMessagesPassed[i]
+		messagesPassed[logKey{messagePassed.Event.BlockHash, messagePassed.Event.LogIndex}] = &messagePassed
+		transactionWithdrawals[i] = database.L2TransactionWithdrawal{
+			WithdrawalHash:       messagePassed.WithdrawalHash,
+			InitiatedL2EventGUID: messagePassed.Event.GUID,
+			Nonce:                messagePassed.Nonce,
+			GasLimit:             messagePassed.GasLimit,
+			Tx:                   messagePassed.Tx,
+		}
+
+		if len(messagePassed.Tx.Data) == 0 && messagePassed.Tx.Amount.Int.BitLen() > 0 {
+			ethWithdrawals = append(ethWithdrawals, database.L2BridgeWithdrawal{
+				TransactionWithdrawalHash: messagePassed.WithdrawalHash,
+				BridgeTransfer:            database.BridgeTransfer{Tx: transactionWithdrawals[i].Tx, TokenPair: database.ETHTokenPair},
+			})
+		}
+	}
+
+	if len(messagesPassed) > 0 {
+		log.Info("detected transaction withdrawals", "size", len(transactionWithdrawals))
+		if err := db.BridgeTransactions.StoreL2TransactionWithdrawals(transactionWithdrawals); err != nil {
+			return err
+		}
+		if len(ethWithdrawals) > 0 {
+			log.Info("detected L2ToL1MessagePasser ETH transfers", "size", len(ethWithdrawals))
+			if err := db.BridgeTransfers.StoreL2BridgeWithdrawals(ethWithdrawals); err != nil {
+				return err
+			}
+		}
+	}
+
+	// (2) L2CrossDomainMessenger
+	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l2", predeploys.L2CrossDomainMessengerAddr, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(crossDomainSentMessages) > len(messagesPassed) {
+		return fmt.Errorf("missing L2ToL1MP withdrawal for each cross-domain message. withdrawals: %d, messages: %d", len(messagesPassed), len(crossDomainSentMessages))
+	}
+
+	sentMessages := make(map[logKey]*contracts.CrossDomainMessengerSentMessageEvent, len(crossDomainSentMessages))
+	l2BridgeMessages := make([]database.L2BridgeMessage, len(crossDomainSentMessages))
+	for i := range crossDomainSentMessages {
+		sentMessage := crossDomainSentMessages[i]
+		sentMessages[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex}] = &sentMessage
+
+		// extract the withdrawal hash from the previous MessagePassed event
+		messagePassed, ok := messagesPassed[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex - 1}]
+		if !ok {
+			return fmt.Errorf("missing expected preceding MessagePassedEvent for SentMessage. tx_hash = %s", sentMessage.Event.TransactionHash)
+		}
+
+		l2BridgeMessages[i] = database.L2BridgeMessage{TransactionWithdrawalHash: messagePassed.WithdrawalHash, BridgeMessage: sentMessage.BridgeMessage}
+	}
+
+	if len(l2BridgeMessages) > 0 {
+		log.Info("detected L2CrossDomainMessenger messages", "size", len(l2BridgeMessages))
+		if err := db.BridgeMessages.StoreL2BridgeMessages(l2BridgeMessages); err != nil {
+			return err
+		}
+	}
+
+	// (3) L2StandardBridge
+	initiatedBridges, err := contracts.StandardBridgeInitiatedEvents("l2", predeploys.L2StandardBridgeAddr, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(initiatedBridges) > len(crossDomainSentMessages) {
+		return fmt.Errorf("missing cross-domain message for each initiated bridge event. messages: %d, bridges: %d", len(crossDomainSentMessages), len(initiatedBridges))
+	}
+
+	l2BridgeWithdrawals := make([]database.L2BridgeWithdrawal, len(initiatedBridges))
+	for i := range initiatedBridges {
+		initiatedBridge := initiatedBridges[i]
+
+		// extract the cross domain message hash & deposit source hash from the following events
+		messagePassed, ok := messagesPassed[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 1}]
+		if !ok {
+			return fmt.Errorf("missing expected following MessagePassed for BridgeInitiated. tx_hash = %s", initiatedBridge.Event.TransactionHash)
+		}
+		sentMessage, ok := sentMessages[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 2}]
+		if !ok {
+			return fmt.Errorf("missing expected following SentMessage for BridgeInitiated. tx_hash = %s", initiatedBridge.Event.TransactionHash)
+		}
+
+		initiatedBridge.BridgeTransfer.CrossDomainMessageHash = &sentMessage.BridgeMessage.MessageHash
+		l2BridgeWithdrawals[i] = database.L2BridgeWithdrawal{TransactionWithdrawalHash: messagePassed.WithdrawalHash, BridgeTransfer: initiatedBridge.BridgeTransfer}
+	}
+
+	if len(l2BridgeWithdrawals) > 0 {
+		log.Info("detected L2StandardBridge withdrawals", "size", len(l2BridgeWithdrawals))
+		if err := db.BridgeTransfers.StoreL2BridgeWithdrawals(l2BridgeWithdrawals); err != nil {
+			return err
+		}
+	}
+
+	// a-ok!
+	return nil
+}
+
+// L2ProcessFinalizedBridgeEvent will query the database for all the finalization markers for all initiated
+// bridge events. This covers every part of the multi-layered stack:
+//  1. L2CrossDomainMessenger (relayMessage marker)
+//  2. L2StandardBridge (no-op, since this is simply a wrapper over the L2CrossDomainMEssenger)
+//
+// NOTE: Unlike L1, there's no L2ToL1MessagePasser stage since transaction deposits are apart of the block derivation process.
+func L2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight *big.Int, toHeight *big.Int) error {
+	// (1) L2CrossDomainMessenger relayedMessage
+	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l2", predeploys.L2CrossDomainMessengerAddr, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+
+	relayedMessages := make(map[logKey]*contracts.CrossDomainMessengerRelayedMessageEvent, len(crossDomainRelayedMessages))
+	for i := range crossDomainRelayedMessages {
+		relayed := crossDomainRelayedMessages[i]
+		relayedMessages[logKey{BlockHash: relayed.Event.BlockHash, LogIndex: relayed.Event.LogIndex}] = &relayed
+		message, err := db.BridgeMessages.L1BridgeMessage(relayed.MessageHash)
+		if err != nil {
+			return err
+		} else if message == nil {
+			log.Crit("missing indexed L1CrossDomainMessenger message", "message_hash", relayed.MessageHash, "tx_hash", relayed.Event.TransactionHash)
+			return fmt.Errorf("missing indexed L1CrossDomainMessager message")
+		}
+
+		if err := db.BridgeMessages.MarkRelayedL1BridgeMessage(relayed.MessageHash, relayed.Event.GUID); err != nil {
+			return err
+		}
+	}
+
+	if len(crossDomainRelayedMessages) > 0 {
+		log.Info("relayed L1CrossDomainMessenger messages", "size", len(crossDomainRelayedMessages))
+	}
+
+	// (2) L2StandardBridge BridgeFinalized
+	finalizedBridges, err := contracts.StandardBridgeFinalizedEvents("l2", predeploys.L2StandardBridgeAddr, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(finalizedBridges) > len(crossDomainRelayedMessages) {
+		return fmt.Errorf("missing cross-domain message for each finalized bridge event. messages: %d, bridges: %d", len(crossDomainRelayedMessages), len(finalizedBridges))
+	}
+
+	for i := range finalizedBridges {
+		// Nothing actionable on the database. However, we can treat the relayed message
+		// as an invariant by ensuring we can query for a deposit by the same hash
+		finalizedBridge := finalizedBridges[i]
+		relayedMessage, ok := relayedMessages[logKey{finalizedBridge.Event.BlockHash, finalizedBridge.Event.LogIndex + 1}]
+		if !ok {
+			return fmt.Errorf("missing following RelayedMessage for BridgeFinalized event. tx_hash = %s", finalizedBridge.Event.TransactionHash)
+		}
+
+		// Since the message hash is computed from the relayed message, this ensures the withdrawal fields must match. For good measure,
+		// we may choose to make sure `deposit.BridgeTransfer` matches with the finalized bridge
+		deposit, err := db.BridgeTransfers.L1BridgeDepositWithFilter(database.BridgeTransfer{CrossDomainMessageHash: &relayedMessage.MessageHash})
+		if err != nil {
+			return err
+		} else if deposit == nil {
+			log.Crit("missing L1StandardBridge deposit on L2 finalization", "tx_hash", finalizedBridge.Event.TransactionHash)
+			return errors.New("missing L1StandardBridge deposit on L2 finalization")
+		}
+	}
+
+	// a-ok!
+	return nil
+}
+
+// L2LatestBridgeEventHeader returns the latest header for which and on-chain event
+// has been observed on L2 -- Both initiated L2 events and finalization markers from L1.
+func L2LatestBridgeEventHeader(db *database.DB) (*types.Header, error) {
+	l2ToL1MessagePasserAbi, err := bindings.L2ToL1MessagePasserMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+	crossDomainMessengerAbi, err := bindings.CrossDomainMessengerMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	messagePassedID := l2ToL1MessagePasserAbi.Events["MessagePassed"].ID
+	relayedEventID := crossDomainMessengerAbi.Events["RelayedMessage"].ID
+
+	// (1) Initiated L2 Events
+	// Since all initiated bridge events eventually reach the L2ToL1MessagePasser to
+	// initiate the withdrawal, we can simply look for the last message passed from
+	// this cont
+	var latestWithdrawHeader *types.Header
+	contractEventFilter := database.ContractEvent{ContractAddress: predeploys.L2ToL1MessagePasserAddr, EventSignature: messagePassedID}
+	withdrawEvent, err := db.ContractEvents.L2LatestContractEventWithFilter(contractEventFilter)
+	if err != nil {
+		return nil, err
+	}
+	if withdrawEvent != nil {
+		l2BlockHeader, err := db.Blocks.L2BlockHeader(withdrawEvent.BlockHash)
+		if err != nil {
+			return nil, err
+		}
+		if l2BlockHeader != nil {
+			latestWithdrawHeader = l2BlockHeader.RLPHeader.Header()
+		}
+	}
+
+	// (2) Finalization markers for L1
+	// Since deposited transactions from L1 are apart of the block derivation process,
+	// there are no native finalization markers for OptimismPortal#TransactionDeposited.
+	// The lowest layer to check for here is the CrossDomainMessenger#RelayedMessage event.
+	// This also converts the StandardBridge which simply is an extension of the messenger.
+	var latestRelayedMessageHeader *types.Header
+	contractEventFilter = database.ContractEvent{ContractAddress: predeploys.L2CrossDomainMessengerAddr, EventSignature: relayedEventID}
+	relayedEvent, err := db.ContractEvents.L2LatestContractEventWithFilter(contractEventFilter)
+	if err != nil {
+		return nil, err
+	}
+	if relayedEvent != nil {
+		l2BlockHeader, err := db.Blocks.L2BlockHeader(relayedEvent.BlockHash)
+		if err != nil {
+			return nil, err
+		}
+		if l2BlockHeader != nil {
+			latestRelayedMessageHeader = l2BlockHeader.RLPHeader.Header()
+		}
+	}
+
+	// No causaal relationship between withdraw and relayed messages
+	if latestWithdrawHeader == nil || latestRelayedMessageHeader == nil {
+		return nil, nil
+	} else {
+		if latestWithdrawHeader.Time > latestRelayedMessageHeader.Time {
+			return latestWithdrawHeader, nil
+		} else {
+			return latestRelayedMessageHeader, nil
+		}
+	}
+}
diff --git a/indexer/processors/bridge/types.go b/indexer/processors/bridge/types.go
new file mode 100644
index 000000000..95e7d7ebf
--- /dev/null
+++ b/indexer/processors/bridge/types.go
@@ -0,0 +1,8 @@
+package bridge
+
+import "github.com/ethereum/go-ethereum/common"
+
+type logKey struct {
+	BlockHash common.Hash
+	LogIndex  uint64
+}
diff --git a/indexer/processors/contracts/cross_domain_messenger.go b/indexer/processors/contracts/cross_domain_messenger.go
new file mode 100644
index 000000000..e354d2bd5
--- /dev/null
+++ b/indexer/processors/contracts/cross_domain_messenger.go
@@ -0,0 +1,167 @@
+package contracts
+
+import (
+	"fmt"
+	"math/big"
+
+	"github.com/ethereum/go-ethereum/accounts/abi"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/crypto"
+
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
+)
+
+var (
+	// Standard ABI types copied from golang ABI tests
+	uint256Type, _ = abi.NewType("uint256", "", nil)
+	bytesType, _   = abi.NewType("bytes", "", nil)
+	addressType, _ = abi.NewType("address", "", nil)
+
+	legacyCrossDomainMessengerRelayMessageMethod = abi.NewMethod(
+		"relayMessage",
+		"relayMessage",
+		abi.Function,
+		"external", // mutability
+		false,      // isConst
+		true,       // payable
+		abi.Arguments{ // inputs
+			{Name: "sender", Type: addressType},
+			{Name: "target", Type: addressType},
+			{Name: "data", Type: bytesType},
+			{Name: "nonce", Type: uint256Type},
+		},
+		abi.Arguments{}, // outputs
+	)
+)
+
+type CrossDomainMessengerSentMessageEvent struct {
+	Event         *database.ContractEvent
+	BridgeMessage database.BridgeMessage
+}
+
+type CrossDomainMessengerRelayedMessageEvent struct {
+	Event       *database.ContractEvent
+	MessageHash common.Hash
+}
+
+func CrossDomainMessengerSentMessageEvents(chainSelector string, contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]CrossDomainMessengerSentMessageEvent, error) {
+	crossDomainMessengerAbi, err := bindings.CrossDomainMessengerMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	sentMessageEventAbi := crossDomainMessengerAbi.Events["SentMessage"]
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: sentMessageEventAbi.ID}
+	sentMessageEvents, err := db.ContractEvents.ContractEventsWithFilter(contractEventFilter, chainSelector, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+	if len(sentMessageEvents) == 0 {
+		// prevent the following db queries if we dont need them
+		return nil, nil
+	}
+
+	sentMessageExtensionEventAbi := crossDomainMessengerAbi.Events["SentMessageExtension1"]
+	contractEventFilter = database.ContractEvent{ContractAddress: contractAddress, EventSignature: sentMessageExtensionEventAbi.ID}
+	sentMessageExtensionEvents, err := db.ContractEvents.ContractEventsWithFilter(contractEventFilter, chainSelector, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+	if len(sentMessageEvents) != len(sentMessageExtensionEvents) {
+		return nil, fmt.Errorf("mismatch in SentMessage events. %d sent messages & %d sent message extensions", len(sentMessageEvents), len(sentMessageExtensionEvents))
+	}
+
+	crossDomainSentMessages := make([]CrossDomainMessengerSentMessageEvent, len(sentMessageEvents))
+	for i := range sentMessageEvents {
+		sentMessage := bindings.CrossDomainMessengerSentMessage{Raw: *sentMessageEvents[i].RLPLog}
+		err = UnpackLog(&sentMessage, sentMessageEvents[i].RLPLog, sentMessageEventAbi.Name, crossDomainMessengerAbi)
+		if err != nil {
+			return nil, err
+		}
+		sentMessageExtension := bindings.CrossDomainMessengerSentMessageExtension1{Raw: *sentMessageExtensionEvents[i].RLPLog}
+		err = UnpackLog(&sentMessageExtension, sentMessageExtensionEvents[i].RLPLog, sentMessageExtensionEventAbi.Name, crossDomainMessengerAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		messageHash, err := CrossDomainMessageHash(crossDomainMessengerAbi, &sentMessage, sentMessageExtension.Value)
+		if err != nil {
+			return nil, err
+		}
+
+		crossDomainSentMessages[i] = CrossDomainMessengerSentMessageEvent{
+			Event: &sentMessageEvents[i],
+			BridgeMessage: database.BridgeMessage{
+				MessageHash:          messageHash,
+				Nonce:                database.U256{Int: sentMessage.MessageNonce},
+				SentMessageEventGUID: sentMessageEvents[i].GUID,
+				GasLimit:             database.U256{Int: sentMessage.GasLimit},
+				Tx: database.Transaction{
+					FromAddress: sentMessage.Sender,
+					ToAddress:   sentMessage.Target,
+					Amount:      database.U256{Int: sentMessageExtension.Value},
+					Data:        sentMessage.Message,
+					Timestamp:   sentMessageEvents[i].Timestamp,
+				},
+			},
+		}
+
+	}
+
+	return crossDomainSentMessages, nil
+}
+
+func CrossDomainMessengerRelayedMessageEvents(chainSelector string, contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]CrossDomainMessengerRelayedMessageEvent, error) {
+	crossDomainMessengerAbi, err := bindings.CrossDomainMessengerMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	relayedMessageEventAbi := crossDomainMessengerAbi.Events["RelayedMessage"]
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: relayedMessageEventAbi.ID}
+	relayedMessageEvents, err := db.ContractEvents.ContractEventsWithFilter(contractEventFilter, chainSelector, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	crossDomainRelayedMessages := make([]CrossDomainMessengerRelayedMessageEvent, len(relayedMessageEvents))
+	for i := range relayedMessageEvents {
+		relayedMessage := bindings.CrossDomainMessengerRelayedMessage{Raw: *relayedMessageEvents[i].RLPLog}
+		err = UnpackLog(&relayedMessage, relayedMessageEvents[i].RLPLog, relayedMessageEventAbi.Name, crossDomainMessengerAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		crossDomainRelayedMessages[i] = CrossDomainMessengerRelayedMessageEvent{
+			Event:       &relayedMessageEvents[i],
+			MessageHash: relayedMessage.MsgHash,
+		}
+	}
+
+	return crossDomainRelayedMessages, nil
+}
+
+// Replica of `Hashing.sol#hashCrossDomainMessage` solidity implementation
+func CrossDomainMessageHash(abi *abi.ABI, sentMsg *bindings.CrossDomainMessengerSentMessage, value *big.Int) (common.Hash, error) {
+	version, _ := DecodeVersionedNonce(sentMsg.MessageNonce)
+	switch version {
+	case 0:
+		// Legacy Message
+		inputBytes, err := legacyCrossDomainMessengerRelayMessageMethod.Inputs.Pack(sentMsg.Sender, sentMsg.Target, sentMsg.Message, sentMsg.MessageNonce)
+		if err != nil {
+			return common.Hash{}, err
+		}
+		msgBytes := append(legacyCrossDomainMessengerRelayMessageMethod.ID, inputBytes...)
+		return crypto.Keccak256Hash(msgBytes), nil
+	case 1:
+		// Current Message
+		msgBytes, err := abi.Pack("relayMessage", sentMsg.MessageNonce, sentMsg.Sender, sentMsg.Target, value, sentMsg.GasLimit, sentMsg.Message)
+		if err != nil {
+			return common.Hash{}, err
+		}
+		return crypto.Keccak256Hash(msgBytes), nil
+	}
+
+	return common.Hash{}, fmt.Errorf("unsupported cross domain messenger version: %d", version)
+}
diff --git a/indexer/processors/contracts/l2_to_l1_message_passer.go b/indexer/processors/contracts/l2_to_l1_message_passer.go
new file mode 100644
index 000000000..59a829e7a
--- /dev/null
+++ b/indexer/processors/contracts/l2_to_l1_message_passer.go
@@ -0,0 +1,56 @@
+package contracts
+
+import (
+	"math/big"
+
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
+	"github.com/ethereum/go-ethereum/common"
+)
+
+type L2ToL1MessagePasserMessagePassed struct {
+	Event          *database.ContractEvent
+	WithdrawalHash common.Hash
+	GasLimit       database.U256
+	Nonce          database.U256
+	Tx             database.Transaction
+}
+
+func L2ToL1MessagePasserMessagePassedEvents(contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]L2ToL1MessagePasserMessagePassed, error) {
+	l2ToL1MessagePasserAbi, err := bindings.L2ToL1MessagePasserMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	messagePassedAbi := l2ToL1MessagePasserAbi.Events["MessagePassed"]
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: messagePassedAbi.ID}
+	messagePassedEvents, err := db.ContractEvents.L2ContractEventsWithFilter(contractEventFilter, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	messagesPassed := make([]L2ToL1MessagePasserMessagePassed, len(messagePassedEvents))
+	for i := range messagePassedEvents {
+		messagePassed := bindings.L2ToL1MessagePasserMessagePassed{Raw: *messagePassedEvents[i].RLPLog}
+		err := UnpackLog(&messagePassed, messagePassedEvents[i].RLPLog, messagePassedAbi.Name, l2ToL1MessagePasserAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		messagesPassed[i] = L2ToL1MessagePasserMessagePassed{
+			Event:          &messagePassedEvents[i].ContractEvent,
+			WithdrawalHash: messagePassed.WithdrawalHash,
+			Nonce:          database.U256{Int: messagePassed.Nonce},
+			GasLimit:       database.U256{Int: messagePassed.GasLimit},
+			Tx: database.Transaction{
+				FromAddress: messagePassed.Sender,
+				ToAddress:   messagePassed.Target,
+				Amount:      database.U256{Int: messagePassed.Value},
+				Data:        messagePassed.Data,
+				Timestamp:   messagePassedEvents[i].Timestamp,
+			},
+		}
+	}
+
+	return messagesPassed, nil
+}
diff --git a/indexer/processors/contracts/optimism_portal.go b/indexer/processors/contracts/optimism_portal.go
new file mode 100644
index 000000000..ba0edeab8
--- /dev/null
+++ b/indexer/processors/contracts/optimism_portal.go
@@ -0,0 +1,142 @@
+package contracts
+
+import (
+	"errors"
+	"math/big"
+
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
+	"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+)
+
+type OptimismPortalTransactionDepositEvent struct {
+	Event     *database.ContractEvent
+	DepositTx *types.DepositTx
+	Tx        database.Transaction
+	GasLimit  database.U256
+}
+
+type OptimismPortalWithdrawalProvenEvent struct {
+	*bindings.OptimismPortalWithdrawalProven
+	Event *database.ContractEvent
+}
+
+type OptimismPortalWithdrawalFinalizedEvent struct {
+	*bindings.OptimismPortalWithdrawalFinalized
+	Event *database.ContractEvent
+}
+
+type OptimismPortalProvenWithdrawal struct {
+	OutputRoot    [32]byte
+	Timestamp     *big.Int
+	L2OutputIndex *big.Int
+}
+
+func OptimismPortalTransactionDepositEvents(contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]OptimismPortalTransactionDepositEvent, error) {
+	optimismPortalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+	transactionDepositedEventAbi := optimismPortalAbi.Events["TransactionDeposited"]
+	if transactionDepositedEventAbi.ID != derive.DepositEventABIHash {
+		return nil, errors.New("op-node DepositEventABIHash & optimism portal TransactionDeposited ID mismatch")
+	}
+
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: transactionDepositedEventAbi.ID}
+	transactionDepositEvents, err := db.ContractEvents.L1ContractEventsWithFilter(contractEventFilter, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	optimismPortalTxDeposits := make([]OptimismPortalTransactionDepositEvent, len(transactionDepositEvents))
+	for i := range transactionDepositEvents {
+		depositTx, err := derive.UnmarshalDepositLogEvent(transactionDepositEvents[i].RLPLog)
+		if err != nil {
+			return nil, err
+		}
+
+		txDeposit := bindings.OptimismPortalTransactionDeposited{Raw: *transactionDepositEvents[i].RLPLog}
+		err = UnpackLog(&txDeposit, transactionDepositEvents[i].RLPLog, transactionDepositedEventAbi.Name, optimismPortalAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		optimismPortalTxDeposits[i] = OptimismPortalTransactionDepositEvent{
+			Event:     &transactionDepositEvents[i].ContractEvent,
+			DepositTx: depositTx,
+			GasLimit:  database.U256{Int: new(big.Int).SetUint64(depositTx.Gas)},
+			Tx: database.Transaction{
+				FromAddress: txDeposit.From,
+				ToAddress:   txDeposit.To,
+				Amount:      database.U256{Int: depositTx.Value},
+				Data:        depositTx.Data,
+				Timestamp:   transactionDepositEvents[i].Timestamp,
+			},
+		}
+	}
+
+	return optimismPortalTxDeposits, nil
+}
+
+func OptimismPortalWithdrawalProvenEvents(contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]OptimismPortalWithdrawalProvenEvent, error) {
+	optimismPortalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	withdrawalProvenEventAbi := optimismPortalAbi.Events["WithdrawalProven"]
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: withdrawalProvenEventAbi.ID}
+	withdrawalProvenEvents, err := db.ContractEvents.L1ContractEventsWithFilter(contractEventFilter, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	provenWithdrawals := make([]OptimismPortalWithdrawalProvenEvent, len(withdrawalProvenEvents))
+	for i := range withdrawalProvenEvents {
+		withdrawalProven := bindings.OptimismPortalWithdrawalProven{Raw: *withdrawalProvenEvents[i].RLPLog}
+		err := UnpackLog(&withdrawalProven, withdrawalProvenEvents[i].RLPLog, withdrawalProvenEventAbi.Name, optimismPortalAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		provenWithdrawals[i] = OptimismPortalWithdrawalProvenEvent{
+			OptimismPortalWithdrawalProven: &withdrawalProven,
+			Event:                          &withdrawalProvenEvents[i].ContractEvent,
+		}
+	}
+
+	return provenWithdrawals, nil
+}
+
+func OptimismPortalWithdrawalFinalizedEvents(contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]OptimismPortalWithdrawalFinalizedEvent, error) {
+	optimismPortalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	withdrawalFinalizedEventAbi := optimismPortalAbi.Events["WithdrawalFinalized"]
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: withdrawalFinalizedEventAbi.ID}
+	withdrawalFinalizedEvents, err := db.ContractEvents.L1ContractEventsWithFilter(contractEventFilter, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	finalizedWithdrawals := make([]OptimismPortalWithdrawalFinalizedEvent, len(withdrawalFinalizedEvents))
+	for i := range withdrawalFinalizedEvents {
+		withdrawalFinalized := bindings.OptimismPortalWithdrawalFinalized{Raw: *withdrawalFinalizedEvents[i].RLPLog}
+		err := UnpackLog(&withdrawalFinalized, withdrawalFinalizedEvents[i].RLPLog, withdrawalFinalizedEventAbi.Name, optimismPortalAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		finalizedWithdrawals[i] = OptimismPortalWithdrawalFinalizedEvent{
+			OptimismPortalWithdrawalFinalized: &withdrawalFinalized,
+			Event:                             &withdrawalFinalizedEvents[i].ContractEvent,
+		}
+	}
+
+	return finalizedWithdrawals, nil
+}
diff --git a/indexer/processors/contracts/standard_bridge.go b/indexer/processors/contracts/standard_bridge.go
new file mode 100644
index 000000000..8bc15df0d
--- /dev/null
+++ b/indexer/processors/contracts/standard_bridge.go
@@ -0,0 +1,173 @@
+package contracts
+
+import (
+	"math/big"
+
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
+	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
+
+	"github.com/ethereum/go-ethereum/common"
+)
+
+type StandardBridgeInitiatedEvent struct {
+	Event          *database.ContractEvent
+	BridgeTransfer database.BridgeTransfer
+}
+
+type StandardBridgeFinalizedEvent struct {
+	Event          *database.ContractEvent
+	BridgeTransfer database.BridgeTransfer
+}
+
+// StandardBridgeInitiatedEvents extracts all initiated bridge events from the contracts that follow the StandardBridge ABI. The
+// correlated CrossDomainMessenger nonce is also parsed from the associated messenger events.
+func StandardBridgeInitiatedEvents(chainSelector string, contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]StandardBridgeInitiatedEvent, error) {
+	ethBridgeInitiatedEvents, err := _standardBridgeInitiatedEvents[bindings.StandardBridgeETHBridgeInitiated](contractAddress, chainSelector, db, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	erc20BridgeInitiatedEvents, err := _standardBridgeInitiatedEvents[bindings.StandardBridgeERC20BridgeInitiated](contractAddress, chainSelector, db, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	return append(ethBridgeInitiatedEvents, erc20BridgeInitiatedEvents...), nil
+}
+
+// StandardBridgeFinalizedEvents extracts all finalization bridge events from the contracts that follow the StandardBridge ABI. The
+// correlated CrossDomainMessenger nonce is also parsed by looking at the parameters of the corresponding relayMessage transaction data.
+func StandardBridgeFinalizedEvents(chainSelector string, contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]StandardBridgeFinalizedEvent, error) {
+	ethBridgeFinalizedEvents, err := _standardBridgeFinalizedEvents[bindings.StandardBridgeETHBridgeFinalized](contractAddress, chainSelector, db, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	erc20BridgeFinalizedEvents, err := _standardBridgeFinalizedEvents[bindings.StandardBridgeERC20BridgeFinalized](contractAddress, chainSelector, db, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	return append(ethBridgeFinalizedEvents, erc20BridgeFinalizedEvents...), nil
+}
+
+// parse out eth or erc20 bridge initiated events
+func _standardBridgeInitiatedEvents[BridgeEventType bindings.StandardBridgeETHBridgeInitiated | bindings.StandardBridgeERC20BridgeInitiated](
+	contractAddress common.Address, chainSelector string, db *database.DB, fromHeight, toHeight *big.Int,
+) ([]StandardBridgeInitiatedEvent, error) {
+	standardBridgeAbi, err := bindings.StandardBridgeMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	var eventType BridgeEventType
+	var eventName string
+	switch any(eventType).(type) {
+	case bindings.StandardBridgeETHBridgeInitiated:
+		eventName = "ETHBridgeInitiated"
+	case bindings.StandardBridgeERC20BridgeInitiated:
+		eventName = "ERC20BridgeInitiated"
+	default:
+		panic("should not be here")
+	}
+
+	initiatedBridgeEventAbi := standardBridgeAbi.Events[eventName]
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: initiatedBridgeEventAbi.ID}
+	initiatedBridgeEvents, err := db.ContractEvents.ContractEventsWithFilter(contractEventFilter, chainSelector, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	standardBridgeInitiatedEvents := make([]StandardBridgeInitiatedEvent, len(initiatedBridgeEvents))
+	for i := range initiatedBridgeEvents {
+		erc20Bridge := bindings.StandardBridgeERC20BridgeInitiated{Raw: *initiatedBridgeEvents[i].RLPLog}
+		err := UnpackLog(&erc20Bridge, initiatedBridgeEvents[i].RLPLog, eventName, standardBridgeAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		// If an ETH bridge, lets fill in the needed fields
+		switch any(eventType).(type) {
+		case bindings.StandardBridgeETHBridgeInitiated:
+			erc20Bridge.LocalToken = predeploys.LegacyERC20ETHAddr
+			erc20Bridge.RemoteToken = predeploys.LegacyERC20ETHAddr
+		}
+
+		standardBridgeInitiatedEvents[i] = StandardBridgeInitiatedEvent{
+			Event: &initiatedBridgeEvents[i],
+			BridgeTransfer: database.BridgeTransfer{
+				TokenPair: database.TokenPair{LocalTokenAddress: erc20Bridge.LocalToken, RemoteTokenAddress: erc20Bridge.RemoteToken},
+				Tx: database.Transaction{
+					FromAddress: erc20Bridge.From,
+					ToAddress:   erc20Bridge.To,
+					Amount:      database.U256{Int: erc20Bridge.Amount},
+					Data:        erc20Bridge.ExtraData,
+					Timestamp:   initiatedBridgeEvents[i].Timestamp,
+				},
+			},
+		}
+	}
+
+	return standardBridgeInitiatedEvents, nil
+}
+
+// parse out eth or erc20 bridge finalization events
+func _standardBridgeFinalizedEvents[BridgeEventType bindings.StandardBridgeETHBridgeFinalized | bindings.StandardBridgeERC20BridgeFinalized](
+	contractAddress common.Address, chainSelector string, db *database.DB, fromHeight, toHeight *big.Int,
+) ([]StandardBridgeFinalizedEvent, error) {
+	standardBridgeAbi, err := bindings.StandardBridgeMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	var eventType BridgeEventType
+	var eventName string
+	switch any(eventType).(type) {
+	case bindings.StandardBridgeETHBridgeFinalized:
+		eventName = "ETHBridgeFinalized"
+	case bindings.StandardBridgeERC20BridgeFinalized:
+		eventName = "ERC20BridgeFinalized"
+	default:
+		panic("should not be here")
+	}
+
+	bridgeFinalizedEventAbi := standardBridgeAbi.Events[eventName]
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: bridgeFinalizedEventAbi.ID}
+	bridgeFinalizedEvents, err := db.ContractEvents.ContractEventsWithFilter(contractEventFilter, chainSelector, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	standardBridgeFinalizedEvents := make([]StandardBridgeFinalizedEvent, len(bridgeFinalizedEvents))
+	for i := range bridgeFinalizedEvents {
+		erc20Bridge := bindings.StandardBridgeERC20BridgeFinalized{Raw: *bridgeFinalizedEvents[i].RLPLog}
+		err := UnpackLog(&erc20Bridge, bridgeFinalizedEvents[i].RLPLog, eventName, standardBridgeAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		// If an ETH bridge, lets fill in the needed fields
+		switch any(eventType).(type) {
+		case bindings.StandardBridgeETHBridgeFinalized:
+			erc20Bridge.LocalToken = predeploys.LegacyERC20ETHAddr
+			erc20Bridge.RemoteToken = predeploys.LegacyERC20ETHAddr
+		}
+
+		standardBridgeFinalizedEvents[i] = StandardBridgeFinalizedEvent{
+			Event: &bridgeFinalizedEvents[i],
+			BridgeTransfer: database.BridgeTransfer{
+				TokenPair: database.TokenPair{LocalTokenAddress: erc20Bridge.LocalToken, RemoteTokenAddress: erc20Bridge.RemoteToken},
+				Tx: database.Transaction{
+					FromAddress: erc20Bridge.From,
+					ToAddress:   erc20Bridge.To,
+					Amount:      database.U256{Int: erc20Bridge.Amount},
+					Data:        erc20Bridge.ExtraData,
+					Timestamp:   bridgeFinalizedEvents[i].Timestamp,
+				},
+			},
+		}
+	}
+
+	return standardBridgeFinalizedEvents, nil
+}
diff --git a/indexer/processors/contracts/utils.go b/indexer/processors/contracts/utils.go
new file mode 100644
index 000000000..d95d18517
--- /dev/null
+++ b/indexer/processors/contracts/utils.go
@@ -0,0 +1,63 @@
+package contracts
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"math/big"
+
+	"github.com/ethereum/go-ethereum/accounts/abi"
+	"github.com/ethereum/go-ethereum/core/types"
+)
+
+// DecodeVersionNonce is an re-implementation of Encoding.sol#decodeVersionedNonce.
+// If the nonce is greater than 32 bytes (solidity uint256), bytes [32:] are ignored
+func DecodeVersionedNonce(nonce *big.Int) (uint16, *big.Int) {
+	nonceBytes := nonce.Bytes()
+	nonceByteLen := len(nonceBytes)
+	if nonceByteLen < 30 {
+		// version is 0x0000
+		return 0, nonce
+	} else if nonceByteLen == 31 {
+		// version is 0x00[01..ff]
+		return uint16(nonceBytes[0]), new(big.Int).SetBytes(nonceBytes[1:])
+	} else {
+		// fully specified
+		version := binary.BigEndian.Uint16(nonceBytes[:2])
+		return version, new(big.Int).SetBytes(nonceBytes[2:])
+	}
+}
+
+func UnpackLog(out interface{}, log *types.Log, name string, contractAbi *abi.ABI) error {
+	eventAbi, ok := contractAbi.Events[name]
+	if !ok {
+		return fmt.Errorf("event %s not present in supplied ABI", name)
+	} else if len(log.Topics) == 0 {
+		return errors.New("anonymous events are not supported")
+	} else if log.Topics[0] != eventAbi.ID {
+		return errors.New("event signature mismatch")
+	}
+
+	err := contractAbi.UnpackIntoInterface(out, name, log.Data)
+	if err != nil {
+		return err
+	}
+
+	// handle topics if present
+	if len(log.Topics) > 1 {
+		var indexedArgs abi.Arguments
+		for _, arg := range eventAbi.Inputs {
+			if arg.Indexed {
+				indexedArgs = append(indexedArgs, arg)
+			}
+		}
+
+		// The first topic (event signature) is omitted
+		err := abi.ParseTopics(out, indexedArgs, log.Topics[1:])
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/op-batcher/batcher/channel_manager.go b/op-batcher/batcher/channel_manager.go
index 9f77a8464..052b9dbe1 100644
--- a/op-batcher/batcher/channel_manager.go
+++ b/op-batcher/batcher/channel_manager.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"sync"
 
 	"github.com/ethereum-optimism/optimism/op-batcher/metrics"
 	"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
@@ -21,8 +22,9 @@ var ErrReorg = errors.New("block does not extend existing chain")
 // For simplicity, it only creates a single pending channel at a time & waits for
 // the channel to either successfully be submitted or timeout before creating a new
 // channel.
-// Functions on channelManager are not safe for concurrent access.
+// Public functions on channelManager are safe for concurrent access.
 type channelManager struct {
+	mu   sync.Mutex
 	log  log.Logger
 	metr metrics.Metricer
 	cfg  ChannelConfig
@@ -55,6 +57,8 @@ func NewChannelManager(log log.Logger, metr metrics.Metricer, cfg ChannelConfig)
 // Clear clears the entire state of the channel manager.
 // It is intended to be used after an L2 reorg.
 func (s *channelManager) Clear() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
 	s.log.Trace("clearing channel manager state")
 	s.blocks = s.blocks[:0]
 	s.tip = common.Hash{}
@@ -67,6 +71,8 @@ func (s *channelManager) Clear() {
 // TxFailed records a transaction as failed. It will attempt to resubmit the data
 // in the failed transaction.
 func (s *channelManager) TxFailed(id txID) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
 	if channel, ok := s.txChannels[id]; ok {
 		delete(s.txChannels, id)
 		channel.TxFailed(id)
@@ -84,6 +90,8 @@ func (s *channelManager) TxFailed(id txID) {
 // resubmitted.
 // This function may reset the pending channel if the pending channel has timed out.
 func (s *channelManager) TxConfirmed(id txID, inclusionBlock eth.BlockID) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
 	if channel, ok := s.txChannels[id]; ok {
 		delete(s.txChannels, id)
 		done, blocks := channel.TxConfirmed(id, inclusionBlock)
@@ -134,6 +142,8 @@ func (s *channelManager) nextTxData(channel *channel) (txData, error) {
 // full, it only returns the remaining frames of this channel until it got
 // successfully fully sent to L1. It returns io.EOF if there's no pending frame.
 func (s *channelManager) TxData(l1Head eth.BlockID) (txData, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
 	var firstWithFrame *channel
 	for _, ch := range s.channelQueue {
 		if ch.HasFrame() {
@@ -298,6 +308,8 @@ func (s *channelManager) outputFrames() error {
 // if the block does not extend the last block loaded into the state. If no
 // blocks were added yet, the parent hash check is skipped.
 func (s *channelManager) AddL2Block(block *types.Block) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
 	if s.tip != (common.Hash{}) && s.tip != block.ParentHash() {
 		return ErrReorg
 	}
@@ -324,6 +336,8 @@ func l2BlockRefFromBlockAndL1Info(block *types.Block, l1info derive.L1BlockInfo)
 // and prevents the creation of any new channels.
 // Any outputted frames still need to be published.
 func (s *channelManager) Close() error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
 	if s.closed {
 		return nil
 	}
diff --git a/op-chain-ops/genesis/layer_two.go b/op-chain-ops/genesis/layer_two.go
index d63dc1e5a..af08dd0c6 100644
--- a/op-chain-ops/genesis/layer_two.go
+++ b/op-chain-ops/genesis/layer_two.go
@@ -23,9 +23,10 @@ func BuildL2Genesis(config *DeployConfig, l1StartBlock *types.Block) (*core.Gene
 	if config.FundDevAccounts {
 		log.Info("Funding developer accounts in L2 genesis")
 		FundDevAccounts(db)
-		SetPrecompileBalances(db)
 	}
 
+	SetPrecompileBalances(db)
+
 	storage, err := NewL2StorageConfig(config, l1StartBlock)
 	if err != nil {
 		return nil, err
diff --git a/op-chain-ops/genesis/setters.go b/op-chain-ops/genesis/setters.go
index e34b9d969..1490ea2f2 100644
--- a/op-chain-ops/genesis/setters.go
+++ b/op-chain-ops/genesis/setters.go
@@ -13,6 +13,11 @@ import (
 	"github.com/ethereum/go-ethereum/log"
 )
 
+// PrecompileCount represents the number of precompile addresses
+// starting from `address(0)` to PrecompileCount that are funded
+// with a single wei in the genesis state.
+const PrecompileCount = 256
+
 // FundDevAccounts will fund each of the development accounts.
 func FundDevAccounts(db vm.StateDB) {
 	for _, account := range DevAccounts {
@@ -52,7 +57,7 @@ func setProxies(db vm.StateDB, proxyAdminAddr common.Address, namespace *big.Int
 // This is an optimization to make calling them cheaper. This should only
 // be used for devnets.
 func SetPrecompileBalances(db vm.StateDB) {
-	for i := 0; i < 256; i++ {
+	for i := 0; i < PrecompileCount; i++ {
 		addr := common.BytesToAddress([]byte{byte(i)})
 		db.CreateAccount(addr)
 		db.AddBalance(addr, common.Big1)


Test patch:
diff --git a/technical-documents/postmortems/2022-02-02-inflation-vuln.md b/docs/postmortems/2022-02-02-inflation-vuln.md
similarity index 100%
rename from technical-documents/postmortems/2022-02-02-inflation-vuln.md
rename to docs/postmortems/2022-02-02-inflation-vuln.md
diff --git a/technical-documents/postmortems/2022-05-31-drop-1.md b/docs/postmortems/2022-05-31-drop-1.md
similarity index 100%
rename from technical-documents/postmortems/2022-05-31-drop-1.md
rename to docs/postmortems/2022-05-31-drop-1.md
diff --git a/technical-documents/postmortems/2023-04-26-transaction-delays.md b/docs/postmortems/2023-04-26-transaction-delays.md
similarity index 100%
rename from technical-documents/postmortems/2023-04-26-transaction-delays.md
rename to docs/postmortems/2023-04-26-transaction-delays.md
diff --git a/technical-documents/postmortems/2023-04-26-transaction-delays/outage.png b/docs/postmortems/2023-04-26-transaction-delays/outage.png
similarity index 100%
rename from technical-documents/postmortems/2023-04-26-transaction-delays/outage.png
rename to docs/postmortems/2023-04-26-transaction-delays/outage.png
diff --git a/technical-documents/security-reviews/2020_10-Rollup-TrailOfBits.pdf b/docs/security-reviews/2020_10-Rollup-TrailOfBits.pdf
similarity index 100%
rename from technical-documents/security-reviews/2020_10-Rollup-TrailOfBits.pdf
rename to docs/security-reviews/2020_10-Rollup-TrailOfBits.pdf
diff --git a/technical-documents/security-reviews/2020_11-Dapphub-ECDSA_Wallet.pdf b/docs/security-reviews/2020_11-Dapphub-ECDSA_Wallet.pdf
similarity index 100%
rename from technical-documents/security-reviews/2020_11-Dapphub-ECDSA_Wallet.pdf
rename to docs/security-reviews/2020_11-Dapphub-ECDSA_Wallet.pdf
diff --git a/technical-documents/security-reviews/2021_03-OVM_and_Rollup-OpenZeppelin.pdf b/docs/security-reviews/2021_03-OVM_and_Rollup-OpenZeppelin.pdf
similarity index 100%
rename from technical-documents/security-reviews/2021_03-OVM_and_Rollup-OpenZeppelin.pdf
rename to docs/security-reviews/2021_03-OVM_and_Rollup-OpenZeppelin.pdf
diff --git a/technical-documents/security-reviews/2021_03-SafetyChecker-ConsenSysDiligence.pdf b/docs/security-reviews/2021_03-SafetyChecker-ConsenSysDiligence.pdf
similarity index 100%
rename from technical-documents/security-reviews/2021_03-SafetyChecker-ConsenSysDiligence.pdf
rename to docs/security-reviews/2021_03-SafetyChecker-ConsenSysDiligence.pdf
diff --git a/technical-documents/security-reviews/2022_05-Bedrock_Contracts-Zeppelin.pdf b/docs/security-reviews/2022_05-Bedrock_Contracts-Zeppelin.pdf
similarity index 100%
rename from technical-documents/security-reviews/2022_05-Bedrock_Contracts-Zeppelin.pdf
rename to docs/security-reviews/2022_05-Bedrock_Contracts-Zeppelin.pdf
diff --git a/technical-documents/security-reviews/2022_05-OpNode-TrailOfBits.pdf b/docs/security-reviews/2022_05-OpNode-TrailOfBits.pdf
similarity index 100%
rename from technical-documents/security-reviews/2022_05-OpNode-TrailOfBits.pdf
rename to docs/security-reviews/2022_05-OpNode-TrailOfBits.pdf
diff --git a/technical-documents/security-reviews/2022_08-Bedrock_GoLang-SigmaPrime.pdf b/docs/security-reviews/2022_08-Bedrock_GoLang-SigmaPrime.pdf
similarity index 100%
rename from technical-documents/security-reviews/2022_08-Bedrock_GoLang-SigmaPrime.pdf
rename to docs/security-reviews/2022_08-Bedrock_GoLang-SigmaPrime.pdf
diff --git a/technical-documents/security-reviews/2022_09-Bedrock_and_Periphery-Zeppelin.pdf b/docs/security-reviews/2022_09-Bedrock_and_Periphery-Zeppelin.pdf
similarity index 100%
rename from technical-documents/security-reviews/2022_09-Bedrock_and_Periphery-Zeppelin.pdf
rename to docs/security-reviews/2022_09-Bedrock_and_Periphery-Zeppelin.pdf
diff --git a/technical-documents/security-reviews/2022_10-Drippie-Spearbit.pdf b/docs/security-reviews/2022_10-Drippie-Spearbit.pdf
similarity index 100%
rename from technical-documents/security-reviews/2022_10-Drippie-Spearbit.pdf
rename to docs/security-reviews/2022_10-Drippie-Spearbit.pdf
diff --git a/technical-documents/security-reviews/2022_11-Invariant_Testing-TrailOfBits.pdf b/docs/security-reviews/2022_11-Invariant_Testing-TrailOfBits.pdf
similarity index 100%
rename from technical-documents/security-reviews/2022_11-Invariant_Testing-TrailOfBits.pdf
rename to docs/security-reviews/2022_11-Invariant_Testing-TrailOfBits.pdf
diff --git a/technical-documents/security-reviews/2023_01-Bedrock_Updates-TrailOfBits.pdf b/docs/security-reviews/2023_01-Bedrock_Updates-TrailOfBits.pdf
similarity index 100%
rename from technical-documents/security-reviews/2023_01-Bedrock_Updates-TrailOfBits.pdf
rename to docs/security-reviews/2023_01-Bedrock_Updates-TrailOfBits.pdf
diff --git a/indexer/api/api_test.go b/indexer/api/api_test.go
index a560a84b9..bcaf8956c 100644
--- a/indexer/api/api_test.go
+++ b/indexer/api/api_test.go
@@ -56,7 +56,7 @@ func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalWithFilter(filter database
 
 func (mbv *MockBridgeTransfersView) L1BridgeDepositsByAddress(address common.Address, cursor string, limit int) (*database.L1BridgeDepositsResponse, error) {
 	return &database.L1BridgeDepositsResponse{
-		Deposits: []*database.L1BridgeDepositWithTransactionHashes{
+		Deposits: []database.L1BridgeDepositWithTransactionHashes{
 			{
 				L1BridgeDeposit:   deposit,
 				L1TransactionHash: common.HexToHash("0x123"),
@@ -67,7 +67,7 @@ func (mbv *MockBridgeTransfersView) L1BridgeDepositsByAddress(address common.Add
 
 func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalsByAddress(address common.Address, cursor string, limit int) (*database.L2BridgeWithdrawalsResponse, error) {
 	return &database.L2BridgeWithdrawalsResponse{
-		Withdrawals: []*database.L2BridgeWithdrawalWithTransactionHashes{
+		Withdrawals: []database.L2BridgeWithdrawalWithTransactionHashes{
 			{
 				L2BridgeWithdrawal: withdrawal,
 				L2TransactionHash:  common.HexToHash("0x789"),
diff --git a/indexer/config/config_test.go b/indexer/config/config_test.go
index f17a3d76b..16775faea 100644
--- a/indexer/config/config_test.go
+++ b/indexer/config/config_test.go
@@ -54,11 +54,10 @@ func TestLoadConfig(t *testing.T) {
 	require.NoError(t, err)
 
 	require.Equal(t, conf.Chain.Preset, 420)
-	require.Equal(t, conf.Chain.L1Contracts.OptimismPortal.String(), presetL1Contracts[420].OptimismPortal.String())
-	require.Equal(t, conf.Chain.L1Contracts.L1CrossDomainMessenger.String(), presetL1Contracts[420].L1CrossDomainMessenger.String())
-	require.Equal(t, conf.Chain.L1Contracts.L1ERC721Bridge.String(), presetL1Contracts[420].L1ERC721Bridge.String())
-	require.Equal(t, conf.Chain.L1Contracts.L1StandardBridge.String(), presetL1Contracts[420].L1StandardBridge.String())
-	require.Equal(t, conf.Chain.L1Contracts.L2OutputOracle.String(), presetL1Contracts[420].L2OutputOracle.String())
+	require.Equal(t, conf.Chain.L1Contracts.OptimismPortalProxy.String(), presetL1Contracts[420].OptimismPortalProxy.String())
+	require.Equal(t, conf.Chain.L1Contracts.L1CrossDomainMessengerProxy.String(), presetL1Contracts[420].L1CrossDomainMessengerProxy.String())
+	require.Equal(t, conf.Chain.L1Contracts.L1StandardBridgeProxy.String(), presetL1Contracts[420].L1StandardBridgeProxy.String())
+	require.Equal(t, conf.Chain.L1Contracts.L2OutputOracleProxy.String(), presetL1Contracts[420].L2OutputOracleProxy.String())
 	require.Equal(t, conf.RPCs.L1RPC, "https://l1.example.com")
 	require.Equal(t, conf.RPCs.L2RPC, "https://l2.example.com")
 	require.Equal(t, conf.DB.Host, "127.0.0.1")
@@ -85,7 +84,6 @@ func TestLoadConfig_WithoutPreset(t *testing.T) {
 		l2-output-oracle = "0x42097868233d1aa22e815a266982f2cf17685a27"
 		l1-cross-domain-messenger = "0x420ce71c97B33Cc4729CF772ae268934F7ab5fA1"
 		l1-standard-bridge = "0x4209fc46f92E8a1c0deC1b1747d010903E884bE1"
-		l1-erc721-bridge = "0x420749f83b81B301cAb5f48EB8516B986DAef23D"
 
         [rpcs]
         l1-rpc = "https://l1.example.com"
@@ -104,11 +102,10 @@ func TestLoadConfig_WithoutPreset(t *testing.T) {
 	conf, err := LoadConfig(logger, tmpfile.Name())
 	require.NoError(t, err)
 
-	require.Equal(t, conf.Chain.L1Contracts.OptimismPortal.String(), common.HexToAddress("0x4205Fc579115071764c7423A4f12eDde41f106Ed").String())
-	require.Equal(t, conf.Chain.L1Contracts.L2OutputOracle.String(), common.HexToAddress("0x42097868233d1aa22e815a266982f2cf17685a27").String())
-	require.Equal(t, conf.Chain.L1Contracts.L1CrossDomainMessenger.String(), common.HexToAddress("0x420ce71c97B33Cc4729CF772ae268934F7ab5fA1").String())
-	require.Equal(t, conf.Chain.L1Contracts.L1StandardBridge.String(), common.HexToAddress("0x4209fc46f92E8a1c0deC1b1747d010903E884bE1").String())
-	require.Equal(t, conf.Chain.L1Contracts.L1ERC721Bridge.String(), common.HexToAddress("0x420749f83b81B301cAb5f48EB8516B986DAef23D").String())
+	require.Equal(t, conf.Chain.L1Contracts.OptimismPortalProxy.String(), common.HexToAddress("0x4205Fc579115071764c7423A4f12eDde41f106Ed").String())
+	require.Equal(t, conf.Chain.L1Contracts.L2OutputOracleProxy.String(), common.HexToAddress("0x42097868233d1aa22e815a266982f2cf17685a27").String())
+	require.Equal(t, conf.Chain.L1Contracts.L1CrossDomainMessengerProxy.String(), common.HexToAddress("0x420ce71c97B33Cc4729CF772ae268934F7ab5fA1").String())
+	require.Equal(t, conf.Chain.L1Contracts.L1StandardBridgeProxy.String(), common.HexToAddress("0x4209fc46f92E8a1c0deC1b1747d010903E884bE1").String())
 	require.Equal(t, conf.Chain.Preset, 0)
 }
 
diff --git a/indexer/e2e_tests/bridge_messages_e2e_test.go b/indexer/e2e_tests/bridge_messages_e2e_test.go
index ec797419b..b847a10fb 100644
--- a/indexer/e2e_tests/bridge_messages_e2e_test.go
+++ b/indexer/e2e_tests/bridge_messages_e2e_test.go
@@ -32,9 +32,6 @@ func TestE2EBridgeL1CrossDomainMessenger(t *testing.T) {
 	require.NoError(t, err)
 	l1Opts.Value = big.NewInt(params.Ether)
 
-	// Pause the processor to track relayed event
-	testSuite.Indexer.L2Processor.PauseForTest()
-
 	// (1) Send the Message
 	sentMsgTx, err := l1CrossDomainMessenger.SendMessage(l1Opts, aliceAddr, calldata, 100_000)
 	require.NoError(t, err)
@@ -46,7 +43,7 @@ func TestE2EBridgeL1CrossDomainMessenger(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= sentMsgReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -70,17 +67,18 @@ func TestE2EBridgeL1CrossDomainMessenger(t *testing.T) {
 	require.ElementsMatch(t, calldata, sentMessage.Tx.Data)
 
 	// (2) Process RelayedMesssage on inclusion
-	require.Nil(t, sentMessage.RelayedMessageEventGUID)
-	testSuite.Indexer.L2Processor.ResumeForTest()
+	//   - We dont assert that `RelayedMessageEventGUID` is nil prior to inclusion since there isn't a
+	//   a straightforward way of pausing/resuming the processors at the right time. The codepath is the
+	//   same for L2->L1 messages which does check for this so we are still covered
 	transaction, err := testSuite.DB.BridgeTransactions.L1TransactionDeposit(sentMessage.TransactionSourceHash)
 	require.NoError(t, err)
 
 	// wait for processor catchup
-	depositReceipt, err := wait.ForReceiptOK(context.Background(), testSuite.L2Client, transaction.L2TransactionHash)
+	l2DepositReceipt, err := wait.ForReceiptOK(context.Background(), testSuite.L2Client, transaction.L2TransactionHash)
 	require.NoError(t, err)
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l2Header := testSuite.Indexer.L2Processor.LatestProcessedHeader()
-		return l2Header != nil && l2Header.Number.Uint64() >= depositReceipt.BlockNumber.Uint64(), nil
+		l2Header := testSuite.Indexer.BridgeProcessor.LatestL2Header
+		return l2Header != nil && l2Header.Number.Uint64() >= l2DepositReceipt.BlockNumber.Uint64(), nil
 	}))
 
 	sentMessage, err = testSuite.DB.BridgeMessages.L1BridgeMessage(parsedMessage.MessageHash)
@@ -132,7 +130,7 @@ func TestE2EBridgeL2CrossDomainMessenger(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l2Header := testSuite.Indexer.L2Processor.LatestProcessedHeader()
+		l2Header := testSuite.Indexer.BridgeProcessor.LatestL2Header
 		return l2Header != nil && l2Header.Number.Uint64() >= sentMsgReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -161,7 +159,7 @@ func TestE2EBridgeL2CrossDomainMessenger(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= finalizedReceipt.BlockNumber.Uint64(), nil
 	}))
 
diff --git a/indexer/e2e_tests/bridge_transactions_e2e_test.go b/indexer/e2e_tests/bridge_transactions_e2e_test.go
index 9a04a48ef..f99a42aef 100644
--- a/indexer/e2e_tests/bridge_transactions_e2e_test.go
+++ b/indexer/e2e_tests/bridge_transactions_e2e_test.go
@@ -46,12 +46,13 @@ func TestE2EBridgeTransactionsOptimismPortalDeposits(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= depositReceipt.BlockNumber.Uint64(), nil
 	}))
 
 	deposit, err := testSuite.DB.BridgeTransactions.L1TransactionDeposit(depositInfo.DepositTx.SourceHash)
 	require.NoError(t, err)
+	require.NotNil(t, deposit)
 	require.Equal(t, depositL2TxHash, deposit.L2TransactionHash)
 	require.Equal(t, big.NewInt(100_000), deposit.GasLimit.Int)
 	require.Equal(t, big.NewInt(params.Ether), deposit.Tx.Amount.Int)
@@ -100,7 +101,7 @@ func TestE2EBridgeTransactionsL2ToL1MessagePasserWithdrawal(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l2Header := testSuite.Indexer.L2Processor.LatestProcessedHeader()
+		l2Header := testSuite.Indexer.BridgeProcessor.LatestL2Header
 		return l2Header != nil && l2Header.Number.Uint64() >= withdrawReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -111,6 +112,7 @@ func TestE2EBridgeTransactionsL2ToL1MessagePasserWithdrawal(t *testing.T) {
 
 	withdraw, err := testSuite.DB.BridgeTransactions.L2TransactionWithdrawal(withdrawalHash)
 	require.NoError(t, err)
+	require.NotNil(t, withdraw)
 	require.Equal(t, msgPassed.Nonce.Uint64(), withdraw.Nonce.Int.Uint64())
 	require.Equal(t, big.NewInt(100_000), withdraw.GasLimit.Int)
 	require.Equal(t, big.NewInt(params.Ether), withdraw.Tx.Amount.Int)
@@ -129,7 +131,7 @@ func TestE2EBridgeTransactionsL2ToL1MessagePasserWithdrawal(t *testing.T) {
 
 	withdrawParams, proveReceipt := op_e2e.ProveWithdrawal(t, *testSuite.OpCfg, testSuite.L1Client, testSuite.OpSys.Nodes["sequencer"], testSuite.OpCfg.Secrets.Alice, withdrawReceipt)
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= proveReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -147,7 +149,7 @@ func TestE2EBridgeTransactionsL2ToL1MessagePasserWithdrawal(t *testing.T) {
 
 	finalizeReceipt := op_e2e.FinalizeWithdrawal(t, *testSuite.OpCfg, testSuite.L1Client, testSuite.OpCfg.Secrets.Alice, proveReceipt, withdrawParams)
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= finalizeReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -189,7 +191,7 @@ func TestE2EBridgeTransactionsL2ToL1MessagePasserFailedWithdrawal(t *testing.T)
 	// Prove&Finalize withdrawal
 	_, finalizeReceipt := op_e2e.ProveAndFinalizeWithdrawal(t, *testSuite.OpCfg, testSuite.L1Client, testSuite.OpSys.Nodes["sequencer"], testSuite.OpCfg.Secrets.Alice, withdrawReceipt)
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= finalizeReceipt.BlockNumber.Uint64(), nil
 	}))
 
diff --git a/indexer/e2e_tests/bridge_transfers_e2e_test.go b/indexer/e2e_tests/bridge_transfers_e2e_test.go
index c080a8c4c..580fd7af5 100644
--- a/indexer/e2e_tests/bridge_transfers_e2e_test.go
+++ b/indexer/e2e_tests/bridge_transfers_e2e_test.go
@@ -43,7 +43,7 @@ func TestE2EBridgeTransfersStandardBridgeETHDeposit(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= depositReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -73,11 +73,11 @@ func TestE2EBridgeTransfersStandardBridgeETHDeposit(t *testing.T) {
 	require.NotNil(t, deposit.CrossDomainMessageHash)
 
 	// (2) Test Deposit Finalization via CrossDomainMessenger relayed message
-	depositReceipt, err = wait.ForReceiptOK(context.Background(), testSuite.L2Client, types.NewTx(depositInfo.DepositTx).Hash())
+	l2DepositReceipt, err := wait.ForReceiptOK(context.Background(), testSuite.L2Client, types.NewTx(depositInfo.DepositTx).Hash())
 	require.NoError(t, err)
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l2Header := testSuite.Indexer.L2Processor.LatestProcessedHeader()
-		return l2Header != nil && l2Header.Number.Uint64() >= depositReceipt.BlockNumber.Uint64(), nil
+		l2Header := testSuite.Indexer.BridgeProcessor.LatestL2Header
+		return l2Header != nil && l2Header.Number.Uint64() >= l2DepositReceipt.BlockNumber.Uint64(), nil
 	}))
 
 	crossDomainBridgeMessage, err := testSuite.DB.BridgeMessages.L1BridgeMessage(*deposit.CrossDomainMessageHash)
@@ -221,7 +221,7 @@ func TestE2EBridgeTransfersOptimismPortalETHReceive(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= portalDepositReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -242,11 +242,11 @@ func TestE2EBridgeTransfersOptimismPortalETHReceive(t *testing.T) {
 	require.Nil(t, deposit.CrossDomainMessageHash)
 
 	// (2) Test Deposit Finalization
-	depositReceipt, err := wait.ForReceiptOK(context.Background(), testSuite.L2Client, types.NewTx(depositInfo.DepositTx).Hash())
+	l2DepositReceipt, err := wait.ForReceiptOK(context.Background(), testSuite.L2Client, types.NewTx(depositInfo.DepositTx).Hash())
 	require.NoError(t, err)
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l2Header := testSuite.Indexer.L2Processor.LatestProcessedHeader()
-		return l2Header != nil && l2Header.Number.Uint64() >= depositReceipt.BlockNumber.Uint64(), nil
+		l2Header := testSuite.Indexer.BridgeProcessor.LatestL2Header
+		return l2Header != nil && l2Header.Number.Uint64() >= l2DepositReceipt.BlockNumber.Uint64(), nil
 	}))
 
 	// Still nil as the withdrawal did not occur through the standard bridge
@@ -286,7 +286,7 @@ func TestE2EBridgeTransfersStandardBridgeETHWithdrawal(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l2Header := testSuite.Indexer.L2Processor.LatestProcessedHeader()
+		l2Header := testSuite.Indexer.BridgeProcessor.LatestL2Header
 		return l2Header != nil && l2Header.Number.Uint64() >= withdrawReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -324,7 +324,7 @@ func TestE2EBridgeTransfersStandardBridgeETHWithdrawal(t *testing.T) {
 	// wait for processor catchup
 	proveReceipt, finalizeReceipt := op_e2e.ProveAndFinalizeWithdrawal(t, *testSuite.OpCfg, testSuite.L1Client, testSuite.OpSys.Nodes["sequencer"], testSuite.OpCfg.Secrets.Alice, withdrawReceipt)
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= finalizeReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -370,7 +370,7 @@ func TestE2EBridgeTransfersL2ToL1MessagePasserReceive(t *testing.T) {
 
 	// wait for processor catchup
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l2Header := testSuite.Indexer.L2Processor.LatestProcessedHeader()
+		l2Header := testSuite.Indexer.BridgeProcessor.LatestL2Header
 		return l2Header != nil && l2Header.Number.Uint64() >= l2ToL1WithdrawReceipt.BlockNumber.Uint64(), nil
 	}))
 
@@ -402,7 +402,7 @@ func TestE2EBridgeTransfersL2ToL1MessagePasserReceive(t *testing.T) {
 	// wait for processor catchup
 	proveReceipt, finalizeReceipt := op_e2e.ProveAndFinalizeWithdrawal(t, *testSuite.OpCfg, testSuite.L1Client, testSuite.OpSys.Nodes["sequencer"], testSuite.OpCfg.Secrets.Alice, l2ToL1WithdrawReceipt)
 	require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
+		l1Header := testSuite.Indexer.BridgeProcessor.LatestL1Header
 		return l1Header != nil && l1Header.Number.Uint64() >= finalizeReceipt.BlockNumber.Uint64(), nil
 	}))
 
diff --git a/indexer/e2e_tests/blocks_e2e_test.go b/indexer/e2e_tests/etl_e2e_test.go
similarity index 51%
rename from indexer/e2e_tests/blocks_e2e_test.go
rename to indexer/e2e_tests/etl_e2e_test.go
index 557da9063..3ad105f99 100644
--- a/indexer/e2e_tests/blocks_e2e_test.go
+++ b/indexer/e2e_tests/etl_e2e_test.go
@@ -6,43 +6,45 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ethereum-optimism/optimism/indexer/node"
+	"github.com/ethereum-optimism/optimism/indexer/database"
 	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
-	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
 	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
 
 	"github.com/ethereum/go-ethereum"
 	"github.com/ethereum/go-ethereum/accounts/abi/bind"
 	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/crypto"
 	"github.com/ethereum/go-ethereum/rlp"
 
 	"github.com/stretchr/testify/require"
 )
 
-func TestE2EBlockHeaders(t *testing.T) {
+func TestE2EETL(t *testing.T) {
 	testSuite := createE2ETestSuite(t)
 
 	l2OutputOracle, err := bindings.NewL2OutputOracle(testSuite.OpCfg.L1Deployments.L2OutputOracleProxy, testSuite.L1Client)
 	require.NoError(t, err)
 
-	// wait for at least 10 L2 blocks to be created & posted on L1
+	// wait for at least 10 L2 blocks posted on L1
 	require.NoError(t, wait.For(context.Background(), time.Second, func() (bool, error) {
 		l2Height, err := l2OutputOracle.LatestBlockNumber(&bind.CallOpts{Context: context.Background()})
 		return l2Height != nil && l2Height.Uint64() >= 9, err
 	}))
 
-	// ensure the processors are caught up to this state
+	// ensure we've indexed up to this state
 	l1Height, err := testSuite.L1Client.BlockNumber(context.Background())
 	require.NoError(t, err)
-	require.NoError(t, wait.For(context.Background(), time.Second, func() (bool, error) {
-		l1Header := testSuite.Indexer.L1Processor.LatestProcessedHeader()
-		l2Header := testSuite.Indexer.L2Processor.LatestProcessedHeader()
-		return (l1Header != nil && l1Header.Number.Uint64() >= l1Height) && (l2Header != nil && l2Header.Number.Uint64() >= 9), nil
+	require.NoError(t, wait.For(context.Background(), 100*time.Millisecond, func() (bool, error) {
+		l1Header, err := testSuite.DB.Blocks.L1LatestBlockHeader()
+		require.NoError(t, err)
+
+		l2Header, err := testSuite.DB.Blocks.L2LatestBlockHeader()
+		require.NoError(t, err)
+
+		return (l1Header != nil && l1Header.Number.Int.Uint64() >= l1Height) && (l2Header != nil && l2Header.Number.Int.Uint64() >= 9), nil
 	}))
 
-	t.Run("indexes L2 blocks", func(t *testing.T) {
-		latestL2Header, err := testSuite.DB.Blocks.LatestL2BlockHeader()
+	t.Run("indexes all L2 blocks", func(t *testing.T) {
+		latestL2Header, err := testSuite.DB.Blocks.L2LatestBlockHeader()
 		require.NoError(t, err)
 		require.NotNil(t, latestL2Header)
 		require.True(t, latestL2Header.Number.Int.Uint64() >= 9)
@@ -50,7 +52,7 @@ func TestE2EBlockHeaders(t *testing.T) {
 		for i := int64(0); i < 10; i++ {
 			height := big.NewInt(i)
 
-			indexedHeader, err := testSuite.DB.Blocks.L2BlockHeader(height)
+			indexedHeader, err := testSuite.DB.Blocks.L2BlockHeaderWithFilter(database.BlockHeader{Number: database.U256{Int: height}})
 			require.NoError(t, err)
 			require.NotNil(t, indexedHeader)
 
@@ -63,59 +65,61 @@ func TestE2EBlockHeaders(t *testing.T) {
 			require.Equal(t, header.ParentHash, indexedHeader.ParentHash)
 			require.Equal(t, header.Time, indexedHeader.Timestamp)
 
-			// ensure the right rlp encoding is stored. checking the hashes sufficies
+			// ensure the right rlp encoding is stored. checking the hashes
+			// suffices as it is based on the rlp bytes of the header
 			require.Equal(t, header.Hash(), indexedHeader.RLPHeader.Hash())
 		}
 	})
 
-	t.Run("indexes L2 checkpoints", func(t *testing.T) {
-		latestOutput, err := testSuite.DB.Blocks.LatestCheckpointedOutput()
-		require.NoError(t, err)
-		require.NotNil(t, latestOutput)
-		require.GreaterOrEqual(t, latestOutput.L2BlockNumber.Int.Uint64(), uint64(9))
-
-		l2EthClient, err := node.DialEthClient(testSuite.OpSys.Nodes["sequencer"].HTTPEndpoint())
-		require.NoError(t, err)
-
-		submissionInterval := testSuite.OpCfg.DeployConfig.L2OutputOracleSubmissionInterval
-		numOutputs := latestOutput.L2BlockNumber.Int.Uint64() / submissionInterval
-		for i := int64(0); i < int64(numOutputs); i++ {
-			blockNumber := big.NewInt((i + 1) * int64(submissionInterval))
-
-			output, err := testSuite.DB.Blocks.OutputProposal(big.NewInt(i))
-			require.NoError(t, err)
-			require.NotNil(t, output)
-			require.Equal(t, i, output.L2OutputIndex.Int.Int64())
-			require.Equal(t, blockNumber, output.L2BlockNumber.Int)
-			require.NotEmpty(t, output.L1ContractEventGUID)
-
-			// we may as well check the integrity of the output root
-			l2Block, err := testSuite.L2Client.BlockByNumber(context.Background(), blockNumber)
-			require.NoError(t, err)
-			messagePasserStorageHash, err := l2EthClient.StorageHash(predeploys.L2ToL1MessagePasserAddr, blockNumber)
-			require.NoError(t, err)
-
-			// construct and check output root
-			outputRootPreImage := [128]byte{}                                 // 4 words (first 32 are zero for version 0)
-			copy(outputRootPreImage[32:64], l2Block.Root().Bytes())           // state root
-			copy(outputRootPreImage[64:96], messagePasserStorageHash.Bytes()) // message passer storage root
-			copy(outputRootPreImage[96:128], l2Block.Hash().Bytes())          // block hash
-			require.Equal(t, crypto.Keccak256Hash(outputRootPreImage[:]), output.OutputRoot)
-		}
-	})
-
-	t.Run("indexes L1 logs and associated blocks", func(t *testing.T) {
-		testCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-		defer cancel()
-
+	/*
+		TODO: ADD THIS BACK IN WHEN THESE MARKERS ARE INDEXED
+			t.Run("indexes L2 checkpoints", func(t *testing.T) {
+				latestOutput, err := testSuite.DB.Blocks.LatestCheckpointedOutput()
+				require.NoError(t, err)
+				require.NotNil(t, latestOutput)
+				require.GreaterOrEqual(t, latestOutput.L2BlockNumber.Int.Uint64(), uint64(9))
+
+				l2EthClient, err := node.DialEthClient(testSuite.OpSys.Nodes["sequencer"].HTTPEndpoint())
+				require.NoError(t, err)
+
+				submissionInterval := testSuite.OpCfg.DeployConfig.L2OutputOracleSubmissionInterval
+				numOutputs := latestOutput.L2BlockNumber.Int.Uint64() / submissionInterval
+				for i := int64(0); i < int64(numOutputs); i++ {
+					blockNumber := big.NewInt((i + 1) * int64(submissionInterval))
+
+					output, err := testSuite.DB.Blocks.OutputProposal(big.NewInt(i))
+					require.NoError(t, err)
+					require.NotNil(t, output)
+					require.Equal(t, i, output.L2OutputIndex.Int.Int64())
+					require.Equal(t, blockNumber, output.L2BlockNumber.Int)
+					require.NotEmpty(t, output.L1ContractEventGUID)
+
+					// we may as well check the integrity of the output root
+					l2Block, err := testSuite.L2Client.BlockByNumber(context.Background(), blockNumber)
+					require.NoError(t, err)
+					messagePasserStorageHash, err := l2EthClient.StorageHash(predeploys.L2ToL1MessagePasserAddr, blockNumber)
+					require.NoError(t, err)
+
+					// construct and check output root
+					outputRootPreImage := [128]byte{}                                 // 4 words (first 32 are zero for version 0)
+					copy(outputRootPreImage[32:64], l2Block.Root().Bytes())           // state root
+					copy(outputRootPreImage[64:96], messagePasserStorageHash.Bytes()) // message passer storage root
+					copy(outputRootPreImage[96:128], l2Block.Hash().Bytes())          // block hash
+					require.Equal(t, crypto.Keccak256Hash(outputRootPreImage[:]), output.OutputRoot)
+				}
+			})
+	*/
+
+	t.Run("indexes L1 blocks with accompanying contract event", func(t *testing.T) {
 		l1Contracts := []common.Address{}
 		testSuite.OpCfg.L1Deployments.ForEach(func(name string, addr common.Address) { l1Contracts = append(l1Contracts, addr) })
 		logFilter := ethereum.FilterQuery{FromBlock: big.NewInt(0), ToBlock: big.NewInt(int64(l1Height)), Addresses: l1Contracts}
-		logs, err := testSuite.L1Client.FilterLogs(testCtx, logFilter) // []types.Log
+		logs, err := testSuite.L1Client.FilterLogs(context.Background(), logFilter) // []types.Log
 		require.NoError(t, err)
 
-		for _, log := range logs {
-			contractEvent, err := testSuite.DB.ContractEvents.L1ContractEventByTxLogIndex(log.TxHash, uint64(log.Index))
+		for i := range logs {
+			log := logs[i]
+			contractEvent, err := testSuite.DB.ContractEvents.L1ContractEventWithFilter(database.ContractEvent{TransactionHash: log.TxHash, LogIndex: uint64(log.Index)})
 			require.NoError(t, err)
 			require.Equal(t, log.Topics[0], contractEvent.EventSignature)
 			require.Equal(t, log.BlockHash, contractEvent.BlockHash)
@@ -131,12 +135,12 @@ func TestE2EBlockHeaders(t *testing.T) {
 			require.ElementsMatch(t, logRlp, contractEventRlp)
 
 			// ensure the block is also indexed
-			block, err := testSuite.L1Client.BlockByNumber(testCtx, big.NewInt(int64(log.BlockNumber)))
+			block, err := testSuite.L1Client.BlockByNumber(context.Background(), big.NewInt(int64(log.BlockNumber)))
 			require.NoError(t, err)
-
 			require.Equal(t, block.Time(), contractEvent.Timestamp)
+			require.Equal(t, block.Hash(), contractEvent.BlockHash)
 
-			l1BlockHeader, err := testSuite.DB.Blocks.L1BlockHeader(block.Number())
+			l1BlockHeader, err := testSuite.DB.Blocks.L1BlockHeader(block.Hash())
 			require.NoError(t, err)
 			require.Equal(t, block.Hash(), l1BlockHeader.Hash)
 			require.Equal(t, block.ParentHash(), l1BlockHeader.ParentHash)
diff --git a/indexer/e2e_tests/setup.go b/indexer/e2e_tests/setup.go
index 32497f706..5d57587c9 100644
--- a/indexer/e2e_tests/setup.go
+++ b/indexer/e2e_tests/setup.go
@@ -71,23 +71,17 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite {
 		},
 		Chain: config.ChainConfig{
 			L1Contracts: config.L1Contracts{
-				OptimismPortal:         opCfg.L1Deployments.OptimismPortalProxy,
-				L2OutputOracle:         opCfg.L1Deployments.L2OutputOracleProxy,
-				L1CrossDomainMessenger: opCfg.L1Deployments.L1CrossDomainMessengerProxy,
-				L1StandardBridge:       opCfg.L1Deployments.L1StandardBridgeProxy,
-				L1ERC721Bridge:         opCfg.L1Deployments.L1ERC721BridgeProxy,
+				OptimismPortalProxy:         opCfg.L1Deployments.OptimismPortalProxy,
+				L2OutputOracleProxy:         opCfg.L1Deployments.L2OutputOracleProxy,
+				L1CrossDomainMessengerProxy: opCfg.L1Deployments.L1CrossDomainMessengerProxy,
+				L1StandardBridgeProxy:       opCfg.L1Deployments.L1StandardBridgeProxy,
 			},
 		},
 	}
 
 	db, err := database.NewDB(indexerCfg.DB)
 	require.NoError(t, err)
-	indexer, err := indexer.NewIndexer(
-		indexerCfg.Chain,
-		indexerCfg.RPCs,
-		db,
-		logger,
-	)
+	indexer, err := indexer.NewIndexer(logger, indexerCfg.Chain, indexerCfg.RPCs, db)
 	require.NoError(t, err)
 
 	indexerStoppedCh := make(chan interface{}, 1)
diff --git a/indexer/e2e_tests/utils/cross_domain_messages.go b/indexer/e2e_tests/utils/cross_domain_messages.go
index 588a4b1cb..e1d4d9279 100644
--- a/indexer/e2e_tests/utils/cross_domain_messages.go
+++ b/indexer/e2e_tests/utils/cross_domain_messages.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 	"math/big"
 
-	"github.com/ethereum-optimism/optimism/indexer/processor"
+	"github.com/ethereum-optimism/optimism/indexer/processors/contracts"
 	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
 
 	"github.com/ethereum/go-ethereum/common"
@@ -57,5 +57,5 @@ func CrossDomainMessengerSentMessageHash(sentMessage *bindings.CrossDomainMessen
 		return common.Hash{}, err
 	}
 
-	return processor.CrossDomainMessageHash(abi, sentMessage, value)
+	return contracts.CrossDomainMessageHash(abi, sentMessage, value)
 }
diff --git a/indexer/node/client_test.go b/indexer/node/client_test.go
index 21ada0749..81e8bbfcb 100644
--- a/indexer/node/client_test.go
+++ b/indexer/node/client_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/ethclient"
 	"github.com/ethereum/go-ethereum/rpc"
 )
 
@@ -21,9 +22,9 @@ func (m *MockEthClient) FinalizedBlockHeight() (*big.Int, error) {
 	return args.Get(0).(*big.Int), args.Error(1)
 }
 
-func (m *MockEthClient) BlockHeadersByRange(from, to *big.Int) ([]*types.Header, error) {
+func (m *MockEthClient) BlockHeadersByRange(from, to *big.Int) ([]types.Header, error) {
 	args := m.Called(from, to)
-	return args.Get(0).([]*types.Header), args.Error(1)
+	return args.Get(0).([]types.Header), args.Error(1)
 }
 
 func (m *MockEthClient) BlockHeaderByHash(hash common.Hash) (*types.Header, error) {
@@ -36,7 +37,12 @@ func (m *MockEthClient) StorageHash(address common.Address, blockNumber *big.Int
 	return args.Get(0).(common.Hash), args.Error(1)
 }
 
-func (m *MockEthClient) RawRpcClient() *rpc.Client {
+func (m *MockEthClient) GethRpcClient() *rpc.Client {
 	args := m.Called()
 	return args.Get(0).(*rpc.Client)
 }
+
+func (m *MockEthClient) GethEthClient() *ethclient.Client {
+	args := m.Called()
+	return args.Get(0).(*ethclient.Client)
+}
diff --git a/indexer/node/header_traversal_test.go b/indexer/node/header_traversal_test.go
index 42c672783..f98f1ec06 100644
--- a/indexer/node/header_traversal_test.go
+++ b/indexer/node/header_traversal_test.go
@@ -11,22 +11,21 @@ import (
 )
 
 // make a set of headers which chain correctly
-func makeHeaders(numHeaders uint64, prevHeader *types.Header) []*types.Header {
-	headers := make([]*types.Header, numHeaders)
+func makeHeaders(numHeaders uint64, prevHeader *types.Header) []types.Header {
+	headers := make([]types.Header, numHeaders)
 	for i := range headers {
 		if i == 0 {
 			if prevHeader == nil {
 				// genesis
-				headers[i] = &types.Header{Number: big.NewInt(0)}
+				headers[i] = types.Header{Number: big.NewInt(0)}
 			} else {
 				// chain onto the previous header
-				headers[i] = &types.Header{Number: big.NewInt(prevHeader.Number.Int64() + 1)}
+				headers[i] = types.Header{Number: big.NewInt(prevHeader.Number.Int64() + 1)}
 				headers[i].ParentHash = prevHeader.Hash()
 			}
 		} else {
-			prevHeader = headers[i-1]
-			headers[i] = &types.Header{Number: big.NewInt(prevHeader.Number.Int64() + 1)}
-			headers[i].ParentHash = prevHeader.Hash()
+			headers[i] = types.Header{Number: big.NewInt(headers[i-1].Number.Int64() + 1)}
+			headers[i].ParentHash = headers[i-1].Hash()
 		}
 	}
 
@@ -62,7 +61,7 @@ func TestHeaderTraversalNextFinalizedHeadersCursored(t *testing.T) {
 	require.Len(t, headers, 5)
 
 	// blocks [5..9]
-	headers = makeHeaders(5, headers[len(headers)-1])
+	headers = makeHeaders(5, &headers[len(headers)-1])
 	client.On("FinalizedBlockHeight").Return(big.NewInt(9), nil)
 	client.On("BlockHeadersByRange", mock.MatchedBy(bigIntMatcher(5)), mock.MatchedBy(bigIntMatcher(9))).Return(headers, nil)
 	headers, err = headerTraversal.NextFinalizedHeaders(5)
@@ -87,7 +86,7 @@ func TestHeaderTraversalNextFinalizedHeadersMaxSize(t *testing.T) {
 	require.Len(t, headers, 5)
 
 	// clamped by the supplied size. FinalizedHeight == 100
-	headers = makeHeaders(10, headers[len(headers)-1])
+	headers = makeHeaders(10, &headers[len(headers)-1])
 	client.On("BlockHeadersByRange", mock.MatchedBy(bigIntMatcher(5)), mock.MatchedBy(bigIntMatcher(14))).Return(headers, nil)
 	headers, err = headerTraversal.NextFinalizedHeaders(10)
 	require.NoError(t, err)
diff --git a/op-chain-ops/genesis/layer_two_test.go b/op-chain-ops/genesis/layer_two_test.go
index 58f676d49..f81b1a09a 100644
--- a/op-chain-ops/genesis/layer_two_test.go
+++ b/op-chain-ops/genesis/layer_two_test.go
@@ -66,6 +66,12 @@ func testBuildL2Genesis(t *testing.T, config *genesis.DeployConfig) *core.Genesi
 		}
 	}
 
+	// All of the precompile addresses should be funded with a single wei
+	for i := 0; i < genesis.PrecompileCount; i++ {
+		addr := common.BytesToAddress([]byte{byte(i)})
+		require.Equal(t, common.Big1, gen.Alloc[addr].Balance)
+	}
+
 	if writeFile {
 		file, _ := json.MarshalIndent(gen, "", " ")
 		_ = os.WriteFile("genesis.json", file, 0644)
@@ -79,7 +85,7 @@ func TestBuildL2MainnetGenesis(t *testing.T) {
 	config.EnableGovernance = true
 	config.FundDevAccounts = false
 	gen := testBuildL2Genesis(t, config)
-	require.Equal(t, 2066, len(gen.Alloc))
+	require.Equal(t, 2322, len(gen.Alloc))
 }
 
 func TestBuildL2MainnetNoGovernanceGenesis(t *testing.T) {
@@ -88,5 +94,5 @@ func TestBuildL2MainnetNoGovernanceGenesis(t *testing.T) {
 	config.EnableGovernance = false
 	config.FundDevAccounts = false
 	gen := testBuildL2Genesis(t, config)
-	require.Equal(t, 2066, len(gen.Alloc))
+	require.Equal(t, 2322, len(gen.Alloc))
 }
diff --git a/op-e2e/e2eutils/challenger/helper.go b/op-e2e/e2eutils/challenger/helper.go
index 252f15c8c..3f436af24 100644
--- a/op-e2e/e2eutils/challenger/helper.go
+++ b/op-e2e/e2eutils/challenger/helper.go
@@ -2,15 +2,22 @@ package challenger
 
 import (
 	"context"
+	"crypto/ecdsa"
+	"encoding/json"
 	"errors"
 	"os"
+	"path/filepath"
 	"testing"
 	"time"
 
 	op_challenger "github.com/ethereum-optimism/optimism/op-challenger"
 	"github.com/ethereum-optimism/optimism/op-challenger/config"
+	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
+	"github.com/ethereum-optimism/optimism/op-node/rollup"
 	"github.com/ethereum-optimism/optimism/op-node/testlog"
 	"github.com/ethereum-optimism/optimism/op-service/txmgr"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core"
 	"github.com/ethereum/go-ethereum/log"
 	"github.com/stretchr/testify/require"
 )
@@ -23,6 +30,67 @@ type Helper struct {
 
 type Option func(config2 *config.Config)
 
+func WithFactoryAddress(addr common.Address) Option {
+	return func(c *config.Config) {
+		c.GameFactoryAddress = addr
+	}
+}
+
+func WithGameAddress(addr common.Address) Option {
+	return func(c *config.Config) {
+		c.GameAddress = addr
+	}
+}
+
+func WithPrivKey(key *ecdsa.PrivateKey) Option {
+	return func(c *config.Config) {
+		c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(key)
+	}
+}
+
+func WithAgreeProposedOutput(agree bool) Option {
+	return func(c *config.Config) {
+		c.AgreeWithProposedOutput = agree
+	}
+}
+
+func WithAlphabet(alphabet string) Option {
+	return func(c *config.Config) {
+		c.TraceType = config.TraceTypeAlphabet
+		c.AlphabetTrace = alphabet
+	}
+}
+
+func WithCannon(
+	t *testing.T,
+	rollupCfg *rollup.Config,
+	l2Genesis *core.Genesis,
+	l2Endpoint string,
+) Option {
+	return func(c *config.Config) {
+		require := require.New(t)
+		c.TraceType = config.TraceTypeCannon
+		c.CannonL2 = l2Endpoint
+		c.CannonDatadir = t.TempDir()
+		c.CannonBin = "../cannon/bin/cannon"
+		c.CannonServer = "../op-program/bin/op-program"
+		c.CannonAbsolutePreState = "../op-program/bin/prestate.json"
+		c.CannonSnapshotFreq = 10_000_000
+
+		genesisBytes, err := json.Marshal(l2Genesis)
+		require.NoError(err, "marshall l2 genesis config")
+		genesisFile := filepath.Join(c.CannonDatadir, "l2-genesis.json")
+		require.NoError(os.WriteFile(genesisFile, genesisBytes, 0644))
+		c.CannonL2GenesisPath = genesisFile
+
+		rollupBytes, err := json.Marshal(rollupCfg)
+		require.NoError(err, "marshall rollup config")
+		rollupFile := filepath.Join(c.CannonDatadir, "rollup.json")
+		require.NoError(os.WriteFile(rollupFile, rollupBytes, 0644))
+		c.CannonRollupConfigPath = rollupFile
+	}
+}
+
 func NewChallenger(t *testing.T, ctx context.Context, l1Endpoint string, name string, options ...Option) *Helper {
 	log := testlog.Logger(t, log.LvlInfo).New("role", name)
 	log.Info("Creating challenger", "l1", l1Endpoint)
diff --git a/op-e2e/e2eutils/disputegame/cannon_helper.go b/op-e2e/e2eutils/disputegame/cannon_helper.go
index a2eb0c29f..7ad48b7c1 100644
--- a/op-e2e/e2eutils/disputegame/cannon_helper.go
+++ b/op-e2e/e2eutils/disputegame/cannon_helper.go
@@ -2,21 +2,14 @@ package disputegame
 
 import (
 	"context"
-	"encoding/json"
-	"os"
-	"path/filepath"
-	"testing"
 
-	"github.com/ethereum-optimism/optimism/op-challenger/config"
 	"github.com/ethereum-optimism/optimism/op-challenger/fault/cannon"
 	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
 	"github.com/ethereum-optimism/optimism/op-node/rollup"
 	"github.com/ethereum-optimism/optimism/op-node/testlog"
 	"github.com/ethereum/go-ethereum/accounts/abi/bind"
-	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core"
 	"github.com/ethereum/go-ethereum/log"
-	"github.com/stretchr/testify/require"
 )
 
 type CannonGameHelper struct {
@@ -24,7 +17,11 @@ type CannonGameHelper struct {
 }
 
 func (g *CannonGameHelper) StartChallenger(ctx context.Context, rollupCfg *rollup.Config, l2Genesis *core.Genesis, l1Endpoint string, l2Endpoint string, name string, options ...challenger.Option) *challenger.Helper {
-	opts := []challenger.Option{createConfigOption(g.t, rollupCfg, l2Genesis, g.factoryAddr, g.addr, l2Endpoint)}
+	opts := []challenger.Option{
+		challenger.WithCannon(g.t, rollupCfg, l2Genesis, l2Endpoint),
+		challenger.WithFactoryAddress(g.factoryAddr),
+		challenger.WithGameAddress(g.addr),
+	}
 	opts = append(opts, options...)
 	c := challenger.NewChallenger(g.t, ctx, l1Endpoint, name, opts...)
 	g.t.Cleanup(func() {
@@ -34,7 +31,11 @@ func (g *CannonGameHelper) StartChallenger(ctx context.Context, rollupCfg *rollu
 }
 
 func (g *CannonGameHelper) CreateHonestActor(ctx context.Context, rollupCfg *rollup.Config, l2Genesis *core.Genesis, l1Client bind.ContractCaller, l1Endpoint string, l2Endpoint string, options ...challenger.Option) *HonestHelper {
-	opts := []challenger.Option{createConfigOption(g.t, rollupCfg, l2Genesis, g.factoryAddr, g.addr, l2Endpoint)}
+	opts := []challenger.Option{
+		challenger.WithCannon(g.t, rollupCfg, l2Genesis, l2Endpoint),
+		challenger.WithFactoryAddress(g.factoryAddr),
+		challenger.WithGameAddress(g.addr),
+	}
 	opts = append(opts, options...)
 	cfg := challenger.NewChallengerConfig(g.t, l1Endpoint, opts...)
 	provider, err := cannon.NewTraceProvider(ctx, testlog.Logger(g.t, log.LvlInfo).New("role", "CorrectTrace"), cfg, l1Client)
@@ -47,38 +48,3 @@ func (g *CannonGameHelper) CreateHonestActor(ctx context.Context, rollupCfg *rol
 		correctTrace: provider,
 	}
 }
-
-func createConfigOption(
-	t *testing.T,
-	rollupCfg *rollup.Config,
-	l2Genesis *core.Genesis,
-	factoryAddr common.Address,
-	gameAddr common.Address,
-	l2Endpoint string,
-) challenger.Option {
-	return func(c *config.Config) {
-		require := require.New(t)
-		c.GameFactoryAddress = factoryAddr
-		c.GameAddress = gameAddr
-		c.TraceType = config.TraceTypeCannon
-		c.AgreeWithProposedOutput = false
-		c.CannonL2 = l2Endpoint
-		c.CannonBin = "../cannon/bin/cannon"
-		c.CannonDatadir = t.TempDir()
-		c.CannonServer = "../op-program/bin/op-program"
-		c.CannonAbsolutePreState = "../op-program/bin/prestate.json"
-		c.CannonSnapshotFreq = 10_000_000
-
-		genesisBytes, err := json.Marshal(l2Genesis)
-		require.NoError(err, "marshall l2 genesis config")
-		genesisFile := filepath.Join(c.CannonDatadir, "l2-genesis.json")
-		require.NoError(os.WriteFile(genesisFile, genesisBytes, 0644))
-		c.CannonL2GenesisPath = genesisFile
-
-		rollupBytes, err := json.Marshal(rollupCfg)
-		require.NoError(err, "marshall rollup config")
-		rollupFile := filepath.Join(c.CannonDatadir, "rollup.json")
-		require.NoError(os.WriteFile(rollupFile, rollupBytes, 0644))
-		c.CannonRollupConfigPath = rollupFile
-	}
-}
diff --git a/op-e2e/e2eutils/disputegame/game_helper.go b/op-e2e/e2eutils/disputegame/game_helper.go
index a5ad22769..6c6ada64d 100644
--- a/op-e2e/e2eutils/disputegame/game_helper.go
+++ b/op-e2e/e2eutils/disputegame/game_helper.go
@@ -60,7 +60,7 @@ func (g *FaultGameHelper) MaxDepth(ctx context.Context) int64 {
 	return depth.Int64()
 }
 
-func (g *FaultGameHelper) WaitForClaim(ctx context.Context, predicate func(claim ContractClaim) bool) {
+func (g *FaultGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) {
 	ctx, cancel := context.WithTimeout(ctx, time.Minute)
 	defer cancel()
 	err := wait.For(ctx, time.Second, func() (bool, error) {
@@ -80,7 +80,15 @@ func (g *FaultGameHelper) WaitForClaim(ctx context.Context, predicate func(claim
 		}
 		return false, nil
 	})
-	g.require.NoError(err)
+	if err != nil { // Avoid waiting time capturing game data when there's no error
+		g.require.NoErrorf(err, "%v\n%v", errorMsg, g.gameData(ctx))
+	}
+}
+
+func (g *FaultGameHelper) GetClaimValue(ctx context.Context, claimIdx int64) common.Hash {
+	g.WaitForClaimCount(ctx, claimIdx+1)
+	claim := g.getClaim(ctx, claimIdx)
+	return claim.Claim
 }
 
 // getClaim retrieves the claim data for a specific index.
@@ -95,10 +103,13 @@ func (g *FaultGameHelper) getClaim(ctx context.Context, claimIdx int64) Contract
 
 func (g *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered bool) {
 	maxDepth := g.MaxDepth(ctx)
-	g.WaitForClaim(ctx, func(claim ContractClaim) bool {
-		pos := types.NewPositionFromGIndex(claim.Position.Uint64())
-		return int64(pos.Depth()) == maxDepth && claim.Countered == countered
-	})
+	g.waitForClaim(
+		ctx,
+		fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered),
+		func(claim ContractClaim) bool {
+			pos := types.NewPositionFromGIndex(claim.Position.Uint64())
+			return int64(pos.Depth()) == maxDepth && claim.Countered == countered
+		})
 }
 
 func (g *FaultGameHelper) Resolve(ctx context.Context) {
@@ -141,7 +152,7 @@ func (g *FaultGameHelper) Defend(ctx context.Context, claimIdx int64, claim comm
 	g.require.NoError(err, "Defend transaction was not OK")
 }
 
-func (g *FaultGameHelper) LogGameData(ctx context.Context) {
+func (g *FaultGameHelper) gameData(ctx context.Context) string {
 	opts := &bind.CallOpts{Context: ctx}
 	maxDepth := int(g.MaxDepth(ctx))
 	claimCount, err := g.game.ClaimDataLen(opts)
@@ -157,5 +168,9 @@ func (g *FaultGameHelper) LogGameData(ctx context.Context) {
 	}
 	status, err := g.game.Status(opts)
 	g.require.NoError(err, "Load game status")
-	g.t.Logf("Game %v (%v):\n%v\n", g.addr, Status(status), info)
+	return fmt.Sprintf("Game %v (%v):\n%v\n", g.addr, Status(status), info)
+}
+
+func (g *FaultGameHelper) LogGameData(ctx context.Context) {
+	g.t.Log(g.gameData(ctx))
 }
diff --git a/op-e2e/e2eutils/disputegame/helper.go b/op-e2e/e2eutils/disputegame/helper.go
index 2ac95458e..2be4c95f5 100644
--- a/op-e2e/e2eutils/disputegame/helper.go
+++ b/op-e2e/e2eutils/disputegame/helper.go
@@ -12,7 +12,6 @@ import (
 	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
 	"github.com/ethereum-optimism/optimism/op-chain-ops/deployer"
 	"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
-	"github.com/ethereum-optimism/optimism/op-challenger/config"
 	"github.com/ethereum-optimism/optimism/op-challenger/fault/alphabet"
 	"github.com/ethereum-optimism/optimism/op-challenger/fault/cannon"
 	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
@@ -138,7 +137,10 @@ func (h *FactoryHelper) StartCannonGame(ctx context.Context, rootClaim common.Ha
 
 func (h *FactoryHelper) StartCannonGameWithCorrectRoot(ctx context.Context, rollupCfg *rollup.Config, l2Genesis *core.Genesis, l1Endpoint string, l2Endpoint string, options ...challenger.Option) (*CannonGameHelper, *HonestHelper) {
 	l2BlockNumber, l1Head := h.prepareCannonGame(ctx)
-	challengerOpts := []challenger.Option{createConfigOption(h.t, rollupCfg, l2Genesis, h.factoryAddr, common.Address{0xaa}, l2Endpoint)}
+	challengerOpts := []challenger.Option{
+		challenger.WithCannon(h.t, rollupCfg, l2Genesis, l2Endpoint),
+		challenger.WithFactoryAddress(h.factoryAddr),
+	}
 	challengerOpts = append(challengerOpts, options...)
 	cfg := challenger.NewChallengerConfig(h.t, l1Endpoint, challengerOpts...)
 	opts := &bind.CallOpts{Context: ctx}
@@ -211,12 +213,10 @@ func (h *FactoryHelper) createCannonGame(ctx context.Context, l2BlockNumber uint
 		},
 	}
 }
+
 func (h *FactoryHelper) StartChallenger(ctx context.Context, l1Endpoint string, name string, options ...challenger.Option) *challenger.Helper {
 	opts := []challenger.Option{
-		func(c *config.Config) {
-			c.GameFactoryAddress = h.factoryAddr
-			c.TraceType = config.TraceTypeAlphabet
-		},
+		challenger.WithFactoryAddress(h.factoryAddr),
 	}
 	opts = append(opts, options...)
 	c := challenger.NewChallenger(h.t, ctx, l1Endpoint, name, opts...)
diff --git a/op-e2e/faultproof_test.go b/op-e2e/faultproof_test.go
index 350c4c06a..fa62bfd1e 100644
--- a/op-e2e/faultproof_test.go
+++ b/op-e2e/faultproof_test.go
@@ -4,8 +4,7 @@ import (
 	"context"
 	"testing"
 
-	"github.com/ethereum-optimism/optimism/op-challenger/config"
-	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
+	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
 	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
 	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
 	"github.com/ethereum/go-ethereum/common"
@@ -23,11 +22,11 @@ func TestCannonMultipleGames(t *testing.T) {
 
 	gameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
 	// Start a challenger with the correct alphabet trace
-	gameFactory.StartChallenger(ctx, sys.NodeEndpoint("l1"), "TowerDefense", func(c *config.Config) {
-		c.AgreeWithProposedOutput = true
-		c.AlphabetTrace = "abcdefg"
-		c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Alice)
-	})
+	gameFactory.StartChallenger(ctx, sys.NodeEndpoint("l1"), "TowerDefense",
+		challenger.WithAlphabet("abcdefg"),
+		challenger.WithPrivKey(sys.cfg.Secrets.Alice),
+		challenger.WithAgreeProposedOutput(true),
+	)
 
 	game1 := gameFactory.StartAlphabetGame(ctx, "abcxyz")
 	// Wait for the challenger to respond to the first game
@@ -44,6 +43,59 @@ func TestCannonMultipleGames(t *testing.T) {
 	game1.WaitForClaimCount(ctx, 4)
 }
 
+func TestMultipleCannonGames(t *testing.T) {
+	t.Skip("Cannon provider doesn't currently isolate different game traces")
+	InitParallel(t)
+
+	ctx := context.Background()
+	sys, l1Client := startFaultDisputeSystem(t)
+	t.Cleanup(sys.Close)
+
+	gameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
+	// Start a challenger with the correct alphabet trace
+	gameFactory.StartChallenger(ctx, sys.NodeEndpoint("l1"), "TowerDefense",
+		challenger.WithCannon(t, sys.RollupConfig, sys.L2GenesisCfg, sys.NodeEndpoint("sequencer")),
+		challenger.WithPrivKey(sys.cfg.Secrets.Alice),
+		challenger.WithAgreeProposedOutput(true),
+	)
+
+	game1 := gameFactory.StartCannonGame(ctx, common.Hash{0xaa})
+	game2 := gameFactory.StartCannonGame(ctx, common.Hash{0xbb})
+
+	game1.WaitForClaimCount(ctx, 2)
+	game2.WaitForClaimCount(ctx, 2)
+
+	game1Claim := game1.GetClaimValue(ctx, 1)
+	game2Claim := game2.GetClaimValue(ctx, 1)
+	require.NotEqual(t, game1Claim, game2Claim, "games should have different cannon traces")
+
+	// Push both games down to the step function
+	maxDepth := game1.MaxDepth(ctx)
+	for claimCount := int64(1); claimCount <= maxDepth; {
+		// Challenger should respond to both games
+		claimCount++
+		game1.WaitForClaimCount(ctx, claimCount)
+		game2.WaitForClaimCount(ctx, claimCount)
+
+		// Progress both games
+		game1.Defend(ctx, claimCount-1, common.Hash{0xaa})
+		game2.Defend(ctx, claimCount-1, common.Hash{0xaa})
+		claimCount++
+	}
+
+	game1.WaitForClaimAtMaxDepth(ctx, true)
+	game2.WaitForClaimAtMaxDepth(ctx, true)
+
+	gameDuration := game1.GameDuration(ctx)
+	sys.TimeTravelClock.AdvanceTime(gameDuration)
+	require.NoError(t, wait.ForNextBlock(ctx, l1Client))
+
+	game1.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
+	game2.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
+	game1.LogGameData(ctx)
+	game2.LogGameData(ctx)
+}
+
 func TestResolveDisputeGame(t *testing.T) {
 	InitParallel(t)
 
@@ -59,11 +111,11 @@ func TestResolveDisputeGame(t *testing.T) {
 
 	game.WaitForGameStatus(ctx, disputegame.StatusInProgress)
 
-	game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "HonestAlice", func(c *config.Config) {
-		c.AgreeWithProposedOutput = true // Agree with the proposed output, so disagree with the root claim
-		c.AlphabetTrace = "abcdefg"
-		c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Alice)
-	})
+	game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "HonestAlice",
+		challenger.WithAgreeProposedOutput(true),
+		challenger.WithAlphabet("abcdefg"),
+		challenger.WithPrivKey(sys.cfg.Secrets.Alice),
+	)
 
 	game.WaitForClaimCount(ctx, 2)
 
@@ -155,15 +207,17 @@ func TestChallengerCompleteDisputeGame(t *testing.T) {
 			require.NotNil(t, game)
 			gameDuration := game.GameDuration(ctx)
 
-			game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "Defender", func(c *config.Config) {
-				c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Mallory)
-			})
+			game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "Defender",
+				challenger.WithAgreeProposedOutput(false),
+				challenger.WithPrivKey(sys.cfg.Secrets.Mallory),
+			)
 
-			game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "Challenger", func(c *config.Config) {
-				c.AgreeWithProposedOutput = true // Agree with the proposed output, so disagree with the root claim
-				c.AlphabetTrace = test.otherAlphabet
-				c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Alice)
-			})
+			game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "Challenger",
+				// Agree with the proposed output, so disagree with the root claim
+				challenger.WithAgreeProposedOutput(true),
+				challenger.WithAlphabet(test.otherAlphabet),
+				challenger.WithPrivKey(sys.cfg.Secrets.Alice),
+			)
 
 			// Wait for a claim at the maximum depth that has been countered to indicate we're ready to resolve the game
 			game.WaitForClaimAtMaxDepth(ctx, test.expectStep)
@@ -201,10 +255,11 @@ func TestCannonDisputeGame(t *testing.T) {
 			require.NotNil(t, game)
 			game.LogGameData(ctx)
 
-			game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, sys.NodeEndpoint("l1"), sys.NodeEndpoint("sequencer"), "Challenger", func(c *config.Config) {
-				c.AgreeWithProposedOutput = true // Agree with the proposed output, so disagree with the root claim
-				c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Alice)
-			})
+			game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, sys.NodeEndpoint("l1"), sys.NodeEndpoint("sequencer"), "Challenger",
+				// Agree with the proposed output, so disagree with the root claim
+				challenger.WithAgreeProposedOutput(true),
+				challenger.WithPrivKey(sys.cfg.Secrets.Alice),
+			)
 
 			maxDepth := game.MaxDepth(ctx)
 			for claimCount := int64(1); claimCount < maxDepth; {
@@ -251,14 +306,15 @@ func TestCannonDefendStep(t *testing.T) {
 
 	l1Endpoint := sys.NodeEndpoint("l1")
 	l2Endpoint := sys.NodeEndpoint("sequencer")
-	game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Challenger", func(c *config.Config) {
-		c.AgreeWithProposedOutput = true // Agree with the proposed output, so disagree with the root claim
-		c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Alice)
-	})
+	game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Challenger",
+		// Agree with the proposed output, so disagree with the root claim
+		challenger.WithAgreeProposedOutput(true),
+		challenger.WithPrivKey(sys.cfg.Secrets.Alice),
+	)
 
-	correctTrace := game.CreateHonestActor(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Client, l1Endpoint, l2Endpoint, func(c *config.Config) {
-		c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Mallory)
-	})
+	correctTrace := game.CreateHonestActor(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Client, l1Endpoint, l2Endpoint,
+		challenger.WithPrivKey(sys.cfg.Secrets.Mallory),
+	)
 
 	maxDepth := game.MaxDepth(ctx)
 	for claimCount := int64(1); claimCount < maxDepth; {
@@ -302,16 +358,17 @@ func TestCannonChallengeWithCorrectRoot(t *testing.T) {
 	l2Endpoint := sys.NodeEndpoint("sequencer")
 
 	disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
-	game, correctTrace := disputeGameFactory.StartCannonGameWithCorrectRoot(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, func(c *config.Config) {
-		c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Mallory)
-	})
+	game, correctTrace := disputeGameFactory.StartCannonGameWithCorrectRoot(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint,
+		challenger.WithPrivKey(sys.cfg.Secrets.Mallory),
+	)
 	require.NotNil(t, game)
 	game.LogGameData(ctx)
 
-	game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Challenger", func(c *config.Config) {
-		c.AgreeWithProposedOutput = true // Agree with the proposed output, so disagree with the root claim
-		c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Alice)
-	})
+	game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Challenger",
+		// Agree with the proposed output, so disagree with the root claim
+		challenger.WithAgreeProposedOutput(true),
+		challenger.WithPrivKey(sys.cfg.Secrets.Alice),
+	)
 
 	maxDepth := game.MaxDepth(ctx)
 	for claimCount := int64(1); claimCount < maxDepth; {