From 03cf967ca7264475e4efcf2682df6ada046d51b9 Mon Sep 17 00:00:00 2001 From: Damilola Edwards Date: Wed, 19 Aug 2026 11:24:12 +0100 Subject: [PATCH] Fix terminal total difficulty gauge overflow The gauge converted TerminalTotalDifficulty with big.Int.Uint64, which the standard library documents as undefined when the value doesn't fit in 64 bits. Mainnet's TTD doesn't fit, so the exported metric silently reported a meaningless number. It now goes through big.Float first, the same way the adjacent trillions gauge already does. --- pkg/beacon/metrics_spec.go | 7 ++++- pkg/beacon/metrics_spec_test.go | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 pkg/beacon/metrics_spec_test.go diff --git a/pkg/beacon/metrics_spec.go b/pkg/beacon/metrics_spec.go index 28095ca..0513cc2 100644 --- a/pkg/beacon/metrics_spec.go +++ b/pkg/beacon/metrics_spec.go @@ -327,7 +327,12 @@ func (s *SpecMetrics) observeSpec(ctx context.Context, spec *state.Spec) error { divided := new(big.Int).Div(&spec.TerminalTotalDifficulty, trillion) asFloat, _ := new(big.Float).SetInt(divided).Float64() s.TerminalTotalDifficultyTrillions.Set(asFloat) - s.TerminalTotalDifficulty.Set(float64(spec.TerminalTotalDifficulty.Uint64())) + + // big.Int.Uint64 is undefined when the value doesn't fit in 64 bits, + // which mainnet's TTD doesn't. Go through big.Float instead so the + // gauge reports the real value rather than a silently wrapped one. + ttdAsFloat, _ := new(big.Float).SetInt(&spec.TerminalTotalDifficulty).Float64() + s.TerminalTotalDifficulty.Set(ttdAsFloat) return nil } diff --git a/pkg/beacon/metrics_spec_test.go b/pkg/beacon/metrics_spec_test.go new file mode 100644 index 0000000..ab76f2b --- /dev/null +++ b/pkg/beacon/metrics_spec_test.go @@ -0,0 +1,51 @@ +package beacon + +import ( + "context" + "math/big" + "testing" + + "github.com/ethpandaops/beacon/pkg/beacon/state" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/sirupsen/logrus" +) + +func gaugeValue(t *testing.T, g prometheus.Gauge) float64 { + t.Helper() + + metricDTO := &dto.Metric{} + if err := g.Write(metricDTO); err != nil { + t.Fatalf("failed to read gauge: %v", err) + } + + return metricDTO.Gauge.GetValue() +} + +func TestObserveSpec_TerminalTotalDifficulty(t *testing.T) { + prometheus.DefaultRegisterer = prometheus.NewRegistry() + + log := logrus.New() + log.SetLevel(logrus.PanicLevel) + + job := NewSpecJob(nil, log, "test_ttd_gauge", map[string]string{}) + + mainnetTTD, ok := new(big.Int).SetString("58750000000000000000000", 10) + if !ok { + t.Fatal("failed to parse mainnet TTD constant") + } + + s := &state.Spec{TerminalTotalDifficulty: *mainnetTTD} + + if err := job.observeSpec(context.Background(), s); err != nil { + t.Fatalf("observeSpec returned error: %v", err) + } + + got := gaugeValue(t, job.TerminalTotalDifficulty) + + want, _ := new(big.Float).SetInt(mainnetTTD).Float64() + + if got != want { + t.Fatalf("expected beacon_spec_terminal_total_difficulty to report the real TTD %v, got %v", want, got) + } +}