From 48e687c53f246a170678d613993c83275cd3d724 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 25 Mar 2026 21:53:47 +0100 Subject: [PATCH 1/3] fix: use SLOT_DURATION_MS instead of deprecated SECONDS_PER_SLOT SECONDS_PER_SLOT has been deprecated and removed from the consensus spec config (ethereum/consensus-specs#4926) in favor of SLOT_DURATION_MS. When CL nodes stop returning SECONDS_PER_SLOT, the ChainSpec field stays at zero, causing a divide-by-zero panic in ethwallclock when computing the current slot. Replace SecondsPerSlot (time.Duration) with SlotDurationMs (uint64) in ChainSpec to match the new spec field directly. Fall back to SECONDS_PER_SLOT for backwards compatibility with older genesis configs. Add a guard in InitWallclock to prevent the panic if neither value is available. Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/assertoor/coordinator.go | 10 ++++++---- pkg/clients/consensus/blockcache.go | 15 ++++++++++++++- pkg/clients/consensus/chainspec.go | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pkg/assertoor/coordinator.go b/pkg/assertoor/coordinator.go index e9f7999b..b725061a 100644 --- a/pkg/assertoor/coordinator.go +++ b/pkg/assertoor/coordinator.go @@ -366,10 +366,12 @@ func (c *Coordinator) runEpochGC(ctx context.Context) { var sleepTime time.Duration networkTime := time.Since(genesis.GenesisTime) + slotDuration := time.Duration(specs.SlotDurationMs) * time.Millisecond //nolint:gosec // G115: slot duration values won't overflow int64 + if networkTime < 0 { sleepTime = networkTime.Abs() } else { - currentSlot := uint64(networkTime / specs.SecondsPerSlot) //nolint:gosec // G115: networkTime is checked non-negative above + currentSlot := uint64(networkTime / slotDuration) //nolint:gosec // G115: networkTime is checked non-negative above currentEpoch := currentSlot / specs.SlotsPerEpoch currentSlotIndex := currentSlot % specs.SlotsPerEpoch nextGcSlot := uint64(0) @@ -385,10 +387,10 @@ func (c *Coordinator) runEpochGC(ctx context.Context) { select { case <-ctx.Done(): return - case <-time.After(specs.SecondsPerSlot / 2): + case <-time.After(slotDuration / 2): } - nextEpochDuration := time.Until(genesis.GenesisTime.Add(time.Duration((currentEpoch+1)*specs.SlotsPerEpoch) * specs.SecondsPerSlot)) //nolint:gosec // G115: epoch*slots product won't overflow + nextEpochDuration := time.Until(genesis.GenesisTime.Add(time.Duration((currentEpoch+1)*specs.SlotsPerEpoch) * slotDuration)) //nolint:gosec // G115: epoch*slots product won't overflow c.log.GetLogger().Infof("run GC (slot %v, %v sec before epoch %v)", currentSlot, nextEpochDuration.Seconds(), currentEpoch+1) runtime.GC() @@ -402,7 +404,7 @@ func (c *Coordinator) runEpochGC(ctx context.Context) { } } - nextRunTime := genesis.GenesisTime.Add(time.Duration(nextGcSlot) * specs.SecondsPerSlot) //nolint:gosec // G115: slot number won't overflow int64 + nextRunTime := genesis.GenesisTime.Add(time.Duration(nextGcSlot) * slotDuration) //nolint:gosec // G115: slot number won't overflow int64 sleepTime = time.Until(nextRunTime) } diff --git a/pkg/clients/consensus/blockcache.go b/pkg/clients/consensus/blockcache.go index fe74bb37..9a453207 100644 --- a/pkg/clients/consensus/blockcache.go +++ b/pkg/clients/consensus/blockcache.go @@ -145,6 +145,14 @@ func (cache *BlockCache) SetClientSpecs(specValues map[string]interface{}) error return err } + if specs.SlotDurationMs == 0 { + if secondsPerSlot, ok := specValues["SECONDS_PER_SLOT"]; ok { + if v, vOk := secondsPerSlot.(time.Duration); vOk { + specs.SlotDurationMs = uint64(v.Milliseconds()) //nolint:gosec // G115: SECONDS_PER_SLOT is always a small positive value + } + } + } + if cache.specs != nil { mismatches := cache.specs.CheckMismatch(&specs) if len(mismatches) > 0 { @@ -185,7 +193,12 @@ func (cache *BlockCache) InitWallclock() { return } - cache.wallclock = ethwallclock.NewEthereumBeaconChain(cache.genesis.GenesisTime, specs.SecondsPerSlot, specs.SlotsPerEpoch) + if specs.SlotDurationMs == 0 || specs.SlotsPerEpoch == 0 { + return + } + + slotDuration := time.Duration(specs.SlotDurationMs) * time.Millisecond //nolint:gosec // G115: slot duration values won't overflow int64 + cache.wallclock = ethwallclock.NewEthereumBeaconChain(cache.genesis.GenesisTime, slotDuration, specs.SlotsPerEpoch) cache.wallclock.OnEpochChanged(func(current ethwallclock.Epoch) { cache.wallclockEpochDispatcher.Fire(¤t) }) diff --git a/pkg/clients/consensus/chainspec.go b/pkg/clients/consensus/chainspec.go index 7de457cf..c855c2c5 100644 --- a/pkg/clients/consensus/chainspec.go +++ b/pkg/clients/consensus/chainspec.go @@ -25,7 +25,7 @@ type ChainSpec struct { BellatrixForkEpoch uint64 `yaml:"BELLATRIX_FORK_EPOCH"` CappellaForkVersion phase0.Version `yaml:"CAPELLA_FORK_VERSION"` CappellaForkEpoch uint64 `yaml:"CAPELLA_FORK_EPOCH"` - SecondsPerSlot time.Duration `yaml:"SECONDS_PER_SLOT"` + SlotDurationMs uint64 `yaml:"SLOT_DURATION_MS"` SlotsPerEpoch uint64 `yaml:"SLOTS_PER_EPOCH"` MaxCommitteesPerSlot uint64 `yaml:"MAX_COMMITTEES_PER_SLOT"` } From 764fecd88897b6594a0ba4429e78c367933bf918 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Fri, 27 Mar 2026 14:49:43 +0100 Subject: [PATCH 2/3] fix: log error when wallclock init skipped due to zero spec values Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/clients/consensus/blockcache.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/clients/consensus/blockcache.go b/pkg/clients/consensus/blockcache.go index 9a453207..10f38788 100644 --- a/pkg/clients/consensus/blockcache.go +++ b/pkg/clients/consensus/blockcache.go @@ -19,6 +19,7 @@ import ( ) type BlockCache struct { + logger logrus.FieldLogger followDistance uint32 specMutex sync.RWMutex @@ -56,6 +57,7 @@ func NewBlockCache(ctx context.Context, logger logrus.FieldLogger, followDistanc } cache := BlockCache{ + logger: logger, followDistance: followDistance, blockSlotMap: make(map[phase0.Slot][]*Block), blockRootMap: make(map[phase0.Root]*Block), @@ -194,6 +196,7 @@ func (cache *BlockCache) InitWallclock() { } if specs.SlotDurationMs == 0 || specs.SlotsPerEpoch == 0 { + cache.logger.Errorf("cannot initialize wallclock: SlotDurationMs=%d, SlotsPerEpoch=%d", specs.SlotDurationMs, specs.SlotsPerEpoch) return } From 63d1f57ed32ef5961c77adee69b026dc03bb6f94 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Fri, 27 Mar 2026 14:53:58 +0100 Subject: [PATCH 3/3] fix: improve wallclock init error message with beacon API context Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/clients/consensus/blockcache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/clients/consensus/blockcache.go b/pkg/clients/consensus/blockcache.go index 10f38788..d904f72f 100644 --- a/pkg/clients/consensus/blockcache.go +++ b/pkg/clients/consensus/blockcache.go @@ -196,7 +196,7 @@ func (cache *BlockCache) InitWallclock() { } if specs.SlotDurationMs == 0 || specs.SlotsPerEpoch == 0 { - cache.logger.Errorf("cannot initialize wallclock: SlotDurationMs=%d, SlotsPerEpoch=%d", specs.SlotDurationMs, specs.SlotsPerEpoch) + cache.logger.Errorf("cannot initialize wallclock: neither SLOT_DURATION_MS nor SECONDS_PER_SLOT are available from the beacon API (SlotDurationMs=%d, SlotsPerEpoch=%d)", specs.SlotDurationMs, specs.SlotsPerEpoch) return }