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

import (
4 5 6 7
	"context"
	"errors"
	"reflect"

8 9
	"github.com/google/uuid"

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

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

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

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

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

37 38 39 40 41 42 43 44
func DevL1Contracts() L1Contracts {
	return L1Contracts{
		OptimismPortal:         common.HexToAddress("0x6900000000000000000000000000000000000000"),
		L2OutputOracle:         common.HexToAddress("0x6900000000000000000000000000000000000001"),
		L1CrossDomainMessenger: common.HexToAddress("0x6900000000000000000000000000000000000002"),
		L1StandardBridge:       common.HexToAddress("0x6900000000000000000000000000000000000003"),
		L1ERC721Bridge:         common.HexToAddress("0x6900000000000000000000000000000000000004"),
	}
45 46
}

47
func (c L1Contracts) ToSlice() []common.Address {
48 49 50 51 52 53 54 55 56 57 58
	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
}

59 60 61 62 63
type checkpointAbi struct {
	l2OutputOracle             *abi.ABI
	legacyStateCommitmentChain *abi.ABI
}

64 65 66 67
type L1Processor struct {
	processor
}

68 69
func NewL1Processor(logger log.Logger, ethClient node.EthClient, db *database.DB, l1Contracts L1Contracts) (*L1Processor, error) {
	l1ProcessLog := logger.New("processor", "l1")
70
	l1ProcessLog.Info("initializing processor")
71

72 73 74 75 76 77 78 79 80 81 82 83 84
	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()
85 86 87 88 89 90
	if err != nil {
		return nil, err
	}

	var fromL1Header *types.Header
	if latestHeader != nil {
91
		l1ProcessLog.Info("detected last indexed block", "height", latestHeader.Number.Int, "hash", latestHeader.Hash)
92 93
		l1Header, err := ethClient.BlockHeaderByHash(latestHeader.Hash)
		if err != nil {
94
			l1ProcessLog.Error("unable to fetch header for last indexed block", "hash", latestHeader.Hash, "err", err)
95 96 97 98 99
			return nil, err
		}

		fromL1Header = l1Header
	} else {
100
		// we shouldn't start from genesis with l1. Need a "genesis" L1 height provided for the rollup
101 102 103 104 105 106
		l1ProcessLog.Info("no indexed state, starting from genesis")
		fromL1Header = nil
	}

	l1Processor := &L1Processor{
		processor: processor{
107
			headerTraversal: node.NewHeaderTraversal(ethClient, fromL1Header),
108 109 110
			db:              db,
			processFn:       l1ProcessFn(l1ProcessLog, ethClient, l1Contracts, checkpointAbi),
			processLog:      l1ProcessLog,
111 112 113 114 115 116
		},
	}

	return l1Processor, nil
}

117
func l1ProcessFn(processLog log.Logger, ethClient node.EthClient, l1Contracts L1Contracts, checkpointAbi checkpointAbi) ProcessFn {
118 119
	rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())

120
	contractAddrs := l1Contracts.ToSlice()
121 122
	processLog.Info("processor configured with contracts", "contracts", l1Contracts)

123 124 125 126 127
	outputProposedEventName := "OutputProposed"
	outputProposedEventSig := checkpointAbi.l2OutputOracle.Events[outputProposedEventName].ID

	legacyStateBatchAppendedEventName := "StateBatchAppended"
	legacyStateBatchAppendedEventSig := checkpointAbi.legacyStateCommitmentChain.Events[legacyStateBatchAppendedEventName].ID
128

129
	return func(db *database.DB, headers []*types.Header) error {
130
		headerMap := make(map[common.Hash]*types.Header)
131
		for _, header := range headers {
132
			headerMap[header.Hash()] = header
133 134
		}

Hamdi Allam's avatar
Hamdi Allam committed
135
		/** Watch for all Optimism Contract Events **/
136

137
		logFilter := ethereum.FilterQuery{FromBlock: headers[0].Number, ToBlock: headers[len(headers)-1].Number, Addresses: contractAddrs}
Hamdi Allam's avatar
Hamdi Allam committed
138
		logs, err := rawEthClient.FilterLogs(context.Background(), logFilter) // []types.Log
139
		if err != nil {
140
			return err
141 142
		}

Hamdi Allam's avatar
Hamdi Allam committed
143
		// L2 checkpoints posted on L1
144 145 146
		outputProposals := []*database.OutputProposal{}
		legacyStateBatches := []*database.LegacyStateBatch{}

147
		l1HeadersOfInterest := make(map[common.Hash]bool)
Hamdi Allam's avatar
Hamdi Allam committed
148 149 150
		l1ContractEvents := make([]*database.L1ContractEvent, len(logs))

		processedContractEvents := NewProcessedContractEvents()
151 152
		for i := range logs {
			log := &logs[i]
153
			header, ok := headerMap[log.BlockHash]
154
			if !ok {
155
				processLog.Error("contract event found with associated header not in the batch", "header", log.BlockHash, "log_index", log.Index)
156
				return errors.New("parsed log with a block hash not in this batch")
157 158
			}

159
			contractEvent := processedContractEvents.AddLog(log, header.Time)
160
			l1HeadersOfInterest[log.BlockHash] = true
Hamdi Allam's avatar
Hamdi Allam committed
161
			l1ContractEvents[i] = &database.L1ContractEvent{ContractEvent: *contractEvent}
162 163 164 165

			// Track Checkpoint Events for L2
			switch contractEvent.EventSignature {
			case outputProposedEventSig:
166 167 168 169
				var outputProposed bindings.L2OutputOracleOutputProposed
				err := UnpackLog(&outputProposed, log, outputProposedEventName, checkpointAbi.l2OutputOracle)
				if err != nil {
					return err
170 171 172
				}

				outputProposals = append(outputProposals, &database.OutputProposal{
173 174 175
					OutputRoot:          outputProposed.OutputRoot,
					L2OutputIndex:       database.U256{Int: outputProposed.L2OutputIndex},
					L2BlockNumber:       database.U256{Int: outputProposed.L2BlockNumber},
176 177 178 179 180
					L1ContractEventGUID: contractEvent.GUID,
				})

			case legacyStateBatchAppendedEventSig:
				var stateBatchAppended legacy_bindings.StateCommitmentChainStateBatchAppended
181 182
				err := UnpackLog(&stateBatchAppended, log, legacyStateBatchAppendedEventName, checkpointAbi.legacyStateCommitmentChain)
				if err != nil {
183
					return err
184 185 186
				}

				legacyStateBatches = append(legacyStateBatches, &database.LegacyStateBatch{
187
					Index:               stateBatchAppended.BatchIndex.Uint64(),
188 189 190 191 192 193
					Root:                stateBatchAppended.BatchRoot,
					Size:                stateBatchAppended.BatchSize.Uint64(),
					PrevTotal:           stateBatchAppended.PrevTotalElements.Uint64(),
					L1ContractEventGUID: contractEvent.GUID,
				})
			}
194 195
		}

196
		/** Aggregate applicable L1 Blocks **/
197

198 199
		// 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
200
		indexedL1Headers := []*database.L1BlockHeader{}
201
		for _, header := range headers {
Hamdi Allam's avatar
Hamdi Allam committed
202 203
			_, hasLogs := l1HeadersOfInterest[header.Hash()]
			if !hasLogs {
204 205 206
				continue
			}

207
			indexedL1Headers = append(indexedL1Headers, &database.L1BlockHeader{BlockHeader: database.BlockHeaderFromGethHeader(header)})
208 209
		}

210 211
		/** Update Database **/

Hamdi Allam's avatar
Hamdi Allam committed
212 213
		numIndexedL1Headers := len(indexedL1Headers)
		if numIndexedL1Headers > 0 {
214
			processLog.Info("saving l1 blocks with optimism logs", "size", numIndexedL1Headers, "batch_size", len(headers))
Hamdi Allam's avatar
Hamdi Allam committed
215 216 217 218
			err = db.Blocks.StoreL1BlockHeaders(indexedL1Headers)
			if err != nil {
				return err
			}
219

Hamdi Allam's avatar
Hamdi Allam committed
220
			// 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
221
			processLog.Info("detected contract logs", "size", len(l1ContractEvents))
Hamdi Allam's avatar
Hamdi Allam committed
222 223 224 225
			err = db.ContractEvents.StoreL1ContractEvents(l1ContractEvents)
			if err != nil {
				return err
			}
226

Hamdi Allam's avatar
Hamdi Allam committed
227 228 229 230 231 232 233
			// 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)
			}
234

Hamdi Allam's avatar
Hamdi Allam committed
235 236 237 238 239 240 241 242 243
			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
				}
			}
