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.
Context
The
Providerfield has been added to allapiv1event 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 inapi/v1and can be modified directly.However, 7 event handlers pass raw consensus spec types that we do not own and cannot modify:
AttestationHandler*spec.VersionedAttestationAttesterSlashingHandler*electra.AttesterSlashingBLSToExecutionChangeHandler*capella.SignedBLSToExecutionChangeContributionAndProofHandler*altair.SignedContributionAndProofProposerSlashingHandler*phase0.ProposerSlashingSingleAttestationHandler*electra.SingleAttestationVoluntaryExitHandler*phase0.SignedVoluntaryExitThere 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.Eventas the data parameterChange the handler signatures to use the existing
*apiv1.Eventstruct, which already hasTopic,Data any, andProvider:Consumer code:
Pros:
*apiv1.EventCons:
Dataisany, requires type assertionHandlerand typed handlersOption B — New
apiv1wrapper types per eventCreate dedicated wrapper types for each consensus-type event, matching the pattern already used by
HeadEvent,BlockEvent, etc.:Consumer code:
Pros:
Datais the concrete type, no assertion neededHeadEvent,BlockEvent, etc.)Cons:
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.