l1_processor.go 13 KB
Newer Older
1 2 3
package processor

import (
4
	"context"
5
	"encoding/hex"
6
	"errors"
7
	"math/big"
8 9
	"reflect"

10 11
	"github.com/google/uuid"

12 13
	"github.com/ethereum-optimism/optimism/indexer/database"
	"github.com/ethereum-optimism/optimism/indexer/node"
14 15
	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
	legacy_bindings "github.com/ethereum-optimism/optimism/op-bindings/legacy-bindings"
16

17
	"github.com/ethereum/go-ethereum"
18
	"github.com/ethereum/go-ethereum/accounts/abi"
19
	"github.com/ethereum/go-ethereum/common"
20
	"github.com/ethereum/go-ethereum/core/types"
21
	"github.com/ethereum/go-ethereum/ethclient"
22 23 24
	"github.com/ethereum/go-ethereum/log"
)

25 26 27 28 29 30 31 32
type L1Contracts struct {
	OptimismPortal         common.Address
	L2OutputOracle         common.Address
	L1CrossDomainMessenger common.Address
	L1StandardBridge       common.Address
	L1ERC721Bridge         common.Address

	// Some more contracts -- ProxyAdmin, SystemConfig, etcc
33
	// Ignore the auxiliary contracts?
34 35 36 37 38

	// Legacy contracts? We'll add this in to index the legacy chain.
	// Remove afterwards?
}

39 40 41 42 43
type checkpointAbi struct {
	l2OutputOracle             *abi.ABI
	legacyStateCommitmentChain *abi.ABI
}

Hamdi Allam's avatar
Hamdi Allam committed
44
func (c L1Contracts) toSlice() []common.Address {
45 46 47 48 49 50 51 52 53 54 55
	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
}

56 57 58 59
type L1Processor struct {
	processor
}

60
func NewL1Processor(ethClient node.EthClient, db *database.DB, l1Contracts L1Contracts) (*L1Processor, error) {
61
	l1ProcessLog := log.New("processor", "l1")
62
	l1ProcessLog.Info("initializing processor")
63

64 65 66 67 68 69 70 71 72 73 74 75 76
	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()
77 78 79 80 81 82
	if err != nil {
		return nil, err
	}

	var fromL1Header *types.Header
	if latestHeader != nil {
83
		l1ProcessLog.Info("detected last indexed block", "height", latestHeader.Number.Int, "hash", latestHeader.Hash)
84 85
		l1Header, err := ethClient.BlockHeaderByHash(latestHeader.Hash)
		if err != nil {
86
			l1ProcessLog.Error("unable to fetch header for last indexed block", "hash", latestHeader.Hash, "err", err)
87 88 89 90 91
			return nil, err
		}

		fromL1Header = l1Header
	} else {
92
		// we shouldn't start from genesis with l1. Need a "genesis" L1 height provided for the rollup
93 94 95 96 97 98
		l1ProcessLog.Info("no indexed state, starting from genesis")
		fromL1Header = nil
	}

	l1Processor := &L1Processor{
		processor: processor{
99
			headerTraversal: node.NewHeaderTraversal(ethClient, fromL1Header),
100 101 102
			db:              db,
			processFn:       l1ProcessFn(l1ProcessLog, ethClient, l1Contracts, checkpointAbi),
			processLog:      l1ProcessLog,
103 104 105 106 107 108
		},
	}

	return l1Processor, nil
}

109
func l1ProcessFn(processLog log.Logger, ethClient node.EthClient, l1Contracts L1Contracts, checkpointAbi checkpointAbi) ProcessFn {
110 111
	rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())

Hamdi Allam's avatar
Hamdi Allam committed
112
	contractAddrs := l1Contracts.toSlice()
113 114
	processLog.Info("processor configured with contracts", "contracts", l1Contracts)

115 116 117
	outputProposedEventSig := checkpointAbi.l2OutputOracle.Events["OutputProposed"].ID
	legacyStateBatchAppendedEventSig := checkpointAbi.legacyStateCommitmentChain.Events["StateBatchAppended"].ID

