diff --git a/op-monitorism/faultproof_withdrawals/README.md b/op-monitorism/faultproof_withdrawals/README.md index 9253fa30..4dcc871f 100644 --- a/op-monitorism/faultproof_withdrawals/README.md +++ b/op-monitorism/faultproof_withdrawals/README.md @@ -38,7 +38,7 @@ DESCRIPTION: OPTIONS: --l1.geth.url value L1 execution layer node URL [$FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL] - --l2.node.url value L2 rollup node consensus layer (op-node) URL [$FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL] + --l2.node.url value [DEPRECATED] L2 rollup node consensus layer (op-node) URL [$FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL] --l2.geth.url value L2 OP Stack execution layer client(op-geth) URL [$FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL] --event.block.range value Max block range when scanning for events (default: 1000) [$FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE] --start.block.height value Starting height to scan for events. This will take precedence if set. (default: 0) [$FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT] @@ -59,7 +59,7 @@ OPTIONS: ```bash L1_GETH_URL="https://..." -L2_OP_NODE_URL="https://..." +L2_OP_NODE_URL="https://..." # [DEPRECATED] This URL is no longer required L2_OP_GETH_URL="https://..." export MONITORISM_LOOP_INTERVAL_MSEC=100 diff --git a/op-monitorism/faultproof_withdrawals/cli.go b/op-monitorism/faultproof_withdrawals/cli.go index 93eaf5f0..56c7a410 100644 --- a/op-monitorism/faultproof_withdrawals/cli.go +++ b/op-monitorism/faultproof_withdrawals/cli.go @@ -11,9 +11,10 @@ import ( ) const ( - L1GethURLFlagName = "l1.geth.url" - L2NodeURLFlagName = "l2.node.url" - L2GethURLFlagName = "l2.geth.url" + L1GethURLFlagName = "l1.geth.url" + L2NodeURLFlagName = "l2.node.url" + L2GethURLFlagName = "l2.geth.url" + L2GethBackupURLsFlagName = "l2.geth.backup.urls" EventBlockRangeFlagName = "event.block.range" StartingL1BlockHeightFlagName = "start.block.height" @@ -23,9 +24,10 @@ const ( ) type CLIConfig struct { - L1GethURL string - L2OpGethURL string - L2OpNodeURL string + L1GethURL string + L2OpGethURL string + L2OpNodeURL string + L2GethBackupURLs []string EventBlockRange uint64 StartingL1BlockHeight int64 @@ -38,7 +40,8 @@ func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) { cfg := CLIConfig{ L1GethURL: ctx.String(L1GethURLFlagName), L2OpGethURL: ctx.String(L2GethURLFlagName), - L2OpNodeURL: ctx.String(L2NodeURLFlagName), + L2GethBackupURLs: ctx.StringSlice(L2GethBackupURLsFlagName), + L2OpNodeURL: "", // Ignored since deprecated EventBlockRange: ctx.Uint64(EventBlockRangeFlagName), StartingL1BlockHeight: ctx.Int64(StartingL1BlockHeightFlagName), HoursInThePastToStartFrom: ctx.Uint64(HoursInThePastToStartFromFlagName), @@ -62,14 +65,20 @@ func CLIFlags(envVar string) []cli.Flag { }, &cli.StringFlag{ Name: L2NodeURLFlagName, - Usage: "L2 rollup node consensus layer (op-node) URL", + Usage: "[DEPRECATED] L2 rollup node consensus layer (op-node) URL - this flag is ignored", EnvVars: opservice.PrefixEnvVar(envVar, "L2_OP_NODE_URL"), + Hidden: true, }, &cli.StringFlag{ Name: L2GethURLFlagName, Usage: "L2 OP Stack execution layer client(op-geth) URL", EnvVars: opservice.PrefixEnvVar(envVar, "L2_OP_GETH_URL"), }, + &cli.StringSliceFlag{ + Name: L2GethBackupURLsFlagName, + Usage: "Backup L2 OP Stack execution layer client URLs (format: name=url,name2=url2)", + EnvVars: opservice.PrefixEnvVar(envVar, "L2_OP_GETH_BACKUP_URLS"), + }, &cli.Uint64Flag{ Name: EventBlockRangeFlagName, Usage: "Max block range when scanning for events", diff --git a/op-monitorism/faultproof_withdrawals/monitor.go b/op-monitorism/faultproof_withdrawals/monitor.go index dccfea57..7df46b34 100644 --- a/op-monitorism/faultproof_withdrawals/monitor.go +++ b/op-monitorism/faultproof_withdrawals/monitor.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/big" + "strings" "time" "github.com/ethereum-optimism/monitorism/op-monitorism/faultproof_withdrawals/validator" @@ -26,12 +27,12 @@ type Monitor struct { ctx context.Context // user arguments - l1GethClient *ethclient.Client - l2OpGethClient *ethclient.Client - l2OpNodeClient *ethclient.Client - l1ChainID *big.Int - l2ChainID *big.Int - maxBlockRange uint64 + l1GethClient *ethclient.Client + l2OpGethClient *ethclient.Client + l2BackupClients map[string]*ethclient.Client + l1ChainID *big.Int + l2ChainID *big.Int + maxBlockRange uint64 // helpers withdrawalValidator validator.ProvenWithdrawalValidator @@ -51,16 +52,32 @@ func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIC if err != nil { return nil, fmt.Errorf("failed to dial l1: %w", err) } + + l1ChainID, err := l1GethClient.ChainID(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get l1 chain id: %w", err) + } + l2OpGethClient, err := ethclient.Dial(cfg.L2OpGethURL) if err != nil { return nil, fmt.Errorf("failed to dial l2: %w", err) } - l2OpNodeClient, err := ethclient.Dial(cfg.L2OpNodeURL) + l2ChainID, err := l2OpGethClient.ChainID(ctx) if err != nil { - return nil, fmt.Errorf("failed to dial l2: %w", err) + return nil, fmt.Errorf("failed to get l2 chain id: %w", err) } - withdrawalValidator, err := validator.NewWithdrawalValidator(ctx, l1GethClient, l2OpGethClient, l2OpNodeClient, cfg.OptimismPortalAddress) + // if backup urls are provided, create a backup client for each + var l2OpGethBackupClients map[string]*ethclient.Client + if len(cfg.L2GethBackupURLs) > 0 { + l2OpGethBackupClients, err = GethBackupClientsDictionary(ctx, cfg.L2GethBackupURLs, l2ChainID) + if err != nil { + return nil, fmt.Errorf("failed to create backup clients: %w", err) + } + + } + + withdrawalValidator, err := validator.NewWithdrawalValidator(ctx, log, l1GethClient, l2OpGethClient, l2OpGethBackupClients, cfg.OptimismPortalAddress) if err != nil { return nil, fmt.Errorf("failed to create withdrawal validator: %w", err) } @@ -70,24 +87,15 @@ func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIC return nil, fmt.Errorf("failed to query latest block number: %w", err) } - l1ChainID, err := l1GethClient.ChainID(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get l1 chain id: %w", err) - } - l2ChainID, err := l2OpGethClient.ChainID(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get l2 chain id: %w", err) - } - metrics := NewMetrics(m) ret := &Monitor{ log: log, - ctx: ctx, - l1GethClient: l1GethClient, - l2OpGethClient: l2OpGethClient, - l2OpNodeClient: l2OpNodeClient, + ctx: ctx, + l1GethClient: l1GethClient, + l2OpGethClient: l2OpGethClient, + l2BackupClients: l2OpGethBackupClients, l1ChainID: l1ChainID, l2ChainID: l2ChainID, @@ -124,7 +132,11 @@ func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIC startingL1BlockHeight = uint64(cfg.StartingL1BlockHeight) } - state, err := NewState(log, startingL1BlockHeight, latestL1Height, ret.withdrawalValidator.GetLatestL2Height()) + latestL2Height, err := ret.withdrawalValidator.L2NodeHelper.BlockNumber() + if err != nil { + return nil, fmt.Errorf("failed to get latest L2 height: %w", err) + } + state, err := NewState(log, startingL1BlockHeight, latestL1Height, latestL2Height) if err != nil { return nil, fmt.Errorf("failed to create state: %w", err) } @@ -137,6 +149,30 @@ func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIC return ret, nil } +func GethBackupClientsDictionary(ctx context.Context, L2GethBackupURLs []string, l2ChainID *big.Int) (map[string]*ethclient.Client, error) { + dictionary := make(map[string]*ethclient.Client) + for _, rawURL := range L2GethBackupURLs { + parts := strings.Split(rawURL, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid backup URL format, expected name=url, got %s", rawURL) + } + name, url := parts[0], parts[1] + backupClient, err := ethclient.Dial(url) + if err != nil { + return nil, fmt.Errorf("failed to dial l2 backup, error: %w", err) + } + backupChainID, err := backupClient.ChainID(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get backup L2 chain ID, error: %w", err) + } + if backupChainID.Cmp(l2ChainID) != 0 { + return nil, fmt.Errorf("backup L2 client chain ID mismatch, expected: %d, got: %d", l2ChainID, backupChainID) + } + dictionary[name] = backupClient + } + return dictionary, nil +} + // getBlockAtApproximateTimeBinarySearch finds the block number corresponding to the timestamp from two weeks ago using a binary search approach. func (m *Monitor) getBlockAtApproximateTimeBinarySearch(ctx context.Context, client *ethclient.Client, latestBlockNumber *big.Int, hoursInThePast *big.Int) (*big.Int, error) { @@ -236,6 +272,7 @@ func (m *Monitor) Run(ctx context.Context) { start := m.state.nextL1Height stop, err := m.GetMaxBlock() + m.state.nodeConnections++ if err != nil { m.state.nodeConnectionFailures++ m.log.Error("failed to get max block", "error", err) @@ -244,6 +281,7 @@ func (m *Monitor) Run(ctx context.Context) { // review previous invalidProposalWithdrawalsEvents err = m.ConsumeEvents(m.state.potentialAttackOnInProgressGames) + m.state.nodeConnections++ if err != nil { m.state.nodeConnectionFailures++ m.log.Error("failed to consume events", "error", err) @@ -253,6 +291,7 @@ func (m *Monitor) Run(ctx context.Context) { // get new events m.log.Info("getting enriched withdrawal events", "start", fmt.Sprintf("%d", start), "stop", fmt.Sprintf("%d", stop)) newEvents, err := m.withdrawalValidator.GetEnrichedWithdrawalsEventsMap(start, &stop) + m.state.nodeConnections++ if err != nil { if start >= stop { m.log.Info("no new events to process", "start", start, "stop", stop) @@ -267,6 +306,7 @@ func (m *Monitor) Run(ctx context.Context) { } err = m.ConsumeEvents(newEvents) + m.state.nodeConnections++ if err != nil { m.state.nodeConnectionFailures++ m.log.Error("failed to consume events", "error", err) @@ -288,8 +328,12 @@ func (m *Monitor) ConsumeEvents(enrichedWithdrawalEvents map[common.Hash]*valida } m.log.Info("processing withdrawal event", "event", enrichedWithdrawalEvent) err := m.withdrawalValidator.UpdateEnrichedWithdrawalEvent(enrichedWithdrawalEvent) + if err != nil { + m.log.Error("failed to update enriched withdrawal event", "error", err) + return err + } //upgrade state to the latest L2 height after the event is processed - m.state.latestL2Height = m.withdrawalValidator.GetLatestL2Height() + m.state.latestL2Height, err = m.withdrawalValidator.L2NodeHelper.BlockNumber() if err != nil { m.log.Error("failed to update enriched withdrawal event", "error", err) return err @@ -297,7 +341,7 @@ func (m *Monitor) ConsumeEvents(enrichedWithdrawalEvents map[common.Hash]*valida err = m.ConsumeEvent(enrichedWithdrawalEvent) if err != nil { - m.log.Error("failed to consume event", "error", err) + m.log.Error("failed to consume event", "error", err, "enrichedWithdrawalEvent", enrichedWithdrawalEvent) return err } } @@ -313,7 +357,7 @@ func (m *Monitor) ConsumeEvent(enrichedWithdrawalEvent *validator.EnrichedProven } valid, err := m.withdrawalValidator.IsWithdrawalEventValid(enrichedWithdrawalEvent) if err != nil { - m.log.Error("failed to check if forgery detected", "error", err) + m.log.Error("failed to check if forgery detected", "error", err, "enrichedWithdrawalEvent", enrichedWithdrawalEvent) return err } diff --git a/op-monitorism/faultproof_withdrawals/state.go b/op-monitorism/faultproof_withdrawals/state.go index d458ec64..a533d5ac 100644 --- a/op-monitorism/faultproof_withdrawals/state.go +++ b/op-monitorism/faultproof_withdrawals/state.go @@ -29,6 +29,7 @@ type State struct { withdrawalsProcessed uint64 // This counts the withdrawals that have being completed and processed and we are not tracking anymore. eventProcessed >= withdrawalsProcessed. withdrawalsProcessed does not includes potential attacks with games in progress. nodeConnectionFailures uint64 + nodeConnections uint64 // possible attacks detected @@ -74,6 +75,7 @@ func NewState(logger log.Logger, nextL1Height uint64, latestL1Height uint64, lat withdrawalsProcessed: 0, nodeConnectionFailures: 0, + nodeConnections: 0, nextL1Height: nextL1Height, latestL1Height: latestL1Height, @@ -100,7 +102,7 @@ func (s *State) LogState() { "eventsProcessed", fmt.Sprintf("%d", s.eventsProcessed), "nodeConnectionFailures", fmt.Sprintf("%d", s.nodeConnectionFailures), - + "nodeConnections", fmt.Sprintf("%d", s.nodeConnections), "potentialAttackOnDefenderWinsGames", fmt.Sprintf("%d", s.numberOfPotentialAttacksOnDefenderWinsGames), "potentialAttackOnInProgressGames", fmt.Sprintf("%d", s.numberOfPotentialAttackOnInProgressGames), "suspiciousEventsOnChallengerWinsGames", fmt.Sprintf("%d", s.numberOfSuspiciousEventsOnChallengerWinsGames), @@ -185,8 +187,8 @@ type Metrics struct { EventsProcessedCounter prometheus.Counter WithdrawalsProcessedCounter prometheus.Counter - NodeConnectionFailuresCounter prometheus.Counter - + NodeConnectionFailuresCounter prometheus.Counter + NodeConnectionsCounter prometheus.Counter PotentialAttackOnDefenderWinsGamesGauge prometheus.Gauge PotentialAttackOnInProgressGamesGauge prometheus.Gauge SuspiciousEventsOnChallengerWinsGamesGauge prometheus.Gauge @@ -199,6 +201,7 @@ type Metrics struct { previousEventsProcessed uint64 previousWithdrawalsProcessed uint64 previousNodeConnectionFailures uint64 + previousNodeConnections uint64 } func (m *Metrics) String() string { @@ -212,6 +215,7 @@ func (m *Metrics) String() string { eventsProcessedCounterValue, _ := GetCounterValue(m.EventsProcessedCounter) nodeConnectionFailuresCounterValue, _ := GetCounterValue(m.NodeConnectionFailuresCounter) + nodeConnectionsCounterValue, _ := GetCounterValue(m.NodeConnectionsCounter) potentialAttackOnDefenderWinsGamesGaugeValue, _ := GetGaugeValue(m.PotentialAttackOnDefenderWinsGamesGauge) potentialAttackOnInProgressGamesGaugeValue, _ := GetGaugeValue(m.PotentialAttackOnInProgressGamesGauge) @@ -220,7 +224,7 @@ func (m *Metrics) String() string { invalidProposalWithdrawalsEventsGaugeVecValue, _ := GetGaugeVecValue(m.PotentialAttackOnInProgressGamesGaugeVec, prometheus.Labels{}) return fmt.Sprintf( - "Up: %d\nInitialL1HeightGauge: %d\nNextL1HeightGauge: %d\nLatestL1HeightGauge: %d\n latestL2HeightGaugeValue: %d\n eventsProcessedCounterValue: %d\nwithdrawalsProcessedCounterValue: %d\nnodeConnectionFailuresCounterValue: %d\n potentialAttackOnDefenderWinsGamesGaugeValue: %d\n potentialAttackOnInProgressGamesGaugeValue: %d\n forgeriesWithdrawalsEventsGaugeVecValue: %d\n invalidProposalWithdrawalsEventsGaugeVecValue: %d\n previousEventsProcessed: %d\n previousWithdrawalsProcessed: %d\n previousNodeConnectionFailures: %d\n", + "Up: %d\nInitialL1HeightGauge: %d\nNextL1HeightGauge: %d\nLatestL1HeightGauge: %d\n latestL2HeightGaugeValue: %d\n eventsProcessedCounterValue: %d\nwithdrawalsProcessedCounterValue: %d\nnodeConnectionFailuresCounterValue: %d\nnodeConnectionsCounterValue: %d\n potentialAttackOnDefenderWinsGamesGaugeValue: %d\n potentialAttackOnInProgressGamesGaugeValue: %d\n forgeriesWithdrawalsEventsGaugeVecValue: %d\n invalidProposalWithdrawalsEventsGaugeVecValue: %d\n previousEventsProcessed: %d\n previousWithdrawalsProcessed: %d\n previousNodeConnectionFailures: %d\n previousNodeConnections: %d\n", uint64(upGaugeValue), uint64(initialL1HeightGaugeValue), uint64(nextL1HeightGaugeValue), @@ -229,6 +233,7 @@ func (m *Metrics) String() string { uint64(eventsProcessedCounterValue), uint64(withdrawalsProcessedCounterValue), uint64(nodeConnectionFailuresCounterValue), + uint64(nodeConnectionsCounterValue), uint64(potentialAttackOnDefenderWinsGamesGaugeValue), uint64(potentialAttackOnInProgressGamesGaugeValue), uint64(forgeriesWithdrawalsEventsGaugeVecValue), @@ -236,6 +241,7 @@ func (m *Metrics) String() string { m.previousEventsProcessed, m.previousWithdrawalsProcessed, m.previousNodeConnectionFailures, + m.previousNodeConnections, ) } @@ -316,6 +322,11 @@ func NewMetrics(m metrics.Factory) *Metrics { Name: "node_connection_failures_total", Help: "Total number of node connection failures", }), + NodeConnectionsCounter: m.NewCounter(prometheus.CounterOpts{ + Namespace: MetricsNamespace, + Name: "node_connections_total", + Help: "Total number of node connections", + }), PotentialAttackOnDefenderWinsGamesGauge: m.NewGauge(prometheus.GaugeOpts{ Namespace: MetricsNamespace, Name: "potential_attack_on_defender_wins_games_count", diff --git a/op-monitorism/faultproof_withdrawals/validator/account_proof.go b/op-monitorism/faultproof_withdrawals/validator/account_proof.go new file mode 100644 index 00000000..0e78d2a9 --- /dev/null +++ b/op-monitorism/faultproof_withdrawals/validator/account_proof.go @@ -0,0 +1,130 @@ +package validator + +import ( + "bytes" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" +) + +// StorageKey is a marshaling utility for hex-encoded storage keys, which can have leading 0s and are +// an arbitrary length. +type StorageKey []byte + +func (k *StorageKey) UnmarshalText(text []byte) error { + textString := string(text) + + if len(textString)%2 != 0 { + // add leading 0 if odd length + if strings.HasPrefix(textString, "0x") { + textString = textString[:2] + "0" + textString[2:] + } else { + textString = "0" + textString + } + } + + // decode hex string + b, err := hexutil.Decode(textString) + if err != nil { + return err + } + + *k = b + return nil +} + +func (k StorageKey) MarshalText() ([]byte, error) { + return []byte(hexutil.Encode(k)), nil +} + +func (k StorageKey) String() string { + return hexutil.Encode(k) +} + +type StorageProofEntry struct { + Key StorageKey `json:"key"` + Value hexutil.Big `json:"value"` + Proof []hexutil.Bytes `json:"proof"` +} + +type AccountResult struct { + AccountProof []hexutil.Bytes `json:"accountProof"` + + Address common.Address `json:"address"` + Balance *hexutil.Big `json:"balance"` + CodeHash common.Hash `json:"codeHash"` + Nonce hexutil.Uint64 `json:"nonce"` + StorageHash common.Hash `json:"storageHash"` + + // Optional + StorageProof []StorageProofEntry `json:"storageProof,omitempty"` +} + +// Verify an account (and optionally storage) proof from the getProof RPC. See https://eips.ethereum.org/EIPS/eip-1186 +func (res *AccountResult) Verify(stateRoot common.Hash) error { + // verify storage proof values, if any, against the storage trie root hash of the account + for i, entry := range res.StorageProof { + // load all MPT nodes into a DB + db := memorydb.New() + for j, encodedNode := range entry.Proof { + nodeKey := encodedNode + if len(encodedNode) >= 32 { // small MPT nodes are not hashed + nodeKey = crypto.Keccak256(encodedNode) + } + if err := db.Put(nodeKey, encodedNode); err != nil { + return fmt.Errorf("failed to load storage proof node %d of storage value %d into mem db: %w", j, i, err) + } + } + path := crypto.Keccak256(entry.Key) + val, err := trie.VerifyProof(res.StorageHash, path, db) + if err != nil { + return fmt.Errorf("failed to verify storage value %d with key %s (path %x) in storage trie %s: %w", i, entry.Key.String(), path, res.StorageHash, err) + } + if val == nil && entry.Value.ToInt().Cmp(common.Big0) == 0 { // empty storage is zero by default + continue + } + comparison, err := rlp.EncodeToBytes(entry.Value.ToInt().Bytes()) + if err != nil { + return fmt.Errorf("failed to encode storage value %d with key %s (path %x) in storage trie %s: %w", i, entry.Key.String(), path, res.StorageHash, err) + } + if !bytes.Equal(val, comparison) { + return fmt.Errorf("value %d in storage proof does not match proven value at key %s (path %x)", i, entry.Key.String(), path) + } + } + + accountClaimed := []any{uint64(res.Nonce), res.Balance.ToInt().Bytes(), res.StorageHash, res.CodeHash} + accountClaimedValue, err := rlp.EncodeToBytes(accountClaimed) + if err != nil { + return fmt.Errorf("failed to encode account from retrieved values: %w", err) + } + + // create a db with all account trie nodes + db := memorydb.New() + for i, encodedNode := range res.AccountProof { + nodeKey := encodedNode + if len(encodedNode) >= 32 { // small MPT nodes are not hashed + nodeKey = crypto.Keccak256(encodedNode) + } + if err := db.Put(nodeKey, encodedNode); err != nil { + return fmt.Errorf("failed to load account proof node %d into mem db: %w", i, err) + } + } + path := crypto.Keccak256(res.Address[:]) + accountProofValue, err := trie.VerifyProof(stateRoot, path, db) + if err != nil { + return fmt.Errorf("failed to verify account value with key %s (path %x) in account trie %s: %w", res.Address, path, stateRoot, err) + } + + if !bytes.Equal(accountClaimedValue, accountProofValue) { + return fmt.Errorf("L1 RPC is tricking us, account proof does not match provided deserialized values:\n"+ + " claimed: %x\n"+ + " proof: %x", accountClaimedValue, accountProofValue) + } + return err +} diff --git a/op-monitorism/faultproof_withdrawals/validator/op_node_helper.go b/op-monitorism/faultproof_withdrawals/validator/op_node_helper.go index 7fa0543c..0e614241 100644 --- a/op-monitorism/faultproof_withdrawals/validator/op_node_helper.go +++ b/op-monitorism/faultproof_withdrawals/validator/op_node_helper.go @@ -10,101 +10,89 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" lru "github.com/hashicorp/golang-lru" ) // OpNodeHelper assists in interacting with the op-node type OpNodeHelper struct { // objects - l2OpNodeClient *ethclient.Client // The op-node (consensus) client. - l2OpGethClient *ethclient.Client // The op-geth client. - rpc_l2Client *rpc.Client // The RPC client for the L2 node. - ctx context.Context // Context for managing cancellation and timeouts. - l2OutputRootCache *lru.Cache // Cache for storing L2 output roots. - LatestKnownL2BlockNumber uint64 // The latest known L2 block number. + l2OpGethClient *ethclient.Client // The op-geth client. + l2OpGethBackupClients map[string]*ethclient.Client // The op-geth backup clients. + ctx context.Context // Context for managing cancellation and timeouts. + l2OutputRootCache *lru.Cache // Cache for storing L2 output roots. } const outputRootCacheSize = 1000 // Size of the output root cache. // NewOpNodeHelper initializes a new OpNodeHelper. // It creates a cache for storing output roots and binds to the L2 node client. -func NewOpNodeHelper(ctx context.Context, l2OpNodeClient *ethclient.Client, l2OpGethClient *ethclient.Client) (*OpNodeHelper, error) { +func NewOpNodeHelper(ctx context.Context, l2OpGethClient *ethclient.Client, l2OpGethBackupClients map[string]*ethclient.Client) (*OpNodeHelper, error) { l2OutputRootCache, err := lru.New(outputRootCacheSize) if err != nil { return nil, fmt.Errorf("failed to create cache: %w", err) } - rpc_l2Client := l2OpNodeClient.Client() ret := OpNodeHelper{ - l2OpNodeClient: l2OpNodeClient, - l2OpGethClient: l2OpGethClient, - rpc_l2Client: rpc_l2Client, - ctx: ctx, - l2OutputRootCache: l2OutputRootCache, - LatestKnownL2BlockNumber: 0, + l2OpGethClient: l2OpGethClient, + l2OpGethBackupClients: l2OpGethBackupClients, + ctx: ctx, + l2OutputRootCache: l2OutputRootCache, } - //ignoring the return value as it is already stored in the struct by the method - latestBlockNumber, err := ret.GetLatestKnownL2BlockNumber() - if err != nil { - return nil, fmt.Errorf("failed to get latest known L2 block number: %w", err) - } - - ret.LatestKnownL2BlockNumber = latestBlockNumber return &ret, nil } // get latest known L2 block number -func (on *OpNodeHelper) GetLatestKnownL2BlockNumber() (uint64, error) { - LatestKnownL2BlockNumber, err := on.l2OpGethClient.BlockNumber(on.ctx) +func (on *OpNodeHelper) BlockNumber() (uint64, error) { + return on.l2OpGethClient.BlockNumber(on.ctx) +} + +// GetOutputRootFromCalculation retrieves the output root by calculating it from the given block number. +// It returns the calculated output root as a Bytes32 array. +func (on *OpNodeHelper) GetOutputRootFromCalculation(blockNumber *big.Int) ([32]byte, string, error) { + // We get the block from our trusted op-geth node + block, err := on.l2OpGethClient.BlockByNumber(on.ctx, blockNumber) if err != nil { - return 0, fmt.Errorf("failed to get latest known L2 block number: %w", err) + return [32]byte{}, "", fmt.Errorf("failed to get output at block for game blockInt:%v error:%w", blockNumber, err) } - on.LatestKnownL2BlockNumber = LatestKnownL2BlockNumber - return LatestKnownL2BlockNumber, nil -} -// GetOutputRootFromTrustedL2Node retrieves the output root for a given L2 block number from a trusted L2 node. -// It returns the output root as a Bytes32 array. -func (on *OpNodeHelper) GetOutputRootFromTrustedL2Node(l2blockNumber *big.Int) ([32]byte, error) { - ret, found := on.l2OutputRootCache.Get(l2blockNumber) + // We get proof from our trusted op-geth node if present + accountResult, clientUsed, err := on.RetrieveEthProof(blockNumber) + if err != nil { + return [32]byte{}, "", fmt.Errorf("failed to get proof: %w", err) + } + // verify the proof when this comes from untrusted node (merkle trie) + err = accountResult.Verify(block.Root()) + if err != nil { + return [32]byte{}, "", fmt.Errorf("failed to verify proof: %w", err) + } + outputRoot := eth.OutputRoot(ð.OutputV0{StateRoot: [32]byte(block.Root()), MessagePasserStorageRoot: [32]byte(accountResult.StorageHash), BlockHash: block.Hash()}) + return outputRoot, clientUsed, nil +} - if !found { - var result OutputResponse - l2blockNumberHex := hexutil.EncodeBig(l2blockNumber) +// we retrieve the proof from the truested op-geth node and eventually from backup nodes if present +func (on *OpNodeHelper) RetrieveEthProof(blockNumber *big.Int) (AccountResult, string, error) { + accountResult := AccountResult{} - err := on.rpc_l2Client.CallContext(on.ctx, &result, "optimism_outputAtBlock", l2blockNumberHex) - //check if error contains "failed to determine L2BlockRef of height" - if err != nil { - return [32]byte{}, fmt.Errorf("failed to get output at block for game block:%v : %w", l2blockNumberHex, err) - } - trustedRootProof, err := StringToBytes32(result.OutputRoot) - if err != nil { - return [32]byte{}, fmt.Errorf("failed to convert output root to Bytes32: %w", err) - } - ret = [32]byte(trustedRootProof) - on.l2OutputRootCache.Add(l2blockNumber, ret) - } + // it will try to retrieve the proof from all the nodes we have starting with op-geth truested node, till when one of the nodes returns a proof, if none of the nodes returns a proof, it will return an error - return ret.([32]byte), nil -} + encodedBlock := hexutil.EncodeBig(blockNumber) -// GetOutputRootFromCalculation retrieves the output root by calculating it from the given block number. -// It returns the calculated output root as a Bytes32 array. -func (on *OpNodeHelper) GetOutputRootFromCalculation(blockNumber *big.Int) ([32]byte, error) { - block, err := on.l2OpNodeClient.BlockByNumber(on.ctx, blockNumber) + err := on.l2OpGethClient.Client().CallContext(on.ctx, &accountResult, "eth_getProof", predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, encodedBlock) if err != nil { - return [32]byte{}, fmt.Errorf("failed to get block by number: %w", err) - } - proof := struct{ StorageHash common.Hash }{} - err = on.l2OpNodeClient.Client().CallContext(on.ctx, &proof, "eth_getProof", predeploys.L2ToL1MessagePasserAddr, nil, hexutil.EncodeBig(blockNumber)) - if err != nil { - return [32]byte{}, fmt.Errorf("failed to get proof: %w", err) + for clientName, client := range on.l2OpGethBackupClients { + + err = client.Client().CallContext(on.ctx, &accountResult, "eth_getProof", predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, encodedBlock) + // if we get a proof, we return it + if err == nil { + return accountResult, clientName, nil + } + } + + return AccountResult{}, "", fmt.Errorf("failed to get proof from any node: %w", err) } - outputRoot := eth.OutputRoot(ð.OutputV0{StateRoot: [32]byte(block.Root()), MessagePasserStorageRoot: [32]byte(proof.StorageHash), BlockHash: block.Hash()}) - return outputRoot, nil + return accountResult, "default", nil } diff --git a/op-monitorism/faultproof_withdrawals/validator/proven_withdrawal_validator.go b/op-monitorism/faultproof_withdrawals/validator/proven_withdrawal_validator.go index abe35348..5d9b7732 100644 --- a/op-monitorism/faultproof_withdrawals/validator/proven_withdrawal_validator.go +++ b/op-monitorism/faultproof_withdrawals/validator/proven_withdrawal_validator.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" ) // ValidateProofWithdrawalState represents the state of the proof validation. @@ -30,26 +31,29 @@ type EnrichedProvenWithdrawalEvent struct { WithdrawalHashPresentOnL2 bool // Indicates if the withdrawal hash is present on L2. Enriched bool // Indicates if the event is enriched. ProcessedTimeStamp float64 // Unix TimeStamp seconds when the event was processed. + ClientUsed string // Client used to get the proof } // ProvenWithdrawalValidator validates proven withdrawal events. type ProvenWithdrawalValidator struct { optimismPortal2Helper *OptimismPortal2Helper // Helper for interacting with Optimism Portal 2. - l2NodeHelper *OpNodeHelper // Helper for L2 node interactions. + L2NodeHelper *OpNodeHelper // Helper for L2 node interactions. l2ToL1MessagePasserHelper *L2ToL1MessagePasserHelper // Helper for L2 to L1 message passing. faultDisputeGameHelper *FaultDisputeGameHelper // Helper for dispute game interactions. ctx context.Context // Context for managing cancellation and timeouts. + log log.Logger // Logger for logging. } // String provides a string representation of EnrichedProvenWithdrawalEvent. func (e *EnrichedProvenWithdrawalEvent) String() string { - return fmt.Sprintf("Event: %v, DisputeGame: %v, ExpectedRootClaim: %s, Blacklisted: %v, withdrawalHashPresentOnL2: %v, Enriched: %v", + return fmt.Sprintf("Event: %v, DisputeGame: %v, ExpectedRootClaim: %s, Blacklisted: %v, withdrawalHashPresentOnL2: %v, Enriched: %v, ClientUsed: %v", e.Event, e.DisputeGame, common.Bytes2Hex(e.ExpectedRootClaim[:]), e.Blacklisted, e.WithdrawalHashPresentOnL2, - e.Enriched) + e.Enriched, + e.ClientUsed) } // String provides a string representation of ValidateProofWithdrawalState. @@ -59,7 +63,7 @@ func (v ValidateProofWithdrawalState) String() string { // NewWithdrawalValidator initializes a new ProvenWithdrawalValidator. // It binds necessary helpers and returns the validator instance. -func NewWithdrawalValidator(ctx context.Context, l1GethClient *ethclient.Client, l2OpGethClient *ethclient.Client, l2OpNodeClient *ethclient.Client, OptimismPortalAddress common.Address) (*ProvenWithdrawalValidator, error) { +func NewWithdrawalValidator(ctx context.Context, log log.Logger, l1GethClient *ethclient.Client, l2OpGethClient *ethclient.Client, l2OpGethBackupClients map[string]*ethclient.Client, OptimismPortalAddress common.Address) (*ProvenWithdrawalValidator, error) { optimismPortal2Helper, err := NewOptimismPortal2Helper(ctx, l1GethClient, OptimismPortalAddress) if err != nil { return nil, fmt.Errorf("failed to bind to the OptimismPortal: %w", err) @@ -75,17 +79,18 @@ func NewWithdrawalValidator(ctx context.Context, l1GethClient *ethclient.Client, return nil, fmt.Errorf("failed to create l2 to l1 message passer helper: %w", err) } - l2NodeHelper, err := NewOpNodeHelper(ctx, l2OpNodeClient, l2OpGethClient) + l2NodeHelper, err := NewOpNodeHelper(ctx, l2OpGethClient, l2OpGethBackupClients) if err != nil { return nil, fmt.Errorf("failed to create l2 node helper: %w", err) } return &ProvenWithdrawalValidator{ optimismPortal2Helper: optimismPortal2Helper, - l2NodeHelper: l2NodeHelper, + L2NodeHelper: l2NodeHelper, l2ToL1MessagePasserHelper: l2ToL1MessagePasserHelper, faultDisputeGameHelper: faultDisputeGameHelper, ctx: ctx, + log: log, }, nil } @@ -113,16 +118,17 @@ func (wv *ProvenWithdrawalValidator) UpdateEnrichedWithdrawalEvent(event *Enrich // Check if the game root claim is valid on L2 only if not confirmed already that it is on L2 if !event.Enriched { - latest_known_l2_block, err := wv.l2NodeHelper.GetLatestKnownL2BlockNumber() + latest_known_l2_block, err := wv.L2NodeHelper.BlockNumber() if err != nil { return fmt.Errorf("failed to get latest known L2 block number: %w", err) } if latest_known_l2_block >= event.DisputeGame.DisputeGameData.L2blockNumber.Uint64() { - trustedRootClaim, err := wv.l2NodeHelper.GetOutputRootFromTrustedL2Node(event.DisputeGame.DisputeGameData.L2blockNumber) + trustedRootClaim, clientUsed, err := wv.L2NodeHelper.GetOutputRootFromCalculation(event.DisputeGame.DisputeGameData.L2blockNumber) if err != nil { - return fmt.Errorf("failed to get trustedRootClaim from Op-node: %w", err) + return fmt.Errorf("failed to get trustedRootClaim from Op-node %s: %w", clientUsed, err) } event.ExpectedRootClaim = trustedRootClaim + event.ClientUsed = clientUsed } else { event.ExpectedRootClaim = [32]byte{} } @@ -250,7 +256,3 @@ func (wv *ProvenWithdrawalValidator) IsWithdrawalEventValid(enrichedWithdrawalEv return false, nil } } - -func (wv *ProvenWithdrawalValidator) GetLatestL2Height() uint64 { - return wv.l2NodeHelper.LatestKnownL2BlockNumber -}