l2_processor.go 14.5 KB
Newer Older
1 2 3
package processor

import (
4
	"bytes"
5 6
	"context"
	"errors"
7
	"fmt"
8 9
	"reflect"

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

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

22 23 24 25 26 27 28
type L2Contracts struct {
	L2CrossDomainMessenger common.Address
	L2StandardBridge       common.Address
	L2ERC721Bridge         common.Address
	L2ToL1MessagePasser    common.Address

	// Some more contracts -- ProxyAdmin, SystemConfig, etcc
29
	// Ignore the auxiliary contracts?
30 31 32 33 34 35 36 37 38 39 40 41 42 43

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

func L2ContractPredeploys() L2Contracts {
	return L2Contracts{
		L2CrossDomainMessenger: common.HexToAddress("0x4200000000000000000000000000000000000007"),
		L2StandardBridge:       common.HexToAddress("0x4200000000000000000000000000000000000010"),
		L2ERC721Bridge:         common.HexToAddress("0x4200000000000000000000000000000000000014"),
		L2ToL1MessagePasser:    common.HexToAddress("0x4200000000000000000000000000000000000016"),
	}
}

44
func (c L2Contracts) 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 L2Processor struct {
	processor
}

60 61
func NewL2Processor(logger log.Logger, ethClient node.EthClient, db *database.DB, l2Contracts L2Contracts) (*L2Processor, error) {
	l2ProcessLog := logger.New("processor", "l2")
62
	l2ProcessLog.Info("initializing processor")
63

64
	latestHeader, err := db.Blocks.LatestL2BlockHeader()
65 66 67 68 69 70
	if err != nil {
		return nil, err
	}

	var fromL2Header *types.Header
	if latestHeader != nil {
71
		l2ProcessLog.Info("detected last indexed block", "height", latestHeader.Number.Int, "hash", latestHeader.Hash)
72 73
		l2Header, err := ethClient.BlockHeaderByHash(latestHeader.Hash)
		if err != nil {
74
			l2ProcessLog.Error("unable to fetch header for last indexed block", "hash", latestHeader.Hash, "err", err)
75 76 77 78 79 80 81 82 83 84 85
			return nil, err
		}

		fromL2Header = l2Header
	} else {
		l2ProcessLog.Info("no indexed state, starting from genesis")
		fromL2Header = nil
	}

	l2Processor := &L2Processor{
		processor: processor{
86
			headerTraversal: node.NewHeaderTraversal(ethClient, fromL2Header),
87 88 89
			db:              db,
			processFn:       l2ProcessFn(l2ProcessLog, ethClient, l2Contracts),
			processLog:      l2ProcessLog,
90 91 92 93 94 95
		},
	}

	return l2Processor, nil
}

96
func l2ProcessFn(processLog log.Logger, ethClient node.EthClient, l2Contracts L2Contracts) ProcessFn {
97 98
	rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())

99
	contractAddrs := l2Contracts.ToSlice()
100
	processLog.Info("processor configured with contracts", "contracts", l2Contracts)
101
	return func(db *database.DB, headers []*types.Header) error {
102 103
		numHeaders := len(headers)

104
		/** Index all L2 blocks **/
105 106

		l2Headers := make([]*database.L2BlockHeader, len(headers))
107
		l2HeaderMap := make(map[common.Hash]*types.Header)
108
		for i, header := range headers {
109
			l2Headers[i] = &database.L2BlockHeader{BlockHeader: database.BlockHeaderFromHeader(header)}
110
			l2HeaderMap[l2Headers[i].Hash] = header
111 112
		}

113
		/** Watch for Contract Events **/
114 115 116 117

		logFilter := ethereum.FilterQuery{FromBlock: headers[0].Number, ToBlock: headers[numHeaders-1].Number, Addresses: contractAddrs}
		logs, err := rawEthClient.FilterLogs(context.Background(), logFilter)
		if err != nil {
118
			return err
119 120
		}

Hamdi Allam's avatar
Hamdi Allam committed
121 122
		l2ContractEvents := make([]*database.L2ContractEvent, len(logs))
		processedContractEvents := NewProcessedContractEvents()
123 124
		for i := range logs {
			log := &logs[i]
125 126
			header, ok := l2HeaderMap[log.BlockHash]
			if !ok {
127
				processLog.Error("contract event found with associated header not in the batch", "header", header, "log_index", log.Index)
128
				return errors.New("parsed log with a block hash not in this batch")
129 130
			}

131
			contractEvent := processedContractEvents.AddLog(log, header.Time)
Hamdi Allam's avatar
Hamdi Allam committed
132
			l2ContractEvents[i] = &database.L2ContractEvent{ContractEvent: *contractEvent}
133 134 135 136
		}

		/** Update Database **/

137
		processLog.Info("saving l2 blocks", "size", numHeaders)
138 139
		err = db.Blocks.StoreL2BlockHeaders(l2Headers)
		if err != nil {
140
			return err
141 142
		}

Hamdi Allam's avatar
Hamdi Allam committed
143
		numLogs := len(l2ContractEvents)
144
		if numLogs > 0 {
145
			processLog.Info("detected contract logs", "size", numLogs)
146 147
			err = db.ContractEvents.StoreL2ContractEvents(l2ContractEvents)
			if err != nil {
148
				return err
149
			}
Hamdi Allam's avatar
Hamdi Allam committed
150

151 152 153 154 155 156
			// forward along contract events to bridge txs processor
			err = l2ProcessContractEventsBridgeTransactions(processLog, db, processedContractEvents)
			if err != nil {
				return err
			}

157 158 159 160 161
			err = l2ProcessContractEventsBridgeCrossDomainMessages(processLog, db, processedContractEvents)
			if err != nil {
				return err
			}

162 163
			// forward along contract events to standard bridge processor
			err = l2ProcessContractEventsStandardBridge(processLog, db, ethClient, processedContractEvents)
Hamdi Allam's avatar
Hamdi Allam committed
164 165 166
			if err != nil {
				return err
			}
167 168
		}

169
		// a-ok!
170
		return nil
171 172
	}
}
Hamdi Allam's avatar
Hamdi Allam committed
173

174 175 176 177 178 179 180 181 182 183 184 185
func l2ProcessContractEventsBridgeTransactions(processLog log.Logger, db *database.DB, events *ProcessedContractEvents) error {
	// (1) Process New Withdrawals
	messagesPassed, err := L2ToL1MessagePasserMessagesPassed(events)
	if err != nil {
		return err
	}

	ethWithdrawals := []*database.L2BridgeWithdrawal{}
	transactionWithdrawals := make([]*database.L2TransactionWithdrawal, len(messagesPassed))
	for i, withdrawalEvent := range messagesPassed {
		transactionWithdrawals[i] = &database.L2TransactionWithdrawal{
			WithdrawalHash:       withdrawalEvent.WithdrawalHash,
186
			InitiatedL2EventGUID: withdrawalEvent.Event.GUID,
187 188 189 190 191 192 193
			Nonce:                database.U256{Int: withdrawalEvent.Nonce},
			GasLimit:             database.U256{Int: withdrawalEvent.GasLimit},
			Tx: database.Transaction{
				FromAddress: withdrawalEvent.Sender,
				ToAddress:   withdrawalEvent.Target,
				Amount:      database.U256{Int: withdrawalEvent.Value},
				Data:        withdrawalEvent.Data,
194
				Timestamp:   withdrawalEvent.Event.Timestamp,
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
			},
		}

		if len(withdrawalEvent.Data) == 0 && withdrawalEvent.Value.BitLen() > 0 {
			ethWithdrawals = append(ethWithdrawals, &database.L2BridgeWithdrawal{
				TransactionWithdrawalHash: withdrawalEvent.WithdrawalHash,
				Tx:                        transactionWithdrawals[i].Tx,
				TokenPair: database.TokenPair{
					L1TokenAddress: predeploys.LegacyERC20ETHAddr,
					L2TokenAddress: predeploys.LegacyERC20ETHAddr,
				},
			})
		}
	}

	if len(transactionWithdrawals) > 0 {
		processLog.Info("detected transaction withdrawals", "size", len(transactionWithdrawals))
Hamdi Allam's avatar
Hamdi Allam committed
212 213 214 215
		err := db.BridgeTransactions.StoreL2TransactionWithdrawals(transactionWithdrawals)
		if err != nil {
			return err
		}
216 217 218 219 220 221 222 223 224 225 226 227

		if len(ethWithdrawals) > 0 {
			processLog.Info("detected L2ToL1MessagePasser ETH transfers", "size", len(ethWithdrawals))
			err := db.BridgeTransfers.StoreL2BridgeWithdrawals(ethWithdrawals)
			if err != nil {
				return err
			}
		}
	}

	// (2) Process Deposit Finalization
	//  - Since L2 deposits are apart of the block derivation processes, we dont track finalization as it's too tricky
Hamdi Allam's avatar
Hamdi Allam committed
228
	// to do so purely from the L2-side since there is not a way to easily identify deposit transactions on L2 without walking
229 230 231 232 233 234
	// the transaction list of every L2 epoch.

	// a-ok!
	return nil
}

235 236 237 238 239 240 241 242 243 244 245 246 247 248
func l2ProcessContractEventsBridgeCrossDomainMessages(processLog log.Logger, db *database.DB, events *ProcessedContractEvents) error {
	l2ToL1MessagePasserABI, err := bindings.NewL2ToL1MessagePasser(common.Address{}, nil)
	if err != nil {
		return err
	}

	// (2) Process New Messages
	sentMessageEvents, err := CrossDomainMessengerSentMessageEvents(events)
	if err != nil {
		return err
	}

	sentMessages := make([]*database.L2BridgeMessage, len(sentMessageEvents))
	for i, sentMessageEvent := range sentMessageEvents {
249
		log := sentMessageEvent.Event.RLPLog
250 251

		// extract the withdrawal hash from the previous MessagePassed event
252
		msgPassedLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index - 1}].RLPLog
253 254 255 256 257 258 259 260 261 262
		msgPassedEvent, err := l2ToL1MessagePasserABI.ParseMessagePassed(*msgPassedLog)
		if err != nil {
			return err
		}

		sentMessages[i] = &database.L2BridgeMessage{
			TransactionWithdrawalHash: msgPassedEvent.WithdrawalHash,
			BridgeMessage: database.BridgeMessage{
				Nonce:                database.U256{Int: sentMessageEvent.MessageNonce},
				MessageHash:          sentMessageEvent.MessageHash,
263
				SentMessageEventGUID: sentMessageEvent.Event.GUID,
264 265 266 267 268 269
				GasLimit:             database.U256{Int: sentMessageEvent.GasLimit},
				Tx: database.Transaction{
					FromAddress: sentMessageEvent.Sender,
					ToAddress:   sentMessageEvent.Target,
					Amount:      database.U256{Int: sentMessageEvent.Value},
					Data:        sentMessageEvent.Message,
270
					Timestamp:   sentMessageEvent.Event.Timestamp,
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
				},
			},
		}
	}

	if len(sentMessages) > 0 {
		processLog.Info("detected L2CrossDomainMessenger messages", "size", len(sentMessages))
		err := db.BridgeMessages.StoreL2BridgeMessages(sentMessages)
		if err != nil {
			return err
		}
	}

	// (2) Process Relayed Messages.
	//
	// NOTE: Should we care about failed messages? A failed message can be
	// inferred via an included deposit on L2 that has not been marked as relayed.
	relayedMessageEvents, err := CrossDomainMessengerRelayedMessageEvents(events)
	if err != nil {
		return err
	}

	latestL1Header, err := db.Blocks.LatestL1BlockHeader()
	if err != nil {
		return err
	} else if len(relayedMessageEvents) > 0 && latestL1Header == nil {
		return errors.New("no indexed L1 headers to relay messages. waiting for L1Processor to catch up")
	}

	for _, relayedMessage := range relayedMessageEvents {
		message, err := db.BridgeMessages.L1BridgeMessageByHash(relayedMessage.MsgHash)
		if err != nil {
			return err
		}

		if message == nil {
			// Since the transaction processor running prior does not ensure the deposit inclusion, we need to
			// ensure we are in a caught up state before claiming a missing event. Since L2 timestamps are derived
			// from L1, we can simply compare the timestamp of this event with the latest L1 header.
310
			if latestL1Header == nil || relayedMessage.Event.Timestamp > latestL1Header.Timestamp {
311 312 313 314 315 316 317 318
				processLog.Warn("waiting for L1Processor to catch up on L1CrossDomainMessages")
				return errors.New("waiting for L1Processor to catch up")
			} else {
				processLog.Crit("missing indexed L1CrossDomainMessenger message", "message_hash", relayedMessage.MsgHash)
				return fmt.Errorf("missing indexed L1CrossDomainMessager mesesage: 0x%x", relayedMessage.MsgHash)
			}
		}

319
		err = db.BridgeMessages.MarkRelayedL1BridgeMessage(relayedMessage.MsgHash, relayedMessage.Event.GUID)
320 321 322 323 324 325 326 327 328 329 330 331 332
		if err != nil {
			return err
		}
	}

	if len(relayedMessageEvents) > 0 {
		processLog.Info("relayed L1CrossDomainMessenger messages", "size", len(relayedMessageEvents))
	}

	// a-ok!
	return nil
}

