diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/WithdrawalPrefixes.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/WithdrawalPrefixes.java index c4d385d5820..d2bd4b0cedd 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/WithdrawalPrefixes.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/WithdrawalPrefixes.java @@ -18,5 +18,7 @@ public class WithdrawalPrefixes { public static final Bytes BLS_WITHDRAWAL_PREFIX = Bytes.fromHexString("0x00"); public static final byte ETH1_ADDRESS_WITHDRAWAL_BYTE = 0x01; + public static final byte COMPOUNDING_WITHDRAWAL_BYTE = 0x02; public static final Bytes ETH1_ADDRESS_WITHDRAWAL_PREFIX = Bytes.of(ETH1_ADDRESS_WITHDRAWAL_BYTE); + public static final Bytes COMPOUNDING_WITHDRAWAL_PREFIX = Bytes.of(COMPOUNDING_WITHDRAWAL_BYTE); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/epoch/AbstractEpochProcessor.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/epoch/AbstractEpochProcessor.java index 962f366f043..7b39790eca5 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/epoch/AbstractEpochProcessor.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/epoch/AbstractEpochProcessor.java @@ -63,6 +63,7 @@ public abstract class AbstractEpochProcessor implements EpochProcessor { protected final BeaconStateMutators beaconStateMutators; private static final Logger LOG = LogManager.getLogger(); + protected final UInt64 maxEffectiveBalance; // Used to log once per epoch (throttlingPeriod = 1) private final Throttler loggerThrottler = new Throttler<>(LOG, UInt64.ONE); @@ -83,6 +84,7 @@ protected AbstractEpochProcessor( this.beaconStateUtil = beaconStateUtil; this.validatorStatusFactory = validatorStatusFactory; this.schemaDefinitions = schemaDefinitions; + this.maxEffectiveBalance = specConfig.getMaxEffectiveBalance(); } /** @@ -310,7 +312,6 @@ public void processRegistryUpdates( SszMutableList validators = state.getValidators(); final UInt64 currentEpoch = beaconStateAccessors.getCurrentEpoch(state); final UInt64 finalizedEpoch = state.getFinalizedCheckpoint().getEpoch(); - final UInt64 maxEffectiveBalance = specConfig.getMaxEffectiveBalance(); final UInt64 ejectionBalance = specConfig.getEjectionBalance(); final Supplier validatorExitContextSupplier = beaconStateMutators.createValidatorExitContextSupplier(state); @@ -318,12 +319,7 @@ public void processRegistryUpdates( for (int index = 0; index < validators.size(); index++) { final ValidatorStatus status = statuses.get(index); - // Slightly optimised form of isEligibleForActivationQueue to avoid accessing the - // state for the majority of validators. Can't be eligible for activation if already active - // or if effective balance is too low. Only get the validator if both those checks pass to - // confirm it isn't already in the queue. - if (!status.isActiveInCurrentEpoch() - && status.getCurrentEpochEffectiveBalance().equals(maxEffectiveBalance)) { + if (isEligibleForActivationQueue(status)) { final Validator validator = validators.get(index); if (validator.getActivationEligibilityEpoch().equals(SpecConfig.FAR_FUTURE_EPOCH)) { validators.set( @@ -381,6 +377,17 @@ public void processRegistryUpdates( } } + /** + * Can't be eligible for activation if already active or if effective balance is too low. + * + * @param status - Validator status + * @return true if validator is eligible to be added to the activation queue + */ + protected boolean isEligibleForActivationQueue(final ValidatorStatus status) { + return !status.isActiveInCurrentEpoch() + && status.getCurrentEpochEffectiveBalance().equals(maxEffectiveBalance); + } + /** Processes slashings */ @Override public void processSlashings( diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/SpecLogicElectra.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/SpecLogicElectra.java index bd8f8ee8e39..aa13b8cc856 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/SpecLogicElectra.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/SpecLogicElectra.java @@ -28,22 +28,22 @@ import tech.pegasys.teku.spec.logic.common.util.LightClientUtil; import tech.pegasys.teku.spec.logic.common.util.SyncCommitteeUtil; import tech.pegasys.teku.spec.logic.common.util.ValidatorsUtil; -import tech.pegasys.teku.spec.logic.versions.altair.helpers.BeaconStateAccessorsAltair; import tech.pegasys.teku.spec.logic.versions.altair.statetransition.epoch.ValidatorStatusFactoryAltair; import tech.pegasys.teku.spec.logic.versions.bellatrix.helpers.BeaconStateMutatorsBellatrix; import tech.pegasys.teku.spec.logic.versions.bellatrix.helpers.BellatrixTransitionHelpers; import tech.pegasys.teku.spec.logic.versions.bellatrix.util.BlindBlockUtilBellatrix; import tech.pegasys.teku.spec.logic.versions.capella.block.BlockProcessorCapella; import tech.pegasys.teku.spec.logic.versions.capella.operations.validation.OperationValidatorCapella; -import tech.pegasys.teku.spec.logic.versions.capella.statetransition.epoch.EpochProcessorCapella; -import tech.pegasys.teku.spec.logic.versions.deneb.helpers.BeaconStateAccessorsDeneb; import tech.pegasys.teku.spec.logic.versions.deneb.helpers.MiscHelpersDeneb; import tech.pegasys.teku.spec.logic.versions.deneb.operations.validation.AttestationDataValidatorDeneb; import tech.pegasys.teku.spec.logic.versions.deneb.util.AttestationUtilDeneb; import tech.pegasys.teku.spec.logic.versions.deneb.util.ForkChoiceUtilDeneb; import tech.pegasys.teku.spec.logic.versions.electra.block.BlockProcessorElectra; import tech.pegasys.teku.spec.logic.versions.electra.forktransition.ElectraStateUpgrade; +import tech.pegasys.teku.spec.logic.versions.electra.helpers.BeaconStateAccessorsElectra; import tech.pegasys.teku.spec.logic.versions.electra.helpers.MiscHelpersElectra; +import tech.pegasys.teku.spec.logic.versions.electra.helpers.PredicatesElectra; +import tech.pegasys.teku.spec.logic.versions.electra.statetransition.epoch.EpochProcessorElectra; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsElectra; public class SpecLogicElectra extends AbstractSpecLogic { @@ -53,7 +53,7 @@ public class SpecLogicElectra extends AbstractSpecLogic { private SpecLogicElectra( final Predicates predicates, final MiscHelpersDeneb miscHelpers, - final BeaconStateAccessorsAltair beaconStateAccessors, + final BeaconStateAccessorsElectra beaconStateAccessors, final BeaconStateMutatorsBellatrix beaconStateMutators, final OperationSignatureVerifier operationSignatureVerifier, final ValidatorsUtil validatorsUtil, @@ -61,7 +61,7 @@ private SpecLogicElectra( final AttestationUtil attestationUtil, final OperationValidator operationValidator, final ValidatorStatusFactoryAltair validatorStatusFactory, - final EpochProcessorCapella epochProcessor, + final EpochProcessorElectra epochProcessor, final BlockProcessorCapella blockProcessor, final ForkChoiceUtil forkChoiceUtil, final BlockProposalUtil blockProposalUtil, @@ -93,11 +93,11 @@ private SpecLogicElectra( public static SpecLogicElectra create( final SpecConfigElectra config, final SchemaDefinitionsElectra schemaDefinitions) { // Helpers - final Predicates predicates = new Predicates(config); + final PredicatesElectra predicates = new PredicatesElectra(config); final MiscHelpersElectra miscHelpers = new MiscHelpersElectra(config, predicates, schemaDefinitions); - final BeaconStateAccessorsDeneb beaconStateAccessors = - new BeaconStateAccessorsDeneb(config, predicates, miscHelpers); + final BeaconStateAccessorsElectra beaconStateAccessors = + new BeaconStateAccessorsElectra(config, predicates, miscHelpers); final BeaconStateMutatorsBellatrix beaconStateMutators = new BeaconStateMutatorsBellatrix(config, miscHelpers, beaconStateAccessors); @@ -126,8 +126,8 @@ public static SpecLogicElectra create( predicates, miscHelpers, beaconStateAccessors); - final EpochProcessorCapella epochProcessor = - new EpochProcessorCapella( + final EpochProcessorElectra epochProcessor = + new EpochProcessorElectra( config, miscHelpers, beaconStateAccessors, diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/BeaconStateAccessorsElectra.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/BeaconStateAccessorsElectra.java new file mode 100644 index 00000000000..39ca126d53a --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/BeaconStateAccessorsElectra.java @@ -0,0 +1,52 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.spec.logic.versions.electra.helpers; + +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.config.SpecConfigDeneb; +import tech.pegasys.teku.spec.datastructures.state.Validator; +import tech.pegasys.teku.spec.logic.versions.deneb.helpers.BeaconStateAccessorsDeneb; +import tech.pegasys.teku.spec.logic.versions.deneb.helpers.MiscHelpersDeneb; + +public class BeaconStateAccessorsElectra extends BeaconStateAccessorsDeneb { + + private final UInt64 maxEffectiveBalanceElectra; + private final UInt64 minActivationBalance; + + protected PredicatesElectra predicatesElectra; + + public BeaconStateAccessorsElectra( + final SpecConfigDeneb config, + final PredicatesElectra predicatesElectra, + final MiscHelpersDeneb miscHelpers) { + super(config, predicatesElectra, miscHelpers); + this.maxEffectiveBalanceElectra = + config.toVersionElectra().orElseThrow().getMaxEffectiveBalanceElectra(); + this.minActivationBalance = config.toVersionElectra().orElseThrow().getMinActivationBalance(); + this.predicatesElectra = predicatesElectra; + } + + /** + * implements get_validator_max_effective_balance state accessor + * + * @param validator - a validator from a state. + * @return the max effective balance for the specified validator based on its withdrawal + * credentials. + */ + public UInt64 getValidatorMaxEffectiveBalance(final Validator validator) { + return predicatesElectra.hasCompoundingWithdrawalCredential(validator) + ? maxEffectiveBalanceElectra + : minActivationBalance; + } +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/PredicatesElectra.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/PredicatesElectra.java new file mode 100644 index 00000000000..404faa9551b --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/PredicatesElectra.java @@ -0,0 +1,62 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.spec.logic.versions.electra.helpers; + +import static tech.pegasys.teku.spec.constants.WithdrawalPrefixes.COMPOUNDING_WITHDRAWAL_BYTE; + +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.config.SpecConfigElectra; +import tech.pegasys.teku.spec.datastructures.state.Validator; +import tech.pegasys.teku.spec.logic.common.helpers.Predicates; + +public class PredicatesElectra extends Predicates { + private final SpecConfigElectra specConfigElectra; + + public PredicatesElectra(SpecConfig specConfig) { + super(specConfig); + this.specConfigElectra = SpecConfigElectra.required(specConfig); + } + + @Override + public boolean isPartiallyWithdrawableValidator(final Validator validator, final UInt64 balance) { + if (hasEth1WithdrawalCredential(validator)) { + return isPartiallyWithdrawableValidatorEth1CredentialsChecked(validator, balance); + } + if (hasCompoundingWithdrawalCredential(validator)) { + return isPartiallyWithdrawableValidatorCompoundingCredentialChecked(validator, balance); + } + + return false; + } + + private boolean isPartiallyWithdrawableValidatorCompoundingCredentialChecked( + final Validator validator, final UInt64 balance) { + final UInt64 maxEffectiveBalance = specConfigElectra.getMaxEffectiveBalanceElectra(); + final boolean hasMaxEffectiveBalance = + validator.getEffectiveBalance().equals(maxEffectiveBalance); + final boolean hasExcessBalance = balance.isGreaterThan(maxEffectiveBalance); + + return hasMaxEffectiveBalance && hasExcessBalance; + } + + protected boolean hasCompoundingWithdrawalCredential(final Validator validator) { + return isCompoundingWithdrawalCredential(validator.getWithdrawalCredentials()); + } + + protected boolean isCompoundingWithdrawalCredential(final Bytes32 withdrawalCredentials) { + return withdrawalCredentials.get(0) == COMPOUNDING_WITHDRAWAL_BYTE; + } +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/statetransition/epoch/EpochProcessorElectra.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/statetransition/epoch/EpochProcessorElectra.java new file mode 100644 index 00000000000..12d70136818 --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/statetransition/epoch/EpochProcessorElectra.java @@ -0,0 +1,59 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.spec.logic.versions.electra.statetransition.epoch; + +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.config.SpecConfigBellatrix; +import tech.pegasys.teku.spec.logic.common.helpers.BeaconStateMutators; +import tech.pegasys.teku.spec.logic.common.statetransition.epoch.status.ValidatorStatus; +import tech.pegasys.teku.spec.logic.common.statetransition.epoch.status.ValidatorStatusFactory; +import tech.pegasys.teku.spec.logic.common.util.BeaconStateUtil; +import tech.pegasys.teku.spec.logic.common.util.ValidatorsUtil; +import tech.pegasys.teku.spec.logic.versions.altair.helpers.BeaconStateAccessorsAltair; +import tech.pegasys.teku.spec.logic.versions.altair.helpers.MiscHelpersAltair; +import tech.pegasys.teku.spec.logic.versions.bellatrix.statetransition.epoch.EpochProcessorBellatrix; +import tech.pegasys.teku.spec.schemas.SchemaDefinitions; + +public class EpochProcessorElectra extends EpochProcessorBellatrix { + + private final UInt64 minActivationBalance; + + public EpochProcessorElectra( + final SpecConfigBellatrix specConfig, + final MiscHelpersAltair miscHelpers, + final BeaconStateAccessorsAltair beaconStateAccessors, + final BeaconStateMutators beaconStateMutators, + final ValidatorsUtil validatorsUtil, + final BeaconStateUtil beaconStateUtil, + final ValidatorStatusFactory validatorStatusFactory, + final SchemaDefinitions schemaDefinitions) { + super( + specConfig, + miscHelpers, + beaconStateAccessors, + beaconStateMutators, + validatorsUtil, + beaconStateUtil, + validatorStatusFactory, + schemaDefinitions); + this.minActivationBalance = + specConfig.toVersionElectra().orElseThrow().getMinActivationBalance(); + } + + @Override + protected boolean isEligibleForActivationQueue(final ValidatorStatus status) { + return !status.isActiveInCurrentEpoch() + && status.getCurrentEpochEffectiveBalance().isGreaterThanOrEqualTo(minActivationBalance); + } +} diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/PredicatesElectraTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/PredicatesElectraTest.java new file mode 100644 index 00000000000..26f8f2ba1b4 --- /dev/null +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/PredicatesElectraTest.java @@ -0,0 +1,85 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.spec.logic.versions.electra.helpers; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +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.state.Validator; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class PredicatesElectraTest { + private final Spec spec = TestSpecFactory.createMinimalElectra(); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private final PredicatesElectra predicates = new PredicatesElectra(spec.getGenesisSpecConfig()); + + private final UInt64 excessLargeValidatorBalance = UInt64.valueOf(2050_000_000_000L); + + private final UInt64 maxEffectiveBalanceNonCompounding = UInt64.THIRTY_TWO_ETH; + + private final UInt64 maxEffectiveBalanceCompounding = UInt64.valueOf(2048_000_000_000L); + + @Test + void isPartiallyWithdrawableValidator_shouldNotDetermineBlsWithdrawalAsNotWithdrawable() { + final Validator validator = + dataStructureUtil + .validatorBuilder() + .withdrawalCredentials(dataStructureUtil.randomBlsWithdrawalCredentials()) + .effectiveBalance(maxEffectiveBalanceNonCompounding) + .build(); + assertThat(predicates.isPartiallyWithdrawableValidator(validator, excessLargeValidatorBalance)) + .isFalse(); + } + + @Test + void isPartiallyWithdrawableValidator_shouldDetermineEth1WithdrawalAsWithdrawable() { + final Validator validator = + dataStructureUtil + .validatorBuilder() + .withdrawalCredentials(dataStructureUtil.randomEth1WithdrawalCredentials()) + .effectiveBalance(maxEffectiveBalanceNonCompounding) + .build(); + assertThat(predicates.isPartiallyWithdrawableValidator(validator, excessLargeValidatorBalance)) + .isTrue(); + } + + @Test + void isPartiallyWithdrawableValidator_shouldDetermineCompoundingWithdrawalAsWithdrawable() { + final Validator validator = + dataStructureUtil + .validatorBuilder() + .withdrawalCredentials(dataStructureUtil.randomCompoundingWithdrawalCredentials()) + .effectiveBalance(maxEffectiveBalanceCompounding) + .build(); + assertThat(predicates.isPartiallyWithdrawableValidator(validator, excessLargeValidatorBalance)) + .isTrue(); + } + + @Test + void isPartiallyWithdrawableValidator_shouldDetermineCompoundingWithdrawalAsAsNotWithdrawable() { + final Validator validator = + dataStructureUtil + .validatorBuilder() + .withdrawalCredentials(dataStructureUtil.randomCompoundingWithdrawalCredentials()) + .effectiveBalance(maxEffectiveBalanceNonCompounding) + .build(); + assertThat( + predicates.isPartiallyWithdrawableValidator( + validator, maxEffectiveBalanceNonCompounding)) + .isFalse(); + } +} diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java index 651a0ef29c5..014c82b9746 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java @@ -2138,6 +2138,13 @@ public Bytes32 randomEth1WithdrawalCredentials() { randomEth1Address().getWrappedBytes())); } + public Bytes32 randomCompoundingWithdrawalCredentials() { + return Bytes32.wrap( + Bytes.concatenate( + Bytes.fromHexString("0x020000000000000000000000"), + randomEth1Address().getWrappedBytes())); + } + public List randomVersionedHashes(final int count) { return IntStream.range(0, count) .mapToObj(__ -> new VersionedHash(randomBytes32())) diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/BeaconNodeReadinessManager.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/BeaconNodeReadinessManager.java index f4822ea111f..acd92c126a2 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/BeaconNodeReadinessManager.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/BeaconNodeReadinessManager.java @@ -77,6 +77,10 @@ public Iterator getFailoversInOrderOfReadin .iterator(); } + public SafeFuture performPrimaryReadinessCheck() { + return performReadinessCheck(primaryBeaconNodeApi, true); + } + @Override protected SafeFuture doStart() { return performReadinessCheckAgainstAllNodes(); @@ -136,10 +140,6 @@ private SafeFuture performReadinessCheckAgainstAllNodes() { return SafeFuture.allOf(primaryReadinessCheck, SafeFuture.allOf(failoverReadinessChecks)); } - private SafeFuture performPrimaryReadinessCheck() { - return performReadinessCheck(primaryBeaconNodeApi, true); - } - private SafeFuture performFailoverReadinessCheck(final RemoteValidatorApiChannel failover) { return performReadinessCheck(failover, false); } diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapter.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapter.java index dd3abeddd3c..d4c781b5dc6 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapter.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapter.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import okhttp3.HttpUrl; @@ -54,7 +55,7 @@ public class EventSourceBeaconChainEventAdapter private final CountDownLatch runningLatch = new CountDownLatch(1); private volatile BackgroundEventSource eventSource; - private volatile RemoteValidatorApiChannel currentBeaconNodeUsedForEventStreaming; + @VisibleForTesting volatile RemoteValidatorApiChannel currentBeaconNodeUsedForEventStreaming; private final BeaconNodeReadinessManager beaconNodeReadinessManager; private final RemoteValidatorApiChannel primaryBeaconNodeApi; @@ -120,9 +121,14 @@ public void onPrimaryNodeNotReady() { } @Override + @SuppressWarnings("FutureReturnValueIgnored") public void onFailoverNodeNotReady(final RemoteValidatorApiChannel failoverNotInSync) { if (currentEventStreamHasSameEndpoint(failoverNotInSync)) { - switchToFailoverEventStreamIfAvailable(); + if (failoverBeaconNodeApis.size() == 1 || !switchToFailoverEventStreamIfAvailable()) { + // No failover switching is available, and we are currently connected to a failover node + // with issues, so trigger the readiness check against the primary BN immediately + beaconNodeReadinessManager.performPrimaryReadinessCheck(); + } } } @@ -170,26 +176,28 @@ private HttpUrl createEventStreamSourceUrl( } // synchronized because of the ConnectionErrorHandler and the BeaconNodeReadinessChannel callbacks - private synchronized void switchToFailoverEventStreamIfAvailable() { + private synchronized boolean switchToFailoverEventStreamIfAvailable() { if (failoverBeaconNodeApis.isEmpty()) { - return; + return false; } - findReadyFailoverAndSwitch(); + return findReadyFailoverAndSwitch(); } - private void findReadyFailoverAndSwitch() { - failoverBeaconNodeApis.stream() - .filter(beaconNodeReadinessManager::isReady) - .findFirst() - .ifPresentOrElse( - this::switchToFailoverEventStream, - validatorLogger::noFailoverBeaconNodesAvailableForEventStreaming); + private boolean findReadyFailoverAndSwitch() { + final Optional readyFailover = + failoverBeaconNodeApis.stream() + .filter(beaconNodeReadinessManager::isReady) + .filter(failover -> !currentEventStreamHasSameEndpoint(failover)) + .findFirst(); + if (readyFailover.isPresent()) { + switchToFailoverEventStream(readyFailover.get()); + return true; + } + validatorLogger.noFailoverBeaconNodesAvailableForEventStreaming(); + return false; } private void switchToFailoverEventStream(final RemoteValidatorApiChannel beaconNodeApi) { - if (currentEventStreamHasSameEndpoint(beaconNodeApi)) { - return; - } eventSource.close(); eventSource = createEventSource(beaconNodeApi); currentBeaconNodeUsedForEventStreaming = beaconNodeApi; diff --git a/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapterTest.java b/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapterTest.java index 8702ba74a03..912b6c32916 100644 --- a/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapterTest.java +++ b/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapterTest.java @@ -21,6 +21,7 @@ import java.net.URI; import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -28,6 +29,7 @@ import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import tech.pegasys.teku.api.response.v1.EventType; @@ -69,6 +71,32 @@ public void shouldSubscribeToSlashingEvents(final boolean shutdownWhenValidatorS verifyEventSourceSubscriptionUrl(httpUrlMock, shutdownWhenValidatorSlashedEnabled); } + @Test + public void performsPrimaryReadinessCheckWhenFailoverNotReadyAndNoOtherFailoversAvailable() { + final BeaconNodeReadinessManager beaconNodeReadinessManager = + mock(BeaconNodeReadinessManager.class); + final RemoteValidatorApiChannel failover = mock(RemoteValidatorApiChannel.class); + final EventSourceBeaconChainEventAdapter eventSourceBeaconChainEventAdapter = + new EventSourceBeaconChainEventAdapter( + beaconNodeReadinessManager, + mock(RemoteValidatorApiChannel.class), + List.of(failover), + mock(OkHttpClient.class), + mock(ValidatorLogger.class), + mock(BeaconChainEventAdapter.class), + mock(ValidatorTimingChannel.class), + metricsSystemMock, + true, + false, + mock(Spec.class)); + + eventSourceBeaconChainEventAdapter.currentBeaconNodeUsedForEventStreaming = failover; + + eventSourceBeaconChainEventAdapter.onFailoverNodeNotReady(failover); + + verify(beaconNodeReadinessManager).performPrimaryReadinessCheck(); + } + private EventSourceBeaconChainEventAdapter initEventSourceBeaconChainEventAdapter( final boolean shutdownWhenValidatorSlashedEnabled) { return new EventSourceBeaconChainEventAdapter(