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

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

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

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

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

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

	// 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"),
	}
}

43
func (c L2Contracts) ToSlice() []common.Address {
44 45 46 47 48 49 50 51 52 53 54
	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
}

55 56 57 58
type L2Processor struct {
	processor
}

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

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

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

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

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

	return l2Processor, nil
}

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

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

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

		l2Headers := make([]*database.L2BlockHeader, len(headers))
106
		l2HeaderMap := make(map[common.Hash]*types.Header)
107
		for i, header := range headers {
108
			blockHash := header.Hash()
109 110
			l2Headers[i] = &database.L2BlockHeader{
				BlockHeader: database.BlockHeader{
111
					Hash:       blockHash,
112 113 114 115 116
					ParentHash: header.ParentHash,
					Number:     database.U256{Int: header.Number},
					Timestamp:  header.Time,
				},
			}
117 118 119 120

			l2HeaderMap[blockHash] = header
		}

121
		/** Watch for Contract Events **/
122 123 124 125

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

Hamdi Allam's avatar
Hamdi Allam committed
129 130
		l2ContractEvents := make([]*database.L2ContractEvent, len(logs))
		processedContractEvents := NewProcessedContractEvents()
131 132
		for i := range logs {
			log := &logs[i]
133 134
			header, ok := l2HeaderMap[log.BlockHash]
			if !ok {
135
				processLog.Error("contract event found with associated header not in the batch", "header", header, "log_index", log.Index)
136
				return errors.New("parsed log with a block hash not in this batch")
137 138
			}

139
			contractEvent := processedContractEvents.AddLog(log, header.Time)
Hamdi Allam's avatar
Hamdi Allam committed
140
			l2ContractEvents[i] = &database.L2ContractEvent{ContractEvent: *contractEvent}
141 142 143 144
		}

		/** Update Database **/

145
		processLog.Info("saving l2 blocks", "size", numHeaders)
146 147
		err = db.Blocks.StoreL2BlockHeaders(l2Headers)
		if err != nil {
148
			return err
149 150
		}

Hamdi Allam's avatar
Hamdi Allam committed
151
		numLogs := len(l2ContractEvents)
152
		if numLogs > 0 {
153
			processLog.Info("detected contract logs", "size", numLogs)
154 155
			err = db.ContractEvents.StoreL2ContractEvents(l2ContractEvents)
			if err != nil {
156
				return err
157
			}
Hamdi Allam's avatar
Hamdi Allam committed
158

159 160 161 162 163 164 165 166
			// forward along contract events to bridge txs processor
			err = l2ProcessContractEventsBridgeTransactions(processLog, db, processedContractEvents)
			if err != nil {
				return err
			}

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

172
		// a-ok!
173
		return nil
174 175
	}
}
Hamdi Allam's avatar
Hamdi Allam committed
176

177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
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,
			InitiatedL2EventGUID: withdrawalEvent.RawEvent.GUID,
			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,
				Timestamp:   withdrawalEvent.RawEvent.Timestamp,
			},
		}

		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
215 216 217 218
		err := db.BridgeTransactions.StoreL2TransactionWithdrawals(transactionWithdrawals)
		if err != nil {
			return err
		}
219 220 221 222 223 224 225 226 227 228 229 230

		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
231
	// to do so purely from the L2-side since there is not a way to easily identify deposit transactions on L2 without walking
232 233 234 235 236 237 238
	// the transaction list of every L2 epoch.

	// a-ok!
	return nil
}

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

241
	l2ToL1MessagePasserABI, err := bindings.NewL2ToL1MessagePasser(common.Address{}, nil)
242 243 244 245
	if err != nil {
		return err
	}

246
	// (1) Process New Withdrawals
247 248 249 250 251
	initiatedWithdrawalEvents, err := StandardBridgeInitiatedEvents(events)
	if err != nil {
		return err
	}

252
	withdrawals := make([]*database.L2BridgeWithdrawal, len(initiatedWithdrawalEvents))
253 254 255
	for i, initiatedBridgeEvent := range initiatedWithdrawalEvents {
		log := events.eventLog[initiatedBridgeEvent.RawEvent.GUID]

256
		// extract the withdrawal hash from the following MessagePassed event
257
		msgPassedLog := events.eventLog[events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 1}].GUID]
258
		msgPassedEvent, err := l2ToL1MessagePasserABI.ParseMessagePassed(*msgPassedLog)
259 260 261 262
		if err != nil {
			return err
		}

263
		withdrawals[i] = &database.L2BridgeWithdrawal{
264 265
			TransactionWithdrawalHash: msgPassedEvent.WithdrawalHash,
			CrossDomainMessengerNonce: &database.U256{Int: initiatedBridgeEvent.CrossDomainMessengerNonce},
266
			TokenPair:                 database.TokenPair{L1TokenAddress: initiatedBridgeEvent.LocalToken, L2TokenAddress: initiatedBridgeEvent.RemoteToken},
267 268 269 270 271 272 273 274 275 276 277 278
			Tx: database.Transaction{
				FromAddress: initiatedBridgeEvent.From,
				ToAddress:   initiatedBridgeEvent.To,
				Amount:      database.U256{Int: initiatedBridgeEvent.Amount},
				Data:        initiatedBridgeEvent.ExtraData,
				Timestamp:   initiatedBridgeEvent.RawEvent.Timestamp,
			},
		}
	}

	if len(withdrawals) > 0 {
		processLog.Info("detected L2StandardBridge withdrawals", "num", len(withdrawals))
279
		err := db.BridgeTransfers.StoreL2BridgeWithdrawals(withdrawals)
280 281 282 283 284
		if err != nil {
			return err
		}
	}

285 286 287 288
	// (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
289

290
	finalizedDepositEvents, err := StandardBridgeFinalizedEvents(rawEthClient, events)
291 292 293 294
	if err != nil {
		return err
	}

295 296
	for _, finalizedDepositEvent := range finalizedDepositEvents {
		deposit, err := db.BridgeTransfers.L1BridgeDepositByCrossDomainMessengerNonce(finalizedDepositEvent.CrossDomainMessengerNonce)
Hamdi Allam's avatar
Hamdi Allam committed
297 298 299
		if err != nil {
			return err
		} else if deposit == nil {
300 301 302
			// NOTE: We'll be indexing CrossDomainMessenger messages that'll ensure we're in a caught up state here
			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
303
		}
Hamdi Allam's avatar
Hamdi Allam committed
304

305 306 307 308 309 310
		// 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
311 312 313
		}
	}

314
	// a-ok!
Hamdi Allam's avatar
Hamdi Allam committed
315 316
	return nil
}