118
	return func(db *database.DB, headers []*types.Header) error {
119
		numHeaders := len(headers)
120
		headerMap := make(map[common.Hash]*types.Header)
121
		for _, header := range headers {
122
			headerMap[header.Hash()] = header
123 124
		}

Hamdi Allam's avatar
Hamdi Allam committed
125
		/** Watch for all Optimism Contract Events **/
126 127

		logFilter := ethereum.FilterQuery{FromBlock: headers[0].Number, ToBlock: headers[numHeaders-1].Number, Addresses: contractAddrs}
Hamdi Allam's avatar
Hamdi Allam committed
128
		logs, err := rawEthClient.FilterLogs(context.Background(), logFilter) // []types.Log
129
		if err != nil {
130
			return err
131 132
		}

Hamdi Allam's avatar
Hamdi Allam committed
133
		// L2 checkpoints posted on L1
134 135 136
		outputProposals := []*database.OutputProposal{}
		legacyStateBatches := []*database.LegacyStateBatch{}

137
		l1HeadersOfInterest := make(map[common.Hash]bool)
Hamdi Allam's avatar
Hamdi Allam committed
138 139 140
		l1ContractEvents := make([]*database.L1ContractEvent, len(logs))

		processedContractEvents := NewProcessedContractEvents()
141
		for i, log := range logs {
142
			header, ok := headerMap[log.BlockHash]
143
			if !ok {
144
				processLog.Error("contract event found with associated header not in the batch", "header", log.BlockHash, "log_index", log.Index)
145
				return errors.New("parsed log with a block hash not in this batch")
146 147
			}

Hamdi Allam's avatar
Hamdi Allam committed
148
			contractEvent := processedContractEvents.AddLog(&logs[i], header.Time)
149
			l1HeadersOfInterest[log.BlockHash] = true
Hamdi Allam's avatar
Hamdi Allam committed
150
			l1ContractEvents[i] = &database.L1ContractEvent{ContractEvent: *contractEvent}
151 152 153 154 155 156

			// Track Checkpoint Events for L2
			switch contractEvent.EventSignature {
			case outputProposedEventSig:
				if len(log.Topics) != 4 {
					processLog.Error("parsed unexpected number of L2OutputOracle#OutputProposed log topics", "log_topics", log.Topics)
157
					return errors.New("parsed unexpected OutputProposed event")
158 159 160 161 162 163 164 165 166 167 168 169 170
				}

				outputProposals = append(outputProposals, &database.OutputProposal{
					OutputRoot:          log.Topics[1],
					L2BlockNumber:       database.U256{Int: new(big.Int).SetBytes(log.Topics[2].Bytes())},
					L1ContractEventGUID: contractEvent.GUID,
				})

			case legacyStateBatchAppendedEventSig:
				var stateBatchAppended legacy_bindings.StateCommitmentChainStateBatchAppended
				err := checkpointAbi.l2OutputOracle.UnpackIntoInterface(&stateBatchAppended, "StateBatchAppended", log.Data)
				if err != nil || len(log.Topics) != 2 {
					processLog.Error("unexpected StateCommitmentChain#StateBatchAppended log data or log topics", "log_topics", log.Topics, "log_data", hex.EncodeToString(log.Data), "err", err)
171
					return err
172 173 174 175 176 177 178 179 180 181
				}

				legacyStateBatches = append(legacyStateBatches, &database.LegacyStateBatch{
					Index:               new(big.Int).SetBytes(log.Topics[1].Bytes()).Uint64(),
					Root:                stateBatchAppended.BatchRoot,
					Size:                stateBatchAppended.BatchSize.Uint64(),
					PrevTotal:           stateBatchAppended.PrevTotalElements.Uint64(),
					L1ContractEventGUID: contractEvent.GUID,
				})
			}
182 183
		}

184
		/** Aggregate applicable L1 Blocks **/
185

186 187
		// 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
Hamdi Allam's avatar
Hamdi Allam committed
188
		indexedL1Headers := []*database.L1BlockHeader{}
189
		for _, header := range headers {
Hamdi Allam's avatar
Hamdi Allam committed
190 191
			_, hasLogs := l1HeadersOfInterest[header.Hash()]
			if !hasLogs {
192 193 194
				continue
			}

195
			indexedL1Headers = append(indexedL1Headers, &database.L1BlockHeader{BlockHeader: database.BlockHeaderFromGethHeader(header)})
196 197
		}

198 199
		/** Update Database **/

Hamdi Allam's avatar
Hamdi Allam committed
200 201
		numIndexedL1Headers := len(indexedL1Headers)
		if numIndexedL1Headers > 0 {
Hamdi Allam's avatar
Hamdi Allam committed
202
			processLog.Info("saving l1 blocks with optimism logs", "size", numIndexedL1Headers, "batch_size", numHeaders)
Hamdi Allam's avatar
Hamdi Allam committed
203 204 205 206
			err = db.Blocks.StoreL1BlockHeaders(indexedL1Headers)
			if err != nil {
				return err
			}
207

Hamdi Allam's avatar
Hamdi Allam committed
208
			// Since the headers to index are derived from the existence of logs, we know in this branch `numLogs > 0`
Hamdi Allam's avatar
Hamdi Allam committed
209
			processLog.Info("detected contract logs", "size", len(l1ContractEvents))
Hamdi Allam's avatar
Hamdi Allam committed
210 211 212 213
			err = db.ContractEvents.StoreL1ContractEvents(l1ContractEvents)
			if err != nil {
				return err
			}
214

Hamdi Allam's avatar
Hamdi Allam committed
215 216 217 218 219 220 221
			// 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)
			}
222

Hamdi Allam's avatar
Hamdi Allam committed
223 224 225 226 227 228 229 230 231
			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
				}
			}
