Item 3 of the #2968 exporter back-compat umbrella. Surfaced by @ovidiu-ssv-labs during the #2975 review (Finding 2) — full mechanism analysis there: #2975 (review) (review of 2026-08-10).
Problem
After #2975, isCommitteeDutyAtSlot (exporter/validator.go) is the only Boole-aware routing point in exporter/. TraceDecidedsCore still routes on a hardcoded role switch with no slot/fork input (exporter/decided.go:41-46):
switch role {
case spectypes.BNRoleAttester, spectypes.BNRoleSyncCommittee:
roleParticipantsIdx, roleErrs = e.getCommitteeDecidedsForRole(slot, indices, role)
default:
roleParticipantsIdx, roleErrs = e.getValidatorDecidedsForRole(slot, indices, role)
}
Post-Boole, AGGREGATOR / SYNC_COMMITTEE_CONTRIBUTION duties run under the RoleAggregatorCommittee runner and their traces are stored on the committee path, so the default arm reads an index that is empty for these roles. /v1/exporter/decideds then returns an empty participants list with a 200 — silent data loss, no error, no note.
Notably, the store layer already supports the committee-side lookup for these roles (committeeRunnerRolesForBeaconRoles maps them to RoleAggregatorCommittee, and TestCollector_GetCommitteeDecideds_RoleFiltering asserts it works) — nothing in production routes to it.
Suggested fix
Route the decideds switch through the same fork-aware predicate the traces path uses:
if e.isCommitteeDutyAtSlot(role, slot) {
roleParticipantsIdx, roleErrs = e.getCommitteeDecidedsForRole(slot, indices, role)
} else {
roleParticipantsIdx, roleErrs = e.getValidatorDecidedsForRole(slot, indices, role)
}
Behavior-preserving pre-Boole, correct post-Boole. Note decided.go nests role-outer/slot-inner (inverse of validator.go), so the predicate must be evaluated inside the slot loop. Add a straddling-range test mirroring TestValidatorTracesCore_StraddlingFork.
Should land before Boole activates on mainnet.
Item 3 of the #2968 exporter back-compat umbrella. Surfaced by @ovidiu-ssv-labs during the #2975 review (Finding 2) — full mechanism analysis there: #2975 (review) (review of 2026-08-10).
Problem
After #2975,
isCommitteeDutyAtSlot(exporter/validator.go) is the only Boole-aware routing point inexporter/.TraceDecidedsCorestill routes on a hardcoded role switch with no slot/fork input (exporter/decided.go:41-46):Post-Boole, AGGREGATOR / SYNC_COMMITTEE_CONTRIBUTION duties run under the
RoleAggregatorCommitteerunner and their traces are stored on the committee path, so thedefaultarm reads an index that is empty for these roles./v1/exporter/decidedsthen returns an emptyparticipantslist with a 200 — silent data loss, no error, no note.Notably, the store layer already supports the committee-side lookup for these roles (
committeeRunnerRolesForBeaconRolesmaps them toRoleAggregatorCommittee, andTestCollector_GetCommitteeDecideds_RoleFilteringasserts it works) — nothing in production routes to it.Suggested fix
Route the decideds switch through the same fork-aware predicate the traces path uses:
Behavior-preserving pre-Boole, correct post-Boole. Note
decided.gonests role-outer/slot-inner (inverse ofvalidator.go), so the predicate must be evaluated inside the slot loop. Add a straddling-range test mirroringTestValidatorTracesCore_StraddlingFork.Should land before Boole activates on mainnet.