diff --git a/storage/src/main/java/tech/pegasys/teku/storage/client/BlockTimelinessTracker.java b/storage/src/main/java/tech/pegasys/teku/storage/client/BlockTimelinessTracker.java deleted file mode 100644 index 3e1482a638b..00000000000 --- a/storage/src/main/java/tech/pegasys/teku/storage/client/BlockTimelinessTracker.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Consensys Software Inc., 2023 - * - * 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 tech.pegasys.teku.storage.client; - -import static tech.pegasys.teku.spec.constants.NetworkConstants.INTERVALS_PER_SLOT; - -import java.util.Map; -import java.util.Optional; -import java.util.function.Supplier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.tuweni.bytes.Bytes32; -import tech.pegasys.teku.infrastructure.collections.LimitedMap; -import tech.pegasys.teku.infrastructure.time.TimeProvider; -import tech.pegasys.teku.infrastructure.unsigned.UInt64; -import tech.pegasys.teku.spec.Spec; -import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; - -public class BlockTimelinessTracker { - private static final Logger LOG = LogManager.getLogger(); - private final Map blockTimeliness; - private final Supplier timeProviderSupplier; - private final Spec spec; - private final RecentChainData recentChainData; - - public BlockTimelinessTracker(final Spec spec, final RecentChainData recentChainData) { - this(spec, recentChainData, recentChainData::getStore); - } - - // implements is_timely from Consensus Spec - BlockTimelinessTracker( - final Spec spec, - final RecentChainData recentChainData, - final Supplier timeProviderSupplier) { - this.spec = spec; - final int epochsForTimeliness = - Math.max(spec.getGenesisSpecConfig().getReorgMaxEpochsSinceFinalization(), 3); - this.blockTimeliness = - LimitedMap.createSynchronizedNatural( - spec.getGenesisSpec().getSlotsPerEpoch() * epochsForTimeliness); - this.timeProviderSupplier = timeProviderSupplier; - this.recentChainData = recentChainData; - } - - public void setBlockTimelinessFromArrivalTime( - final SignedBeaconBlock block, final UInt64 arrivalTimeMillis) { - if (blockTimeliness.get(block.getRoot()) != null) { - return; - } - final UInt64 computedSlot = - spec.getCurrentSlot( - timeProviderSupplier.get().getTimeInSeconds(), recentChainData.getGenesisTime()); - final Bytes32 root = block.getRoot(); - if (computedSlot.isGreaterThan(block.getMessage().getSlot())) { - LOG.debug( - "Block {}:{} is before computed slot {}, timeliness set to false.", - root, - block.getSlot(), - computedSlot); - blockTimeliness.put(root, false); - return; - } - recentChainData - .getCurrentSlot() - .ifPresent( - slot -> { - final UInt64 slotStartTimeMillis = - spec.getSlotStartTimeMillis(slot, recentChainData.getGenesisTimeMillis()); - final int millisIntoSlot = - arrivalTimeMillis.minusMinZero(slotStartTimeMillis).intValue(); - - final UInt64 timelinessLimit = - spec.getMillisPerSlot(slot).dividedBy(INTERVALS_PER_SLOT); - - final boolean isTimely = - block.getMessage().getSlot().equals(slot) - && timelinessLimit.isGreaterThan(millisIntoSlot); - LOG.debug( - "Block {}:{} arrived at {} ms into slot {}, timeliness limit is {} ms. result: {}", - root, - block.getSlot(), - millisIntoSlot, - computedSlot, - timelinessLimit, - isTimely); - blockTimeliness.put(root, isTimely); - }); - } - - Optional isBlockTimely(final Bytes32 root) { - return Optional.ofNullable(blockTimeliness.get(root)); - } - - // is_proposing_on_time from consensus-spec - // 'on time' is before we're half-way to the attester time. logically, if the slot is 3 segments, - // then splitting into 6 segments is half-way to the attestation time. - public boolean isProposingOnTime(final UInt64 slot) { - final UInt64 slotStartTimeMillis = - spec.getSlotStartTimeMillis(slot, recentChainData.getGenesisTimeMillis()); - final UInt64 timelinessLimit = spec.getMillisPerSlot(slot).dividedBy(INTERVALS_PER_SLOT * 2); - final UInt64 currentTimeMillis = timeProviderSupplier.get().getTimeInMillis(); - final boolean isTimely = - currentTimeMillis.minusMinZero(slotStartTimeMillis).isLessThan(timelinessLimit); - LOG.debug( - "Check ProposingOnTime for slot {}, slot start time is {} ms and current time is {} ms, limit is {} ms result: {}", - slot, - slotStartTimeMillis, - currentTimeMillis, - timelinessLimit, - isTimely); - return isTimely; - } - - // Implements is_head_late form consensus-spec - // caveat: if the root was not found, will default to it being timely, - // on the basis that it's not safe to make choices about blocks we don't know about - public boolean isBlockLate(final Bytes32 root) { - return !isBlockTimely(root).orElse(true); - } -} diff --git a/storage/src/main/java/tech/pegasys/teku/storage/client/LateBlockReorgLogic.java b/storage/src/main/java/tech/pegasys/teku/storage/client/LateBlockReorgLogic.java new file mode 100644 index 00000000000..c114737f632 --- /dev/null +++ b/storage/src/main/java/tech/pegasys/teku/storage/client/LateBlockReorgLogic.java @@ -0,0 +1,333 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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 tech.pegasys.teku.storage.client; + +import static tech.pegasys.teku.spec.constants.NetworkConstants.INTERVALS_PER_SLOT; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.collections.LimitedMap; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.forkchoice.ReadOnlyStore; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.EpochProcessingException; +import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.SlotProcessingException; +import tech.pegasys.teku.spec.logic.common.util.ForkChoiceUtil; + +public class LateBlockReorgLogic { + private static final Logger LOG = LogManager.getLogger(); + protected final Map blockTimeliness; + private final Supplier timeProviderSupplier; + private final Spec spec; + private final RecentChainData recentChainData; + + public LateBlockReorgLogic(final Spec spec, final RecentChainData recentChainData) { + this(spec, recentChainData, recentChainData::getStore); + } + + LateBlockReorgLogic( + final Spec spec, + final RecentChainData recentChainData, + final Supplier timeProviderSupplier) { + this.spec = spec; + final int epochsForTimeliness = + Math.max(spec.getGenesisSpecConfig().getReorgMaxEpochsSinceFinalization(), 3); + this.blockTimeliness = + LimitedMap.createSynchronizedNatural( + spec.getGenesisSpec().getSlotsPerEpoch() * epochsForTimeliness); + this.timeProviderSupplier = timeProviderSupplier; + this.recentChainData = recentChainData; + } + + public void setBlockTimelinessFromArrivalTime( + final SignedBeaconBlock block, final UInt64 arrivalTimeMillis) { + if (blockTimeliness.get(block.getRoot()) != null) { + return; + } + final UInt64 computedSlot = + spec.getCurrentSlot( + timeProviderSupplier.get().getTimeInSeconds(), recentChainData.getGenesisTime()); + final Bytes32 root = block.getRoot(); + if (computedSlot.isGreaterThan(block.getMessage().getSlot())) { + LOG.debug( + "Block {}:{} is before computed slot {}, timeliness set to false.", + root, + block.getSlot(), + computedSlot); + blockTimeliness.put(root, false); + return; + } + recentChainData + .getCurrentSlot() + .ifPresent( + slot -> { + final UInt64 slotStartTimeMillis = + spec.getSlotStartTimeMillis(slot, recentChainData.getGenesisTimeMillis()); + final int millisIntoSlot = + arrivalTimeMillis.minusMinZero(slotStartTimeMillis).intValue(); + + final UInt64 timelinessLimit = + spec.getMillisPerSlot(slot).dividedBy(INTERVALS_PER_SLOT); + + final boolean isTimely = + block.getMessage().getSlot().equals(slot) + && timelinessLimit.isGreaterThan(millisIntoSlot); + LOG.debug( + "Block {}:{} arrived at {} ms into slot {}, timeliness limit is {} ms. result: {}", + root, + block.getSlot(), + millisIntoSlot, + computedSlot, + timelinessLimit, + isTimely); + blockTimeliness.put(root, isTimely); + }); + } + + // implements is_timely from Consensus Spec + Optional isBlockTimely(final Bytes32 root) { + return Optional.ofNullable(blockTimeliness.get(root)); + } + + // is_proposing_on_time from consensus-spec + // 'on time' is before we're half-way to the attester time. logically, if the slot is 3 segments, + // then splitting into 6 segments is half-way to the attestation time. + public boolean isProposingOnTime(final UInt64 slot) { + final UInt64 slotStartTimeMillis = + spec.getSlotStartTimeMillis(slot, recentChainData.getGenesisTimeMillis()); + final UInt64 timelinessLimit = spec.getMillisPerSlot(slot).dividedBy(INTERVALS_PER_SLOT * 2); + final UInt64 currentTimeMillis = timeProviderSupplier.get().getTimeInMillis(); + final boolean isTimely = + currentTimeMillis.minusMinZero(slotStartTimeMillis).isLessThan(timelinessLimit); + LOG.debug( + "Check ProposingOnTime for slot {}, slot start time is {} ms and current time is {} ms, limit is {} ms result: {}", + slot, + slotStartTimeMillis, + currentTimeMillis, + timelinessLimit, + isTimely); + return isTimely; + } + + // Implements is_head_late form consensus-spec + // caveat: if the root was not found, will default to it being timely, + // on the basis that it's not safe to make choices about blocks we don't know about + public boolean isBlockLate(final Bytes32 root) { + return !isBlockTimely(root).orElse(true); + } + + // implements get_proposer_head from Consensus Spec + public Bytes32 getProposerHead(final Bytes32 headRoot, final UInt64 slot) { + LOG.debug("start getProposerHead"); + final boolean isProposerBoostActive = isProposerBoostActive(headRoot); + final boolean isShufflingStableAndForkChoiceOk = isForkChoiceStableAndFinalizationOk(slot); + final boolean isProposingOnTime = isProposingOnTime(slot); + final boolean isHeadLate = isBlockLate(headRoot); + final Optional maybeHead = getStore().getBlockIfAvailable(headRoot); + // cheap checks of that list: + // (isHeadLate, isShufflingStable, isFinalizationOk, isProposingOnTime); + // and isProposerBoostActive (assert condition); + // finally need head block to make further checks + if (!isHeadLate + || !isShufflingStableAndForkChoiceOk + || !isProposingOnTime + || isProposerBoostActive + || maybeHead.isEmpty()) { + LOG.debug( + "getProposerHead - return headRoot - isHeadLate {}, isForkChoiceStableAndFinalizationOk {}, isProposingOnTime {}, isProposerBoostActive {}, head.isEmpty {}", + () -> isHeadLate, + () -> isShufflingStableAndForkChoiceOk, + () -> isProposingOnTime, + () -> isProposerBoostActive, + headRoot::isEmpty); + return headRoot; + } + + final SignedBeaconBlock head = maybeHead.get(); + final boolean isFfgCompetitive = isFfgCompetetive(headRoot, head.getParentRoot()); + final boolean isSingleSlotReorg = isSingleSlotReorg(head, slot); + + // from the initial list, check + // isFfgCompetitive, isSingleSlotReorg + if (!isFfgCompetitive || !isSingleSlotReorg) { + LOG.debug( + "getProposerHead - return headRoot - isFfgCompetitive {}, isSingleSlotReorg {}", + isFfgCompetitive, + isSingleSlotReorg); + return headRoot; + } + final boolean isHeadWeak = getStore().isHeadWeak(headRoot); + final boolean isParentStrong = getStore().isParentStrong(head.getParentRoot()); + // finally, the parent must be strong, and the current head must be weak. + if (isHeadWeak && isParentStrong) { + LOG.debug("getProposerHead - return parentRoot - isHeadWeak true && isParentStrong true"); + return head.getParentRoot(); + } + + LOG.debug("getProposerHead - return headRoot"); + return headRoot; + } + + // implements should_override_forkchoice_update from Consensus-spec + // return all([head_late, shuffling_stable, ffg_competitive, finalization_ok, + // proposing_reorg_slot, single_slot_reorg, + // head_weak, parent_strong]) + public boolean shouldOverrideForkChoiceUpdate(final Bytes32 headRoot) { + final Optional maybeHead = getStore().getBlockIfAvailable(headRoot); + final Optional maybeCurrentSlot = recentChainData.getCurrentSlot(); + if (isMissingData(maybeHead, maybeCurrentSlot)) { + LOG.debug( + "shouldOverrideForkChoiceUpdate head - maybeHead {}, maybeCurrentSlot {}.", + maybeHead, + maybeCurrentSlot); + return false; + } + if (!isBlockLate(headRoot)) { + // ! isHeadLate, or we don't have data we need (currentSlot and the block in question) + LOG.debug("shouldOverrideForkChoiceUpdate head - is not late."); + return false; + } + final SignedBeaconBlock head = maybeHead.orElseThrow(); + final UInt64 currentSlot = maybeCurrentSlot.orElseThrow(); + final UInt64 proposalSlot = head.getSlot().increment(); + final boolean isShufflingStableAndForkChoiceOk = + isForkChoiceStableAndFinalizationOk(proposalSlot); + + final boolean isFfgCompetitive = isFfgCompetetive(headRoot, head.getParentRoot()); + final Optional maybeParentSlot = + recentChainData.getSlotForBlockRoot(head.getParentRoot()); + + if (!isShufflingStableAndForkChoiceOk || !isFfgCompetitive || maybeParentSlot.isEmpty()) { + LOG.debug( + "shouldOverrideForkChoiceUpdate isShufflingStableAndForkChoiceOk {}, isFfgCompetitive {}, maybeParentSlot {}", + isShufflingStableAndForkChoiceOk, + isFfgCompetitive, + maybeParentSlot); + return false; + } + + if (!shouldOverrideFcuCheckWeights(head, headRoot, proposalSlot, currentSlot)) { + return false; + } + + return shouldOverrideFcuCheckProposerPreState(proposalSlot, head.getParentRoot()); + } + + boolean shouldOverrideFcuCheckWeights( + final SignedBeaconBlock head, + final Bytes32 headRoot, + final UInt64 proposalSlot, + final UInt64 currentSlot) { + + final boolean isProposingOnTime = isProposingOnTime(proposalSlot); + final boolean isCurrentTimeOk = + head.getSlot().equals(currentSlot) + || (currentSlot.equals(proposalSlot) && isProposingOnTime); + final boolean isSingleSlotReorg = isSingleSlotReorg(head, proposalSlot); + + if (!isSingleSlotReorg || !isCurrentTimeOk) { + LOG.debug( + "shouldOverrideForkChoiceUpdate isSingleSlotReorg {}, isCurrentTimeOk {}", + isSingleSlotReorg, + isCurrentTimeOk); + return false; + } + if (currentSlot.isGreaterThan(head.getSlot())) { + final boolean isHeadWeak = getStore().isHeadWeak(headRoot); + final boolean isParentStrong = getStore().isParentStrong(head.getParentRoot()); + if (!isHeadWeak || !isParentStrong) { + LOG.debug( + "shouldOverrideForkChoiceUpdate isHeadWeak {}, isParentStrong {}", + isHeadWeak, + isParentStrong); + return false; + } + } + return true; + } + + boolean shouldOverrideFcuCheckProposerPreState( + final UInt64 proposalSlot, final Bytes32 parentRoot) { + // Only suppress the fork choice update if we are confident that we will propose the next + LOG.debug("Need parent state"); + final Optional maybeParentState = getStore().getBlockStateIfAvailable(parentRoot); + if (maybeParentState.isEmpty()) { + LOG.debug("shouldOverrideForkChoice could not retrieve parent state from cache"); + return false; + } + try { + final BeaconState proposerPreState = spec.processSlots(maybeParentState.get(), proposalSlot); + final int proposerIndex = getProposerIndex(proposerPreState, proposalSlot); + if (!recentChainData.validatorIsConnected(proposerIndex, proposalSlot)) { + LOG.debug( + "shouldOverrideForkChoiceUpdate isValidatorConnected({}) {}, ", proposerIndex, false); + return false; + } + } catch (SlotProcessingException | EpochProcessingException e) { + LOG.trace("Failed to process", e); + return false; + } + + return true; + } + + boolean isSingleSlotReorg(final SignedBeaconBlock head, final UInt64 slot) { + final Optional maybeParentSlot = + recentChainData.getSlotForBlockRoot(head.getParentRoot()); + return maybeParentSlot.map(uInt64 -> uInt64.increment().equals(head.getSlot())).orElse(false) + && head.getSlot().increment().equals(slot); + } + + boolean isProposerBoostActive(final Bytes32 headRoot) { + return getStore().getProposerBoostRoot().map(root -> !root.equals(headRoot)).orElse(false); + } + + boolean isFfgCompetetive(final Bytes32 headRoot, final Bytes32 parentRoot) { + return getStore().isFfgCompetitive(headRoot, parentRoot).orElse(false); + } + + boolean isForkChoiceStableAndFinalizationOk(final UInt64 slot) { + final ForkChoiceUtil forkChoiceUtil = spec.atSlot(slot).getForkChoiceUtil(); + return forkChoiceUtil.isShufflingStable(slot) + && forkChoiceUtil.isFinalizationOk(getStore(), slot); + } + + boolean isMissingData(Optional maybeHead, Optional maybeCurrentSlot) { + if (maybeHead.isEmpty() || maybeCurrentSlot.isEmpty()) { + LOG.debug( + "shouldOverrideForkChoiceUpdate head {}, currentSlot {}", + () -> maybeHead.map(SignedBeaconBlock::getRoot), + () -> maybeCurrentSlot); + return true; + } + return false; + } + + @VisibleForTesting + protected int getProposerIndex(final BeaconState proposerPreState, final UInt64 proposalSlot) { + return spec.getBeaconProposerIndex(proposerPreState, proposalSlot); + } + + private ReadOnlyStore getStore() { + return recentChainData.getStore(); + } +} diff --git a/storage/src/main/java/tech/pegasys/teku/storage/client/RecentChainData.java b/storage/src/main/java/tech/pegasys/teku/storage/client/RecentChainData.java index a9434d7ef4d..f971a2ffcff 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/client/RecentChainData.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/client/RecentChainData.java @@ -37,7 +37,6 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; -import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; @@ -58,8 +57,6 @@ import tech.pegasys.teku.spec.datastructures.state.Fork; import tech.pegasys.teku.spec.datastructures.state.ForkInfo; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; -import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.EpochProcessingException; -import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.SlotProcessingException; import tech.pegasys.teku.spec.logic.common.util.BeaconStateUtil; import tech.pegasys.teku.storage.api.ChainHeadChannel; import tech.pegasys.teku.storage.api.FinalizedCheckpointChannel; @@ -106,7 +103,7 @@ public abstract class RecentChainData implements StoreUpdateHandler { private final SingleBlockProvider validatedBlockProvider; private final SingleBlobSidecarProvider validatedBlobSidecarProvider; - private final BlockTimelinessTracker blockTimelinessTracker; + private final LateBlockReorgLogic lateBlockReorgLogic; private final ValidatorIsConnectedProvider validatorIsConnectedProvider; @@ -114,7 +111,6 @@ public abstract class RecentChainData implements StoreUpdateHandler { final AsyncRunner asyncRunner, final MetricsSystem metricsSystem, final StoreConfig storeConfig, - final TimeProvider timeProvider, final BlockProvider blockProvider, final SingleBlockProvider validatedBlockProvider, final SingleBlobSidecarProvider validatedBlobSidecarProvider, @@ -138,7 +134,7 @@ public abstract class RecentChainData implements StoreUpdateHandler { this.chainHeadChannel = chainHeadChannel; this.storageUpdateChannel = storageUpdateChannel; this.finalizedCheckpointChannel = finalizedCheckpointChannel; - this.blockTimelinessTracker = new BlockTimelinessTracker(spec, this); + this.lateBlockReorgLogic = new LateBlockReorgLogic(spec, this); reorgCounter = metricsSystem.createCounter( TekuMetricCategory.BEACON, @@ -643,152 +639,12 @@ public List getAllBlockRootsAtSlot(final UInt64 slot) { .orElse(Collections.emptyList()); } - // implements get_proposer_head from Consensus Spec public Bytes32 getProposerHead(final Bytes32 headRoot, final UInt64 slot) { - LOG.debug("start getProposerHead"); - // if proposer boost is still active, don't attempt to override head - final boolean isProposerBoostActive = - store.getProposerBoostRoot().map(root -> !root.equals(headRoot)).orElse(false); - final boolean isShufflingStable = spec.atSlot(slot).getForkChoiceUtil().isShufflingStable(slot); - final boolean isFinalizationOk = - spec.atSlot(slot).getForkChoiceUtil().isFinalizationOk(store, slot); - final boolean isProposingOnTime = isProposingOnTime(slot); - final boolean isHeadLate = isBlockLate(headRoot); - final Optional maybeHead = store.getBlockIfAvailable(headRoot); - - // to return parent root, we need all of (isHeadLate, isShufflingStable, isFfgCompetitive, - // isFinalizationOk, isProposingOnTime, isSingleSlotReorg, isHeadWeak, isParentStrong) - - // cheap checks of that list: - // (isHeadLate, isShufflingStable, isFinalizationOk, isProposingOnTime); - // and isProposerBoostActive (assert condition); - // finally need head block to make further checks - if (!isHeadLate - || !isShufflingStable - || !isFinalizationOk - || !isProposingOnTime - || isProposerBoostActive - || maybeHead.isEmpty()) { - LOG.debug( - "getProposerHead - return headRoot - isHeadLate {}, isShufflingStable {}, isFinalizationOk {}, isProposingOnTime {}, isProposerBoostActive {}, head.isEmpty {}", - () -> isHeadLate, - () -> isShufflingStable, - () -> isFinalizationOk, - () -> isProposingOnTime, - () -> isProposerBoostActive, - () -> headRoot.isEmpty()); - return headRoot; - } - - final SignedBeaconBlock head = maybeHead.get(); - final boolean isFfgCompetitive = - store.isFfgCompetitive(headRoot, head.getParentRoot()).orElse(false); - - final Optional maybeParentSlot = getSlotForBlockRoot(head.getParentRoot()); - final boolean isParentSlotOk = - maybeParentSlot.map(uInt64 -> uInt64.increment().equals(head.getSlot())).orElse(false); - final boolean isCurrentTimeOk = head.getSlot().increment().equals(slot); - final boolean isSingleSlotReorg = isParentSlotOk && isCurrentTimeOk; - - // from the initial list, check - // isFfgCompetitive, isSingleSlotReorg - if (!isFfgCompetitive || !isSingleSlotReorg) { - LOG.debug( - "getProposerHead - return headRoot - isFfgCompetitive {}, isSingleSlotReorg {}", - isFfgCompetitive, - isSingleSlotReorg); - return headRoot; - } - - LOG.debug("getProposerHead - return headRoot"); - return headRoot; + return lateBlockReorgLogic.getProposerHead(headRoot, slot); } - // implements should_override_forkchoice_update from Consensus-spec - // return all([head_late, shuffling_stable, ffg_competitive, finalization_ok, - // proposing_reorg_slot, single_slot_reorg, - // head_weak, parent_strong]) public boolean shouldOverrideForkChoiceUpdate(final Bytes32 headRoot) { - final Optional maybeHead = store.getBlockIfAvailable(headRoot); - final Optional maybeCurrentSlot = getCurrentSlot(); - final boolean isHeadLate = isBlockLate(headRoot); - if (maybeHead.isEmpty() || maybeCurrentSlot.isEmpty() || !isHeadLate) { - // ! isHeadLate, or we don't have data we need (currentSlot and the block in question) - LOG.debug( - "shouldOverrideForkChoiceUpdate head {}, currentSlot {}, isHeadLate {}", - () -> maybeHead.map(SignedBeaconBlock::getRoot), - () -> maybeCurrentSlot, - () -> isHeadLate); - return false; - } - final SignedBeaconBlock head = maybeHead.get(); - final UInt64 currentSlot = maybeCurrentSlot.get(); - final UInt64 proposalSlot = maybeHead.get().getSlot().increment(); - final boolean isShufflingStable = - spec.atSlot(proposalSlot).getForkChoiceUtil().isShufflingStable(proposalSlot); - - final boolean isFfgCompetitive = - store.isFfgCompetitive(headRoot, head.getParentRoot()).orElse(false); - final boolean isFinalizationOk = - spec.atSlot(proposalSlot).getForkChoiceUtil().isFinalizationOk(store, proposalSlot); - final Optional maybeParentSlot = getSlotForBlockRoot(head.getParentRoot()); - - if (!isShufflingStable || !isFfgCompetitive || !isFinalizationOk || maybeParentSlot.isEmpty()) { - // !shufflingStable or !ffgCompetetive or !finalizationOk, or parentSlot is not found - LOG.debug( - "shouldOverrideForkChoiceUpdate isShufflingStable {}, isFfgCompetitive {}, isFinalizationOk {}, maybeParentSlot {}", - isShufflingStable, - isFfgCompetitive, - isFinalizationOk, - maybeParentSlot); - return false; - } - - final boolean isParentSlotOk = - maybeParentSlot.map(uInt64 -> uInt64.increment().equals(head.getSlot())).orElse(false); - final boolean isProposingOnTime = isProposingOnTime(proposalSlot); - final boolean isCurrentTimeOk = - head.getSlot().equals(currentSlot) - || (currentSlot.equals(proposalSlot) && isProposingOnTime); - final boolean isHeadWeak; - final boolean isParentStrong; - if (currentSlot.isGreaterThan(head.getSlot())) { - isHeadWeak = store.isHeadWeak(headRoot); - isParentStrong = store.isParentStrong(head.getParentRoot()); - } else { - isHeadWeak = true; - isParentStrong = true; - } - final boolean isSingleSlotReorg = isParentSlotOk && isCurrentTimeOk; - if (!isSingleSlotReorg || !isHeadWeak || !isParentStrong) { - LOG.debug( - "shouldOverrideForkChoiceUpdate isSingleSlotReorg {}, isHeadWeak {}, isParentStrong {}", - isSingleSlotReorg, - isHeadWeak, - isParentStrong); - return false; - } - - // Only suppress the fork choice update if we are confident that we will propose the next block. - final Optional maybeParentState = - store.getBlockStateIfAvailable(head.getParentRoot()); - if (maybeParentState.isEmpty()) { - return false; - } - try { - final BeaconState proposerPreState = spec.processSlots(maybeParentState.get(), proposalSlot); - final int proposerIndex = spec.getBeaconProposerIndex(proposerPreState, proposalSlot); - if (!validatorIsConnectedProvider.isValidatorConnected(proposerIndex, proposalSlot)) { - LOG.debug( - "shouldOverrideForkChoiceUpdate isValidatorConnected({}) {}, ", proposerIndex, false); - return false; - } - } catch (SlotProcessingException | EpochProcessingException e) { - LOG.trace("Failed to process", e); - return false; - } - - return true; + return lateBlockReorgLogic.shouldOverrideForkChoiceUpdate(headRoot); } public boolean validatorIsConnected(final int validatorIndex, final UInt64 slot) { @@ -797,19 +653,10 @@ public boolean validatorIsConnected(final int validatorIndex, final UInt64 slot) public void setBlockTimelinessFromArrivalTime( final SignedBeaconBlock block, final UInt64 arrivalTime) { - blockTimelinessTracker.setBlockTimelinessFromArrivalTime(block, arrivalTime); - } - - // implements is_head_late from consensus spec - public boolean isBlockLate(final Bytes32 blockRoot) { - return blockTimelinessTracker.isBlockLate(blockRoot); - } - - public boolean isProposingOnTime(final UInt64 slot) { - return blockTimelinessTracker.isProposingOnTime(slot); + lateBlockReorgLogic.setBlockTimelinessFromArrivalTime(block, arrivalTime); } public void setBlockTimelinessIfEmpty(SignedBeaconBlock block) { - blockTimelinessTracker.setBlockTimelinessFromArrivalTime(block, store.getTimeInMillis()); + lateBlockReorgLogic.setBlockTimelinessFromArrivalTime(block, store.getTimeInMillis()); } } diff --git a/storage/src/main/java/tech/pegasys/teku/storage/client/StorageBackedRecentChainData.java b/storage/src/main/java/tech/pegasys/teku/storage/client/StorageBackedRecentChainData.java index c311f219407..83766dc9558 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/client/StorageBackedRecentChainData.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/client/StorageBackedRecentChainData.java @@ -66,7 +66,6 @@ public StorageBackedRecentChainData( asyncRunner, metricsSystem, storeConfig, - timeProvider, storageQueryChannel::getHotBlocksByRoot, validatedBlockProvider, validatedBlobSidecarProvider, diff --git a/storage/src/test/java/tech/pegasys/teku/storage/client/BlockTimelinessTrackerTest.java b/storage/src/test/java/tech/pegasys/teku/storage/client/BlockTimelinessTrackerTest.java deleted file mode 100644 index 4e2ff3c408b..00000000000 --- a/storage/src/test/java/tech/pegasys/teku/storage/client/BlockTimelinessTrackerTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright Consensys Software Inc., 2023 - * - * 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 tech.pegasys.teku.storage.client; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Optional; -import org.apache.tuweni.bytes.Bytes32; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import tech.pegasys.teku.infrastructure.time.StubTimeProvider; -import tech.pegasys.teku.infrastructure.unsigned.UInt64; -import tech.pegasys.teku.spec.Spec; -import tech.pegasys.teku.spec.TestSpecFactory; -import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockAndState; -import tech.pegasys.teku.spec.util.DataStructureUtil; - -class BlockTimelinessTrackerTest { - private final Spec spec = TestSpecFactory.createDefault(); - private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); - private final int millisPerSlot = spec.getGenesisSpecConfig().getSecondsPerSlot() * 1000; - private final RecentChainData recentChainData = mock(RecentChainData.class); - private final UInt64 slot = UInt64.ONE; - - private StubTimeProvider timeProvider; - private Bytes32 blockRoot; - private SignedBlockAndState signedBlockAndState; - private BlockTimelinessTracker tracker; - - @BeforeEach - void setup() { - signedBlockAndState = dataStructureUtil.randomSignedBlockAndState(slot); - blockRoot = signedBlockAndState.getBlock().getMessage().getRoot(); - timeProvider = - StubTimeProvider.withTimeInSeconds(signedBlockAndState.getState().getGenesisTime()); - tracker = new BlockTimelinessTracker(spec, recentChainData, () -> timeProvider); - - when(recentChainData.getGenesisTime()) - .thenReturn(signedBlockAndState.getState().getGenesisTime()); - when(recentChainData.getGenesisTimeMillis()) - .thenReturn(signedBlockAndState.getState().getGenesisTime().times(1000)); - when(recentChainData.getCurrentSlot()).thenReturn(Optional.of(UInt64.ONE)); - } - - @Test - void blockTimeliness_shouldReportTimelinessIfSet() { - final UInt64 computedTime = computeTime(slot, 500); - - tracker.setBlockTimelinessFromArrivalTime(signedBlockAndState.getBlock(), computedTime); - assertThat(tracker.isBlockTimely(blockRoot)).contains(true); - assertThat(tracker.isBlockLate(blockRoot)).isFalse(); - } - - @Test - void blockTimeliness_shouldSetTimelinessOnce() { - final UInt64 computedTime = computeTime(slot, 500); - - tracker.setBlockTimelinessFromArrivalTime(signedBlockAndState.getBlock(), computedTime); - // The block would be late in the tracker if this set is not ignored, but it should be ignored - // because its already been set - tracker.setBlockTimelinessFromArrivalTime( - signedBlockAndState.getBlock(), computedTime.plus(3000)); - assertThat(tracker.isBlockTimely(blockRoot)).contains(true); - assertThat(tracker.isBlockLate(blockRoot)).isFalse(); - } - - @Test - void blockTimeliness_shouldReportFalseIfLate() { - final UInt64 computedTime = computeTime(slot, 2100); - - tracker.setBlockTimelinessFromArrivalTime(signedBlockAndState.getBlock(), computedTime); - assertThat(tracker.isBlockTimely(blockRoot)).contains(false); - assertThat(tracker.isBlockLate(blockRoot)).isTrue(); - } - - @Test - void blockTimeliness_shouldReportFalseIfAtLimit() { - final UInt64 computedTime = computeTime(slot, 2000); - - tracker.setBlockTimelinessFromArrivalTime(signedBlockAndState.getBlock(), computedTime); - assertThat(tracker.isBlockTimely(blockRoot)).contains(false); - assertThat(tracker.isBlockLate(blockRoot)).isTrue(); - } - - @Test - void blockTimeliness_ifBlockFromFuture() { - final UInt64 computedTime = computeTime(slot, 2100); - - tracker.setBlockTimelinessFromArrivalTime( - dataStructureUtil.randomSignedBeaconBlock(0), computedTime); - assertThat(tracker.isBlockTimely(blockRoot)).isEmpty(); - assertThat(tracker.isBlockLate(blockRoot)).isFalse(); - } - - @Test - void blockTimeliness_shouldReportEmptyIfNotSet() { - assertThat(tracker.isBlockTimely(blockRoot)).isEmpty(); - assertThat(tracker.isBlockLate(blockRoot)).isFalse(); - } - - @Test - void isProposingOnTime_shouldDetectBeforeSlotStartAsOk() { - // Advance time to 500ms before slot start - timeProvider.advanceTimeByMillis(millisPerSlot - 500); - assertThat(tracker.isProposingOnTime(slot)).isTrue(); - } - - @Test - void isProposingOnTime_shouldDetectSlotStartAsOnTime() { - // Advance time by 1 slot, leaving us at exactly slot time - timeProvider.advanceTimeByMillis(millisPerSlot); - assertThat(tracker.isProposingOnTime(slot)).isTrue(); - } - - @Test - void isProposingOnTime_shouldDetectLateIfAttestationsDue() { - // attestation is due 2 seconds into slot - timeProvider.advanceTimeByMillis(millisPerSlot + 2000); - assertThat(tracker.isProposingOnTime(slot)).isFalse(); - } - - @Test - void isProposingOnTime_shouldDetectOnTimeBeforeCutoff() { - /// 999 ms into slot, cutoff is 1000ms - timeProvider.advanceTimeByMillis(millisPerSlot + 999); - assertThat(tracker.isProposingOnTime(slot)).isTrue(); - } - - @Test - void isProposingOnTime_shouldDetectLateIfHalfWayToAttestationDue() { - timeProvider.advanceTimeByMillis(millisPerSlot + 1000); - assertThat(tracker.isProposingOnTime(slot)).isFalse(); - } - - private UInt64 computeTime(final UInt64 slot, final long timeIntoSlot) { - return timeProvider.getTimeInMillis().plus(slot.times(millisPerSlot)).plus(timeIntoSlot); - } -} diff --git a/storage/src/test/java/tech/pegasys/teku/storage/client/LateBlockReorgLogicTest.java b/storage/src/test/java/tech/pegasys/teku/storage/client/LateBlockReorgLogicTest.java new file mode 100644 index 00000000000..53c8ff33577 --- /dev/null +++ b/storage/src/test/java/tech/pegasys/teku/storage/client/LateBlockReorgLogicTest.java @@ -0,0 +1,537 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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 tech.pegasys.teku.storage.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tech.pegasys.teku.infrastructure.time.StubTimeProvider; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockAndState; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.util.DataStructureUtil; +import tech.pegasys.teku.storage.store.UpdatableStore; + +class LateBlockReorgLogicTest { + private final Spec spec = TestSpecFactory.createDefault(); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private final int millisPerSlot = spec.getGenesisSpecConfig().getSecondsPerSlot() * 1000; + private final RecentChainData recentChainData = mock(RecentChainData.class); + private final UInt64 slot = UInt64.ONE; + + private StubTimeProvider timeProvider; + private Bytes32 blockRoot; + private SignedBlockAndState signedBlockAndState; + private LateBlockReorgLogicInstrumented reorgLogicInstrumented; + + private final UpdatableStore store = mock(UpdatableStore.class); + + @BeforeEach + void setup() { + signedBlockAndState = dataStructureUtil.randomSignedBlockAndState(slot); + blockRoot = signedBlockAndState.getBlock().getMessage().getRoot(); + timeProvider = + StubTimeProvider.withTimeInSeconds(signedBlockAndState.getState().getGenesisTime()); + reorgLogicInstrumented = + new LateBlockReorgLogicInstrumented(spec, recentChainData, () -> timeProvider); + + when(recentChainData.getGenesisTime()) + .thenReturn(signedBlockAndState.getState().getGenesisTime()); + when(recentChainData.getGenesisTimeMillis()) + .thenReturn(signedBlockAndState.getState().getGenesisTime().times(1000)); + when(recentChainData.getCurrentSlot()).thenReturn(Optional.of(UInt64.ONE)); + when(recentChainData.getStore()).thenReturn(store); + + when(store.getFinalizedCheckpoint()) + .thenReturn(dataStructureUtil.randomCheckpoint(UInt64.ZERO)); + } + + @Test + void blockTimeliness_shouldReportTimelinessIfSet() { + final UInt64 computedTime = computeTime(slot, 500); + + reorgLogicInstrumented.setBlockTimelinessFromArrivalTime( + signedBlockAndState.getBlock(), computedTime); + assertThat(reorgLogicInstrumented.isBlockTimely(blockRoot)).contains(true); + assertThat(reorgLogicInstrumented.isBlockLate(blockRoot)).isFalse(); + } + + @Test + void blockTimeliness_shouldSetTimelinessOnce() { + final UInt64 computedTime = computeTime(slot, 500); + + reorgLogicInstrumented.setBlockTimelinessFromArrivalTime( + signedBlockAndState.getBlock(), computedTime); + // The block would be late in the tracker if this set is not ignored, but it should be ignored + // because its already been set + reorgLogicInstrumented.setBlockTimelinessFromArrivalTime( + signedBlockAndState.getBlock(), computedTime.plus(3000)); + assertThat(reorgLogicInstrumented.isBlockTimely(blockRoot)).contains(true); + assertThat(reorgLogicInstrumented.isBlockLate(blockRoot)).isFalse(); + } + + @Test + void blockTimeliness_shouldReportFalseIfLate() { + final UInt64 computedTime = computeTime(slot, 2100); + + reorgLogicInstrumented.setBlockTimelinessFromArrivalTime( + signedBlockAndState.getBlock(), computedTime); + assertThat(reorgLogicInstrumented.isBlockTimely(blockRoot)).contains(false); + assertThat(reorgLogicInstrumented.isBlockLate(blockRoot)).isTrue(); + } + + @Test + void blockTimeliness_shouldReportFalseIfAtLimit() { + final UInt64 computedTime = computeTime(slot, 2000); + + reorgLogicInstrumented.setBlockTimelinessFromArrivalTime( + signedBlockAndState.getBlock(), computedTime); + assertThat(reorgLogicInstrumented.isBlockTimely(blockRoot)).contains(false); + assertThat(reorgLogicInstrumented.isBlockLate(blockRoot)).isTrue(); + } + + @Test + void blockTimeliness_shouldReportEmptyIfNotSet() { + assertThat(reorgLogicInstrumented.isBlockTimely(blockRoot)).isEmpty(); + assertThat(reorgLogicInstrumented.isBlockLate(blockRoot)).isFalse(); + } + + @Test + void isProposingOnTime_shouldDetectBeforeSlotStartAsOk() { + // Advance time to 500ms before slot start + timeProvider.advanceTimeByMillis(millisPerSlot - 500); + assertThat(reorgLogicInstrumented.isProposingOnTime(slot)).isTrue(); + } + + @Test + void isProposingOnTime_shouldDetectSlotStartAsOnTime() { + // Advance time by 1 slot, leaving us at exactly slot time + timeProvider.advanceTimeByMillis(millisPerSlot); + assertThat(reorgLogicInstrumented.isProposingOnTime(slot)).isTrue(); + } + + @Test + void isProposingOnTime_shouldDetectLateIfAttestationsDue() { + // attestation is due 2 seconds into slot + timeProvider.advanceTimeByMillis(millisPerSlot + 2000); + assertThat(reorgLogicInstrumented.isProposingOnTime(slot)).isFalse(); + } + + @Test + void isProposingOnTime_shouldDetectOnTimeBeforeCutoff() { + /// 999 ms into slot, cutoff is 1000ms + timeProvider.advanceTimeByMillis(millisPerSlot + 999); + assertThat(reorgLogicInstrumented.isProposingOnTime(slot)).isTrue(); + } + + @Test + void isProposingOnTime_shouldDetectLateIfHalfWayToAttestationDue() { + timeProvider.advanceTimeByMillis(millisPerSlot + 1000); + assertThat(reorgLogicInstrumented.isProposingOnTime(slot)).isFalse(); + } + + @Test + void shouldOverrideFcuCheckWeights_MultiSlotReorg() { + withLateBlock(blockRoot); + withParentSlot(Optional.of(UInt64.ZERO)); + withHeadBlock(); + when(store.isHeadWeak(any())).thenReturn(true); + when(store.isParentStrong(any())).thenReturn(false); + + // because we're at head slot 1, slot 3 is too far ahead to be allowing a single slot reorg + assertThat( + reorgLogicInstrumented.shouldOverrideFcuCheckWeights( + signedBlockAndState.getSignedBeaconBlock().orElseThrow(), + blockRoot, + UInt64.valueOf(3), + UInt64.valueOf(3))) + .isFalse(); + } + + @Test + void shouldOverrideFcuCheckWeights_weakHead() { + withLateBlock(blockRoot); + withParentSlot(Optional.of(UInt64.ZERO)); + withHeadBlock(); + when(store.isHeadWeak(any())).thenReturn(true); + when(store.isParentStrong(any())).thenReturn(true); + + assertThat( + reorgLogicInstrumented.shouldOverrideFcuCheckWeights( + signedBlockAndState.getSignedBeaconBlock().orElseThrow(), + blockRoot, + UInt64.valueOf(2), + UInt64.valueOf(2))) + .isTrue(); + } + + @Test + void shouldOverrideFcuCheckWeights_strongHead() { + withLateBlock(blockRoot); + withParentSlot(Optional.of(UInt64.ZERO)); + withHeadBlock(); + when(store.isHeadWeak(any())).thenReturn(false); + when(store.isParentStrong(any())).thenReturn(true); + + assertThat( + reorgLogicInstrumented.shouldOverrideFcuCheckWeights( + signedBlockAndState.getSignedBeaconBlock().orElseThrow(), + blockRoot, + UInt64.valueOf(2), + UInt64.valueOf(2))) + .isFalse(); + } + + @Test + void shouldOverrideFcuCheckWeights_weakParent() { + withLateBlock(blockRoot); + withParentSlot(Optional.of(UInt64.ZERO)); + withHeadBlock(); + when(store.isHeadWeak(any())).thenReturn(true); + when(store.isParentStrong(any())).thenReturn(false); + + assertThat( + reorgLogicInstrumented.shouldOverrideFcuCheckWeights( + signedBlockAndState.getSignedBeaconBlock().orElseThrow(), + blockRoot, + UInt64.valueOf(2), + UInt64.valueOf(2))) + .isFalse(); + } + + @Test + void isSingleSlotReorg_whenTrue() { + withTimelyBlock(blockRoot); + when(recentChainData.getSlotForBlockRoot(signedBlockAndState.getParentRoot())) + .thenReturn(Optional.of(UInt64.ZERO)); + assertThat( + reorgLogicInstrumented.isSingleSlotReorg( + signedBlockAndState.getBlock(), UInt64.valueOf(2))) + .isTrue(); + } + + @Test + void isSingleSlotReorg_whenFalse() { + withTimelyBlock(blockRoot); + when(recentChainData.getSlotForBlockRoot(signedBlockAndState.getParentRoot())) + .thenReturn(Optional.of(UInt64.ZERO)); + assertThat( + reorgLogicInstrumented.isSingleSlotReorg( + signedBlockAndState.getBlock(), UInt64.valueOf(3))) + .isFalse(); + } + + @Test + void isProposerBoostActive() { + when(store.getProposerBoostRoot()).thenReturn(Optional.of(blockRoot)); + assertThat(reorgLogicInstrumented.isProposerBoostActive(blockRoot)).isFalse(); + assertThat(reorgLogicInstrumented.isProposerBoostActive(dataStructureUtil.randomBytes32())) + .isTrue(); + } + + @ParameterizedTest + @MethodSource("stableForkChoiceTests") + void isForkChoiceStableAndFinalizationOk(final int slot, final boolean expectation) { + withTimelyBlock(blockRoot); + assertThat(reorgLogicInstrumented.isForkChoiceStableAndFinalizationOk(UInt64.valueOf(slot))) + .isEqualTo(expectation); + } + + @ParameterizedTest + @MethodSource("isMissingDataTests") + void isMissingData( + final Optional maybeBlock, + Optional maybeCurrentSlot, + final boolean expectedResult) { + assertThat(reorgLogicInstrumented.isMissingData(maybeBlock, maybeCurrentSlot)) + .isEqualTo(expectedResult); + } + + @Test + void getProposerHead_boostActiveShortCircuitsGetProposerHead() { + withLateBlock(blockRoot); + withHeadBlock(); + withProposerBoostRoot(dataStructureUtil.randomBytes32()); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.ONE)).isEqualTo(blockRoot); + } + + @Test + void getProposerHead_isHeadOnTimeShortCircuitsGetProposerHead() { + withTimelyBlock(blockRoot); + withHeadBlock(); + withProposerBoostRoot(null); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.ONE)).isEqualTo(blockRoot); + } + + @Test + void getProposerHead_shufflingNotStable() { + withLateBlock(blockRoot); + withHeadBlock(); + when(store.getProposerBoostRoot()).thenReturn(Optional.empty()); + when(store.getFinalizedCheckpoint()) + .thenReturn(dataStructureUtil.randomCheckpoint(UInt64.ZERO)); + + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.valueOf(8))) + .isEqualTo(blockRoot); + } + + @Test + void getProposerHead_withoutHeadBlockInStore() { + withLateBlock(blockRoot); + when(store.getProposerBoostRoot()).thenReturn(Optional.empty()); + when(store.getFinalizedCheckpoint()) + .thenReturn(dataStructureUtil.randomCheckpoint(UInt64.ZERO)); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.ONE)).isEqualTo(blockRoot); + } + + @Test + void getProposerHead_finalizationNotOk() { + withLateBlock(blockRoot); + withHeadBlock(); + when(store.getProposerBoostRoot()).thenReturn(Optional.empty()); + when(store.getFinalizedCheckpoint()) + .thenReturn(dataStructureUtil.randomCheckpoint(UInt64.ZERO)); + + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.valueOf(25))) + .isEqualTo(blockRoot); + } + + @Test + void getProposerHead_ffgNotCompetitive() { + getProposerHeadPassFirstGate(); + withFfgNotCompetetive(); + withParentSlot(Optional.of(UInt64.ZERO)); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.valueOf(2))) + .isEqualTo(blockRoot); + } + + @Test + void getProposerHead_notSingleSlotReorgBecauseParentSlot() { + getProposerHeadPassFirstGate(); + withFfgIsCompetetive(); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.valueOf(2))) + .isEqualTo(blockRoot); + } + + @Test + void getProposerHead_notSingleSlotReorgBecauseCurrentSlot() { + getProposerHeadPassFirstGate(); + withParentSlot(Optional.of(UInt64.ZERO)); + withFfgIsCompetetive(); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.valueOf(3))) + .isEqualTo(blockRoot); + } + + @Test + void getProposerHead_shouldGiveParentRoot() { + getProposerHeadPassSecondGate(); + when(store.isHeadWeak(any())).thenReturn(true); + when(store.isParentStrong(any())).thenReturn(true); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.valueOf(2))) + .isEqualTo(signedBlockAndState.getBlock().getParentRoot()); + } + + @Test + void getProposerHead_isHeadStrong() { + getProposerHeadPassSecondGate(); + when(store.isHeadWeak(any())).thenReturn(false); + when(store.isParentStrong(any())).thenReturn(true); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.valueOf(2))) + .isEqualTo(blockRoot); + } + + @Test + void getProposerHead_isParentWeak() { + getProposerHeadPassSecondGate(); + when(store.isHeadWeak(any())).thenReturn(true); + when(store.isParentStrong(any())).thenReturn(false); + assertThat(reorgLogicInstrumented.getProposerHead(blockRoot, UInt64.valueOf(2))) + .isEqualTo(blockRoot); + } + + @Test + void shouldOverrideForkChoice_headOnTime() { + withTimelyBlock(blockRoot); + withHeadBlock(); + assertThat(reorgLogicInstrumented.shouldOverrideForkChoiceUpdate(blockRoot)).isFalse(); + } + + @Test + void shouldOverrideForkChoice_headBlockMissing() { + withLateBlock(blockRoot); + withCurrentSlot(Optional.of(UInt64.ONE)); + assertThat(reorgLogicInstrumented.shouldOverrideForkChoiceUpdate(blockRoot)).isFalse(); + } + + @Test + void shouldOverrideForkChoice_ffgNotCompetetive() { + shouldOverrideForkChoicePassFirstGate(); + withParentSlot(Optional.of(UInt64.ZERO)); + withFfgNotCompetetive(); + assertThat(reorgLogicInstrumented.shouldOverrideForkChoiceUpdate(blockRoot)).isFalse(); + } + + @Test + void shouldOverrideForkChoice_parentSlotMissing() { + shouldOverrideForkChoicePassFirstGate(); + withFfgIsCompetetive(); + assertThat(reorgLogicInstrumented.shouldOverrideForkChoiceUpdate(blockRoot)).isFalse(); + } + + @Test + void shouldOverrideFcuCheckProposerPreState_preStateMissing() { + when(store.getBlockStateIfAvailable(any())).thenReturn(Optional.empty()); + assertThat( + reorgLogicInstrumented.shouldOverrideFcuCheckProposerPreState( + UInt64.valueOf(2), dataStructureUtil.randomBytes32())) + .isFalse(); + } + + @Test + void shouldOverrideFcuCheckProposerPreState_validatorNotConnected() { + + final Optional maybeParentState = Optional.of(signedBlockAndState.getState()); + when(store.getBlockStateIfAvailable(any())).thenReturn(maybeParentState); + when(recentChainData.validatorIsConnected(anyInt(), any())).thenReturn(false); + assertThat( + reorgLogicInstrumented.shouldOverrideFcuCheckProposerPreState( + UInt64.valueOf(2), dataStructureUtil.randomBytes32())) + .isFalse(); + } + + @Test + void shouldOverrideFcuCheckProposerPreState_validatorIsConnected() { + + final Optional maybeParentState = Optional.of(signedBlockAndState.getState()); + when(store.getBlockStateIfAvailable(any())).thenReturn(maybeParentState); + when(recentChainData.validatorIsConnected(anyInt(), any())).thenReturn(true); + assertThat( + reorgLogicInstrumented.shouldOverrideFcuCheckProposerPreState( + UInt64.valueOf(2), dataStructureUtil.randomBytes32())) + .isTrue(); + } + + private void shouldOverrideForkChoicePassFirstGate() { + withLateBlock(blockRoot); + withCurrentSlot(Optional.of(UInt64.ONE)); + withHeadBlock(); + } + + private void getProposerHeadPassFirstGate() { + withLateBlock(blockRoot); + withHeadBlock(); + when(store.getProposerBoostRoot()).thenReturn(Optional.empty()); + when(store.getFinalizedCheckpoint()) + .thenReturn(dataStructureUtil.randomCheckpoint(UInt64.ZERO)); + } + + private void getProposerHeadPassSecondGate() { + getProposerHeadPassFirstGate(); + withFfgIsCompetetive(); + withParentSlot(Optional.of(UInt64.ZERO)); + } + + private UInt64 computeTime(final UInt64 slot, final long timeIntoSlot) { + return timeProvider.getTimeInMillis().plus(slot.times(millisPerSlot)).plus(timeIntoSlot); + } + + private void withParentSlot(final Optional maybeSlot) { + when(recentChainData.getSlotForBlockRoot(any())).thenReturn(maybeSlot); + } + + private void withCurrentSlot(final Optional maybeSlot) { + when(recentChainData.getCurrentSlot()).thenReturn(maybeSlot); + } + + private void withHeadBlock() { + when(store.getBlockIfAvailable(any())).thenReturn(signedBlockAndState.getSignedBeaconBlock()); + } + + private void withFfgIsCompetetive() { + when(store.isFfgCompetitive(any(), any())).thenReturn(Optional.of(true)); + } + + private void withFfgNotCompetetive() { + when(store.isFfgCompetitive(any(), any())).thenReturn(Optional.of(false)); + } + + private void withTimelyBlock(final Bytes32 blockRoot) { + reorgLogicInstrumented.setBlockTimeliness(blockRoot, true); + } + + private void withLateBlock(final Bytes32 blockRoot) { + reorgLogicInstrumented.setBlockTimeliness(blockRoot, false); + } + + private void withProposerBoostRoot(final Bytes32 boostRoot) { + when(store.getProposerBoostRoot()).thenReturn(Optional.ofNullable(boostRoot)); + } + + public static Stream isMissingDataTests() { + final DataStructureUtil dataStructureUtil = + new DataStructureUtil(TestSpecFactory.createDefault()); + final SignedBeaconBlock block = dataStructureUtil.randomSignedBeaconBlock(); + final ArrayList args = new ArrayList<>(); + args.add(Arguments.of(Optional.of(block), Optional.of(UInt64.ZERO), false)); + args.add(Arguments.of(Optional.empty(), Optional.of(UInt64.ZERO), true)); + args.add(Arguments.of(Optional.of(block), Optional.empty(), true)); + args.add(Arguments.of(Optional.empty(), Optional.empty(), true)); + return args.stream(); + } + + public static Stream stableForkChoiceTests() { + final ArrayList args = new ArrayList<>(); + args.add(Arguments.of(0, false)); + args.add(Arguments.of(1, true)); + args.add(Arguments.of(7, true)); + args.add(Arguments.of(8, false)); + args.add(Arguments.of(23, true)); // last slot where it's ok based on finalization + args.add(Arguments.of(24, false)); // boundary of epoch + args.add(Arguments.of(25, false)); // because finalization no longer ok + return args.stream(); + } + + static class LateBlockReorgLogicInstrumented extends LateBlockReorgLogic { + public LateBlockReorgLogicInstrumented( + Spec spec, RecentChainData recentChainData, Supplier timeProviderSupplier) { + super(spec, recentChainData, timeProviderSupplier); + } + + @Override + protected int getProposerIndex(final BeaconState proposerPreState, final UInt64 proposalSlot) { + return 1; + } + + public void setBlockTimeliness(final Bytes32 root, final boolean isTimely) { + blockTimeliness.put(root, isTimely); + } + } +} diff --git a/storage/src/testFixtures/java/tech/pegasys/teku/storage/client/MemoryOnlyRecentChainData.java b/storage/src/testFixtures/java/tech/pegasys/teku/storage/client/MemoryOnlyRecentChainData.java index 9559266c151..0f429961b94 100644 --- a/storage/src/testFixtures/java/tech/pegasys/teku/storage/client/MemoryOnlyRecentChainData.java +++ b/storage/src/testFixtures/java/tech/pegasys/teku/storage/client/MemoryOnlyRecentChainData.java @@ -24,8 +24,6 @@ import tech.pegasys.teku.dataproviders.lookup.SingleBlockProvider; import tech.pegasys.teku.dataproviders.lookup.StateAndBlockSummaryProvider; import tech.pegasys.teku.infrastructure.async.AsyncRunner; -import tech.pegasys.teku.infrastructure.time.SystemTimeProvider; -import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.storage.api.ChainHeadChannel; @@ -43,7 +41,6 @@ private MemoryOnlyRecentChainData( final AsyncRunner asyncRunner, final MetricsSystem metricsSystem, final StoreConfig storeConfig, - final TimeProvider timeProvider, final StorageUpdateChannel storageUpdateChannel, final VoteUpdateChannel voteUpdateChannel, final FinalizedCheckpointChannel finalizedCheckpointChannel, @@ -54,7 +51,6 @@ private MemoryOnlyRecentChainData( asyncRunner, metricsSystem, storeConfig, - timeProvider, BlockProvider.NOOP, SingleBlockProvider.NOOP, SingleBlobSidecarProvider.NOOP, @@ -100,7 +96,6 @@ public RecentChainData build() { SYNC_RUNNER, new NoOpMetricsSystem(), storeConfig, - new SystemTimeProvider(), storageUpdateChannel, votes -> {}, finalizedCheckpointChannel,