Repository: ethereum-optimism/optimism
Pull request #6838 (2023-08-16 22:00:43)

feat(indexer) decouple the bridge and general etl to eliminate circular dependencies in bridge processing
In it's current implementation, the bridge is coupled with the ETL phase of the chain
which introduces a circular depedency if concurrent finalization events on L2/L1 exists
in the same ETL batch that requires them waiting on each other. 

This PR splits the ETL/Bridge into independent go-routines.

1. The ETL for both L1 and L2 indexes the respective chains and contract events always at HEAD. 
This package will also responsible for eventual reorg handling

2. The BridgeProcessor simply walks indexed contracts events on both L1/L2 when it is certain that it
can process all initiated and finalization events on l1/l2 without conflict. It does this by only indexing
up till the latest Epoch indexed for both L1 & L2 (every ~6 L1 blocks)


This PR is ready for review but has some TODOs that are trivial to address. Dind't want to prolong
publishing this even longer since it's ready to generally get look at.
- Retry semantics in L1ETL & L2ETL
- Index OutputProposals on L1
- Feed in the contracts from config to the L1/L2 ETL than relying on devnet address


hamdiallam: Current dependencies on/for this PR:
* develop
  * **PR #6838** <a href="https://app.graphite.dev/github/pr/ethereum-optimism/optimism/6838" target="_blank"><img src="https://static.graphite.dev/graphite-32x32.png" alt="Graphite" width="10px" height="10px"/></a>  👈

