Skip to content

Add provider address to consensus-type event handlers #288

Description

@AntiD2ta

Context

The Provider field has been added to all apiv1 event structs (HeadEvent, BlockEvent, ChainReorgEvent, etc.) in #286 so consumers can identify which beacon node delivered the event. This works well for events whose data structs live in api/v1 and can be modified directly.

However, 7 event handlers pass raw consensus spec types that we do not own and cannot modify:

Handler Data type
AttestationHandler *spec.VersionedAttestation
AttesterSlashingHandler *electra.AttesterSlashing
BLSToExecutionChangeHandler *capella.SignedBLSToExecutionChange
ContributionAndProofHandler *altair.SignedContributionAndProof
ProposerSlashingHandler *phase0.ProposerSlashing
SingleAttestationHandler *electra.SingleAttestation
VoluntaryExitHandler *phase0.SignedVoluntaryExit

There is currently no way for consumers of these typed handlers to know which provider delivered the event.

Proposed solutions

Both options are breaking changes to the handler signatures — consumers must update either way.

Option A — Use *apiv1.Event as the data parameter

Change the handler signatures to use the existing *apiv1.Event struct, which already has Topic, Data any, and Provider:

// Before
type SingleAttestationEventHandlerFunc func(context.Context, *electra.SingleAttestation)

// After
type SingleAttestationEventHandlerFunc func(context.Context, *apiv1.Event)

Consumer code:

func handleSingleAttestation(ctx context.Context, event *apiv1.Event) {
    attestation := event.Data.(*electra.SingleAttestation)
    provider := event.Provider
    // ...
}

Pros:

  • No new types — reuses existing *apiv1.Event
  • Uniform signature across all handlers

Cons:

  • Loses compile-time type safety — Data is any, requires type assertion
  • Blurs the distinction between the generic Handler and typed handlers
  • Consumer errors (wrong type assertion) become runtime panics instead of compile errors

Option B — New apiv1 wrapper types per event

Create dedicated wrapper types for each consensus-type event, matching the pattern already used by HeadEvent, BlockEvent, etc.:

// api/v1/singleattestationevent.go
type SingleAttestationEvent struct {
    Data     *electra.SingleAttestation
    Provider string
}

type SingleAttestationEventHandlerFunc func(context.Context, *apiv1.SingleAttestationEvent)

Consumer code:

func handleSingleAttestation(ctx context.Context, event *apiv1.SingleAttestationEvent) {
    attestation := event.Data
    provider := event.Provider
    // ...
}

Pros:

  • Type-safe — Data is the concrete type, no assertion needed
  • Consistent with how existing apiv1 events already work (HeadEvent, BlockEvent, etc.)
  • Extensible — future metadata fields can be added to the wrapper
  • Consumer errors caught at compile time

Cons:

  • 7 new types to create and maintain

Recommendation

I prefer Option B. Since both options are equally breaking, Option B is strictly better:

Same migration cost for consumers, while retaining compile-time type safety and consistency with existing library patterns.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions