diff --git a/docs/metrics/prometheus.md b/docs/metrics/prometheus.md index ad380180..d9f53b2f 100644 --- a/docs/metrics/prometheus.md +++ b/docs/metrics/prometheus.md @@ -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. diff --git a/mock/eth2client.go b/mock/eth2client.go index 958c8366..e9077224 100644 --- a/mock/eth2client.go +++ b/mock/eth2client.go @@ -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{} diff --git a/services/controller/standard/events.go b/services/controller/standard/events.go index 0fcb0268..701dcaa9 100644 --- a/services/controller/standard/events.go +++ b/services/controller/standard/events.go @@ -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" @@ -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() + } + 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. diff --git a/services/controller/standard/metrics.go b/services/controller/standard/metrics.go index 3227bea0..2e65882e 100644 --- a/services/controller/standard/metrics.go +++ b/services/controller/standard/metrics.go @@ -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 { @@ -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() { diff --git a/services/controller/standard/metrics_test.go b/services/controller/standard/metrics_test.go new file mode 100644 index 00000000..2bd7cefe --- /dev/null +++ b/services/controller/standard/metrics_test.go @@ -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) + }) + } +} diff --git a/services/controller/standard/service.go b/services/controller/standard/service.go index bb456f56..a40ffdee 100644 --- a/services/controller/standard/service.go +++ b/services/controller/standard/service.go @@ -88,6 +88,7 @@ type Service struct { fastTrackSyncCommittees bool fastTrackGrace time.Duration multiInstance multiinstance.Service + eventsProvider eth2client.EventsProvider // Hard fork control. handlingAltair bool @@ -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,