Skip to content

Commit

Permalink
Merge branch 'main' into faultproof_withdrawals-improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
raffaele-oplabs committed Oct 10, 2024
2 parents 4f22cf7 + 88bb56c commit 12e2649
Show file tree
Hide file tree
Showing 14 changed files with 710 additions and 281 deletions.
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ workflows:
docker_name: op-monitorism
docker_tags: <<pipeline.git.revision>>
requires: ["hold"]
platforms: "linux/amd64"
platforms: "linux/amd64,linux/arm64"
publish: true
release: true
context:
Expand All @@ -297,7 +297,7 @@ workflows:
docker_name: op-defender
docker_tags: <<pipeline.git.revision>>
requires: ["hold"]
platforms: "linux/amd64"
platforms: "linux/amd64,linux/arm64"
publish: true
release: true
context:
Expand Down
9 changes: 9 additions & 0 deletions op-monitorism/faultproof_withdrawals/.env.op.mainnet.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL="<geth-url>"
FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL="<op-geth-node>"
FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL="<op-geth-url>"
FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL="0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" # This is the address of the Optimism portal contract, this should be for the chain you are monitoring
FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT=20872390 # This is the block height from which the monitoring will start, decide at which block height you want to start monitoring
FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE=1000 # This is the range of blocks to be monitored
MONITORISM_LOOP_INTERVAL_MSEC=100 # This is the interval in milliseconds for the monitoring loop
MONITORISM_METRICS_PORT=7300 # This is the port on which the metrics server will run
MONITORISM_METRICS_ENABLED=true # This is the flag to enable/disable the metrics server
2 changes: 0 additions & 2 deletions op-monitorism/faultproof_withdrawals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ or
cd ../
go run ./cmd/monitorism faultproof_withdrawals --metrics.enabled --metrics.port 7300
```
## Available Metrics and Meaning


# Cli options

Expand Down
84 changes: 32 additions & 52 deletions op-monitorism/faultproof_withdrawals/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/ethereum-optimism/monitorism/op-monitorism/faultproof_withdrawals/validator"
"github.com/ethereum-optimism/optimism/op-service/metrics"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
Expand Down Expand Up @@ -123,14 +124,14 @@ func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIC
startingL1BlockHeight = uint64(cfg.StartingL1BlockHeight)
}

state, err := NewState(log, startingL1BlockHeight, latestL1Height)
state, err := NewState(log, startingL1BlockHeight, latestL1Height, ret.withdrawalValidator.GetLatestL2Height())
if err != nil {
return nil, fmt.Errorf("failed to create state: %w", err)
}
ret.state = *state

// log state and metrics
ret.state.LogState(ret.log)
ret.state.LogState()
ret.metrics.UpdateMetricsFromState(&ret.state)

return ret, nil
Expand Down Expand Up @@ -228,6 +229,10 @@ func (m *Monitor) GetMaxBlock() (uint64, error) {
// Run executes the main monitoring loop.
// It retrieves new events, processes them, and updates the state accordingly.
func (m *Monitor) Run(ctx context.Context) {
// Defer the update function
defer m.metrics.UpdateMetricsFromState(&m.state)
defer m.state.LogState()

start := m.state.nextL1Height

stop, err := m.GetMaxBlock()
Expand All @@ -238,18 +243,16 @@ func (m *Monitor) Run(ctx context.Context) {
}

// review previous invalidProposalWithdrawalsEvents
invalidProposalWithdrawalsEvents, err := m.ConsumeEvents(m.state.invalidProposalWithdrawalsEvents)
err = m.ConsumeEvents(m.state.potentialAttackOnInProgressGames)
if err != nil {
m.state.nodeConnectionFailures++
m.log.Error("failed to consume events", "error", err)
return
}

// update state
m.state.invalidProposalWithdrawalsEvents = *invalidProposalWithdrawalsEvents

// get new events

newEvents, err := m.withdrawalValidator.GetEnrichedWithdrawalsEvents(start, &stop)
m.log.Info("getting enriched withdrawal events", "start", fmt.Sprintf("%d", start), "stop", fmt.Sprintf("%d", stop))
newEvents, err := m.withdrawalValidator.GetEnrichedWithdrawalsEventsMap(start, &stop)
if err != nil {
if start >= stop {
m.log.Info("no new events to process", "start", start, "stop", stop)
Expand All @@ -262,99 +265,76 @@ func (m *Monitor) Run(ctx context.Context) {
}
return
}
newInvalidProposalWithdrawalsEvents, err := m.ConsumeEvents(newEvents)

err = m.ConsumeEvents(newEvents)
if err != nil {
m.state.nodeConnectionFailures++
m.log.Error("failed to consume events", "error", err)
return
}

// update state
if len(*newInvalidProposalWithdrawalsEvents) > 0 && newInvalidProposalWithdrawalsEvents != nil {
m.state.invalidProposalWithdrawalsEvents = append(m.state.invalidProposalWithdrawalsEvents, *newInvalidProposalWithdrawalsEvents...)
}

// update state
m.state.nextL1Height = stop

// log state and metrics
m.state.LogState(m.log)
m.metrics.UpdateMetricsFromState(&m.state)
}

// ConsumeEvents processes a slice of enriched withdrawal events and updates their states.
// It returns any events detected during the consumption that requires to be re-analysed again at a later stage (when the event referenced DisputeGame completes).
func (m *Monitor) ConsumeEvents(enrichedWithdrawalEvent []validator.EnrichedProvenWithdrawalEvent) (*[]validator.EnrichedProvenWithdrawalEvent, error) {
var newForgeriesGameInProgressEvent []validator.EnrichedProvenWithdrawalEvent = make([]validator.EnrichedProvenWithdrawalEvent, 0)
for _, enrichedWithdrawalEvent := range enrichedWithdrawalEvent {
func (m *Monitor) ConsumeEvents(enrichedWithdrawalEvents map[common.Hash]validator.EnrichedProvenWithdrawalEvent) error {
for _, enrichedWithdrawalEvent := range enrichedWithdrawalEvents {
m.log.Info("processing withdrawal event", "event", &enrichedWithdrawalEvent)
err := m.withdrawalValidator.UpdateEnrichedWithdrawalEvent(&enrichedWithdrawalEvent)
//upgrade state to the latest L2 height after the event is processed
m.state.latestL2Height = m.withdrawalValidator.GetLatestL2Height()
if err != nil {
m.state.nodeConnectionFailures++
m.log.Error("failed to update enriched withdrawal event", "error", err)
return nil, err
return err
}

consumedEvent, err := m.ConsumeEvent(enrichedWithdrawalEvent)
err = m.ConsumeEvent(enrichedWithdrawalEvent)
if err != nil {
m.log.Error("failed to consume event", "error", err)
return nil, err
} else if !consumedEvent {
newForgeriesGameInProgressEvent = append(newForgeriesGameInProgressEvent, enrichedWithdrawalEvent)
return err
}
}

return &newForgeriesGameInProgressEvent, nil
return nil
}

// ConsumeEvent processes a single enriched withdrawal event.
// It logs the event details and checks for any forgery detection.
func (m *Monitor) ConsumeEvent(enrichedWithdrawalEvent validator.EnrichedProvenWithdrawalEvent) (bool, error) {
m.log.Info("processing withdrawal event", "event", enrichedWithdrawalEvent.Event)
func (m *Monitor) ConsumeEvent(enrichedWithdrawalEvent validator.EnrichedProvenWithdrawalEvent) error {
if enrichedWithdrawalEvent.DisputeGame.DisputeGameData.L2ChainID.Cmp(m.l2ChainID) != 0 {
m.log.Error("l2ChainID mismatch", "expected", fmt.Sprintf("%d", m.l2ChainID), "got", fmt.Sprintf("%d", enrichedWithdrawalEvent.DisputeGame.DisputeGameData.L2ChainID))
}
valid, err := m.withdrawalValidator.IsWithdrawalEventValid(&enrichedWithdrawalEvent)
if err != nil {
m.state.nodeConnectionFailures++
m.log.Error("failed to check if forgery detected", "error", err)
return false, err
return err
}
eventConsumed := false

if !valid {
m.state.numberOfInvalidWithdrawals++
if !enrichedWithdrawalEvent.Blacklisted {
if enrichedWithdrawalEvent.DisputeGame.DisputeGameData.Status == validator.CHALLENGER_WINS {
m.log.Warn("WITHDRAWAL: is NOT valid, but the game is correctly resolved", "enrichedWithdrawalEvent", enrichedWithdrawalEvent)
m.state.withdrawalsValidated++
eventConsumed = true
m.state.IncrementSuspiciousEventsOnChallengerWinsGames(enrichedWithdrawalEvent)
} else if enrichedWithdrawalEvent.DisputeGame.DisputeGameData.Status == validator.DEFENDER_WINS {
m.log.Error("WITHDRAWAL: is NOT valid, forgery detected", "enrichedWithdrawalEvent", enrichedWithdrawalEvent)
m.state.numberOfDetectedForgery++
// add to forgeries
m.state.forgeriesWithdrawalsEvents = append(m.state.forgeriesWithdrawalsEvents, enrichedWithdrawalEvent)
eventConsumed = true
m.state.IncrementPotentialAttackOnDefenderWinsGames(enrichedWithdrawalEvent)
} else if enrichedWithdrawalEvent.DisputeGame.DisputeGameData.Status == validator.IN_PROGRESS {
m.log.Warn("WITHDRAWAL: is NOT valid, game is still in progress.", "enrichedWithdrawalEvent", enrichedWithdrawalEvent)
m.state.IncrementPotentialAttackOnInProgressGames(enrichedWithdrawalEvent)
// add to events to be re-processed
eventConsumed = false
} else {
m.log.Error("WITHDRAWAL: is NOT valid, game status is unknown. UNKNOWN STATE SHOULD NEVER HAPPEN", "enrichedWithdrawalEvent", enrichedWithdrawalEvent)
eventConsumed = false
}

} else {
m.log.Warn("WITHDRAWAL: is NOT valid, but game is blacklisted", "enrichedWithdrawalEvent", enrichedWithdrawalEvent)
m.state.withdrawalsValidated++
eventConsumed = true
m.state.IncrementSuspiciousEventsOnChallengerWinsGames(enrichedWithdrawalEvent)
}
} else {
m.log.Info("WITHDRAWAL: is valid", "enrichedWithdrawalEvent", enrichedWithdrawalEvent)
m.state.withdrawalsValidated++
eventConsumed = true
m.state.IncrementWithdrawalsValidated(enrichedWithdrawalEvent)
}
m.state.processedProvenWithdrawalsExtension1Events++
m.state.eventsProcessed++
m.metrics.UpdateMetricsFromState(&m.state)
return eventConsumed, nil
return nil
}

// Close gracefully shuts down the Monitor by closing the Geth clients.
Expand Down
173 changes: 173 additions & 0 deletions op-monitorism/faultproof_withdrawals/monitor_live_mainnet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//go:build live
// +build live

package faultproof_withdrawals

import (
"context"
"io"
"math/big"
"testing"

oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum/go-ethereum/common"
"github.com/joho/godotenv"
"github.com/stretchr/testify/require"
)

// NewTestMonitorMainnet initializes and returns a new Monitor instance for testing.
// It sets up the necessary environment variables and configurations required for the monitor.
func NewTestMonitorMainnet() *Monitor {
envmap, err := godotenv.Read(".env.op.mainnet")
if err != nil {
panic("error")
}

ctx := context.Background()
L1GethURL := envmap["FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL"]
L2OpNodeURL := envmap["FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL"]
L2OpGethURL := envmap["FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL"]

FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL := "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"
FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE := uint64(1000)
FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT := int64(6789100)

cfg := CLIConfig{
L1GethURL: L1GethURL,
L2OpGethURL: L2OpGethURL,
L2OpNodeURL: L2OpNodeURL,
EventBlockRange: FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE,
StartingL1BlockHeight: FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT,
OptimismPortalAddress: common.HexToAddress(FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL),
}

clicfg := oplog.DefaultCLIConfig()
output_writer := io.Discard // discard log output during tests to avoid pollution of the standard output
log := oplog.NewLogger(output_writer, clicfg)

metricsRegistry := opmetrics.NewRegistry()
monitor, err := NewMonitor(ctx, log, opmetrics.With(metricsRegistry), cfg)
if err != nil {
panic(err)
}
return monitor
}

// TestSingleRunMainnet tests a single execution of the monitor's Run method.
// It verifies that the state updates correctly after running.
func TestSingleRunMainnet(t *testing.T) {
test_monitor := NewTestMonitorMainnet()

initialBlock := test_monitor.state.nextL1Height
blockIncrement := test_monitor.maxBlockRange
finalBlock := initialBlock + blockIncrement

test_monitor.Run(test_monitor.ctx)

require.Equal(t, finalBlock, test_monitor.state.nextL1Height)
require.Equal(t, uint64(0), test_monitor.state.withdrawalsProcessed)
require.Equal(t, uint64(0), test_monitor.state.eventsProcessed)
require.Equal(t, uint64(0), test_monitor.state.numberOfPotentialAttackOnInProgressGames)
require.Equal(t, uint64(0), test_monitor.state.numberOfPotentialAttacksOnDefenderWinsGames)
require.Equal(t, uint64(0), test_monitor.state.numberOfSuspiciousEventsOnChallengerWinsGames)

require.Equal(t, test_monitor.state.numberOfPotentialAttackOnInProgressGames, uint64(len(test_monitor.state.potentialAttackOnInProgressGames)))
require.Equal(t, test_monitor.state.numberOfPotentialAttacksOnDefenderWinsGames, uint64(len(test_monitor.state.potentialAttackOnDefenderWinsGames)))
require.Equal(t, test_monitor.state.numberOfSuspiciousEventsOnChallengerWinsGames, uint64(test_monitor.state.suspiciousEventsOnChallengerWinsGames.Len()))

}

// TestRun5Cycle1000BlocksMainnet tests multiple executions of the monitor's Run method over several cycles.
// It verifies that the state updates correctly after each cycle.
func TestRun5Cycle1000BlocksMainnet(t *testing.T) {
test_monitor := NewTestMonitorMainnet()

maxCycle := uint64(5)
initialBlock := test_monitor.state.nextL1Height
blockIncrement := test_monitor.maxBlockRange

for cycle := uint64(1); cycle <= maxCycle; cycle++ {
test_monitor.Run(test_monitor.ctx)
}

initialL1HeightGaugeValue, _ := GetGaugeValue(test_monitor.metrics.InitialL1HeightGauge)
nextL1HeightGaugeValue, _ := GetGaugeValue(test_monitor.metrics.NextL1HeightGauge)

withdrawalsProcessedCounterValue, _ := GetCounterValue(test_monitor.metrics.WithdrawalsProcessedCounter)
eventsProcessedCounterValue, _ := GetCounterValue(test_monitor.metrics.EventsProcessedCounter)

nodeConnectionFailuresCounterValue, _ := GetCounterValue(test_monitor.metrics.NodeConnectionFailuresCounter)

expected_end_block := blockIncrement*maxCycle + initialBlock
require.Equal(t, uint64(initialBlock), uint64(initialL1HeightGaugeValue))
require.Equal(t, uint64(expected_end_block), uint64(nextL1HeightGaugeValue))

require.Equal(t, uint64(0), uint64(eventsProcessedCounterValue))
require.Equal(t, uint64(0), uint64(withdrawalsProcessedCounterValue))
require.Equal(t, uint64(0), uint64(nodeConnectionFailuresCounterValue))

require.Equal(t, uint64(0), test_monitor.metrics.previousEventsProcessed)
require.Equal(t, uint64(0), test_monitor.metrics.previousWithdrawalsProcessed)

}

func TestRunSingleBlocksMainnet(t *testing.T) {
test_monitor := NewTestMonitorMainnet()

maxCycle := 1
initialBlock := test_monitor.state.nextL1Height
blockIncrement := test_monitor.maxBlockRange
finalBlock := initialBlock + blockIncrement

for cycle := 1; cycle <= maxCycle; cycle++ {
test_monitor.Run(test_monitor.ctx)
}

require.Equal(t, test_monitor.state.nextL1Height, finalBlock)
require.Equal(t, uint64(0), test_monitor.state.withdrawalsProcessed)
require.Equal(t, uint64(0), test_monitor.state.eventsProcessed)
require.Equal(t, 0, len(test_monitor.state.potentialAttackOnDefenderWinsGames))
require.Equal(t, 0, len(test_monitor.state.potentialAttackOnInProgressGames))
require.Equal(t, 0, test_monitor.state.suspiciousEventsOnChallengerWinsGames.Len())
}

func TestInvalidWithdrawalsOnMainnet(t *testing.T) {
test_monitor := NewTestMonitorMainnet()

// On mainnet for OP OptimismPortal, the block number 20873192 is known to have only 1 event
start := uint64(20873192)
stop := uint64(20873193)
newEvents, err := test_monitor.withdrawalValidator.GetEnrichedWithdrawalsEvents(start, &stop)
require.NoError(t, err)
require.Equal(t, len(newEvents), 1)

event := newEvents[0]
require.NotNil(t, event)

// Expected event:
//{WithdrawalHash: 0x45fd4bbcf3386b1fdf75929345b9243c05cd7431a707e84c293b710d40220ebd, ProofSubmitter: 0x394400571C825Da37ca4D6780417DFB514141b1f}
require.Equal(t, event.Event.WithdrawalHash, [32]byte(common.HexToHash("0x45fd4bbcf3386b1fdf75929345b9243c05cd7431a707e84c293b710d40220ebd")))
require.Equal(t, event.Event.ProofSubmitter, common.HexToAddress("0x394400571C825Da37ca4D6780417DFB514141b1f"))

//Expected DisputeGameData:
// Game address: 0x52cE243d552369b11D6445Cd187F6393d3B42D4a
require.Equal(t, event.DisputeGame.DisputeGameData.ProxyAddress, common.HexToAddress("0x52cE243d552369b11D6445Cd187F6393d3B42D4a"))

// Expected Game root claim
// 0xbc1c5ba13b936c6c23b7c51d425f25a8c9444771e851b6790f817a6002a14a33
require.Equal(t, event.DisputeGame.DisputeGameData.RootClaim, [32]byte(common.HexToHash("0xbc1c5ba13b936c6c23b7c51d425f25a8c9444771e851b6790f817a6002a14a33")))

// Expected L2 block number 1276288764
require.Equal(t, event.DisputeGame.DisputeGameData.L2blockNumber, big.NewInt(1276288764))

isValid, err := test_monitor.withdrawalValidator.IsWithdrawalEventValid(&event)
require.EqualError(t, err, "game not enriched")
require.False(t, isValid)
err = test_monitor.withdrawalValidator.UpdateEnrichedWithdrawalEvent(&event)
require.NoError(t, err)
isValid, err = test_monitor.withdrawalValidator.IsWithdrawalEventValid(&event)
require.NoError(t, err)
require.False(t, isValid)

}
Loading

0 comments on commit 12e2649

Please sign in to comment.