This comment was auto-generated by [Graphite](https://app.graphite.dev/github/pr/ethereum-optimism/optimism/6838?utm_source=stack-comment).
mergify[bot]: Hey @hamdiallam! This PR has merge conflicts. Please fix them before continuing review.

Patch:
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..ffbb45ce2 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,14 +65,18 @@ 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 {
@@ -114,21 +118,24 @@ func (db *blocksDB) StoreOutputProposals(outputs []*OutputProposal) error {
 	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 {
@@ -177,31 +184,66 @@ func (db *blocksDB) StoreL2BlockHeaders(headers []*L2BlockHeader) error {
 	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_transfers.go b/indexer/database/bridge_transfers.go
index a5ded0aae..eb2a71456 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"`
 }
 
diff --git a/indexer/database/contract_events.go b/indexer/database/contract_events.go
index 265961dc2..11d6e0fd5 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,12 +75,16 @@ 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 {
@@ -109,27 +114,16 @@ func (db *contractEventsDB) StoreL1ContractEvents(events []*L1ContractEvent) err
 }
 
 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,6 +160,19 @@ func (db *contractEventsDB) L1ContractEventsWithFilter(filter ContractEvent, fro
 	return events, nil
 }
 
+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 &l1ContractEvent, nil
+}
+
 // L2
 
 func (db *contractEventsDB) StoreL2ContractEvents(events []*L2ContractEvent) error {
@@ -168,27 +181,16 @@ func (db *contractEventsDB) StoreL2ContractEvents(events []*L2ContractEvent) err
 }
 
 func (db *contractEventsDB) L2ContractEvent(uuid uuid.UUID) (*L2ContractEvent, error) {
-	var l2ContractEvent L2ContractEvent
-	result := db.gorm.Where(&ContractEvent{GUID: uuid}).Take(&l2ContractEvent)
-	if result.Error != nil {
-		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
-			return nil, nil
-		}
-
-		return nil, result.Error
-	}
-
-	return &l2ContractEvent, nil
+	return db.L2ContractEventWithFilter(ContractEvent{GUID: uuid})
 }
 
-func (db *contractEventsDB) L2ContractEventByTxLogIndex(txHash common.Hash, logIndex uint64) (*L2ContractEvent, error) {
+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..be1836a7d
--- /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/backoff"
+	"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
+			backoffStrategy := &backoff.ExponentialStrategy{Min: 1000, Max: 20_000, MaxJitter: 250}
+			_, err := backoff.Do[interface{}](ctx, 10, backoffStrategy, 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..422f83de1
--- /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/backoff"
+	"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
+			backoffStrategy := &backoff.ExponentialStrategy{Min: 1000, Max: 20_000, MaxJitter: 250}
+			_, err := backoff.Do[interface{}](ctx, 10, backoffStrategy, 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..17fcebed2
--- /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..6440878be
--- /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/packages/common-ts/package.json b/packages/common-ts/package.json
index 481b4bac4..e65d401c4 100644
--- a/packages/common-ts/package.json
+++ b/packages/common-ts/package.json
@@ -46,7 +46,7 @@
     "express-prom-bundle": "^6.4.1",
     "lodash": "^4.17.21",
     "morgan": "^1.10.0",
-    "pino": "^8.15.0",
+    "pino": "^6.11.3",
     "pino-multi-stream": "^5.3.0",
     "pino-sentry": "^0.14.0",
     "prom-client": "^13.1.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0ec627ea9..29f643438 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -228,8 +228,8 @@ importers:
         specifier: ^1.10.0
         version: 1.10.0
       pino:
-        specifier: ^8.15.0
-        version: 8.15.0
+        specifier: ^6.11.3
+        version: 6.13.1
       pino-multi-stream:
         specifier: ^5.3.0
         version: 5.3.0
@@ -5666,6 +5666,7 @@ packages:
     engines: {node: '>=6.5'}
     dependencies:
       event-target-shim: 5.0.1
+    dev: true
 
   /abstract-leveldown@6.2.3:
     resolution: {integrity: sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==}
@@ -8851,6 +8852,7 @@ packages:
   /event-target-shim@5.0.1:
     resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
     engines: {node: '>=6'}
+    dev: true
 
   /eventemitter3@4.0.7:
     resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
@@ -9051,6 +9053,11 @@ packages:
     resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
     dev: true
 
+  /fast-redact@3.0.1:
+    resolution: {integrity: sha512-kYpn4Y/valC9MdrISg47tZOpYBNoTXKgT9GYXFpHN/jYFs+lFkPoisY+LcBODdKVMY96ATzvzsWv+ES/4Kmufw==}
+    engines: {node: '>=6'}
+    dev: false
+
   /fast-redact@3.2.0:
     resolution: {integrity: sha512-zaTadChr+NekyzallAMXATXLOR8MNx3zqpZ0MUF2aGf4EathnG0f32VLODNlY8IuGY3HoRO2L6/6fSzNsLaHIw==}
     engines: {node: '>=6'}
@@ -13090,10 +13097,6 @@ packages:
   /on-exit-leak-free@0.2.0:
     resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==}
 
-  /on-exit-leak-free@2.1.0:
-    resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==}
-    dev: false
-
   /on-finished@2.3.0:
     resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
     engines: {node: '>= 0.8'}
@@ -13624,13 +13627,6 @@ packages:
       duplexify: 4.1.2
       split2: 4.2.0
 
-  /pino-abstract-transport@1.0.0:
-    resolution: {integrity: sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==}
-    dependencies:
-      readable-stream: 4.4.2
-      split2: 4.2.0
-    dev: false
-
   /pino-multi-stream@5.3.0:
     resolution: {integrity: sha512-4fAGCRll18I+JmoAbxDvU9zc5sera/3c+VgTtUdoNMOZ/VSHB+HMAYtixKpeRmZTDHDDdE2rtwjVkuwWB8mYQA==}
     deprecated: No longer supported. Use the multi-stream support in the latest core Pino
@@ -13659,20 +13655,16 @@ packages:
   /pino-std-serializers@4.0.0:
     resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==}
 
-  /pino-std-serializers@6.2.2:
-    resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==}
-    dev: false
-
   /pino@6.13.1:
     resolution: {integrity: sha512-QQf67BU+cANnc/2U+wzUV20UjO5oBryWpnNyKshdLfT9BdeiXlh9wxLGmOjAuBWMYITdMs+BtJSQQNlGRNbWpA==}
     hasBin: true
     dependencies:
-      fast-redact: 3.2.0
+      fast-redact: 3.0.1
       fast-safe-stringify: 2.0.8
       fastify-warning: 0.2.0
       flatstr: 1.0.12
       pino-std-serializers: 3.2.0
-      quick-format-unescaped: 4.0.4
+      quick-format-unescaped: 4.0.3
       sonic-boom: 1.4.1
     dev: false
 
@@ -13692,23 +13684,6 @@ packages:
       sonic-boom: 2.8.0
       thread-stream: 0.15.2
 
-  /pino@8.15.0:
-    resolution: {integrity: sha512-olUADJByk4twxccmAxb1RiGKOSvddHugCV3wkqjyv+3Sooa2KLrmXrKEWOKi0XPCLasRR5jBXxioE1jxUa4KzQ==}
-    hasBin: true
-    dependencies:
-      atomic-sleep: 1.0.0
-      fast-redact: 3.2.0
-      on-exit-leak-free: 2.1.0
-      pino-abstract-transport: 1.0.0
-      pino-std-serializers: 6.2.2
-      process-warning: 2.2.0
-      quick-format-unescaped: 4.0.4
-      real-require: 0.2.0
-      safe-stable-stringify: 2.4.3
-      sonic-boom: 3.3.0
-      thread-stream: 2.4.0
-    dev: false
-
   /pirates@4.0.6:
     resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
     engines: {node: '>= 6'}
@@ -13849,10 +13824,6 @@ packages:
   /process-warning@1.0.0:
     resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==}
 