333
func l2ProcessContractEventsStandardBridge(processLog log.Logger, db *database.DB, ethClient node.EthClient, events *ProcessedContractEvents) error {
Hamdi Allam's avatar
Hamdi Allam committed
334 335
	rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())

336
	l2ToL1MessagePasserABI, err := bindings.NewL2ToL1MessagePasser(common.Address{}, nil)
337 338 339 340
	if err != nil {
		return err
	}

341
	// (1) Process New Withdrawals
342 343 344 345 346
	initiatedWithdrawalEvents, err := StandardBridgeInitiatedEvents(events)
	if err != nil {
		return err
	}

347
	withdrawals := make([]*database.L2BridgeWithdrawal, len(initiatedWithdrawalEvents))
348
	for i, initiatedBridgeEvent := range initiatedWithdrawalEvents {
349
		log := initiatedBridgeEvent.Event.RLPLog
350

351
		// extract the withdrawal hash from the following MessagePassed event
352
		msgPassedLog := events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 1}].RLPLog
353
		msgPassedEvent, err := l2ToL1MessagePasserABI.ParseMessagePassed(*msgPassedLog)
354 355 356 357
		if err != nil {
			return err
		}

358
		withdrawals[i] = &database.L2BridgeWithdrawal{
359 360
			TransactionWithdrawalHash: msgPassedEvent.WithdrawalHash,
			CrossDomainMessengerNonce: &database.U256{Int: initiatedBridgeEvent.CrossDomainMessengerNonce},
361
			TokenPair:                 database.TokenPair{L1TokenAddress: initiatedBridgeEvent.LocalToken, L2TokenAddress: initiatedBridgeEvent.RemoteToken},
362 363 364 365 366
			Tx: database.Transaction{
				FromAddress: initiatedBridgeEvent.From,
				ToAddress:   initiatedBridgeEvent.To,
				Amount:      database.U256{Int: initiatedBridgeEvent.Amount},
				Data:        initiatedBridgeEvent.ExtraData,
367
				Timestamp:   initiatedBridgeEvent.Event.Timestamp,
368 369 370 371 372 373
			},
		}
	}

	if len(withdrawals) > 0 {
		processLog.Info("detected L2StandardBridge withdrawals", "num", len(withdrawals))
374
		err := db.BridgeTransfers.StoreL2BridgeWithdrawals(withdrawals)
375 376 377 378 379
		if err != nil {
			return err
		}
	}

