diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/attestation/ValidatableAttestation.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/attestation/ValidatableAttestation.java index 1d03ea97357..f346cd307cc 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/attestation/ValidatableAttestation.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/attestation/ValidatableAttestation.java @@ -193,11 +193,7 @@ public void setIndexedAttestation(final IndexedAttestation indexedAttestation) { public void saveCommitteeShufflingSeedAndCommitteesSize(final BeaconState state) { saveCommitteeShufflingSeed(state); - // The committees size is only required when the committee_bits field is present in the - // Attestation - if (attestation.isSingleAttestation() || attestation.requiresCommitteeBits()) { - saveCommitteesSize(state); - } + saveCommitteesSize(state); } private void saveCommitteeShufflingSeed(final BeaconState state) { @@ -212,10 +208,16 @@ private void saveCommitteeShufflingSeed(final BeaconState state) { this.committeeShufflingSeed = Optional.of(committeeShufflingSeed); } - private void saveCommitteesSize(final BeaconState state) { + public void saveCommitteesSize(final BeaconState state) { if (committeesSize.isPresent()) { return; } + + if (!(attestation.isSingleAttestation() || attestation.requiresCommitteeBits())) { + // it isn't a PECTRA attestation, do nothing + return; + } + final Int2IntMap committeesSize = spec.getBeaconCommitteesSize(state, attestation.getData().getSlot()); this.committeesSize = Optional.of(committeesSize); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPool.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPool.java index 0471f5fde83..ce22614d843 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPool.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPool.java @@ -15,15 +15,31 @@ import java.util.List; import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.ethereum.events.SlotEventsChannel; +import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation; import tech.pegasys.teku.spec.datastructures.operations.Attestation; +import tech.pegasys.teku.spec.datastructures.operations.AttestationData; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; +import tech.pegasys.teku.storage.client.RecentChainData; + +public abstract class AggregatingAttestationPool implements SlotEventsChannel { + private static final Logger LOG = LogManager.getLogger(); + protected final Spec spec; + protected final RecentChainData recentChainData; + + AggregatingAttestationPool(final Spec spec, final RecentChainData recentChainData) { + this.spec = spec; + this.recentChainData = recentChainData; + } -public interface AggregatingAttestationPool extends SlotEventsChannel { /** * Default maximum number of attestations to store in the pool. * @@ -33,25 +49,91 @@ public interface AggregatingAttestationPool extends SlotEventsChannel { *

Strictly to cache all attestations for a full 2 epochs is significantly larger than this * cache. */ - int DEFAULT_MAXIMUM_ATTESTATION_COUNT = 187_500; + public static final int DEFAULT_MAXIMUM_ATTESTATION_COUNT = 187_500; /** The valid attestation retention period is 64 slots in deneb */ - long ATTESTATION_RETENTION_SLOTS = 64; + public static final long ATTESTATION_RETENTION_SLOTS = 64; - int getSize(); + public abstract int getSize(); - void add(ValidatableAttestation attestation); + public abstract void add(ValidatableAttestation attestation); - SszList getAttestationsForBlock( + public abstract SszList getAttestationsForBlock( BeaconState stateAtBlockSlot, AttestationForkChecker forkChecker); - Optional createAggregateFor( + public abstract Optional createAggregateFor( Bytes32 attestationHashTreeRoot, Optional committeeIndex); - List getAttestations( + public abstract List getAttestations( Optional maybeSlot, Optional maybeCommitteeIndex); - void onAttestationsIncludedInBlock(UInt64 slot, Iterable attestations); + public abstract void onAttestationsIncludedInBlock( + UInt64 slot, Iterable attestations); + + public abstract void onReorg(UInt64 commonAncestorSlot); + + /** + * Ensures that the committees size is set in the attestation. This is needed for the + * + * @return false if it was not possible to set the committees size but was required, true + * otherwise. + */ + protected boolean ensureCommitteesSizeInAttestation(final ValidatableAttestation attestation) { + if (attestation.getCommitteesSize().isPresent() + || !attestation.getAttestation().requiresCommitteeBits()) { + return true; + } + + final Optional maybeState = + retrieveStateForAttestation(attestation.getAttestation().getData()); + if (maybeState.isEmpty()) { + return false; + } + + attestation.saveCommitteesSize(maybeState.get()); + + return true; + } + + private Optional retrieveStateForAttestation(final AttestationData attestationData) { + // we can use the first state of the epoch to get committees for an attestation + final MiscHelpers miscHelpers = spec.atSlot(attestationData.getSlot()).miscHelpers(); + final Optional maybeEpoch = recentChainData.getCurrentEpoch(); + // the only reason this can happen is we don't have a store yet. + if (maybeEpoch.isEmpty()) { + return Optional.empty(); + } + final UInt64 currentEpoch = maybeEpoch.get(); + final UInt64 attestationEpoch = miscHelpers.computeEpochAtSlot(attestationData.getSlot()); + + LOG.debug("currentEpoch {}, attestationEpoch {}", currentEpoch, attestationEpoch); + if (attestationEpoch.equals(currentEpoch) + || attestationEpoch.equals(currentEpoch.minusMinZero(1))) { + + try { + return recentChainData.getBestState().map(SafeFuture::getImmediately); + } catch (final IllegalStateException e) { + LOG.debug("Couldn't retrieve state for attestation at slot {}", attestationData.getSlot()); + return Optional.empty(); + } + } - void onReorg(UInt64 commonAncestorSlot); + // attestation is not from the current or previous epoch + // this is really an edge case because the current or previous epoch is at least 31 slots + // and the attestation is only valid for 64 slots, so it may be epoch-2 but not beyond. + final UInt64 attestationEpochStartSlot = miscHelpers.computeStartSlotAtEpoch(attestationEpoch); + LOG.debug("State at slot {} needed", attestationEpochStartSlot); + try { + // Assuming retrieveStateInEffectAtSlot and getBeaconCommitteesSize are thread-safe + return recentChainData + .retrieveStateInEffectAtSlot(attestationEpochStartSlot) + .getImmediately(); + } catch (final IllegalStateException e) { + LOG.debug( + "Couldn't retrieve state in effect at slot {} for attestation at slot {}", + attestationEpochStartSlot, + attestationData.getSlot()); + return Optional.empty(); + } + } } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPoolV1.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPoolV1.java index d7caca9ed2f..31c0ee02526 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPoolV1.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPoolV1.java @@ -44,7 +44,6 @@ import tech.pegasys.teku.spec.datastructures.operations.AttestationData; import tech.pegasys.teku.spec.datastructures.operations.AttestationSchema; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; -import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; import tech.pegasys.teku.spec.schemas.SchemaDefinitions; import tech.pegasys.teku.storage.client.RecentChainData; @@ -54,7 +53,7 @@ * cases the returned attestations are aggregated to maximise the number of validators that can be * included. */ -public class AggregatingAttestationPoolV1 implements AggregatingAttestationPool { +public class AggregatingAttestationPoolV1 extends AggregatingAttestationPool { private static final Logger LOG = LogManager.getLogger(); static final Comparator ATTESTATION_INCLUSION_COMPARATOR = @@ -66,8 +65,6 @@ public class AggregatingAttestationPoolV1 implements AggregatingAttestationPool new HashMap<>(); private final NavigableMap> dataHashBySlot = new TreeMap<>(); - private final Spec spec; - private final RecentChainData recentChainData; private final SettableGauge sizeGauge; private final int maximumAttestationCount; @@ -78,8 +75,7 @@ public AggregatingAttestationPoolV1( final RecentChainData recentChainData, final MetricsSystem metricsSystem, final int maximumAttestationCount) { - this.spec = spec; - this.recentChainData = recentChainData; + super(spec, recentChainData); this.sizeGauge = SettableGauge.create( metricsSystem, @@ -91,9 +87,16 @@ public AggregatingAttestationPoolV1( @Override public synchronized void add(final ValidatableAttestation attestation) { - final Optional committeesSize = - attestation.getCommitteesSize().or(() -> getCommitteesSize(attestation.getAttestation())); - getOrCreateAttestationGroup(attestation.getAttestation(), committeesSize) + if (!ensureCommitteesSizeInAttestation(attestation)) { + LOG.debug( + "Attestation at slot {}, block root {} and target root {} has no committee size. Will NOT add this attestation to the pool.", + attestation.getData().getSlot(), + attestation.getData().getBeaconBlockRoot(), + attestation.getData().getTarget().getRoot()); + return; + } + + getOrCreateAttestationGroup(attestation.getData(), attestation.getCommitteesSize()) .ifPresent( attestationGroup -> { final boolean added = @@ -114,30 +117,12 @@ public synchronized void add(final ValidatableAttestation attestation) { } } - private Optional getCommitteesSize(final Attestation attestation) { - if (attestation.requiresCommitteeBits()) { - return getCommitteesSizeUsingTheState(attestation.getData()); - } - return Optional.empty(); - } - /** * @param committeesSize Required for aggregating attestations as per EIP-7549 */ private Optional getOrCreateAttestationGroup( - final Attestation attestation, final Optional committeesSize) { - final AttestationData attestationData = attestation.getData(); - // if an attestation has committee bits, committees size should have been computed. If this is - // not the case, we should ignore this attestation and not add it to the pool - if (attestation.requiresCommitteeBits() && committeesSize.isEmpty()) { - LOG.debug( - "Committees size couldn't be retrieved for attestation at slot {}, block root {} and target root {}. Will NOT add this attestation to the pool.", - attestationData.getSlot(), - attestationData.getBeaconBlockRoot(), - attestationData.getTarget().getRoot()); - return Optional.empty(); - } + final AttestationData attestationData, final Optional committeesSize) { dataHashBySlot .computeIfAbsent(attestationData.getSlot(), slot -> new HashSet<>()) .add(attestationData.hashTreeRoot()); @@ -148,58 +133,6 @@ private Optional getOrCreateAttestationGroup( return Optional.of(attestationGroup); } - private Optional getCommitteesSizeUsingTheState( - final AttestationData attestationData) { - // we can use the first state of the epoch to get committees for an attestation - final MiscHelpers miscHelpers = spec.atSlot(attestationData.getSlot()).miscHelpers(); - final Optional maybeEpoch = recentChainData.getCurrentEpoch(); - // the only reason this can happen is we don't have a store yet. - if (maybeEpoch.isEmpty()) { - return Optional.empty(); - } - final UInt64 currentEpoch = maybeEpoch.get(); - final UInt64 attestationEpoch = miscHelpers.computeEpochAtSlot(attestationData.getSlot()); - - LOG.debug("currentEpoch {}, attestationEpoch {}", currentEpoch, attestationEpoch); - if (attestationEpoch.equals(currentEpoch) - || attestationEpoch.equals(currentEpoch.minusMinZero(1))) { - - return recentChainData - .getBestState() - .flatMap( - state -> { - try { - return Optional.of( - spec.getBeaconCommitteesSize( - state.getImmediately(), attestationData.getSlot())); - } catch (IllegalStateException e) { - LOG.debug( - "Couldn't retrieve state for committee calculation of slot {}", - attestationData.getSlot()); - return Optional.empty(); - } - }); - } - - // attestation is not from the current or previous epoch - // this is really an edge case because the current or previous epoch is at least 31 slots - // and the attestation is only valid for 64 slots, so it may be epoch-2 but not beyond. - final UInt64 attestationEpochStartSlot = miscHelpers.computeStartSlotAtEpoch(attestationEpoch); - LOG.debug("State at slot {} needed", attestationEpochStartSlot); - try { - return recentChainData - .retrieveStateInEffectAtSlot(attestationEpochStartSlot) - .getImmediately() - .map(state -> spec.getBeaconCommitteesSize(state, attestationData.getSlot())); - } catch (final IllegalStateException e) { - LOG.debug( - "Couldn't retrieve state in effect at slot {} for committee calculation of slot {}", - attestationEpochStartSlot, - attestationData.getSlot()); - return Optional.empty(); - } - } - @Override public synchronized void onSlot(final UInt64 slot) { if (slot.compareTo(ATTESTATION_RETENTION_SLOTS) <= 0) { @@ -236,7 +169,17 @@ public synchronized void onAttestationsIncludedInBlock( } private void onAttestationIncludedInBlock(final UInt64 slot, final Attestation attestation) { - getOrCreateAttestationGroup(attestation, getCommitteesSize(attestation)) + final ValidatableAttestation validatableAttestation = + ValidatableAttestation.from(spec, attestation); + if (!ensureCommitteesSizeInAttestation(validatableAttestation)) { + LOG.debug( + "Attestation at slot {}, block root {} and target root {} has no committee size. Unable to call onAttestationIncludedInBlock.", + attestation.getData().getSlot(), + attestation.getData().getBeaconBlockRoot(), + attestation.getData().getTarget().getRoot()); + return; + } + getOrCreateAttestationGroup(attestation.getData(), validatableAttestation.getCommitteesSize()) .ifPresent( attestationGroup -> { final int numRemoved = diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPoolTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPoolTest.java index 33dde53b9ca..a6d827ae58d 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPoolTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/attestation/AggregatingAttestationPoolTest.java @@ -17,7 +17,11 @@ import static org.assertj.core.api.Assumptions.assumeThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ONE; import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ZERO; @@ -74,6 +78,7 @@ abstract class AggregatingAttestationPoolTest { private final AttestationForkChecker forkChecker = mock(AttestationForkChecker.class); + private BeaconState state; private Int2IntMap committeeSizes; abstract AggregatingAttestationPool instantiatePool( @@ -98,15 +103,19 @@ public void setUp(final SpecContext specContext) { dataStructureUtil.randomUInt64( spec.getGenesisSpec().getConfig().getMaxCommitteesPerSlot())); - final BeaconState state = dataStructureUtil.randomBeaconState(); + state = dataStructureUtil.randomBeaconState(); final UpdatableStore mockStore = mock(UpdatableStore.class); + when(mockRecentChainData.getCurrentEpoch()).thenReturn(Optional.of(ZERO)); when(mockRecentChainData.getStore()).thenReturn(mockStore); when(mockRecentChainData.getBestState()) .thenReturn(Optional.of(SafeFuture.completedFuture(state))); + when(mockRecentChainData.retrieveStateInEffectAtSlot(any())) + .thenReturn(SafeFuture.completedFuture(Optional.of(state))); when(mockSpec.getBeaconCommitteesSize(any(), any())).thenReturn(committeeSizes); } when(forkChecker.areAttestationsFromCorrectFork(any())).thenReturn(true); + when(mockSpec.getPreviousEpochAttestationCapacity(any())).thenReturn(Integer.MAX_VALUE); // Fwd some calls to the real spec when(mockSpec.computeEpochAtSlot(any())) @@ -118,6 +127,69 @@ public void setUp(final SpecContext specContext) { when(mockSpec.getGenesisSchemaDefinitions()).thenReturn(spec.getGenesisSchemaDefinitions()); } + @TestTemplate + public void add_shouldRetrieveCommitteeSizesFromStateWhenMissing() { + final AttestationData attestationData = dataStructureUtil.randomAttestationData(ZERO); + + final Attestation attestation = createAttestation(attestationData, spec, 1); + + final ValidatableAttestation validatableAttestation = + ValidatableAttestation.from(mockSpec, attestation); + + assertThat(validatableAttestation.getCommitteesSize()).isEmpty(); + + aggregatingPool.add(validatableAttestation); + + final int expectedCalls = specMilestone.isGreaterThanOrEqualTo(ELECTRA) ? 1 : 0; + + verify(mockSpec, times(expectedCalls)) + .getBeaconCommitteesSize(eq(state), eq(attestationData.getSlot())); + + assertThat(aggregatingPool.getSize()).isEqualTo(1); + } + + @TestTemplate + public void add_shouldNotRetrieveCommitteeSizesWhenNotNeeded() { + final AttestationData attestationData = dataStructureUtil.randomAttestationData(ZERO); + + final Attestation attestation = createAttestation(attestationData, spec, 1); + + final ValidatableAttestation validatableAttestation = + ValidatableAttestation.from(mockSpec, attestation, committeeSizes); + + assertThat(validatableAttestation.getCommitteesSize()).isNotEmpty(); + + aggregatingPool.add(validatableAttestation); + + verify(mockSpec, never()).getBeaconCommitteesSize(eq(state), eq(attestationData.getSlot())); + + assertThat(aggregatingPool.getSize()).isEqualTo(1); + } + + @TestTemplate + public void add_shouldNotAddIfFailsRetrievingCommitteesSize() { + when(mockRecentChainData.getBestState()).thenReturn(Optional.empty()); + when(mockRecentChainData.retrieveStateInEffectAtSlot(any())) + .thenReturn(SafeFuture.completedFuture(Optional.empty())); + + final AttestationData attestationData = dataStructureUtil.randomAttestationData(ZERO); + + final Attestation attestation = createAttestation(attestationData, spec, 1); + + final ValidatableAttestation validatableAttestation = + ValidatableAttestation.from(mockSpec, attestation); + + assertThat(validatableAttestation.getCommitteesSize()).isEmpty(); + + aggregatingPool.add(validatableAttestation); + + if (specMilestone.isGreaterThanOrEqualTo(ELECTRA)) { + assertThat(aggregatingPool.getSize()).isZero(); + } else { + assertThat(aggregatingPool.getSize()).isEqualTo(1); + } + } + @TestTemplate public void createAggregateFor_shouldReturnEmptyWhenNoAttestationsMatchGivenData() { final Optional result = @@ -179,7 +251,7 @@ void getAttestationsForBlock_shouldNotThrowExceptionWhenShufflingSeedIsUnknown() // Receive the attestation from a block, prior to receiving it via gossip aggregatingPool.onAttestationsIncludedInBlock(ONE, List.of(attestation)); // Attestation isn't added because it's already redundant - aggregatingPool.add(ValidatableAttestation.fromValidator(spec, attestation)); + aggregatingPool.add(ValidatableAttestation.fromValidator(mockSpec, attestation)); assertThat(aggregatingPool.getSize()).isZero(); // But we now have a MatchingDataAttestationGroup with unknown shuffling seed present @@ -398,7 +470,6 @@ public void getSize_shouldDecreaseWhenAttestationsRemoved() { addAttestationFromValidators(attestationData, 1, 2, 3, 4); final Attestation attestationToRemove = addAttestationFromValidators(attestationData, 2, 5); - when(mockRecentChainData.getCurrentEpoch()).thenReturn(Optional.of(ZERO)); aggregatingPool.onAttestationsIncludedInBlock(ZERO, List.of(attestationToRemove)); assertThat(aggregatingPool.getSize()).isEqualTo(1); } @@ -408,7 +479,7 @@ public void getSize_shouldNotIncrementWhenAttestationAlreadyExists() { final AttestationData attestationData = dataStructureUtil.randomAttestationData(); final Attestation attestation = addAttestationFromValidators(attestationData, 1, 2, 3, 4); - aggregatingPool.add(ValidatableAttestation.from(spec, attestation)); + aggregatingPool.add(ValidatableAttestation.from(mockSpec, attestation)); assertThat(aggregatingPool.getSize()).isEqualTo(1); } @@ -421,7 +492,6 @@ public void getSize_shouldDecrementForAllRemovedAttestations() { final Attestation attestationToRemove = addAttestationFromValidators(attestationData, 1, 2, 3, 4, 5); - when(mockRecentChainData.getCurrentEpoch()).thenReturn(Optional.of(ZERO)); aggregatingPool.onAttestationsIncludedInBlock(ZERO, List.of(attestationToRemove)); assertThat(aggregatingPool.getSize()).isEqualTo(0); } @@ -450,7 +520,6 @@ public void getSize_shouldDecrementForAllRemovedAttestationsWhileKeepingOthers() addAttestationFromValidators(attestationData, 1, 2, 3, 4, 5); assertThat(aggregatingPool.getSize()).isEqualTo(5); - when(mockRecentChainData.getCurrentEpoch()).thenReturn(Optional.of(ONE)); aggregatingPool.onAttestationsIncludedInBlock(ZERO, List.of(attestationToRemove)); assertThat(aggregatingPool.getSize()).isEqualTo(2); } @@ -620,7 +689,6 @@ public void getAttestations_shouldReturnAttestationsForGivenSlotOnly() { @TestTemplate void onAttestationsIncludedInBlock_shouldNotAddAttestationsAlreadySeenInABlock() { final AttestationData attestationData = dataStructureUtil.randomAttestationData(ZERO); - when(mockRecentChainData.getCurrentEpoch()).thenReturn(Optional.of(ZERO)); // Included in block before we see any attestations with this data aggregatingPool.onAttestationsIncludedInBlock( ONE, List.of(createAttestation(attestationData, 1, 2, 3, 4))); @@ -633,7 +701,6 @@ void onAttestationsIncludedInBlock_shouldNotAddAttestationsAlreadySeenInABlock() @TestTemplate void onAttestationsIncludedInBlock_shouldRemoveAttestationsWhenSeenInABlock() { final AttestationData attestationData = dataStructureUtil.randomAttestationData(ZERO); - when(mockRecentChainData.getCurrentEpoch()).thenReturn(Optional.of(ZERO)); addAttestationFromValidators(attestationData, 2, 3); aggregatingPool.onAttestationsIncludedInBlock( @@ -642,6 +709,40 @@ void onAttestationsIncludedInBlock_shouldRemoveAttestationsWhenSeenInABlock() { assertThat(aggregatingPool.getSize()).isZero(); } + @TestTemplate + public void onAttestationsIncludedInBlock_shouldRetrieveCommitteeSizesFromStateWhenMissing() { + final AttestationData attestationData = dataStructureUtil.randomAttestationData(ZERO); + + final Attestation attestation = createAttestation(attestationData, spec, 1); + + aggregatingPool.onAttestationsIncludedInBlock(ONE, List.of(attestation)); + + final int expectedCalls = specMilestone.isGreaterThanOrEqualTo(ELECTRA) ? 1 : 0; + + verify(mockSpec, times(expectedCalls)) + .getBeaconCommitteesSize(eq(state), eq(attestationData.getSlot())); + } + + @TestTemplate + public void onAttestationsIncludedInBlock_shouldNotAddIfFailsRetrievingCommitteesSize() { + final AttestationData attestationData = dataStructureUtil.randomAttestationData(ZERO); + addAttestationFromValidators(attestationData, 2, 3); + + when(mockRecentChainData.getBestState()).thenReturn(Optional.empty()); + when(mockRecentChainData.retrieveStateInEffectAtSlot(any())) + .thenReturn(SafeFuture.completedFuture(Optional.empty())); + + aggregatingPool.onAttestationsIncludedInBlock( + ONE, List.of(createAttestation(attestationData, 1, 2, 3, 4))); + + if (specMilestone.isGreaterThanOrEqualTo(ELECTRA)) { + // we can't process onAttestationsIncludedInBlock wihthout committees size + assertThat(aggregatingPool.getSize()).isEqualTo(1); + } else { + assertThat(aggregatingPool.getSize()).isZero(); + } + } + @TestTemplate void onReorg_shouldBeAbleToReadAttestations() { final AttestationData attestationData = dataStructureUtil.randomAttestationData(ZERO);