l1_processor.go 13.2 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
func (c L1Contracts) ToSlice() []common.Address {
38 39 40 41 42 43 44 45 46 47 48
	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
}

49 50 51 52 53
type checkpointAbi struct {
	l2OutputOracle             *abi.ABI
	legacyStateCommitmentChain *abi.ABI
}

54 55 56 57
type L1Processor struct {
	processor
}

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

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

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

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

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

	return l1Processor, nil
}

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

110
	contractAddrs := l1Contracts.ToSlice()
111 112
	processLog.Info("processor configured with contracts", "contracts", l1Contracts)

113 114 115 116 117
	outputProposedEventName := "OutputProposed"
	outputProposedEventSig := checkpointAbi.l2OutputOracle.Events[outputProposedEventName].ID

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

119
	return func(db *database.DB, headers []*types.Header) error {
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[len(headers)-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 142
		for i := range logs {
			log := &logs[i]
143
			header, ok := headerMap[log.BlockHash]
144
			if !ok {
145
				processLog.Error("contract event found with associated header not in the batch", "header", log.BlockHash, "log_index", log.Index)
146
				return errors.New("parsed log with a block hash not in this batch")
147 148
			}

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

			// Track Checkpoint Events for L2
			switch contractEvent.EventSignature {
			case outputProposedEventSig:
156 157 158 159
				var outputProposed bindings.L2OutputOracleOutputProposed
				err := UnpackLog(&outputProposed, log, outputProposedEventName, checkpointAbi.l2OutputOracle)
				if err != nil {
					return err
160 161 162
				}

				outputProposals = append(outputProposals, &database.OutputProposal{
163 164 165
					OutputRoot:          outputProposed.OutputRoot,
					L2OutputIndex:       database.U256{Int: outputProposed.L2OutputIndex},
					L2BlockNumber:       database.U256{Int: outputProposed.L2BlockNumber},
166 167 168 169 170
					L1ContractEventGUID: contractEvent.GUID,
				})

			case legacyStateBatchAppendedEventSig:
				var stateBatchAppended legacy_bindings.StateCommitmentChainStateBatchAppended
171 172
				err := UnpackLog(&stateBatchAppended, log, legacyStateBatchAppendedEventName, checkpointAbi.legacyStateCommitmentChain)
				if err != nil {
173
					return err
174 175 176
				}

				legacyStateBatches = append(legacyStateBatches, &database.LegacyStateBatch{
177
					Index:               stateBatchAppended.BatchIndex.Uint64(),
178 179 180 181 182 183
					Root:                stateBatchAppended.BatchRoot,
					Size:                stateBatchAppended.BatchSize.Uint64(),
					PrevTotal:           stateBatchAppended.PrevTotalElements.Uint64(),
					L1ContractEventGUID: contractEvent.GUID,
				})
			}
184 185
		}

186
		/** Aggregate applicable L1 Blocks **/
187

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

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

200 201
		/** Update Database **/

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

Hamdi Allam's avatar
Hamdi Allam committed
210
			// 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
211
			processLog.Info("detected contract logs", "size", len(l1ContractEvents))
Hamdi Allam's avatar
Hamdi Allam committed
212 213 214 215
			err = db.ContractEvents.StoreL1ContractEvents(l1ContractEvents)
			if err != nil {
				return err
			}
216

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

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

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

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

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

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

258 259 260
	deposits := make([]*database.Deposit, len(initiatedDepositEvents))
	for i, initiatedBridgeEvent := range initiatedDepositEvents {
		deposits[i] = &database.Deposit{
Hamdi Allam's avatar
Hamdi Allam committed
261 262 263 264 265 266 267 268 269 270 271
			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
272 273 274
		}
	}

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

283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
	// 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 {
301 302

			// 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
303
			// bridge message. This will easier to do once we index passed messages separately which will include the right To/From fields
304
			if provenWithdrawalEvent.From != common.HexToAddress("0x4200000000000000000000000000000000000007") || provenWithdrawalEvent.To != l1Contracts.L1CrossDomainMessenger {
305 306 307 308 309
				// non-bridge withdrawal
				continue
			}

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

355 356
		// 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
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
		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
374 375
	return nil
}