Skip to content

Commit 9f4486f

Browse files
* feat(op-monitorism): enhance error handling and logging in withdrawal processes - Added additional logging details in the monitor for enriched withdrawal events, including L1 block range. - Improved error messages in L2 proxy and withdrawal validator to include context about failed backup clients. - Updated the GethBackupClientsDictionary function to return a map of bad clients for better diagnostics. - Enhanced the GetProvenWithdrawalsEventsIterator function to clarify block range in error messages. * fix(monitor): update L2 Geth backup URLs logging format - Changed the logging of L2 Geth backup URLs to join keys into a comma-separated string for improved readability. - This enhancement ensures better clarity in the monitor's output regarding backup client configurations. * refactor(env): remove deprecated L2 OP node URL and add backup Geth URLs - Removed the L2 OP node URL from example environment files for clarity. - Introduced a new configuration for L2 OP Geth backup URLs to enhance redundancy in monitoring setups. * enhance(test): improve test command functionality in Makefile - Updated the test target to allow for specific package testing by filtering based on command-line arguments. - Added a placeholder rule to enable passing additional arguments to the test target, enhancing flexibility in test execution. * docs(Makefile): update test command help message for clarity - Modified the help output for the test command to specify that it can run tests for a specific module, enhancing user understanding of its functionality. * feat(monitor): enhance block search algorithm and add unit tests - Refactored the binary search method for finding blocks at approximate timestamps, improving logging and error handling. - Introduced a new acceptable time difference constant for better clarity in block selection. - Added unit tests for state management and event handling in faultproof withdrawals, ensuring robustness of the new features.
1 parent d3ac6ec commit 9f4486f

File tree

8 files changed

+251
-106
lines changed

8 files changed

+251
-106
lines changed

op-monitorism/Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,15 @@ clean:
4242
.PHONY: test
4343
test:
4444
@echo "Running tests..."
45-
$(GOTEST) ./... -v
45+
@if [ "$(filter-out $@,$(MAKECMDGOALS))" != "" ]; then \
46+
$(GOTEST) -v -count=1 ./$(filter-out $@,$(MAKECMDGOALS))/...; \
47+
else \
48+
$(GOTEST) ./... -v; \
49+
fi
50+
51+
# This allows passing arguments to the test target
52+
%:
53+
@:
4654

4755
#include tests that require live resources
4856
#these resources are meant to be real and not mocked
@@ -64,7 +72,7 @@ help:
6472
@echo " make build"
6573
@echo " make run"
6674
@echo " make clean"
67-
@echo " make test"
75+
@echo " make test [module] # Run all tests or tests for a specific module (e.g. make test faultproof_withdrawals)"
6876
@echo " make test-live"
6977
@echo " make tidy"
7078
@echo " make help"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL="<geth-url>"
2-
FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL="<op-geth-node>"
32
FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL="<op-geth-url>"
43
FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL="0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" # This is the address of the Optimism portal contract, this should be for the chain you are monitoring
54
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
65
FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE=1000 # This is the range of blocks to be monitored
76
MONITORISM_LOOP_INTERVAL_MSEC=100 # This is the interval in milliseconds for the monitoring loop
87
MONITORISM_METRICS_PORT=7300 # This is the port on which the metrics server will run
98
MONITORISM_METRICS_ENABLED=true # This is the flag to enable/disable the metrics server
9+
FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_BACKUP_URLS="alchemy=<alchemy-op-geth-url>,infura=<infura-op-geth-url>"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL="<geth-url>"
2-
FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL="<op-geth-node>"
32
FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL="<op-geth-url>"
43
FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL="0x16Fc5058F25648194471939df75CF27A2fdC48BC" # This is the address of the Optimism portal contract, this should be for the chain you are monitoring
54
FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT=5914813 # This is the block height from which the monitoring will start, decide at which block height you want to start monitoring
65
FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE=1000 # This is the range of blocks to be monitored
76
MONITORISM_LOOP_INTERVAL_MSEC=100 # This is the interval in milliseconds for the monitoring loop
87
MONITORISM_METRICS_PORT=7300 # This is the port on which the metrics server will run
98
MONITORISM_METRICS_ENABLED=true # This is the flag to enable/disable the metrics server
9+
FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_BACKUP_URLS="alchemy=<alchemy-op-geth-url>,infura=<infura-op-geth-url>"