380 381 382 383
	// (2) Process Finalized Deposits
	//  - We dont need do anything actionable on the database here as this is layered on top of the
	// bridge transaction & messages that have a tracked lifecyle. We simply walk through and ensure
	// that the corresponding initiated deposits exist as an integrity check
Hamdi Allam's avatar
Hamdi Allam committed
384

385
	finalizedDepositEvents, err := StandardBridgeFinalizedEvents(rawEthClient, events)
386 387 388 389
	if err != nil {
		return err
	}

390 391
	for _, finalizedDepositEvent := range finalizedDepositEvents {
		deposit, err := db.BridgeTransfers.L1BridgeDepositByCrossDomainMessengerNonce(finalizedDepositEvent.CrossDomainMessengerNonce)
Hamdi Allam's avatar
Hamdi Allam committed
392 393 394
		if err != nil {
			return err
		} else if deposit == nil {
395
			// Indexed CrossDomainMessenger messages ensure we're in a caught up state here
396 397
			processLog.Error("missing indexed L1StandardBridge deposit on finalization", "cross_domain_messenger_nonce", finalizedDepositEvent.CrossDomainMessengerNonce)
			return errors.New("missing indexed L1StandardBridge deposit on finalization")
Hamdi Allam's avatar
Hamdi Allam committed
398
		}
Hamdi Allam's avatar
Hamdi Allam committed
399

400 401 402 403 404 405
		// sanity check on the bridge fields
		if finalizedDepositEvent.From != deposit.Tx.FromAddress || finalizedDepositEvent.To != deposit.Tx.ToAddress ||
			finalizedDepositEvent.Amount.Cmp(deposit.Tx.Amount.Int) != 0 || !bytes.Equal(finalizedDepositEvent.ExtraData, deposit.Tx.Data) ||
			finalizedDepositEvent.LocalToken != deposit.TokenPair.L1TokenAddress || finalizedDepositEvent.RemoteToken != deposit.TokenPair.L2TokenAddress {
			processLog.Error("bridge finalization fields mismatch with initiated fields!", "tx_source_hash", deposit.TransactionSourceHash, "cross_domain_messenger_nonce", deposit.CrossDomainMessengerNonce.Int)
			return errors.New("bridge tx mismatch")
Hamdi Allam's avatar
Hamdi Allam committed
406 407 408
		}
	}

409
	// a-ok!
Hamdi Allam's avatar
Hamdi Allam committed
410 411
	return nil
}