From 25f585797ec2e8d2d72de87e3427c15ab5d3c916 Mon Sep 17 00:00:00 2001 From: Damilola Edwards Date: Sat, 11 Jul 2026 09:45:52 +0100 Subject: [PATCH] Enforce maxTotalReorgs in the consensus reorg check The maxTotalReorgs option was declared and documented but never read, so a playbook that set it to bound the total number of reorgs had no effect and the check passed no matter how many reorgs accumulated. Evaluate it alongside the other reorg thresholds, and extract the threshold logic into a helper so it can be tested without the wall clock service. --- pkg/tasks/check_consensus_reorgs/task.go | 10 +++ pkg/tasks/check_consensus_reorgs/task_test.go | 63 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 pkg/tasks/check_consensus_reorgs/task_test.go diff --git a/pkg/tasks/check_consensus_reorgs/task.go b/pkg/tasks/check_consensus_reorgs/task.go index 6a17899b..8b06c7f6 100644 --- a/pkg/tasks/check_consensus_reorgs/task.go +++ b/pkg/tasks/check_consensus_reorgs/task.go @@ -159,6 +159,11 @@ func (t *Task) runCheck() types.TaskResult { epochCount := currentEpoch.Number() - t.startEpoch + return t.evaluateReorgs(epochCount) +} + +// evaluateReorgs applies the configured reorg thresholds to the observed counts. +func (t *Task) evaluateReorgs(epochCount uint64) types.TaskResult { if t.config.MinCheckEpochCount > 0 && epochCount < t.config.MinCheckEpochCount { t.logger.Warnf("Check missed: checked %v epochs, but need >= %v", epochCount, t.config.MinCheckEpochCount) return types.TaskResultNone @@ -169,6 +174,11 @@ func (t *Task) runCheck() types.TaskResult { return types.TaskResultFailure } + if t.config.MaxTotalReorgs > 0 && t.totalReorgs > t.config.MaxTotalReorgs { + t.logger.Warnf("check failed: max total reorgs exceeded. (have: %v, want <= %v)", t.totalReorgs, t.config.MaxTotalReorgs) + return types.TaskResultFailure + } + return types.TaskResultSuccess } diff --git a/pkg/tasks/check_consensus_reorgs/task_test.go b/pkg/tasks/check_consensus_reorgs/task_test.go new file mode 100644 index 00000000..3f413aa4 --- /dev/null +++ b/pkg/tasks/check_consensus_reorgs/task_test.go @@ -0,0 +1,63 @@ +package checkconsensusreorgs + +import ( + "io" + "testing" + + "github.com/ethpandaops/assertoor/pkg/types" + "github.com/sirupsen/logrus" +) + +func newEvalTask(cfg Config, totalReorgs uint64) *Task { + log := logrus.New() + log.SetOutput(io.Discard) + + return &Task{ + config: cfg, + logger: log, + totalReorgs: totalReorgs, + } +} + +// TestEvaluateReorgsMaxTotal covers the maxTotalReorgs bound, which was declared and +// documented but never enforced, so a run exceeding it still passed. +func TestEvaluateReorgsMaxTotal(t *testing.T) { + tests := []struct { + name string + maxTotal uint64 + totalReorgs uint64 + epochCount uint64 + want types.TaskResult + }{ + {"under the bound passes", 5, 3, 10, types.TaskResultSuccess}, + {"at the bound passes", 5, 5, 10, types.TaskResultSuccess}, + {"over the bound fails", 5, 6, 10, types.TaskResultFailure}, + {"unset bound is ignored", 0, 1000, 10, types.TaskResultSuccess}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + task := newEvalTask(Config{MaxTotalReorgs: tc.maxTotal}, tc.totalReorgs) + + if got := task.evaluateReorgs(tc.epochCount); got != tc.want { + t.Fatalf("maxTotalReorgs=%d totalReorgs=%d: got %v, want %v", tc.maxTotal, tc.totalReorgs, got, tc.want) + } + }) + } +} + +// TestEvaluateReorgsOtherBounds guards the neighbouring thresholds so the added +// check does not shadow them. +func TestEvaluateReorgsOtherBounds(t *testing.T) { + // minCheckEpochCount not yet reached -> inconclusive + task := newEvalTask(Config{MinCheckEpochCount: 5}, 0) + if got := task.evaluateReorgs(2); got != types.TaskResultNone { + t.Fatalf("min epoch not reached: got %v, want None", got) + } + + // maxReorgsPerEpoch exceeded -> failure + task = newEvalTask(Config{MaxReorgsPerEpoch: 1.0}, 30) + if got := task.evaluateReorgs(10); got != types.TaskResultFailure { + t.Fatalf("max reorgs per epoch exceeded: got %v, want Failure", got) + } +}