op-monitorism/faultproof_withdrawals/monitor.go

Lines changed: 100 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package faultproof_withdrawals
33
import (
44
"context"
55
"fmt"
6+
"math"
67
"math/big"
78
"strings"
89
"time"
@@ -11,13 +12,16 @@ import (
1112
"github.com/ethereum-optimism/optimism/op-service/metrics"
1213

1314
"github.com/ethereum/go-ethereum/common"
15+
"github.com/ethereum/go-ethereum/core/types"
1416
"github.com/ethereum/go-ethereum/ethclient"
1517
"github.com/ethereum/go-ethereum/log"
18+
"golang.org/x/exp/maps"
1619
)
1720

1821
const (
1922
MetricsNamespace = "faultproof_withdrawals"
2023
DefaultHoursInThePastToStartFrom = 14 * 24 //14 days
24+
acceptableDiffSec = 3600 // 1h
2125
)
2226

2327
// Monitor monitors the state and events related to withdrawal forgery.
@@ -163,100 +167,114 @@ func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIC
163167
"startingL1Height", startingL1BlockHeight,
164168
"latestL1Height", latestL1Height,
165169
"latestL2Height", latestL2Height,
166-
"maxBlockRange", cfg.EventBlockRange)
170+
"maxBlockRange", cfg.EventBlockRange,
171+
"l1GethURL", cfg.L1GethURL,
172+
"l2GethURL", cfg.L2OpGethURL,
173+
"optimismPortalAddress", cfg.OptimismPortalAddress,
174+
"l2GethBackupURLs", strings.Join(maps.Keys(mapL2GethBackupURLs), ","),
175+
"hoursInThePastToStartFrom", hoursInThePastToStartFrom,
176+
"startingL1BlockHeight", cfg.StartingL1BlockHeight,
177+
"eventBlockRange", cfg.EventBlockRange,
178+
)
167179

168180
return ret, nil
169181
}
170182

