Repository: ethereum-optimism/optimism
Pull request #7119 (2023-09-06 16:37:31)

feat(indexer): bridge process pre-bedrock blocks
This adds support for pre-bedrock blocks for op-mainnet and op-goerli. 

1. Bedrock starting heights added only via preset for mainnet/goerli. CTC contracts also added
2. Bridge processor runs the legacy pathway and switches once reaching bedrock

Closes ethereum-optimism/devx#67

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

This comment was auto-generated by [Graphite](https://app.graphite.dev/github/pr/ethereum-optimism/optimism/7119?utm_source=stack-comment).
semgrep-app[bot]: Semgrep found **1** [`gorm-hardcoded-secret`](https://semgrep.dev/playground/r/QkTW7yg/go.secrets.gorm.gorm-hardcoded-secret.gorm-hardcoded-secret) finding:

* indexer/database/db.go: [L41-48](https://github.com/ethereum-optimism/optimism/blob/685658ee5f8237a0ccf18d69bc141dabd0b1d08d/indexer/database/db.go#L41-48)

A secret is hard-coded in the application. Secrets stored in source code, such as credentials, identifiers, and other types of sensitive data, can be leaked and used by internal or external malicious actors. Use environment variables to securely provide credentials and other secrets or retrieve them from a secure vault or Hardware Security Module (HSM).

<details>
<summary>View Dataflow Graph</summary>

```mermaid
flowchart LR
    classDef invis fill:white, stroke: none
    classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none

    subgraph File0["<b>indexer/database/db.go</b>"]
        direction LR
        %% Source

        subgraph Source
            direction LR

            v0("<b>[Line: 39]</b> #quot; password=%s#quot;")
        end
        %% Intermediate

        subgraph Traces0[Traces]
            direction TB

            v2("<b>[Line: 39]</b> dsn")
        end
        %% Sink

        subgraph Sink
            direction LR

            v1("<b>[Line: 41]</b> gorm.Open(postgres.Open(dsn), &gorm.Config{<br>		// The indexer will explicitly manage the transactions<br>		SkipDefaultTransaction: true,<br><br>		// We may choose to create an adapter such that the<br>		// logger emits to the geth logger when on DEBUG mode<br>		Logger: logger.Default.LogMode(logger.Silent),<br>	})")
        end
    end
    %% Class Assignment
    Source:::invis
    Sink:::invis

    Traces0:::invis
    File0:::invis

    %% Connections

    Source --> Traces0
    Traces0 --> Sink

    %% Clickable

    click v0 href "https://github.com/ethereum-optimism/optimism/blob/685658ee5f8237a0ccf18d69bc141dabd0b1d08d/indexer/database/db.go#L39" "View in source" _blank
    click v1 href "https://github.com/ethereum-optimism/optimism/blob/685658ee5f8237a0ccf18d69bc141dabd0b1d08d/indexer/database/db.go#L41" "View in source" _blank
    click v2 href "https://github.com/ethereum-optimism/optimism/blob/685658ee5f8237a0ccf18d69bc141dabd0b1d08d/indexer/database/db.go#L39" "View in source" _blank
```
</details>

<sub>
<a href="https://semgrep.dev/docs/ignoring-files-folders-code">Ignore this finding</a> from <a href="https://semgrep.dev/playground/r/QkTW7yg/go.secrets.gorm.gorm-hardcoded-secret.gorm-hardcoded-secret?utm_campaign=finding_notification&utm_medium=review_comment&utm_source=github&utm_content=rule">gorm-hardcoded-secret</a>.
</sub>


<!--

🤫 WELCOME TO SECRET SEMGREP! 🤫
This information is for debugging purposes and does not appear in rendered PR comments.

Finding id: 34626993
Syntactic id: 988566182e44a4d08bf1f5a5dd533e16
Start line: 41,15
End line: 48,4
Index: 0
-->

#

Semgrep found **1** [`gorm-empty-password`](https://semgrep.dev/playground/r/gET3A40/go.secrets.gorm.gorm-empty-password.gorm-empty-password) finding:

* indexer/database/db.go: [L41-48](https://github.com/ethereum-optimism/optimism/blob/685658ee5f8237a0ccf18d69bc141dabd0b1d08d/indexer/database/db.go#L41-48)

The application uses an empty credential. This can lead to unauthorized access by either an internal or external malicious actor. Use environment variables to securely provide credentials and other secrets or retrieve them from a secure vault or Hardware Security Module (HSM).

<details>
<summary>View Dataflow Graph</summary>

```mermaid
flowchart LR
    classDef invis fill:white, stroke: none
    classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none

    subgraph File0["<b>indexer/database/db.go</b>"]
        direction LR
        %% Source

        subgraph Source
            direction LR

            v0("<b>[Line: 39]</b> #quot; password=%s#quot;")
        end
        %% Intermediate

        subgraph Traces0[Traces]
            direction TB

            v2("<b>[Line: 39]</b> dsn")
        end
        %% Sink

        subgraph Sink
            direction LR

            v1("<b>[Line: 41]</b> gorm.Open(postgres.Open(dsn), &gorm.Config{<br>		// The indexer will explicitly manage the transactions<br>		SkipDefaultTransaction: true,<br><br>		// We may choose to create an adapter such that the<br>		// logger emits to the geth logger when on DEBUG mode<br>		Logger: logger.Default.LogMode(logger.Silent),<br>	})")
        end
    end
    %% Class Assignment
    Source:::invis
    Sink:::invis

    Traces0:::invis
    File0:::invis

    %% Connections

    Source --> Traces0
    Traces0 --> Sink

    %% Clickable

    click v0 href "https://github.com/ethereum-optimism/optimism/blob/685658ee5f8237a0ccf18d69bc141dabd0b1d08d/indexer/database/db.go#L39" "View in source" _blank
    click v1 href "https://github.com/ethereum-optimism/optimism/blob/685658ee5f8237a0ccf18d69bc141dabd0b1d08d/indexer/database/db.go#L41" "View in source" _blank
    click v2 href "https://github.com/ethereum-optimism/optimism/blob/685658ee5f8237a0ccf18d69bc141dabd0b1d08d/indexer/database/db.go#L39" "View in source" _blank
```
</details>

<sub>
<a href="https://semgrep.dev/docs/ignoring-files-folders-code">Ignore this finding</a> from <a href="https://semgrep.dev/playground/r/gET3A40/go.secrets.gorm.gorm-empty-password.gorm-empty-password?utm_campaign=finding_notification&utm_medium=review_comment&utm_source=github&utm_content=rule">gorm-empty-password</a>.
</sub>


<!--

🤫 WELCOME TO SECRET SEMGREP! 🤫
This information is for debugging purposes and does not appear in rendered PR comments.

Finding id: 34626994
Syntactic id: ef1dea5391d06b7d9d2c17eb91a623a3
Start line: 41,15
End line: 48,4
Index: 0
-->

Patch:
diff --git a/indexer/bigint/bigint.go b/indexer/bigint/bigint.go
index f44a78951..9238222e3 100644
--- a/indexer/bigint/bigint.go
+++ b/indexer/bigint/bigint.go
@@ -37,7 +37,6 @@ func Grouped(start, end *big.Int, size uint64) []Range {
 	if end.Cmp(start) < 0 || size == 0 {
 		return nil
 	}
-
 	bigMaxDiff := big.NewInt(int64(size - 1))
 
 	groups := []Range{}
diff --git a/indexer/config/config.go b/indexer/config/config.go
index 940d278dc..69eb82284 100644
--- a/indexer/config/config.go
+++ b/indexer/config/config.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/BurntSushi/toml"
 	"github.com/ethereum/go-ethereum/common"
-	geth_log "github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/log"
 )
 
 const (
@@ -34,6 +34,9 @@ type L1Contracts struct {
 	L1CrossDomainMessengerProxy common.Address `toml:"l1-cross-domain-messenger"`
 	L1StandardBridgeProxy       common.Address `toml:"l1-standard-bridge"`
 
+	// Pre-Bedrock Legacy Contracts
+	LegacyCanonicalTransactionChain common.Address `toml:"l1-canonical-transaction-chain"`
+
 	// Some more contracts -- L1ERC721Bridge, ProxyAdmin, SystemConfig, etc
 	// Ignore the auxiliary contracts?
 
@@ -69,6 +72,10 @@ type ChainConfig struct {
 	L1Contracts      L1Contracts `toml:"l1-contracts"`
 	L1StartingHeight uint        `toml:"l1-starting-height"`
 
+	// Bedrock starting heights only applicable for OP-Mainnet & OP-Goerli
+	L1BedrockStartingHeight uint `toml:"-"`
+	L2BedrockStartingHeight uint `toml:"-"`
+
 	// These configuration options will be removed once
 	// native reorg handling is implemented
 	L1ConfirmationDepth uint `toml:"l1-confirmation-depth"`
@@ -103,8 +110,8 @@ type ServerConfig struct {
 }
 
 // LoadConfig loads the `indexer.toml` config file from a given path
-func LoadConfig(logger geth_log.Logger, path string) (Config, error) {
-	logger.Debug("loading config", "path", path)
+func LoadConfig(log log.Logger, path string) (Config, error) {
+	log.Debug("loading config", "path", path)
 
 	var conf Config
 	data, err := os.ReadFile(path)
@@ -113,9 +120,9 @@ func LoadConfig(logger geth_log.Logger, path string) (Config, error) {
 	}
 
 	data = []byte(os.ExpandEnv(string(data)))
-	logger.Debug("parsed config file", "data", string(data))
+	log.Debug("parsed config file", "data", string(data))
 	if _, err := toml.Decode(string(data), &conf); err != nil {
-		logger.Info("failed to decode config file", "err", err)
+		log.Info("failed to decode config file", "err", err)
 		return conf, err
 	}
 
@@ -124,31 +131,34 @@ func LoadConfig(logger geth_log.Logger, path string) (Config, error) {
 		if !ok {
 			return conf, fmt.Errorf("unknown preset: %d", conf.Chain.Preset)
 		}
+
 		conf.Chain.L1Contracts = knownPreset.L1Contracts
 		conf.Chain.L1StartingHeight = knownPreset.L1StartingHeight
+		conf.Chain.L1BedrockStartingHeight = knownPreset.L1BedrockStartingHeight
+		conf.Chain.L2BedrockStartingHeight = knownPreset.L1BedrockStartingHeight
 	}
 
 	// Set polling defaults if not set
 	if conf.Chain.L1PollingInterval == 0 {
-		logger.Info("setting default L1 polling interval", "interval", defaultLoopInterval)
+		log.Info("setting default L1 polling interval", "interval", defaultLoopInterval)
 		conf.Chain.L1PollingInterval = defaultLoopInterval
 	}
 
 	if conf.Chain.L2PollingInterval == 0 {
-		logger.Info("setting default L2 polling interval", "interval", defaultLoopInterval)
+		log.Info("setting default L2 polling interval", "interval", defaultLoopInterval)
 		conf.Chain.L2PollingInterval = defaultLoopInterval
 	}
 
 	if conf.Chain.L1HeaderBufferSize == 0 {
-		logger.Info("setting default L1 header buffer", "size", defaultHeaderBufferSize)
+		log.Info("setting default L1 header buffer", "size", defaultHeaderBufferSize)
 		conf.Chain.L1HeaderBufferSize = defaultHeaderBufferSize
 	}
 
 	if conf.Chain.L2HeaderBufferSize == 0 {
-		logger.Info("setting default L2 header buffer", "size", defaultHeaderBufferSize)
+		log.Info("setting default L2 header buffer", "size", defaultHeaderBufferSize)
 		conf.Chain.L2HeaderBufferSize = defaultHeaderBufferSize
 	}
 
-	logger.Info("loaded config")
+	log.Info("loaded config")
 	return conf, nil
 }
diff --git a/indexer/config/presets.go b/indexer/config/presets.go
index be65ab785..c4acec6fd 100644
--- a/indexer/config/presets.go
+++ b/indexer/config/presets.go
@@ -10,23 +10,28 @@ var presetConfigs = map[int]ChainConfig{
 	// OP Mainnet
 	10: {
 		L1Contracts: L1Contracts{
-			OptimismPortalProxy:         common.HexToAddress("0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"),
-			L2OutputOracleProxy:         common.HexToAddress("0xdfe97868233d1aa22e815a266982f2cf17685a27"),
-			L1CrossDomainMessengerProxy: common.HexToAddress("0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1"),
-			L1StandardBridgeProxy:       common.HexToAddress("0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1"),
-			// LegacyCanonicalTransactionChain: common.HexToAddress("0x5e4e65926ba27467555eb562121fac00d24e9dd2"),
+			OptimismPortalProxy:             common.HexToAddress("0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"),
+			L2OutputOracleProxy:             common.HexToAddress("0xdfe97868233d1aa22e815a266982f2cf17685a27"),
+			L1CrossDomainMessengerProxy:     common.HexToAddress("0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1"),
+			L1StandardBridgeProxy:           common.HexToAddress("0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1"),
+			LegacyCanonicalTransactionChain: common.HexToAddress("0x5e4e65926ba27467555eb562121fac00d24e9dd2"),
 		},
-		L1StartingHeight: 13596466,
+		L1StartingHeight:        13596466,
+		L1BedrockStartingHeight: 17422590,
+		L2BedrockStartingHeight: 105235063,
 	},
 	// OP Goerli
 	420: {
 		L1Contracts: L1Contracts{
-			OptimismPortalProxy:         common.HexToAddress("0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383"),
-			L2OutputOracleProxy:         common.HexToAddress("0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0"),
-			L1CrossDomainMessengerProxy: common.HexToAddress("0x5086d1eEF304eb5284A0f6720f79403b4e9bE294"),
-			L1StandardBridgeProxy:       common.HexToAddress("0x636Af16bf2f682dD3109e60102b8E1A089FedAa8"),
+			OptimismPortalProxy:             common.HexToAddress("0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383"),
+			L2OutputOracleProxy:             common.HexToAddress("0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0"),
+			L1CrossDomainMessengerProxy:     common.HexToAddress("0x5086d1eEF304eb5284A0f6720f79403b4e9bE294"),
+			L1StandardBridgeProxy:           common.HexToAddress("0x636Af16bf2f682dD3109e60102b8E1A089FedAa8"),
+			LegacyCanonicalTransactionChain: common.HexToAddress("0x607F755149cFEB3a14E1Dc3A4E2450Cde7dfb04D"),
 		},
-		L1StartingHeight: 7017096,
+		L1StartingHeight:        7017096,
+		L1BedrockStartingHeight: 8300214,
+		L2BedrockStartingHeight: 4061224,
 	},
 	// Base Mainnet
 	8453: {
diff --git a/indexer/etl/etl.go b/indexer/etl/etl.go
index 0ebcd9553..23214c2b7 100644
--- a/indexer/etl/etl.go
+++ b/indexer/etl/etl.go
@@ -29,9 +29,10 @@ type ETL struct {
 	headerBufferSize uint64
 	headerTraversal  *node.HeaderTraversal
 
-	ethClient  node.EthClient
 	contracts  []common.Address
 	etlBatches chan ETLBatch
+
+	EthClient node.EthClient
 }
 
 type ETLBatch struct {
@@ -104,7 +105,7 @@ func (etl *ETL) processBatch(headers []types.Header) error {
 	}
 
 	headersWithLog := make(map[common.Hash]bool, len(headers))
-	logs, err := etl.ethClient.FilterLogs(ethereum.FilterQuery{FromBlock: firstHeader.Number, ToBlock: lastHeader.Number, Addresses: etl.contracts})
+	logs, err := etl.EthClient.FilterLogs(ethereum.FilterQuery{FromBlock: firstHeader.Number, ToBlock: lastHeader.Number, Addresses: etl.contracts})
 	if err != nil {
 		batchLog.Info("unable to extract logs", "err", err)
 		return err
diff --git a/indexer/etl/l1_etl.go b/indexer/etl/l1_etl.go
index 56892aafb..1ab44de6f 100644
--- a/indexer/etl/l1_etl.go
+++ b/indexer/etl/l1_etl.go
@@ -66,9 +66,10 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
 		log:             log,
 		metrics:         metrics,
 		headerTraversal: node.NewHeaderTraversal(client, fromHeader, cfg.ConfirmationDepth),
-		ethClient:       client,
 		contracts:       cSlice,
 		etlBatches:      etlBatches,
+
+		EthClient: client,
 	}
 
 	return &L1ETL{ETL: etl, db: db, mu: new(sync.Mutex)}, nil
diff --git a/indexer/etl/l2_etl.go b/indexer/etl/l2_etl.go
index e552862e6..ce2559e5d 100644
--- a/indexer/etl/l2_etl.go
+++ b/indexer/etl/l2_etl.go
@@ -50,9 +50,10 @@ func NewL2ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
 		log:             log,
 		metrics:         metrics,
 		headerTraversal: node.NewHeaderTraversal(client, fromHeader, cfg.ConfirmationDepth),
-		ethClient:       client,
 		contracts:       l2Contracts,
 		etlBatches:      etlBatches,
+
+		EthClient: client,
 	}
 
 	return &L2ETL{ETL: etl, db: db}, nil
diff --git a/indexer/indexer.toml b/indexer/indexer.toml
index 1cf4bd5e5..dbea56e25 100644
--- a/indexer/indexer.toml
+++ b/indexer/indexer.toml
@@ -2,7 +2,6 @@
 # Can configure them manually or use a preset l2 ChainId for known chains including OP Mainnet, OP Goerli, Base, Base Goerli, Zora, and Zora goerli
 [chain]
 
-# OP Goerli
 preset = $INDEXER_CHAIN_PRESET
 
 # L1 Config
diff --git a/indexer/migrations/20230523_create_schema.sql b/indexer/migrations/20230523_create_schema.sql
index f0e568d5f..e205db9f2 100644
--- a/indexer/migrations/20230523_create_schema.sql
+++ b/indexer/migrations/20230523_create_schema.sql
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS l2_block_headers (
     hash        VARCHAR PRIMARY KEY,
     parent_hash VARCHAR NOT NULL UNIQUE,
     number      UINT256 NOT NULL UNIQUE,
-    timestamp   INTEGER NOT NULL UNIQUE CHECK (timestamp > 0),
+    timestamp   INTEGER NOT NULL,
 
     -- Raw Data
     rlp_bytes VARCHAR NOT NULL
diff --git a/indexer/node/client.go b/indexer/node/client.go
index 4917e3630..7af8868e8 100644
--- a/indexer/node/client.go
+++ b/indexer/node/client.go
@@ -29,6 +29,8 @@ type EthClient interface {
 	BlockHeaderByHash(common.Hash) (*types.Header, error)
 	BlockHeadersByRange(*big.Int, *big.Int) ([]types.Header, error)
 
+	TxByHash(common.Hash) (*types.Transaction, error)
+
 	StorageHash(common.Address, *big.Int) (common.Hash, error)
 	FilterLogs(ethereum.FilterQuery) ([]types.Log, error)
 }
@@ -147,6 +149,21 @@ func (c *client) BlockHeadersByRange(startHeight, endHeight *big.Int) ([]types.H
 	return headers, nil
 }
 
+func (c *client) TxByHash(hash common.Hash) (*types.Transaction, error) {
+	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
+	defer cancel()
+
+	var tx *types.Transaction
+	err := c.rpc.CallContext(ctxwt, &tx, "eth_getTransactionByHash", hash)
+	if err != nil {
+		return nil, err
+	} else if tx == nil {
+		return nil, ethereum.NotFound
+	}
+
+	return tx, nil
+}
+
 // StorageHash returns the sha3 of the storage root for the specified account
 func (c *client) StorageHash(address common.Address, blockNumber *big.Int) (common.Hash, error) {
 	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
diff --git a/indexer/node/mocks.go b/indexer/node/mocks.go
index 19ccbbda8..115a81767 100644
--- a/indexer/node/mocks.go
+++ b/indexer/node/mocks.go
@@ -31,6 +31,11 @@ func (m *MockEthClient) BlockHeadersByRange(from, to *big.Int) ([]types.Header,
 	return args.Get(0).([]types.Header), args.Error(1)
 }
 
+func (m *MockEthClient) TxByHash(hash common.Hash) (*types.Transaction, error) {
+	args := m.Called(hash)
+	return args.Get(0).(*types.Transaction), args.Error(1)
+}
+
 func (m *MockEthClient) StorageHash(address common.Address, blockNumber *big.Int) (common.Hash, error) {
 	args := m.Called(address, blockNumber)
 	return args.Get(0).(common.Hash), args.Error(1)
diff --git a/indexer/processors/bridge.go b/indexer/processors/bridge.go
index e56d588b4..992928498 100644
--- a/indexer/processors/bridge.go
+++ b/indexer/processors/bridge.go
@@ -124,19 +124,78 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
 			fromL2Height = new(big.Int).Add(b.LatestL2Header.Number, bigint.One)
 		}
 
+		l1BedrockStartingHeight := big.NewInt(int64(b.chainConfig.L1BedrockStartingHeight))
+		l2BedrockStartingHeight := big.NewInt(int64(b.chainConfig.L2BedrockStartingHeight))
+
 		batchLog := b.log.New("epoch_start_number", fromL1Height, "epoch_end_number", toL1Height)
 		batchLog.Info("unobserved epochs")
 		err = b.db.Transaction(func(tx *database.DB) error {
+			// In the event where we have a large number of un-observed blocks, group the block range
+			// on the order of 10k blocks at a time. If this turns out to be a bottleneck, we can
+			// parallelize these operations
+			maxBlockRange := uint64(10_000)
 			l1BridgeLog := b.log.New("bridge", "l1")
 			l2BridgeLog := b.log.New("bridge", "l2")
 
-			// In the event where we have a large number of un-observed blocks, group the block range
-			// on the order of 10k blocks at a time. If this turns out to be a bottleneck, we can
-			// parallelize these operations for significant improvements as well
-			l1BlockGroups := bigint.Grouped(fromL1Height, toL1Height, 10_000)
-			l2BlockGroups := bigint.Grouped(fromL2Height, toL2Height, 10_000)
+			// FOR OP-MAINNET, OP-GOERLI ONLY! Specially handle the existence of pre-bedrock blocks
+			if l1BedrockStartingHeight.Cmp(fromL1Height) > 0 {
+				l1BridgeLog := l1BridgeLog.New("mode", "legacy")
+				l2BridgeLog := l2BridgeLog.New("mode", "legacy")
+
+				legacyFromL1Height, legacyToL1Height := fromL1Height, toL1Height
+				legacyFromL2Height, legacyToL2Height := fromL2Height, toL2Height
+				if l1BedrockStartingHeight.Cmp(toL1Height) <= 0 {
+					legacyToL1Height = new(big.Int).Sub(l1BedrockStartingHeight, big.NewInt(1))
+					legacyToL2Height = new(big.Int).Sub(l2BedrockStartingHeight, big.NewInt(1))
+				}
+
+				// First, find all possible initiated bridge events
+				l1BlockGroups := bigint.Grouped(legacyFromL1Height, legacyToL1Height, maxBlockRange)
+				l2BlockGroups := bigint.Grouped(legacyFromL2Height, legacyToL2Height, maxBlockRange)
+				for _, group := range l1BlockGroups {
+					log := l1BridgeLog.New("from_l1_block_number", group.Start, "to_l1_block_number", group.End)
+					log.Info("scanning for initiated bridge events")
+					if err := bridge.LegacyL1ProcessInitiatedBridgeEvents(log, tx, b.chainConfig, group.Start, group.End); err != nil {
+						return err
+					}
+				}
+				for _, group := range l2BlockGroups {
+					log := l2BridgeLog.New("from_l2_block_number", group.Start, "to_l2_block_number", group.End)
+					log.Info("scanning for initiated bridge events")
+					if err := bridge.LegacyL2ProcessInitiatedBridgeEvents(log, tx, group.Start, group.End); err != nil {
+						return err
+					}
+				}
+
+				// Now that all initiated events have been indexed, it is ensured that all finalization can find their counterpart.
+				for _, group := range l1BlockGroups {
+					log := l1BridgeLog.New("from_l1_block_number", group.Start, "to_l1_block_number", group.End)
+					log.Info("scanning for finalized bridge events")
+					if err := bridge.LegacyL1ProcessFinalizedBridgeEvents(log, tx, b.l1Etl.EthClient, b.chainConfig, group.Start, group.End); err != nil {
+						return err
+					}
+				}
+				for _, group := range l2BlockGroups {
+					log := l2BridgeLog.New("from_l2_block_number", group.Start, "to_l2_block_number", group.End)
+					log.Info("scanning for finalized bridge events")
+					if err := bridge.LegacyL2ProcessFinalizedBridgeEvents(log, tx, group.Start, group.End); err != nil {
+						return err
+					}
+				}
+
+				if legacyToL1Height.Cmp(toL1Height) == 0 {
+					// a-ok! entire batch was legacy blocks
+					return nil
+				}
+
+				batchLog.Info("detected switch to bedrock", "l1_bedrock_starting_height", l1BedrockStartingHeight, "l2_bedrock_starting_height", l2BedrockStartingHeight)
+				fromL1Height = l1BedrockStartingHeight
+				fromL2Height = l2BedrockStartingHeight
+			}
 
 			// First, find all possible initiated bridge events
+			l1BlockGroups := bigint.Grouped(fromL1Height, toL1Height, maxBlockRange)
+			l2BlockGroups := bigint.Grouped(fromL2Height, toL2Height, maxBlockRange)
 			for _, group := range l1BlockGroups {
 				log := l1BridgeLog.New("from_block_number", group.Start, "to_block_number", group.End)
 				log.Info("scanning for initiated bridge events")
@@ -181,4 +240,5 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
 			b.LatestL2Header = latestEpoch.L2BlockHeader.RLPHeader.Header()
 		}
 	}
+
 }
diff --git a/indexer/processors/bridge/l1_bridge_processor.go b/indexer/processors/bridge/l1_bridge_processor.go
index 7aafb0f29..a35611fad 100644
--- a/indexer/processors/bridge/l1_bridge_processor.go
+++ b/indexer/processors/bridge/l1_bridge_processor.go
@@ -12,7 +12,7 @@ import (
 	"github.com/ethereum/go-ethereum/log"
 )
 
-// L1ProcessInitiatedBridgeEvents will query the database for new bridge events that have been initiated between
+// L1ProcessInitiatedBridgeEvents will query the database for bridge events that have been initiated between
 // the specified block range. This covers every part of the multi-layered stack:
 //  1. OptimismPortal
 //  2. L1CrossDomainMessenger
diff --git a/indexer/processors/bridge/l2_bridge_processor.go b/indexer/processors/bridge/l2_bridge_processor.go
index 0e0f649dc..790faede6 100644
--- a/indexer/processors/bridge/l2_bridge_processor.go
+++ b/indexer/processors/bridge/l2_bridge_processor.go
@@ -12,7 +12,7 @@ import (
 	"github.com/ethereum/go-ethereum/log"
 )
 
-// L2ProcessInitiatedBridgeEvents will query the database for new bridge events that have been initiated between
+// L2ProcessInitiatedBridgeEvents will query the database for bridge events that have been initiated between
 // the specified block range. This covers every part of the multi-layered stack:
 //  1. OptimismPortal
 //  2. L2CrossDomainMessenger
diff --git a/indexer/processors/bridge/legacy_bridge_processor.go b/indexer/processors/bridge/legacy_bridge_processor.go
new file mode 100644
index 000000000..f78513959
--- /dev/null
+++ b/indexer/processors/bridge/legacy_bridge_processor.go
@@ -0,0 +1,340 @@
+package bridge
+
+import (
+	"fmt"
+	"math/big"
+
+	"github.com/ethereum/go-ethereum/crypto"
+	"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/node"
+	"github.com/ethereum-optimism/optimism/indexer/processors/contracts"
+	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
+)
+
+// Legacy Bridge Initiation
+
+// LegacyL1ProcessInitiatedEvents will query the data for bridge events within the specified block range
+// according the pre-bedrock protocol. This follows:
+//  1. CanonicalTransactionChain
+//  2. L1CrossDomainMessenger
+//  3. L1StandardBridge
+func LegacyL1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig config.ChainConfig, fromHeight *big.Int, toHeight *big.Int) error {
+	// (1) CanonicalTransactionChain
+	ctcTxDepositEvents, err := contracts.LegacyCTCDepositEvents(chainConfig.L1Contracts.LegacyCanonicalTransactionChain, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(ctcTxDepositEvents) > 0 {
+		log.Info("detected legacy transaction deposits", "size", len(ctcTxDepositEvents))
+	}
+
+	ctcTxDeposits := make(map[logKey]*contracts.LegacyCTCDepositEvent, len(ctcTxDepositEvents))
+	transactionDeposits := make([]database.L1TransactionDeposit, len(ctcTxDepositEvents))
+	for i := range ctcTxDepositEvents {
+		deposit := ctcTxDepositEvents[i]
+		ctcTxDeposits[logKey{deposit.Event.BlockHash, deposit.Event.LogIndex}] = &deposit
+		transactionDeposits[i] = database.L1TransactionDeposit{
+			// We re-use the L2 Transaction hash as the source hash
+			// to remain consistent in the schema.
+			SourceHash:        deposit.TxHash,
+			L2TransactionHash: deposit.TxHash,
+
+			InitiatedL1EventGUID: deposit.Event.GUID,
+			GasLimit:             deposit.GasLimit,
+			Tx:                   deposit.Tx,
+		}
+	}
+	if len(ctcTxDepositEvents) > 0 {
+		if err := db.BridgeTransactions.StoreL1TransactionDeposits(transactionDeposits); 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) > 0 {
+		log.Info("detected legacy sent messages", "size", 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
+		ctcTxDeposit, ok := ctcTxDeposits[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex - 1}]
+		if !ok {
+			log.Error("missing transaction deposit for cross domain message", "tx_hash", sentMessage.Event.TransactionHash.String())
+			return fmt.Errorf("missing preceding TransactionEnqueued for SentMessage event. tx_hash = %s", sentMessage.Event.TransactionHash.String())
+		}
+
+		l1BridgeMessages[i] = database.L1BridgeMessage{TransactionSourceHash: ctcTxDeposit.TxHash, BridgeMessage: sentMessage.BridgeMessage}
+	}
+	if len(l1BridgeMessages) > 0 {
+		if err := db.BridgeMessages.StoreL1BridgeMessages(l1BridgeMessages); err != nil {
+			return err
+		}
+	}
+
+	// (3) L1StandardBridge
+	initiatedBridges, err := contracts.L1StandardBridgeLegacyDepositInitiatedEvents(chainConfig.L1Contracts.L1StandardBridgeProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(initiatedBridges) > 0 {
+		log.Info("detected iegacy bridge deposits", "size", 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
+		// Unlike bedrock, the bridge events are emitted AFTER sending the cross domain message
+		// 	- Event Flow: TransactionEnqueued -> SentMessage -> DepositInitiated
+		sentMessage, ok := sentMessages[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex - 1}]
+		if !ok {
+			log.Error("missing cross domain message for bridge transfer", "tx_hash", initiatedBridge.Event.TransactionHash.String())
+			return fmt.Errorf("expected SentMessage preceding DepositInitiated event. tx_hash = %s", initiatedBridge.Event.TransactionHash.String())
+		}
+		ctcTxDeposit, ok := ctcTxDeposits[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex - 2}]
+		if !ok {
+			log.Error("missing transaction deposit for bridge transfer", "tx_hash", initiatedBridge.Event.TransactionHash.String())
+			return fmt.Errorf("expected TransactionEnqueued preceding DepostInitiated event. tx_hash = %s", initiatedBridge.Event.TransactionHash.String())
+		}
+
+		initiatedBridge.BridgeTransfer.CrossDomainMessageHash = &sentMessage.BridgeMessage.MessageHash
+		l1BridgeDeposits[i] = database.L1BridgeDeposit{
+			TransactionSourceHash: ctcTxDeposit.TxHash,
+			BridgeTransfer:        initiatedBridge.BridgeTransfer,
+		}
+	}
+	if len(l1BridgeDeposits) > 0 {
+		if err := db.BridgeTransfers.StoreL1BridgeDeposits(l1BridgeDeposits); err != nil {
+			return err
+		}
+	}
+
+	// a-ok!
+	return nil
+}
+
+// LegacyL2ProcessInitiatedEvents will query the data for bridge events within the specified block range
+// according the pre-bedrock protocol. This follows:
+//  1. L2CrossDomainMessenger - The LegacyMessagePasser contract cannot be used as entrypoint to bridge transactions from L2. The protocol
+//     only allows the L2CrossDomainMessenger as the sole sender when relaying a bridged message.
+//  2. L2StandardBridge
+func LegacyL2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight *big.Int, toHeight *big.Int) error {
+	// (1) L2CrossDomainMessenger
+	crossDomainSentMessages, err := contracts.CrossDomainMessengerSentMessageEvents("l2", predeploys.L2CrossDomainMessengerAddr, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+
+	if len(crossDomainSentMessages) > 0 {
+		log.Info("detected legacy transaction withdrawals (via L2CrossDomainMessenger)", "size", len(crossDomainSentMessages))
+	}
+
+	sentMessages := make(map[logKey]*contracts.CrossDomainMessengerSentMessageEvent, len(crossDomainSentMessages))
+	l2BridgeMessages := make([]database.L2BridgeMessage, len(crossDomainSentMessages))
+	l2TransactionWithdrawals := make([]database.L2TransactionWithdrawal, len(crossDomainSentMessages))
+	for i := range crossDomainSentMessages {
+		sentMessage := crossDomainSentMessages[i]
+		sentMessages[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex}] = &sentMessage
+
+		// To ensure consistency in the schema, we duplicate this as the "root" transaction withdrawal. The storage key in the message
+		// passer contract is sha3(calldata + sender). The sender always being the L2CrossDomainMessenger pre-bedrock.
+		withdrawalHash := crypto.Keccak256Hash(append(sentMessage.MessageCalldata, predeploys.L2CrossDomainMessengerAddr[:]...))
+		l2TransactionWithdrawals[i] = database.L2TransactionWithdrawal{
+			WithdrawalHash:       withdrawalHash,
+			InitiatedL2EventGUID: sentMessage.Event.GUID,
+			Nonce:                sentMessage.BridgeMessage.Nonce,
+			GasLimit:             sentMessage.BridgeMessage.GasLimit,
+			Tx: database.Transaction{
+				FromAddress: sentMessage.BridgeMessage.Tx.FromAddress,
+				ToAddress:   sentMessage.BridgeMessage.Tx.ToAddress,
+				Amount:      big.NewInt(0),
+				Data:        sentMessage.BridgeMessage.Tx.Data,
+				Timestamp:   sentMessage.Event.Timestamp,
+			},
+		}
+
+		l2BridgeMessages[i] = database.L2BridgeMessage{
+			TransactionWithdrawalHash: withdrawalHash,
+			BridgeMessage:             sentMessage.BridgeMessage,
+		}
+	}
+
+	if len(l2BridgeMessages) > 0 {
+		if err := db.BridgeTransactions.StoreL2TransactionWithdrawals(l2TransactionWithdrawals); err != nil {
+			return err
+		}
+		if err := db.BridgeMessages.StoreL2BridgeMessages(l2BridgeMessages); err != nil {
+			return err
+		}
+	}
+
+	// (2) L2StandardBridge
+	initiatedBridges, err := contracts.L2StandardBridgeLegacyWithdrawalInitiatedEvents(predeploys.L2StandardBridgeAddr, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(initiatedBridges) > 0 {
+		log.Info("detected legacy bridge withdrawals", "size", 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
+		// Unlike bedrock, the bridge events are emitted AFTER sending the cross domain message
+		// 	- Event Flow: TransactionEnqueued -> SentMessage -> DepositInitiated
+		sentMessage, ok := sentMessages[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex - 1}]
+		if !ok {
+			log.Error("expected SentMessage preceding DepositInitiated event", "tx_hash", initiatedBridge.Event.TransactionHash.String())
+			return fmt.Errorf("expected SentMessage preceding DepositInitiated event. tx_hash = %s", initiatedBridge.Event.TransactionHash)
+		}
+
+		initiatedBridge.BridgeTransfer.CrossDomainMessageHash = &sentMessage.BridgeMessage.MessageHash
+		l2BridgeWithdrawals[i] = database.L2BridgeWithdrawal{
+			TransactionWithdrawalHash: sentMessage.BridgeMessage.MessageHash,
+			BridgeTransfer:            initiatedBridge.BridgeTransfer,
+		}
+	}
+	if len(l2BridgeWithdrawals) > 0 {
+		if err := db.BridgeTransfers.StoreL2BridgeWithdrawals(l2BridgeWithdrawals); err != nil {
+			return err
+		}
+	}
+
+	// a-ok
+	return nil
+}
+
+// Legacy Bridge Finalization
+
+// LegacyL1ProcessFinalizedBridgeEvents will query for bridge events within the specified block range
+// according to the pre-bedrock protocol. This follows:
+//  1. L1CrossDomainMessenger
+//  2. L1StandardBridge
+func LegacyL1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, l1Client node.EthClient, chainConfig config.ChainConfig, fromHeight *big.Int, toHeight *big.Int) error {
+	// (1) L1CrossDomainMessenger -> This is the root-most contract from which bridge events are finalized since withdrawals must be initiated from the
+	// L2CrossDomainMessenger. Since there's no two-step withdrawal process, we mark the transaction as proven/finalized in the same step
+	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l1", chainConfig.L1Contracts.L1CrossDomainMessengerProxy, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(crossDomainRelayedMessages) > 0 {
+		log.Info("detected relayed messages", "size", len(crossDomainRelayedMessages))
+	}
+
+	skippedPreRegenesisMessages := 0
+	for i := range crossDomainRelayedMessages {
+		relayedMessage := crossDomainRelayedMessages[i]
+		message, err := db.BridgeMessages.L2BridgeMessage(relayedMessage.MessageHash)
+		if err != nil {
+			return err
+		} else if message == nil {
+			// Before surfacing an error about a missing withdrawal, we need to handle an edge case
+			// for OP-Mainnet pre-regensis withdrawals that no longer exist on L2.
+			tx, err := l1Client.TxByHash(relayedMessage.Event.TransactionHash)
+			if err != nil {
+				return err
+			} else if tx == nil {
+				log.Error("missing tx for relayed message", "tx_hash", relayedMessage.Event.TransactionHash.String())
+				return fmt.Errorf("missing tx for relayed message. tx_hash = %s", relayedMessage.Event.TransactionHash.String())
+			}
+
+			relayMessageData := tx.Data()[4:]
+			inputs, err := contracts.CrossDomainMessengerLegacyRelayMessageEncoding.Inputs.Unpack(relayMessageData)
+			if err != nil || inputs == nil {
+				log.Error("failed to extract XDomainCallData from relayMessage transaction", "err", err, "tx_hash", relayedMessage.Event.TransactionHash.String())
+				return fmt.Errorf("unable to extract XDomainCallData from relayMessage transaction. err = %w. tx_hash = %s", err, relayedMessage.Event.TransactionHash.String())
+			}
+
+			// NOTE: Since OP-Mainnet is the only network to go through a regensis we can simply harcode the
+			// the starting message nonce at genesis (100k). Any relayed withdrawal on L1 with a lesser nonce
+			// is a clear indicator of a pre-regenesis withdrawal.
+			if inputs[3].(*big.Int).Int64() < 100_000 {
+				// skip pre-regenesis withdrawals
+				skippedPreRegenesisMessages++
+				continue
+			} else {
+				log.Error("missing indexed legacy L2CrossDomainMessenger message", "tx_hash", relayedMessage.Event.TransactionHash.String())
+				return fmt.Errorf("missing indexed L2CrossDomainMessager message. tx_hash %s", relayedMessage.Event.TransactionHash.String())
+			}
+		}
+
+		// Mark the associated tx withdrawal as proven/finalized with the same event
+		if err := db.BridgeTransactions.MarkL2TransactionWithdrawalProvenEvent(relayedMessage.MessageHash, relayedMessage.Event.GUID); err != nil {
+			log.Error("failed to mark withdrawal as proven", "err", err)
+			return err
+		}
+		if err := db.BridgeTransactions.MarkL2TransactionWithdrawalFinalizedEvent(relayedMessage.MessageHash, relayedMessage.Event.GUID, true); err != nil {
+			log.Error("failed to mark withdrawal as finalzed", "err", err)
+			return err
+		}
+		if err := db.BridgeMessages.MarkRelayedL2BridgeMessage(relayedMessage.MessageHash, relayedMessage.Event.GUID); err != nil {
+			log.Error("failed to relay cross domain message", "err", err)
+			return err
+		}
+	}
+
+	if skippedPreRegenesisMessages > 0 {
+		// Logged as a warning just for visibility
+		log.Warn("skipped pre-regensis relayed L2CrossDomainMessenger withdrawals", "size", skippedPreRegenesisMessages)
+	}
+
+	// (2) L2StandardBridge -- no-op for now as there's nothing actionable to do here besides
+	// santiy checks which is not important for legacy code. The message status is already tracked
+	// via the relayed bridge messed through the cross domain messenger.
+
+	// a-ok!
+	return nil
+}
+
+// LegacyL2ProcessFinalizedBridgeEvents will query for bridge events within the specified block range
+// according to the pre-bedrock protocol. This follows:
+//  1. L2CrossDomainMessenger
+//  2. L2StandardBridge
+func LegacyL2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight *big.Int, toHeight *big.Int) error {
+	// (1) L2CrossDomainMessenger
+	crossDomainRelayedMessages, err := contracts.CrossDomainMessengerRelayedMessageEvents("l2", predeploys.L2CrossDomainMessengerAddr, db, fromHeight, toHeight)
+	if err != nil {
+		return err
+	}
+	if len(crossDomainRelayedMessages) > 0 {
+		log.Info("detected relayed legacy messages", "size", len(crossDomainRelayedMessages))
+	}
+
+	for i := range crossDomainRelayedMessages {
+		relayedMessage := crossDomainRelayedMessages[i]
+		message, err := db.BridgeMessages.L1BridgeMessage(relayedMessage.MessageHash)
+		if err != nil {
+			return err
+		} else if message == nil {
+			log.Error("missing indexed legacy L1CrossDomainMessenger message", "tx_hash", relayedMessage.Event.TransactionHash.String())
+			return fmt.Errorf("missing indexed L1CrossDomainMessager message. tx_hash = %s", relayedMessage.Event.TransactionHash.String())
+		}
+
+		if err := db.BridgeMessages.MarkRelayedL1BridgeMessage(relayedMessage.MessageHash, relayedMessage.Event.GUID); err != nil {
+			log.Error("failed to relay cross domain message", "err", err)
+			return err
+		}
+	}
+
+	// (2) L2StandardBridge -- no-op for now as there's nothing actionable to do here besides
+	// santiy checks which is not important for legacy code. The message status is already tracked
+	// via the relayed bridge messed through the cross domain messenger.
+
+	// a-ok!
+	return nil
+}
diff --git a/indexer/processors/contracts/cross_domain_messenger.go b/indexer/processors/contracts/cross_domain_messenger.go
index 2888b70d5..b74e2fc24 100644
--- a/indexer/processors/contracts/cross_domain_messenger.go
+++ b/indexer/processors/contracts/cross_domain_messenger.go
@@ -18,7 +18,7 @@ var (
 	bytesType, _   = abi.NewType("bytes", "", nil)
 	addressType, _ = abi.NewType("address", "", nil)
 
-	legacyCrossDomainMessengerRelayMessageMethod = abi.NewMethod(
+	CrossDomainMessengerLegacyRelayMessageEncoding = abi.NewMethod(
 		"relayMessage",
 		"relayMessage",
 		abi.Function,
@@ -26,18 +26,21 @@ var (
 		false,      // isConst
 		true,       // payable
 		abi.Arguments{ // inputs
-			{Name: "sender", Type: addressType},
 			{Name: "target", Type: addressType},
+			{Name: "sender", Type: addressType},
 			{Name: "data", Type: bytesType},
 			{Name: "nonce", Type: uint256Type},
+			// The actual transaction on the legacy L1CrossDomainMessenger has a trailing
+			// proof argument but is ignored for the `XDomainCallData` encoding
 		},
 		abi.Arguments{}, // outputs
 	)
 )
 
 type CrossDomainMessengerSentMessageEvent struct {
-	Event         *database.ContractEvent
-	BridgeMessage database.BridgeMessage
+	Event           *database.ContractEvent
+	MessageCalldata []byte
+	BridgeMessage   database.BridgeMessage
 }
 
 type CrossDomainMessengerRelayedMessageEvent struct {
@@ -58,7 +61,6 @@ func CrossDomainMessengerSentMessageEvents(chainSelector string, contractAddress
 		return nil, err
 	}
 	if len(sentMessageEvents) == 0 {
-		// prevent the following db queries if we dont need them
 		return nil, nil
 	}
 
@@ -68,10 +70,13 @@ func CrossDomainMessengerSentMessageEvents(chainSelector string, contractAddress
 	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))
+	if len(sentMessageExtensionEvents) > len(sentMessageEvents) {
+		return nil, fmt.Errorf("mismatch. %d sent messages & %d sent message extensions", len(sentMessageEvents), len(sentMessageExtensionEvents))
 	}
 
+	// We handle version zero messages uniquely since the first version of cross domain messages
+	// do not have the SentMessageExtension1 event emitted, introduced in version 1.
+	numVersionZeroMessages := len(sentMessageEvents) - len(sentMessageExtensionEvents)
 	crossDomainSentMessages := make([]CrossDomainMessengerSentMessageEvent, len(sentMessageEvents))
 	for i := range sentMessageEvents {
 		sentMessage := bindings.CrossDomainMessengerSentMessage{Raw: *sentMessageEvents[i].RLPLog}
@@ -79,34 +84,45 @@ func CrossDomainMessengerSentMessageEvents(chainSelector string, contractAddress
 		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
+
+		version, _ := DecodeVersionedNonce(sentMessage.MessageNonce)
+		if i < numVersionZeroMessages && version != 0 {
+			return nil, fmt.Errorf("expected version zero nonce. nonce %d tx_hash %s", sentMessage.MessageNonce, sentMessage.Raw.TxHash)
 		}
 
-		messageHash, err := CrossDomainMessageHash(crossDomainMessengerAbi, &sentMessage, sentMessageExtension.Value)
+		// In version zero, to value is bridged through the cross domain messenger.
+		value := big.NewInt(0)
+		if version > 0 {
+			sentMessageExtension := bindings.CrossDomainMessengerSentMessageExtension1{Raw: *sentMessageExtensionEvents[i].RLPLog}
+			err = UnpackLog(&sentMessageExtension, sentMessageExtensionEvents[i].RLPLog, sentMessageExtensionEventAbi.Name, crossDomainMessengerAbi)
+			if err != nil {
+				return nil, err
+			}
+			value = sentMessageExtension.Value
+		}
+
+		messageCalldata, err := CrossDomainMessageCalldata(crossDomainMessengerAbi, &sentMessage, value)
 		if err != nil {
 			return nil, err
 		}
 
 		crossDomainSentMessages[i] = CrossDomainMessengerSentMessageEvent{
-			Event: &sentMessageEvents[i],
+			Event:           &sentMessageEvents[i],
+			MessageCalldata: messageCalldata,
 			BridgeMessage: database.BridgeMessage{
-				MessageHash:          messageHash,
+				MessageHash:          crypto.Keccak256Hash(messageCalldata),
 				Nonce:                sentMessage.MessageNonce,
 				SentMessageEventGUID: sentMessageEvents[i].GUID,
 				GasLimit:             sentMessage.GasLimit,
 				Tx: database.Transaction{
 					FromAddress: sentMessage.Sender,
 					ToAddress:   sentMessage.Target,
-					Amount:      sentMessageExtension.Value,
+					Amount:      value,
 					Data:        sentMessage.Message,
 					Timestamp:   sentMessageEvents[i].Timestamp,
 				},
 			},
 		}
-
 	}
 
 	return crossDomainSentMessages, nil
@@ -142,26 +158,25 @@ func CrossDomainMessengerRelayedMessageEvents(chainSelector string, contractAddr
 	return crossDomainRelayedMessages, nil
 }
 
-// Replica of `Hashing.sol#hashCrossDomainMessage` solidity implementation
-func CrossDomainMessageHash(abi *abi.ABI, sentMsg *bindings.CrossDomainMessengerSentMessage, value *big.Int) (common.Hash, error) {
+// Replica of `Encoding.sol#encodeCrossDomainMessage` solidity implementation
+func CrossDomainMessageCalldata(abi *abi.ABI, sentMsg *bindings.CrossDomainMessengerSentMessage, value *big.Int) ([]byte, error) {
 	version, _ := DecodeVersionedNonce(sentMsg.MessageNonce)
 	switch version {
 	case 0:
 		// Legacy Message
-		inputBytes, err := legacyCrossDomainMessengerRelayMessageMethod.Inputs.Pack(sentMsg.Sender, sentMsg.Target, sentMsg.Message, sentMsg.MessageNonce)
+		inputBytes, err := CrossDomainMessengerLegacyRelayMessageEncoding.Inputs.Pack(sentMsg.Target, sentMsg.Sender, sentMsg.Message, sentMsg.MessageNonce)
 		if err != nil {
-			return common.Hash{}, err
+			return nil, err
 		}
-		msgBytes := append(legacyCrossDomainMessengerRelayMessageMethod.ID, inputBytes...)
-		return crypto.Keccak256Hash(msgBytes), nil
+		return append(CrossDomainMessengerLegacyRelayMessageEncoding.ID, inputBytes...), 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 nil, err
 		}
-		return crypto.Keccak256Hash(msgBytes), nil
+		return msgBytes, nil
 	}
 
-	return common.Hash{}, fmt.Errorf("unsupported cross domain messenger version: %d", version)
+	return nil, fmt.Errorf("unsupported cross domain messenger version: %d", version)
 }
diff --git a/indexer/processors/contracts/legacy_ctc.go b/indexer/processors/contracts/legacy_ctc.go
new file mode 100644
index 000000000..b34c35ef8
--- /dev/null
+++ b/indexer/processors/contracts/legacy_ctc.go
@@ -0,0 +1,58 @@
+package contracts
+
+import (
+	"math/big"
+
+	"github.com/ethereum-optimism/optimism/indexer/bigint"
+	"github.com/ethereum-optimism/optimism/indexer/database"
+	legacy_bindings "github.com/ethereum-optimism/optimism/op-bindings/legacy-bindings"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+)
+
+type LegacyCTCDepositEvent struct {
+	Event    *database.ContractEvent
+	Tx       database.Transaction
+	TxHash   common.Hash
+	GasLimit *big.Int
+}
+
+func LegacyCTCDepositEvents(contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]LegacyCTCDepositEvent, error) {
+	ctcAbi, err := legacy_bindings.CanonicalTransactionChainMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	transactionEnqueuedEventAbi := ctcAbi.Events["TransactionEnqueued"]
+	contractEventFilter := database.ContractEvent{ContractAddress: contractAddress, EventSignature: transactionEnqueuedEventAbi.ID}
+	events, err := db.ContractEvents.L1ContractEventsWithFilter(contractEventFilter, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	ctcTxDeposits := make([]LegacyCTCDepositEvent, len(events))
+	for i := range events {
+		txEnqueued := legacy_bindings.CanonicalTransactionChainTransactionEnqueued{Raw: *events[i].RLPLog}
+		err = UnpackLog(&txEnqueued, events[i].RLPLog, transactionEnqueuedEventAbi.Name, ctcAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		// Enqueued Deposits do not carry a `msg.value` amount. ETH is only minted on L2 via the L1StandardBrige
+		ctcTxDeposits[i] = LegacyCTCDepositEvent{
+			Event:    &events[i].ContractEvent,
+			GasLimit: txEnqueued.GasLimit,
+			TxHash:   types.NewTransaction(0, txEnqueued.Target, bigint.Zero, txEnqueued.GasLimit.Uint64(), nil, txEnqueued.Data).Hash(),
+			Tx: database.Transaction{
+				FromAddress: txEnqueued.L1TxOrigin,
+				ToAddress:   txEnqueued.Target,
+				Amount:      bigint.Zero,
+				Data:        txEnqueued.Data,
+				Timestamp:   events[i].Timestamp,
+			},
+		}
+	}
+
+	return ctcTxDeposits, nil
+}
diff --git a/indexer/processors/contracts/legacy_standard_bridge.go b/indexer/processors/contracts/legacy_standard_bridge.go
new file mode 100644
index 000000000..aae9fefa7
--- /dev/null
+++ b/indexer/processors/contracts/legacy_standard_bridge.go
@@ -0,0 +1,119 @@
+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 LegacyBridgeEvent struct {
+	Event          *database.ContractEvent
+	BridgeTransfer database.BridgeTransfer
+}
+
+func L1StandardBridgeLegacyDepositInitiatedEvents(contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]LegacyBridgeEvent, error) {
+	l1StandardBridgeAbi, err := bindings.L1StandardBridgeMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	// The L1StandardBridge contains the legacy events
+	ethDepositEventAbi := l1StandardBridgeAbi.Events["ETHDepositInitiated"]
+	erc20DepositEventAbi := l1StandardBridgeAbi.Events["ERC20DepositInitiated"]
+
+	// Grab both ETH & ERC20 Events
+	ethDepositEvents, err := db.ContractEvents.L1ContractEventsWithFilter(database.ContractEvent{ContractAddress: contractAddress, EventSignature: ethDepositEventAbi.ID}, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+	erc20DepositEvents, err := db.ContractEvents.L1ContractEventsWithFilter(database.ContractEvent{ContractAddress: contractAddress, EventSignature: erc20DepositEventAbi.ID}, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	// Represent the ETH deposits via the ETH ERC20 predeploy address
+	deposits := make([]LegacyBridgeEvent, len(ethDepositEvents)+len(erc20DepositEvents))
+	for i := range ethDepositEvents {
+		bridgeEvent := bindings.L1StandardBridgeETHDepositInitiated{Raw: *ethDepositEvents[i].RLPLog}
+		err := UnpackLog(&bridgeEvent, &bridgeEvent.Raw, ethDepositEventAbi.Name, l1StandardBridgeAbi)
+		if err != nil {
+			return nil, err
+		}
+		deposits[i] = LegacyBridgeEvent{
+			Event: &ethDepositEvents[i].ContractEvent,
+			BridgeTransfer: database.BridgeTransfer{
+				TokenPair: database.ETHTokenPair,
+				Tx: database.Transaction{
+					FromAddress: bridgeEvent.From,
+					ToAddress:   bridgeEvent.To,
+					Amount:      bridgeEvent.Amount,
+					Data:        bridgeEvent.ExtraData,
+					Timestamp:   ethDepositEvents[i].Timestamp,
+				},
+			},
+		}
+	}
+	for i := range erc20DepositEvents {
+		bridgeEvent := bindings.L1StandardBridgeERC20DepositInitiated{Raw: *erc20DepositEvents[i].RLPLog}
+		err := UnpackLog(&bridgeEvent, &bridgeEvent.Raw, erc20DepositEventAbi.Name, l1StandardBridgeAbi)
+		if err != nil {
+			return nil, err
+		}
+		deposits[len(ethDepositEvents)+i] = LegacyBridgeEvent{
+			Event: &erc20DepositEvents[i].ContractEvent,
+			BridgeTransfer: database.BridgeTransfer{
+				TokenPair: database.TokenPair{LocalTokenAddress: bridgeEvent.L1Token, RemoteTokenAddress: bridgeEvent.L2Token},
+				Tx: database.Transaction{
+					FromAddress: bridgeEvent.From,
+					ToAddress:   bridgeEvent.To,
+					Amount:      bridgeEvent.Amount,
+					Data:        bridgeEvent.ExtraData,
+					Timestamp:   erc20DepositEvents[i].Timestamp,
+				},
+			},
+		}
+	}
+
+	return deposits, nil
+}
+
+func L2StandardBridgeLegacyWithdrawalInitiatedEvents(contractAddress common.Address, db *database.DB, fromHeight, toHeight *big.Int) ([]LegacyBridgeEvent, error) {
+	l2StandardBridgeAbi, err := bindings.L2StandardBridgeMetaData.GetAbi()
+	if err != nil {
+		return nil, err
+	}
+
+	withdrawalInitiatedEventAbi := l2StandardBridgeAbi.Events["WithdrawalInitiated"]
+	withdrawalEvents, err := db.ContractEvents.L2ContractEventsWithFilter(database.ContractEvent{ContractAddress: contractAddress, EventSignature: withdrawalInitiatedEventAbi.ID}, fromHeight, toHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	withdrawals := make([]LegacyBridgeEvent, len(withdrawalEvents))
+	for i := range withdrawalEvents {
+		bridgeEvent := bindings.L2StandardBridgeWithdrawalInitiated{Raw: *withdrawalEvents[i].RLPLog}
+		err := UnpackLog(&bridgeEvent, &bridgeEvent.Raw, withdrawalInitiatedEventAbi.Name, l2StandardBridgeAbi)
+		if err != nil {
+			return nil, err
+		}
+
+		withdrawals[i] = LegacyBridgeEvent{
+			Event: &withdrawalEvents[i].ContractEvent,
+			BridgeTransfer: database.BridgeTransfer{
+				TokenPair: database.ETHTokenPair,
+				Tx: database.Transaction{
+					FromAddress: bridgeEvent.From,
+					ToAddress:   bridgeEvent.To,
+					Amount:      bridgeEvent.Amount,
+					Data:        bridgeEvent.ExtraData,
+					Timestamp:   withdrawalEvents[i].Timestamp,
+				},
+			},
+		}
+	}
+
+	return withdrawals, nil
+}


Test patch:
diff --git a/indexer/config/config_test.go b/indexer/config/config_test.go
index c81db45df..681fd3c8a 100644
--- a/indexer/config/config_test.go
+++ b/indexer/config/config_test.go
@@ -192,9 +192,11 @@ func Test_AsSliceSuccess(t *testing.T) {
 
 	slice, err := testCfg.AsSlice()
 	require.NoError(t, err)
-	require.Equal(t, len(slice), 4)
+	require.Equal(t, len(slice), 5)
 	require.Equal(t, slice[0].String(), testCfg.OptimismPortalProxy.String())
 	require.Equal(t, slice[1].String(), testCfg.L2OutputOracleProxy.String())
 	require.Equal(t, slice[2].String(), testCfg.L1CrossDomainMessengerProxy.String())
 	require.Equal(t, slice[3].String(), testCfg.L1StandardBridgeProxy.String())
+
+	// LegacyCanonicalTransactionChain is the 4th slot
 }
diff --git a/indexer/e2e_tests/utils/cross_domain_messages.go b/indexer/e2e_tests/utils/cross_domain_messages.go
index e1d4d9279..eb30b072b 100644
--- a/indexer/e2e_tests/utils/cross_domain_messages.go
+++ b/indexer/e2e_tests/utils/cross_domain_messages.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/crypto"
 )
 
 type CrossDomainMessengerSentMessage struct {
@@ -57,5 +58,10 @@ func CrossDomainMessengerSentMessageHash(sentMessage *bindings.CrossDomainMessen
 		return common.Hash{}, err
 	}
 
-	return contracts.CrossDomainMessageHash(abi, sentMessage, value)
+	calldata, err := contracts.CrossDomainMessageCalldata(abi, sentMessage, value)
+	if err != nil {
+		return common.Hash{}, err
+	}
+
+	return crypto.Keccak256Hash(calldata), nil
 }