244

Hamdi Allam's avatar
Hamdi Allam committed
245
			// forward along contract events to the bridge processor
Hamdi Allam's avatar
Hamdi Allam committed
246
			err = l1BridgeProcessContractEvents(processLog, db, ethClient, processedContractEvents, l1Contracts)
247
			if err != nil {
248
				return err
249
			}
Hamdi Allam's avatar
Hamdi Allam committed
250 251
		} else {
			processLog.Info("no l1 blocks of interest within batch")
252 253
		}

254
		// a-ok!
255
		return nil
256 257
	}
}
Hamdi Allam's avatar
Hamdi Allam committed
258

259
func l1BridgeProcessContractEvents(processLog log.Logger, db *database.DB, ethClient node.EthClient, events *ProcessedContractEvents, l1Contracts L1Contracts) error {
260 261 262 263
	rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())

	// Process New Deposits
	initiatedDepositEvents, err := StandardBridgeInitiatedEvents(events)
Hamdi Allam's avatar
Hamdi Allam committed
264 265 266 267
	if err != nil {
		return err
	}

268 269 270
	deposits := make([]*database.Deposit, len(initiatedDepositEvents))
	for i, initiatedBridgeEvent := range initiatedDepositEvents {
		deposits[i] = &database.Deposit{
Hamdi Allam's avatar
Hamdi Allam committed
271 272 273 274 275 276 277 278 279 280 281
			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
282 283 284
		}
	}

285 286 287
	if len(deposits) > 0 {
		processLog.Info("detected L1StandardBridge deposits", "num", len(deposits))
		err := db.Bridge.StoreDeposits(deposits)
Hamdi Allam's avatar
Hamdi Allam committed
288 289 290
		if err != nil {
			return err
		}
Hamdi Allam's avatar
Hamdi Allam committed
291 292
	}

293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
	// 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 {
311 312

			// This needs to be updated to read from config as well as correctly identify if the CrossDomainMessenger message is a standard
Hamdi Allam's avatar
Hamdi Allam committed
313
			// bridge message. This will easier to do once we index passed messages separately which will include the right To/From fields
314
			if provenWithdrawalEvent.From != common.HexToAddress("0x4200000000000000000000000000000000000007") || provenWithdrawalEvent.To != l1Contracts.L1CrossDomainMessenger {
315 316 317 318 319
				// non-bridge withdrawal
				continue
			}

			// Query for the the proven withdrawal on-chain
320
			provenWithdrawal, err := OptimismPortalQueryProvenWithdrawal(rawEthClient, l1Contracts.OptimismPortal, withdrawalHash)
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
			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
		}

365 366
		// 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 event
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
		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
384 385
	return nil
}