l1_processor.go 13.1 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
	deposits := make([]*database.L1BridgeDeposit, len(initiatedDepositEvents))
259
	for i, initiatedBridgeEvent := range initiatedDepositEvents {
260 261 262 263 264
		deposits[i] = &database.L1BridgeDeposit{
			GUID:                      uuid.New(),
			InitiatedL1EventGUID:      initiatedBridgeEvent.RawEvent.GUID,
			CrossDomainMessengerNonce: database.U256{Int: initiatedBridgeEvent.CrossDomainMessengerNonce},
			TokenPair:                 database.TokenPair{L1TokenAddress: initiatedBridgeEvent.LocalToken, L2TokenAddress: initiatedBridgeEvent.RemoteToken},
Hamdi Allam's avatar
Hamdi Allam committed
265 266 267 268 269 270 271
			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
	if len(deposits) > 0 {
		processLog.Info("detected L1StandardBridge deposits", "num", len(deposits))
277
		err := db.BridgeTransfers.StoreL1BridgeDeposits(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
	// Prove L2 Withdrawals
	provenWithdrawalEvents, err := OptimismPortalWithdrawalProvenEvents(events)
	if err != nil {
		return err
	}

289 290 291 292 293 294 295
	latestL2Header, err := db.Blocks.LatestL2BlockHeader()
	if err != nil {
		return err
	} else if len(provenWithdrawalEvents) > 0 && latestL2Header == nil {
		return errors.New("no indexed L2 state to process any proven L1 transactions")
	}

296 297 298
	numProvenWithdrawals := 0
	for _, provenWithdrawalEvent := range provenWithdrawalEvents {
		withdrawalHash := provenWithdrawalEvent.WithdrawalHash
299
		withdrawal, err := db.BridgeTransfers.L2BridgeWithdrawalByWithdrawalHash(withdrawalHash)
300 301
		if err != nil {
			return err
302 303 304
		} else if withdrawal == nil {
			// NOTE: This needs to be updated to identify if this CrossDomainMessenger message is a StandardBridge message. This
			// will be easier to do once we index cross domain messages and track its lifecyle separately
305
			if provenWithdrawalEvent.From != common.HexToAddress("0x4200000000000000000000000000000000000007") || provenWithdrawalEvent.To != l1Contracts.L1CrossDomainMessenger {
306 307 308 309
				// non-bridge withdrawal
				continue
			}

310 311 312 313
			// Check if the L2Processor is behind or really has missed an event. Since L2 timestamps
			// are derived from L1, we can simply compare timestamps
			if provenWithdrawalEvent.RawEvent.Timestamp > latestL2Header.Timestamp {
				processLog.Warn("behind on indexed L2StandardBridge withdrawals")
314 315
				return errors.New("waiting for L2Processor to catch up")
			} else {
316
				processLog.Crit("missing indexed L2StandardBridge withdrawal for this proven event")
317 318 319 320
				return errors.New("missing withdrawal message")
			}
		}

321
		err = db.BridgeTransfers.MarkProvenL2BridgeWithdrawalEvent(withdrawal.GUID, provenWithdrawalEvent.RawEvent.GUID)
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
		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
341
		withdrawal, err := db.BridgeTransfers.L2BridgeWithdrawalByCrossDomainMessengerNonce(nonce)
342 343 344 345 346
		if err != nil {
			processLog.Error("error querying associated withdrawal messsage using nonce", "cross_domain_messenger_nonce", nonce)
			return err
		}

347 348
		// 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
349 350 351 352 353
		if withdrawal == nil {
			processLog.Crit("missing indexed withdrawal for this finalization event")
			return errors.New("missing withdrawal message")
		}

354
		err = db.BridgeTransfers.MarkFinalizedL2BridgeWithdrawalEvent(withdrawal.GUID, finalizedBridgeEvent.RawEvent.GUID)
355 356 357 358 359 360 361 362 363 364 365
		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
366 367
	return nil
}