-  /process-warning@2.2.0:
-    resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==}
-    dev: false
-
   /process@0.11.10:
     resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
     engines: {node: '>= 0.6.0'}
@@ -14041,6 +14012,10 @@ packages:
   /queue-microtask@1.2.3:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
 
+  /quick-format-unescaped@4.0.3:
+    resolution: {integrity: sha512-MaL/oqh02mhEo5m5J2rwsVL23Iw2PEaGVHgT2vFt8AAsr0lfvQA5dpXo9TPu0rz7tSBdUPgkbam0j/fj5ZM8yg==}
+    dev: false
+
   /quick-format-unescaped@4.0.4:
     resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
 
@@ -14262,17 +14237,6 @@ packages:
       string_decoder: 1.3.0
       util-deprecate: 1.0.2
 
-  /readable-stream@4.4.2:
-    resolution: {integrity: sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    dependencies:
-      abort-controller: 3.0.0
-      buffer: 6.0.3
-      events: 3.3.0
-      process: 0.11.10
-      string_decoder: 1.3.0
-    dev: false
-
   /readdirp@3.6.0:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
@@ -14284,11 +14248,6 @@ packages:
     resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==}
     engines: {node: '>= 12.13.0'}
 
-  /real-require@0.2.0:
-    resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
-    engines: {node: '>= 12.13.0'}
-    dev: false
-
   /redent@3.0.0:
     resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
     engines: {node: '>=8'}
@@ -15056,12 +15015,6 @@ packages:
     dependencies:
       atomic-sleep: 1.0.0
 
-  /sonic-boom@3.3.0:
-    resolution: {integrity: sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==}
-    dependencies:
-      atomic-sleep: 1.0.0
-    dev: false
-
   /sort-keys@2.0.0:
     resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==}
     engines: {node: '>=4'}
@@ -15619,12 +15572,6 @@ packages:
     dependencies:
       real-require: 0.1.0
 
-  /thread-stream@2.4.0:
-    resolution: {integrity: sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==}
-    dependencies:
-      real-require: 0.2.0
-    dev: false
-
   /through2@2.0.5:
     resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
     dependencies:


Test patch:
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)