232

Hamdi Allam's avatar
Hamdi Allam committed
233
			// forward along contract events to the bridge processor
Hamdi Allam's avatar
Hamdi Allam committed
234
			err = l1BridgeProcessContractEvents(processLog, db, ethClient, processedContractEvents, l1Contracts)
235
			if err != nil {
236
				return err
237
			}
Hamdi Allam's avatar
Hamdi Allam committed
238 239
		} else {
			processLog.Info("no l1 blocks of interest within batch")
240 241
		}

242
		// a-ok!
243
		return nil
244 245
	}
}
Hamdi Allam's avatar
Hamdi Allam committed
246

247
func l1BridgeProcessContractEvents(processLog log.Logger, db *database.DB, ethClient node.EthClient, events *ProcessedContractEvents, l1Contracts L1Contracts) error {
248 249 250 251
	rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())

	// Process New Deposits
	initiatedDepositEvents, err := StandardBridgeInitiatedEvents(events)
Hamdi Allam's avatar
Hamdi Allam committed
252 253 254 255
	if err != nil {
		return err
	}

256 257 258
	deposits := make([]*database.Deposit, len(initiatedDepositEvents))
	for i, initiatedBridgeEvent := range initiatedDepositEvents {
		deposits[i] = &database.Deposit{
Hamdi Allam's avatar
Hamdi Allam committed
259 260 261 262 263 264 265 266 267 268 269
			GUID:                 uuid.New(),
			InitiatedL1EventGUID: initiatedBridgeEvent.RawEvent.GUID,
			SentMessageNonce:     database.U256{Int: initiatedBridgeEvent.CrossDomainMessengerNonce},
			TokenPair:            database.TokenPair{L1TokenAddress: initiatedBridgeEvent.LocalToken, L2TokenAddress: initiatedBridgeEvent.RemoteToken},
			Tx: database.Transaction{
				FromAddress: initiatedBridgeEvent.From,
				ToAddress:   initiatedBridgeEvent.To,
				Amount:      database.U256{Int: initiatedBridgeEvent.Amount},
				Data:        initiatedBridgeEvent.ExtraData,
				Timestamp:   initiatedBridgeEvent.RawEvent.Timestamp,
			},
Hamdi Allam's avatar
Hamdi Allam committed
270 271 272
		}
	}

