Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/metrics/prometheus.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ There is also a companion metric `vouch_client_operation_requests_total`, which

Network metrics provide information about the network from Vouch's point of view. Although these are not under Vouch's control, they have an impact on the performance of the validator. The specific metrics are:

- `vouch_block_receipt_delay_seconds` the delay between the start of a slot and the arrival of the block for that slot. This metric is provided as a histogram, with buckets in increments of 0.1 seconds up to 12 seconds. This has a label `epoch_slot` which is the position of the slot in the epoch (0 through 31, inclusive)
- `vouch_block_receipt_delay_seconds` the delay between the start of a slot and the arrival of the block for that slot. This metric is provided as a histogram, with buckets in increments of 0.1 seconds up to 12 seconds. This has labels `epoch_slot` which is the position of the slot in the epoch (0 through 31, inclusive) and `provider` which is the address of the beacon node that delivered the block event
- `vouch_attestationaggregation_coverage_ratio` the ratio of the number of attestations included in the aggregate to the total number of attestations for the aggregate. This metric is provided as a histogram, with buckets in increments of 0.1 up to 1.
- `vouch_synccommitteeaggregation_coverage_ratio` the ratio of the number of sync committee messages included in the aggregate to the total number of members of the sync committee for the aggregate. This metric is provided as a histogram, with buckets in increments of 0.1 up to 1.

Expand Down
28 changes: 28 additions & 0 deletions mock/eth2client.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,34 @@ func (*EventsProvider) Events(_ context.Context, _ *api.EventsOpts) error {
return nil
}

// AddressableEventsProvider is a mock for eth2client.EventsProvider that also
// implements eth2client.Service, allowing the address to be extracted.
type AddressableEventsProvider struct {
address string
}

// NewAddressableEventsProvider returns a mock events provider with a known address.
func NewAddressableEventsProvider(address string) *AddressableEventsProvider {
return &AddressableEventsProvider{address: address}
}

// Events is a mock.
func (*AddressableEventsProvider) Events(_ context.Context, _ *api.EventsOpts) error {
return nil
}

// Name returns the name of the mock client.
func (*AddressableEventsProvider) Name() string { return "mock" }

// Address returns the address of the mock client.
func (m *AddressableEventsProvider) Address() string { return m.address }

// IsActive returns true.
func (*AddressableEventsProvider) IsActive() bool { return true }

// IsSynced returns true.
func (*AddressableEventsProvider) IsSynced() bool { return true }

// ErroringEventsProvider is a mock for eth2client.EventsProvider.
type ErroringEventsProvider struct{}

Expand Down
7 changes: 6 additions & 1 deletion services/controller/standard/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"time"

eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
Expand Down Expand Up @@ -50,7 +51,11 @@ func (s *Service) HandleHeadEvent(ctx context.Context, data *apiv1.HeadEvent) {
s.lastBlockRoot = data.Block
epoch := s.chainTimeService.SlotToEpoch(data.Slot)

monitorBlockDelay(uint(uint64(data.Slot)%s.slotsPerEpoch), time.Since(s.chainTimeService.StartOfSlot(data.Slot)))
var provider string
if svc, ok := s.eventsProvider.(eth2client.Service); ok {
provider = svc.Address()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty convinced this will not work as expected. If you take a look here, the multi-client only returns the first active client in the array.

I think this might require work in go-eth2-client to pass back the provider in the head event data struct. This should tell us the actual provider that sent the event.

}
monitorBlockDelay(uint(uint64(data.Slot)%s.slotsPerEpoch), time.Since(s.chainTimeService.StartOfSlot(data.Slot)), provider)

// If this block is for the prior slot and we may have a proposal waiting then kick
// off any proposal for this slot.
Expand Down
6 changes: 3 additions & 3 deletions services/controller/standard/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func registerPrometheusMetrics(_ context.Context) error {
10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9, 11.0,
11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8, 11.9, 12.0,
},
}, []string{"epoch_slot"})
}, []string{"epoch_slot", "provider"})
if err := prometheus.Register(blockReceiptDelay); err != nil {
var alreadyRegisteredError prometheus.AlreadyRegisteredError
if ok := errors.As(err, &alreadyRegisteredError); ok {
Expand Down Expand Up @@ -180,11 +180,11 @@ func monitorNewEpoch() {
epochsProcessed.Inc()
}

func monitorBlockDelay(epochSlot uint, delay time.Duration) {
func monitorBlockDelay(epochSlot uint, delay time.Duration, provider string) {
if blockReceiptDelay == nil {
return
}
blockReceiptDelay.WithLabelValues(fmt.Sprintf("%d", epochSlot)).Observe(delay.Seconds())
blockReceiptDelay.WithLabelValues(fmt.Sprintf("%d", epochSlot), provider).Observe(delay.Seconds())
}

func monitorSyncCommitteeSyncAggregateFoundInc() {
Expand Down
143 changes: 143 additions & 0 deletions services/controller/standard/metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright © 2026 Attestant Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package standard_test

import (
"context"
"testing"
"time"

eth2client "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/vouch/mock"
mockaccountmanager "github.com/attestantio/vouch/services/accountmanager/mock"
mockattestationaggregator "github.com/attestantio/vouch/services/attestationaggregator/mock"
mockattester "github.com/attestantio/vouch/services/attester/mock"
mockbeaconblockproposer "github.com/attestantio/vouch/services/beaconblockproposer/mock"
mockbeaconcommitteesubscriber "github.com/attestantio/vouch/services/beaconcommitteesubscriber/mock"
"github.com/attestantio/vouch/services/cache"
mockcache "github.com/attestantio/vouch/services/cache/mock"
standardchaintime "github.com/attestantio/vouch/services/chaintime/standard"
"github.com/attestantio/vouch/services/controller/standard"
prometheusmetrics "github.com/attestantio/vouch/services/metrics/prometheus"
alwaysmultiinstance "github.com/attestantio/vouch/services/multiinstance/always"
mockproposalpreparer "github.com/attestantio/vouch/services/proposalpreparer/mock"
mockscheduler "github.com/attestantio/vouch/services/scheduler/mock"
mocksynccommitteeaggregator "github.com/attestantio/vouch/services/synccommitteeaggregator/mock"
mocksynccommitteemessenger "github.com/attestantio/vouch/services/synccommitteemessenger/mock"
mocksynccommitteesubscriber "github.com/attestantio/vouch/services/synccommitteesubscriber/mock"
"github.com/prometheus/client_golang/prometheus"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)

func TestHandleHeadEventBlockReceiptDelay(t *testing.T) {
ctx := context.Background()

zerolog.SetGlobalLevel(zerolog.Disabled)

genesisTime := time.Now()
genesisProvider := mock.NewGenesisProvider(genesisTime)
specProvider := mock.NewSpecProvider()
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithLogLevel(zerolog.Disabled),
standardchaintime.WithGenesisProvider(genesisProvider),
standardchaintime.WithSpecProvider(specProvider),
)
require.NoError(t, err)

monitor, err := prometheusmetrics.New(ctx,
prometheusmetrics.WithLogLevel(zerolog.Disabled),
prometheusmetrics.WithAddress("localhost:0"),
)
require.NoError(t, err)

multiInstance, err := alwaysmultiinstance.New(ctx)
require.NoError(t, err)

tests := []struct {
name string
eventsProvider eth2client.EventsProvider
expectedProvider string
}{
{
name: "WithAddressableProvider",
eventsProvider: mock.NewAddressableEventsProvider("http://lighthouse:5052"),
expectedProvider: "http://lighthouse:5052",
},
{
name: "WithPlainProvider",
eventsProvider: mock.NewEventsProvider(),
expectedProvider: "",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
svc, err := standard.New(ctx,
standard.WithLogLevel(zerolog.Disabled),
standard.WithMonitor(monitor),
standard.WithSpecProvider(specProvider),
standard.WithChainTimeService(chainTime),
standard.WithProposerDutiesProvider(mock.NewProposerDutiesProvider()),
standard.WithAttesterDutiesProvider(mock.NewAttesterDutiesProvider()),
standard.WithSyncCommitteeDutiesProvider(mock.NewSyncCommitteeDutiesProvider()),
standard.WithEventsProvider(test.eventsProvider),
standard.WithValidatingAccountsProvider(mockaccountmanager.NewValidatingAccountsProvider()),
standard.WithProposalsPreparer(mockproposalpreparer.New()),
standard.WithScheduler(mockscheduler.New()),
standard.WithAttester(mockattester.New()),
standard.WithSyncCommitteeMessenger(mocksynccommitteemessenger.New()),
standard.WithSyncCommitteeAggregator(mocksynccommitteeaggregator.New()),
standard.WithSyncCommitteeSubscriber(mocksynccommitteesubscriber.New()),
standard.WithBeaconBlockProposer(mockbeaconblockproposer.New()),
standard.WithBeaconCommitteeSubscriber(mockbeaconcommitteesubscriber.New()),
standard.WithAttestationAggregator(mockattestationaggregator.New()),
standard.WithAccountsRefresher(mockaccountmanager.NewRefresher()),
standard.WithBlockToSlotSetter(mockcache.New(map[phase0.Root]phase0.Slot{}).(cache.BlockRootToSlotSetter)),
standard.WithBeaconBlockHeadersProvider(mock.NewBeaconBlockHeadersProvider()),
standard.WithSignedBeaconBlockProvider(mock.NewSignedBeaconBlockProvider()),
standard.WithMultiInstance(multiInstance),
)
require.NoError(t, err)

data := &apiv1.HeadEvent{
Slot: phase0.Slot(0),
}
svc.HandleHeadEvent(ctx, data)

metrics, err := prometheus.DefaultGatherer.Gather()
require.NoError(t, err)

found := false
for _, mf := range metrics {
if mf.GetName() == "vouch_block_receipt_delay_seconds" {
for _, m := range mf.GetMetric() {
providerValue := ""
for _, lp := range m.GetLabel() {
if lp.GetName() == "provider" {
providerValue = lp.GetValue()
}
}
if providerValue == test.expectedProvider {
found = true
}
}
}
}
require.True(t, found, "expected to find vouch_block_receipt_delay_seconds metric with provider label %q", test.expectedProvider)
})
}
}
2 changes: 2 additions & 0 deletions services/controller/standard/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ type Service struct {
fastTrackSyncCommittees bool
fastTrackGrace time.Duration
multiInstance multiinstance.Service
eventsProvider eth2client.EventsProvider

// Hard fork control.
handlingAltair bool
Expand Down Expand Up @@ -169,6 +170,7 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
fastTrackSyncCommittees: parameters.fastTrackSyncCommittees,
fastTrackGrace: parameters.fastTrackGrace,
multiInstance: parameters.multiInstance,
eventsProvider: parameters.eventsProvider,
subscriptionInfos: make(map[phase0.Epoch]map[phase0.Slot]map[phase0.CommitteeIndex]*beaconcommitteesubscriber.Subscription),
handlingAltair: handlingAltair,
altairForkEpoch: altairForkEpoch,
Expand Down
Loading