171183
// getBlockAtApproximateTimeBinarySearch finds the block number corresponding to the timestamp from two weeks ago using a binary search approach.
172-
func (m *Monitor) getBlockAtApproximateTimeBinarySearch(ctx context.Context, client *ethclient.Client, latestBlockNumber *big.Int, hoursInThePast *big.Int) (*big.Int, error) {
173-
174-
secondsInThePast := hoursInThePast.Mul(hoursInThePast, big.NewInt(60*60))
175-
m.log.Info("Looking for a block at approximate time of hours back",
176-
"secondsInThePast", fmt.Sprintf("%v", secondsInThePast),
177-
"time", fmt.Sprintf("%v", time.Now().Format("2006-01-02 15:04:05 MST")),
178-
"latestBlockNumber", fmt.Sprintf("%v", latestBlockNumber))
179-
// Calculate the total seconds in two weeks
180-
targetTime := big.NewInt(time.Now().Unix())
181-
targetTime.Sub(targetTime, secondsInThePast)
182-
183-
// Initialize the search range
184-
left := big.NewInt(0)
185-
right := new(big.Int).Set(latestBlockNumber)
186-
187-
var mid *big.Int
188-
acceptablediff := big.NewInt(60 * 60) //60 minutes
184+
func (m *Monitor) getBlockAtApproximateTimeBinarySearch(
185+
ctx context.Context,
186+
client *ethclient.Client,
187+
latest *big.Int,
188+
hoursAgo *big.Int,
189+
) (*big.Int, error) {
190+
m.log.Info("Starting binary search for block",
191+
"hoursAgo", hoursAgo,
192+
"latestBlock", fmt.Sprintf("%d", latest),
193+
"acceptableDiffSec", acceptableDiffSec)
194+
195+
secondsAgo := new(big.Int).Mul(hoursAgo, big.NewInt(3600))
196+
targetTs := big.NewInt(time.Now().Unix()).Sub(big.NewInt(time.Now().Unix()), secondsAgo)
197+
198+
m.log.Debug("Search parameters",
199+
"secondsAgo", secondsAgo,
200+
"targetTimestamp", time.Unix(targetTs.Int64(), 0).UTC().Format("2006-01-02 15:04:05 UTC"))
189201

190-
m.log.Debug("Starting binary search",
191-
"targetTime", time.Unix(targetTime.Int64(), 0).Format("2006-01-02 15:04:05 MST"),
192-
"leftBound", left.String(),
193-
"rightBound", right.String(),
194-
"acceptableDiffSeconds", acceptablediff.String())
202+
left := big.NewInt(0)
203+
right := new(big.Int).Set(latest)
204+
best := new(big.Int)
205+
var bestHdr *types.Header
206+
bestDiffAbs := new(big.Int).SetUint64(math.MaxUint64)
207+
iterations := 0
195208

196-
// Perform binary search
197209
for left.Cmp(right) <= 0 {
198-
//interrupt in case of context cancellation
199-
select {
200-
case <-ctx.Done():
201-
return nil, fmt.Errorf("context cancelled")
202-
default:
203-
}
204-
205-
// Calculate the midpoint
206-
mid = new(big.Int).Add(left, right)
210+
iterations++
211+
mid := new(big.Int).Add(left, right)
207212
mid.Div(mid, big.NewInt(2))
208213

209214
m.log.Debug("Binary search iteration",
210-
"left", left.String(),
211-
"right", right.String(),
212-
"mid", mid.String())
215+
"iteration", iterations,
216+
"left", left,
217+
"right", right,
218+
"mid", mid)
213219

214-
// Get the block at mid
215-
block, err := client.BlockByNumber(context.Background(), mid)
220+
hdr, err := client.HeaderByNumber(ctx, mid)
216221
if err != nil {
217-
m.log.Error("Failed to get block", "blockNumber", mid.String(), "error", err)
222+
m.log.Error("Failed to get block header", "block", mid, "error", err)
218223
return nil, err
219224
}
220225

221-
// Check the block's timestamp
222-
blockTime := big.NewInt(int64(block.Time()))
223-
blockTimeFormatted := time.Unix(blockTime.Int64(), 0).Format("2006-01-02 15:04:05 MST")
224-
225-
//calculate the difference between the block time and the target time
226-
diff := new(big.Int).Sub(blockTime, targetTime)
227-
diffHours := new(big.Int).Div(diff, big.NewInt(3600))
226+
blockTs := big.NewInt(int64(hdr.Time))
227+
diffAbs := new(big.Int).Abs(new(big.Int).Sub(blockTs, targetTs))
228+
diffHours := new(big.Int).Div(diffAbs, big.NewInt(3600))
228229

229230
m.log.Debug("Block time comparison",
230-
"blockNumber", mid.String(),
231-
"blockTime", blockTimeFormatted,
232-
"timeDiffHours", diffHours.String(),
233-
"timeDiffSeconds", diff.String())
234-
235-
// If block time is less than or equal to target time, check if we need to search to the right
236-
if blockTime.Cmp(targetTime) <= 0 {
237-
left.Set(mid) // Move left boundary up to mid
238-
m.log.Debug("Moving left boundary up", "newLeft", left.String())
239-
} else {
240-
right.Sub(mid, big.NewInt(1)) // Move right boundary down
241-
m.log.Debug("Moving right boundary down", "newRight", right.String())
231+
"block", fmt.Sprintf("%d", mid),
232+
"blockTime", time.Unix(int64(hdr.Time), 0).UTC().Format("2006-01-02 15:04:05 UTC"),
233+
"timeDiffHours", diffHours,
234+
"timeDiffSeconds", diffAbs)
235+
236+
if diffAbs.Cmp(bestDiffAbs) < 0 {
237+
best.Set(mid)
238+
bestHdr = hdr
239+
bestDiffAbs.Set(diffAbs)
240+
m.log.Debug("New best block found",
241+
"block", fmt.Sprintf("%d", mid),
242+
"timeDiff", diffAbs)
243+
if bestDiffAbs.Cmp(big.NewInt(acceptableDiffSec)) <= 0 {
244+
m.log.Debug("Found block within acceptable time difference", "diffSeconds", bestDiffAbs)
245+
break
246+
}
242247
}
243-
if new(big.Int).Abs(diff).Cmp(acceptablediff) <= 0 {
244-
//if the difference is less than or equal to 1 hour, we can consider this block as the block closest to the target time
245-
m.log.Debug("Found acceptable block",
246-
"blockNumber", mid.String(),
247-
"blockTime", blockTimeFormatted,
248-
"timeDiffHours", diffHours.String())
249-
break
248+
249+
if blockTs.Cmp(targetTs) < 0 {
250+
left.Set(mid)
251+
m.log.Debug("Moving left boundary", "newLeft", left)
252+
} else {
253+
right.Sub(mid, big.NewInt(1))
254+
m.log.Debug("Moving right boundary", "newRight", right)
250255
}
251256
}
252257

253-
// log the block number closest to the target time and the time
254258
m.log.Info("Found block closest to target time",
255-
"block", left.String(),
256-
"time", time.Unix(targetTime.Int64(), 0).Format("2006-01-02 15:04:05 MST"),
257-
"blockTime", time.Unix(targetTime.Int64(), 0).Format("2006-01-02 15:04:05 MST"))
258-
// After exiting the loop, left should be the block number closest to the target time
259-
return left, nil
259+
"block", fmt.Sprintf("%d", best),
260+
"blockTs", time.Unix(int64(bestHdr.Time), 0).UTC().Format("2006-01-02 15:04:05 UTC"),
261+
"targetTs", time.Unix(targetTs.Int64(), 0).UTC().Format("2006-01-02 15:04:05 UTC"),
262+
"timeDiffHours", new(big.Int).Div(bestDiffAbs, big.NewInt(3600)),
263+
"timeDiffSeconds", bestDiffAbs,
264+
"iterations", iterations)
265+
266+
maxAllowedDiffSeconds := big.NewInt(30 * 60) // 30 minutes
267+
// Validate that the found block is within a few minutes of the target time
268+
if bestDiffAbs.Cmp(maxAllowedDiffSeconds) > 0 {
269+
m.log.Error("Found block is too far from target time",
270+
"block", fmt.Sprintf("%d", best),
271+
"blockTime", time.Unix(int64(bestHdr.Time), 0).UTC().Format("2006-01-02 15:04:05 UTC"),
272+
"targetTime", time.Unix(targetTs.Int64(), 0).UTC().Format("2006-01-02 15:04:05 UTC"),
273+
"timeDiffSeconds", bestDiffAbs,
274+
"maxAllowedDiffSeconds", maxAllowedDiffSeconds)
275+
}
276+
277+
return best, nil
260278
}
261279