273 274 275
	if len(deposits) > 0 {
		processLog.Info("detected L1StandardBridge deposits", "num", len(deposits))
		err := db.Bridge.StoreDeposits(deposits)
Hamdi Allam's avatar
Hamdi Allam committed
276 277 278
		if err != nil {
			return err
		}
Hamdi Allam's avatar
Hamdi Allam committed
279 280
	}

281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
	// Prove L2 Withdrawals
	provenWithdrawalEvents, err := OptimismPortalWithdrawalProvenEvents(events)
	if err != nil {
		return err
	}

	// we manually keep track since not every proven withdrawal is a standard bridge withdrawal
	numProvenWithdrawals := 0
	for _, provenWithdrawalEvent := range provenWithdrawalEvents {
		withdrawalHash := provenWithdrawalEvent.WithdrawalHash
		withdrawal, err := db.Bridge.WithdrawalByHash(withdrawalHash)
		if err != nil {
			return err
		}

		// Check if the L2Processor is behind or really has missed an event. We can compare against the
		// OptimismPortal#ProvenWithdrawal on-chain mapping relative to the latest indexed L2 height
		if withdrawal == nil {
299 300
			bridgeAddress := l1Contracts.L1StandardBridge
			portalAddress := l1Contracts.OptimismPortal
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
			if provenWithdrawalEvent.From != bridgeAddress || provenWithdrawalEvent.To != bridgeAddress {
				// non-bridge withdrawal
				continue
			}

			// Query for the the proven withdrawal on-chain
			provenWithdrawal, err := OptimismPortalQueryProvenWithdrawal(rawEthClient, portalAddress, withdrawalHash)
			if err != nil {
				return err
			}

			latestL2Header, err := db.Blocks.LatestL2BlockHeader()
			if err != nil {
				return err
			}

			if latestL2Header == nil || provenWithdrawal.L2OutputIndex.Cmp(latestL2Header.Number.Int) > 0 {
				processLog.Warn("behind on indexed L2 withdrawals")
				return errors.New("waiting for L2Processor to catch up")
			} else {
				processLog.Crit("missing indexed withdrawal for this proven event")
				return errors.New("missing withdrawal message")
			}
		}

		err = db.Bridge.MarkProvenWithdrawalEvent(withdrawal.GUID, provenWithdrawalEvent.RawEvent.GUID)
		if err != nil {
			return err
		}

		numProvenWithdrawals++
	}

	if numProvenWithdrawals > 0 {
		processLog.Info("proven L2StandardBridge withdrawals", "size", numProvenWithdrawals)
	}

	// Finalize Pending Withdrawals
	finalizedWithdrawalEvents, err := StandardBridgeFinalizedEvents(rawEthClient, events)
	if err != nil {
		return err
	}

	for _, finalizedBridgeEvent := range finalizedWithdrawalEvents {
		nonce := finalizedBridgeEvent.CrossDomainMessengerNonce
		withdrawal, err := db.Bridge.WithdrawalByMessageNonce(nonce)
		if err != nil {
			processLog.Error("error querying associated withdrawal messsage using nonce", "cross_domain_messenger_nonce", nonce)
			return err
		}

		// Since we have to prove the event on-chain first, we don't need to check if the processor is
		// behind. we're definitely in an error state if we cannot find the withdrawal when parsing this even
		if withdrawal == nil {
			processLog.Crit("missing indexed withdrawal for this finalization event")
			return errors.New("missing withdrawal message")
		}

		err = db.Bridge.MarkFinalizedWithdrawalEvent(withdrawal.GUID, finalizedBridgeEvent.RawEvent.GUID)
		if err != nil {
			processLog.Error("error finalizing withdrawal", "err", err)
			return err
		}
	}

	if len(finalizedWithdrawalEvents) > 0 {
		processLog.Info("finalized L2StandardBridge withdrawals", "num", len(finalizedWithdrawalEvents))
	}

	// a-ok!
Hamdi Allam's avatar
Hamdi Allam committed
371 372
	return nil
}