262280
// GetLatestBlock retrieves the latest block number from the L1 Geth client.
@@ -324,24 +342,26 @@ func (m *Monitor) Run(ctx context.Context) {
324342

325343
// get new events
326344
m.log.Info("Getting enriched withdrawal events",
327-
"start", start,
328-
"stop", stop,
329-
"range", stop-start)
345+
"l1BlockStart", fmt.Sprintf("%d", start),
346+
"l1BlockStop", fmt.Sprintf("%d", stop),
347+
"l1BlockRange", fmt.Sprintf("%d", stop-start))
330348
newEvents, err := m.withdrawalValidator.GetEnrichedWithdrawalsEventsMap(start, &stop)
331349

332350
if err != nil {
333351
if start >= stop {
334352
m.log.Info("No new events to process", "start", start, "stop", stop)
335353
} else if stop-start <= 1 {
336354
m.log.Info("Failed to get enriched withdrawal events, range too small",
337-
"error", err,
338-
"start", start,
339-
"stop", stop)
355+
"l1BlockStart", fmt.Sprintf("%d", start),
356+
"l1BlockStop", fmt.Sprintf("%d", stop),
357+
"l1BlockRange", fmt.Sprintf("%d", stop-start),
358+
"error", err)
340359
} else {
341360
m.log.Error("Failed to get enriched withdrawal events",
342-
"error", err,
343-
"start", start,
344-
"stop", stop)
361+
"l1BlockStart", fmt.Sprintf("%d", start),
362+
"l1BlockStop", fmt.Sprintf("%d", stop),
363+
"l1BlockRange", fmt.Sprintf("%d", stop-start),
364+
"error", err)
345365
}
346366
return
347367
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package faultproof_withdrawals
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum-optimism/monitorism/op-monitorism/faultproof_withdrawals/validator"
7+
"github.com/ethereum/go-ethereum/common"
8+
oplog "github.com/ethereum/go-ethereum/log"
9+
lru "github.com/hashicorp/golang-lru"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// newTestState creates a minimal State instance with the internal maps / cache
14+
// initialised so that the increment-helpers can run without panicking.
15+
func newTestState() *State {
16+
cache, _ := lru.New(10)
17+
18+
return &State{
19+
logger: oplog.New(),
20+
potentialAttackOnDefenderWinsGames: make(map[common.Hash]*validator.EnrichedProvenWithdrawalEvent),
21+
potentialAttackOnInProgressGames: make(map[common.Hash]*validator.EnrichedProvenWithdrawalEvent),
22+
suspiciousEventsOnChallengerWinsGames: cache,
23+
}
24+
}
25+
26+
// newTestEvent builds a synthetic EnrichedProvenWithdrawalEvent whose
27+
// TxHash is derived from the supplied hex string.
28+
func newTestEvent(txHashHex string) *validator.EnrichedProvenWithdrawalEvent {
29+
txHash := common.HexToHash(txHashHex)
30+
withdrawalEvent := &validator.WithdrawalProvenExtension1Event{
31+
Raw: validator.Raw{
32+
BlockNumber: 1,
33+
TxHash: txHash,
34+
},
35+
}
36+
37+
return &validator.EnrichedProvenWithdrawalEvent{
38+
Event: withdrawalEvent,
39+
}
40+
}
41+
42+
func TestIncrementWithdrawalsValidated(t *testing.T) {
43+
st := newTestState()
44+
evt := newTestEvent("0x1")
45+
46+
st.IncrementWithdrawalsValidated(evt)
47+
48+
require.Equal(t, uint64(1), st.withdrawalsProcessed)
49+
require.NotZero(t, evt.ProcessedTimeStamp)
50+
}
51+
52+
func TestIncrementPotentialAttackOnDefenderWinsGames(t *testing.T) {
53+
st := newTestState()
54+
evt := newTestEvent("0x2")
55+
56+
st.IncrementPotentialAttackOnDefenderWinsGames(evt)
57+
58+
require.Equal(t, uint64(1), st.numberOfPotentialAttacksOnDefenderWinsGames)
59+
require.Equal(t, 1, len(st.potentialAttackOnDefenderWinsGames))
60+
require.Equal(t, uint64(1), st.withdrawalsProcessed)
61+
require.NotZero(t, evt.ProcessedTimeStamp)
62+
}
63+
64+
func TestIncrementPotentialAttackOnInProgressGames(t *testing.T) {
65+
st := newTestState()
66+
evt := newTestEvent("0x3")
67+
68+
st.IncrementPotentialAttackOnInProgressGames(evt)
69+
70+
require.Equal(t, uint64(1), st.numberOfPotentialAttackOnInProgressGames)
71+
require.Equal(t, 1, len(st.potentialAttackOnInProgressGames))
72+
require.Equal(t, uint64(0), st.withdrawalsProcessed)
73+
require.NotZero(t, evt.ProcessedTimeStamp)
74+
}
75+
76+
func TestIncrementSuspiciousEventsOnChallengerWinsGames(t *testing.T) {
77+
st := newTestState()
78+
evt := newTestEvent("0x4")
79+
80+
// First mark the event as in-progress to exercise the removal path.
81+
st.IncrementPotentialAttackOnInProgressGames(evt)
82+
83+
require.Equal(t, uint64(1), st.numberOfPotentialAttackOnInProgressGames, "setup failed: expected 1 in-progress event")
84+
85+
st.IncrementSuspiciousEventsOnChallengerWinsGames(evt)
86+
87+
require.Equal(t, uint64(1), st.numberOfSuspiciousEventsOnChallengerWinsGames)
88+
require.Equal(t, 1, st.suspiciousEventsOnChallengerWinsGames.Len())
89+
require.Equal(t, uint64(0), st.numberOfPotentialAttackOnInProgressGames, "expected 0 after removal")
90+
require.Equal(t, uint64(1), st.withdrawalsProcessed)
91+
require.NotZero(t, evt.ProcessedTimeStamp)
92+
}

0 commit comments

Comments
 (0)