From b7fdfa810e90ffcac7cea50620f94779ad7aa7fc Mon Sep 17 00:00:00 2001 From: Enrico Del Fante Date: Tue, 25 Jan 2022 16:25:23 +0100 Subject: [PATCH] big step --- ...lizer.java => Bytes48KeyDeserializer.java} | 6 +- .../pegasys/teku/provider/JsonProvider.java | 4 +- .../cli/options/ValidatorProposerOptions.java | 14 +-- .../teku/validator/api/ValidatorConfig.java | 25 ++++-- .../client/BeaconProposerPreparer.java | 84 +++++++++++++----- .../teku/validator/client/ProposerConfig.java | 25 +++++- .../client/ProposerConfigService.java | 63 -------------- .../client/ValidatorClientService.java | 11 ++- .../client/ValidatorIndexProvider.java | 5 ++ .../AbstractProposerConfigProvider.java | 80 +++++++++++++++++ .../FileProposerConfigProvider.java | 33 +++++++ .../ProposerConfigProvider.java | 43 +++++++++ .../UrlProposerConfigProvider.java | 33 +++++++ .../loader/ProposerConfigLoader.java | 33 ++++--- .../client/BeaconProposerPreparerTest.java | 87 ++++++++++++++----- .../loader/ProposerConfigLoaderTest.java | 1 + 16 files changed, 402 insertions(+), 145 deletions(-) rename data/serializer/src/main/java/tech/pegasys/teku/provider/{BLSPubKeyKeyDeserializer.java => Bytes48KeyDeserializer.java} (84%) delete mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfigService.java create mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/AbstractProposerConfigProvider.java create mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/FileProposerConfigProvider.java create mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProvider.java create mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/UrlProposerConfigProvider.java rename validator/client/src/main/java/tech/pegasys/teku/validator/client/{ => proposerconfig}/loader/ProposerConfigLoader.java (65%) diff --git a/data/serializer/src/main/java/tech/pegasys/teku/provider/BLSPubKeyKeyDeserializer.java b/data/serializer/src/main/java/tech/pegasys/teku/provider/Bytes48KeyDeserializer.java similarity index 84% rename from data/serializer/src/main/java/tech/pegasys/teku/provider/BLSPubKeyKeyDeserializer.java rename to data/serializer/src/main/java/tech/pegasys/teku/provider/Bytes48KeyDeserializer.java index 673d5d12482..0273f1623e6 100644 --- a/data/serializer/src/main/java/tech/pegasys/teku/provider/BLSPubKeyKeyDeserializer.java +++ b/data/serializer/src/main/java/tech/pegasys/teku/provider/Bytes48KeyDeserializer.java @@ -15,12 +15,12 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.KeyDeserializer; -import tech.pegasys.teku.api.schema.BLSPubKey; +import org.apache.tuweni.bytes.Bytes48; -public class BLSPubKeyKeyDeserializer extends KeyDeserializer { +public class Bytes48KeyDeserializer extends KeyDeserializer { @Override public Object deserializeKey(String key, DeserializationContext ctxt) { - return BLSPubKey.fromHexString(key); + return Bytes48.fromHexString(key); } } diff --git a/data/serializer/src/main/java/tech/pegasys/teku/provider/JsonProvider.java b/data/serializer/src/main/java/tech/pegasys/teku/provider/JsonProvider.java index 8d2b0cb61d9..7d527a85b3d 100644 --- a/data/serializer/src/main/java/tech/pegasys/teku/provider/JsonProvider.java +++ b/data/serializer/src/main/java/tech/pegasys/teku/provider/JsonProvider.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.Bytes48; import org.apache.tuweni.units.bigints.UInt256; import tech.pegasys.teku.api.response.v1.validator.GetNewBlockResponse; import tech.pegasys.teku.api.response.v2.debug.GetStateResponseV2; @@ -44,7 +45,8 @@ private void addTekuMappers() { module.addDeserializer(BLSPubKey.class, new BLSPubKeyDeserializer()); module.addDeserializer(BLSSignature.class, new BLSSignatureDeserializer()); module.addSerializer(BLSSignature.class, new BLSSignatureSerializer()); - module.addKeyDeserializer(BLSPubKey.class, new BLSPubKeyKeyDeserializer()); + + module.addKeyDeserializer(Bytes48.class, new Bytes48KeyDeserializer()); module.addDeserializer(Bytes32.class, new Bytes32Deserializer()); module.addDeserializer(Bytes4.class, new Bytes4Deserializer()); diff --git a/teku/src/main/java/tech/pegasys/teku/cli/options/ValidatorProposerOptions.java b/teku/src/main/java/tech/pegasys/teku/cli/options/ValidatorProposerOptions.java index 00d55dfeee2..9b7983f2026 100644 --- a/teku/src/main/java/tech/pegasys/teku/cli/options/ValidatorProposerOptions.java +++ b/teku/src/main/java/tech/pegasys/teku/cli/options/ValidatorProposerOptions.java @@ -36,15 +36,15 @@ public class ValidatorProposerOptions { private String proposerConfig = null; @Option( - names = {"--Xvalidators-proposer-config-refresh-rate"}, - paramLabel = "", + names = {"--Xvalidators-proposer-config-refresh-enabled"}, + paramLabel = "", description = - "Sets the frequency, in seconds, at which the proposer configuration is reloaded. " - + "0 means never refresh.", + "Enable the proposer configuration reload on every proposer preparation (once per epoch)", arity = "0..1", + fallbackValue = "true", hidden = true) - private long proposerConfigRefreshRate = - ValidatorConfig.DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_RATE.toSeconds(); + private boolean proposerConfigRefreshEnabled = + ValidatorConfig.DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_ENABLED; public void configure(TekuConfiguration.Builder builder) { builder.validator( @@ -52,6 +52,6 @@ public void configure(TekuConfiguration.Builder builder) { config .proposerDefaultFeeRecipient(proposerDefaultFeeRecipient) .proposerConfigSource(proposerConfig) - .proposerConfigSourceRefreshRate(proposerConfigRefreshRate)); + .refreshProposerConfigFromSource(proposerConfigRefreshEnabled)); } } diff --git a/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorConfig.java b/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorConfig.java index a107b3071ab..bf08578a4ec 100644 --- a/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorConfig.java +++ b/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorConfig.java @@ -40,8 +40,7 @@ public class ValidatorConfig { public static final boolean DEFAULT_GENERATE_EARLY_ATTESTATIONS = true; public static final boolean DEFAULT_SEND_ATTESTATIONS_AS_BATCH = true; public static final Optional DEFAULT_GRAFFITI = Optional.empty(); - public static final Duration DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_RATE = - Duration.ofSeconds(60); + public static final boolean DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_ENABLED = false; private final List validatorKeys; private final List validatorExternalSignerPublicKeySources; @@ -61,7 +60,7 @@ public class ValidatorConfig { private final boolean generateEarlyAttestations; private final Optional proposerDefaultFeeRecipient; private final String proposerConfigSource; - private final long proposerConfigSourceRefreshRate; + private final boolean refreshProposerConfigFromSource; private ValidatorConfig( final List validatorKeys, @@ -82,7 +81,7 @@ private ValidatorConfig( final boolean generateEarlyAttestations, final Optional proposerDefaultFeeRecipient, final String proposerConfigSource, - final long proposerConfigSourceRefreshRate) { + final boolean refreshProposerConfigFromSource) { this.validatorKeys = validatorKeys; this.validatorExternalSignerPublicKeySources = validatorExternalSignerPublicKeySources; this.validatorExternalSignerUrl = validatorExternalSignerUrl; @@ -104,7 +103,7 @@ private ValidatorConfig( this.generateEarlyAttestations = generateEarlyAttestations; this.proposerDefaultFeeRecipient = proposerDefaultFeeRecipient; this.proposerConfigSource = proposerConfigSource; - this.proposerConfigSourceRefreshRate = proposerConfigSourceRefreshRate; + this.refreshProposerConfigFromSource = refreshProposerConfigFromSource; } public static Builder builder() { @@ -173,6 +172,14 @@ public Optional getProposerDefaultFeeRecipient() { return proposerDefaultFeeRecipient; } + public String getProposerConfigSource() { + return proposerConfigSource; + } + + public boolean getRefreshProposerConfigFromSource() { + return refreshProposerConfigFromSource; + } + private void validateProposerDefaultFeeRecipient() { if (proposerDefaultFeeRecipient.isEmpty() && !(validatorKeys.isEmpty() && validatorExternalSignerPublicKeySources.isEmpty())) { @@ -204,7 +211,7 @@ public static final class Builder { private boolean generateEarlyAttestations = DEFAULT_GENERATE_EARLY_ATTESTATIONS; private Optional proposerDefaultFeeRecipient = Optional.empty(); private String proposerConfigSource; - private long proposerConfigSourceRefreshRate; + private boolean refreshProposerConfigFromSource; private Builder() {} @@ -319,8 +326,8 @@ public Builder proposerConfigSource(final String proposerConfigSource) { return this; } - public Builder proposerConfigSourceRefreshRate(final long proposerConfigSourceRefreshRate) { - this.proposerConfigSourceRefreshRate = proposerConfigSourceRefreshRate; + public Builder refreshProposerConfigFromSource(final boolean refreshProposerConfigFromSource) { + this.refreshProposerConfigFromSource = refreshProposerConfigFromSource; return this; } @@ -348,7 +355,7 @@ public ValidatorConfig build() { generateEarlyAttestations, proposerDefaultFeeRecipient, proposerConfigSource, - proposerConfigSourceRefreshRate); + refreshProposerConfigFromSource); } private void validateExternalSignerUrlAndPublicKeys() { diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/BeaconProposerPreparer.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/BeaconProposerPreparer.java index da4b623842a..6290fe2ca5b 100644 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/BeaconProposerPreparer.java +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/BeaconProposerPreparer.java @@ -15,9 +15,15 @@ import static tech.pegasys.teku.infrastructure.logging.ValidatorLogger.VALIDATOR_LOGGER; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException; import tech.pegasys.teku.infrastructure.ssz.type.Bytes20; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -25,26 +31,29 @@ import tech.pegasys.teku.spec.datastructures.operations.versions.bellatrix.BeaconPreparableProposer; import tech.pegasys.teku.validator.api.ValidatorApiChannel; import tech.pegasys.teku.validator.api.ValidatorTimingChannel; -import tech.pegasys.teku.validator.client.loader.OwnedValidators; +import tech.pegasys.teku.validator.client.ProposerConfig.Config; +import tech.pegasys.teku.validator.client.proposerconfig.ProposerConfigProvider; public class BeaconProposerPreparer implements ValidatorTimingChannel { + private static final Logger LOG = LogManager.getLogger(); + private final ValidatorApiChannel validatorApiChannel; private final ValidatorIndexProvider validatorIndexProvider; - private final OwnedValidators validators; + private final ProposerConfigProvider proposerConfigProvider; private final Spec spec; - private final Optional feeRecipient; + private final Optional defaultFeeRecipient; private boolean firstCallDone = false; public BeaconProposerPreparer( ValidatorApiChannel validatorApiChannel, ValidatorIndexProvider validatorIndexProvider, - Optional feeRecipient, - OwnedValidators validators, + ProposerConfigProvider proposerConfigProvider, + Optional defaultFeeRecipient, Spec spec) { this.validatorApiChannel = validatorApiChannel; this.validatorIndexProvider = validatorIndexProvider; - this.validators = validators; - this.feeRecipient = feeRecipient; + this.proposerConfigProvider = proposerConfigProvider; + this.defaultFeeRecipient = defaultFeeRecipient; this.spec = spec; } @@ -52,26 +61,61 @@ public BeaconProposerPreparer( public void onSlot(UInt64 slot) { if (slot.mod(spec.getSlotsPerEpoch(slot)).isZero() || !firstCallDone) { firstCallDone = true; + + SafeFuture> proposerConfigFuture = + proposerConfigProvider.getProposerConfig(); + validatorIndexProvider - .getValidatorIndices(validators.getPublicKeys()) + .getValidatorIndexesByPublicKey() .thenApply( - integers -> - integers.stream() - .map( - index -> - new BeaconPreparableProposer( - UInt64.valueOf(index), getFeeRecipient())) - .collect(Collectors.toList())) + blsPublicKeyIntegerMap -> + proposerConfigFuture + .thenApply( + proposerConfig -> + buildBeaconPreparableProposerList( + proposerConfig, blsPublicKeyIntegerMap)) + .exceptionally( + throwable -> { + LOG.warn( + "An error occurred while obtaining proposer config", throwable); + return buildBeaconPreparableProposerList( + Optional.empty(), blsPublicKeyIntegerMap); + }) + .join()) .thenAccept(validatorApiChannel::prepareBeaconProposer) .finish(VALIDATOR_LOGGER::beaconProposerPreparationFailed); } } - private Bytes20 getFeeRecipient() { - return feeRecipient.orElseThrow( - () -> - new InvalidConfigurationException( - "Invalid configuration. --Xvalidators-proposer-default-fee-recipient must be specified when Bellatrix milestone is active")); + private List buildBeaconPreparableProposerList( + Optional maybeProposerConfig, + Map blsPublicKeyIntegerMap) { + return blsPublicKeyIntegerMap.entrySet().stream() + .map( + blsPublicKeyIntegerEntry -> + new BeaconPreparableProposer( + UInt64.valueOf(blsPublicKeyIntegerEntry.getValue()), + getFeeRecipient(maybeProposerConfig, blsPublicKeyIntegerEntry.getKey()))) + .collect(Collectors.toList()); + } + + private Bytes20 getFeeRecipient( + Optional maybeProposerConfig, BLSPublicKey blsPublicKey) { + return maybeProposerConfig + .flatMap(proposerConfig -> proposerConfig.getConfigForPubKey(blsPublicKey)) + .map(Config::getFeeRecipient) + .orElseGet(() -> getDefaultFeeRecipient(maybeProposerConfig)); + } + + private Bytes20 getDefaultFeeRecipient(Optional maybeProposerConfig) { + return maybeProposerConfig + .flatMap(proposerConfig -> proposerConfig.getDefaultConfig()) + .map(Config::getFeeRecipient) + .or(() -> defaultFeeRecipient) + .orElseThrow( + () -> + new InvalidConfigurationException( + "Invalid configuration. --Xvalidators-proposer-default-fee-recipient must be specified when Bellatrix milestone is active")); } @Override diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfig.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfig.java index 7a02cf4521e..8d3de2674b2 100644 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfig.java +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfig.java @@ -13,25 +13,37 @@ package tech.pegasys.teku.validator.client; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Map; import java.util.Optional; -import tech.pegasys.teku.api.schema.BLSPubKey; +import org.apache.tuweni.bytes.Bytes48; +import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.infrastructure.ssz.type.Bytes20; public class ProposerConfig { @JsonProperty(value = "proposer_config", required = true) - private Map proposerConfig; + private Map proposerConfig; @JsonProperty(value = "default_config", required = true) private Config defaultConfig; + @JsonCreator + ProposerConfig(@JsonProperty(value = "proposer_config", required = true) final Map proposerConfig, @JsonProperty(value = "default_config", required = true) final Config defaultConfig) { + this.proposerConfig = proposerConfig; + this.defaultConfig = defaultConfig; + } + public Optional getConfigForPubKey(final String pubKey) { - return getConfigForPubKey(BLSPubKey.fromHexString(pubKey)); + return getConfigForPubKey(Bytes48.fromHexString(pubKey)); + } + + public Optional getConfigForPubKey(final BLSPublicKey pubKey) { + return getConfigForPubKey(pubKey.toBytesCompressed()); } - public Optional getConfigForPubKey(final BLSPubKey pubKey) { + public Optional getConfigForPubKey(final Bytes48 pubKey) { return Optional.ofNullable(proposerConfig.get(pubKey)); } @@ -44,6 +56,11 @@ public static class Config { @JsonProperty(value = "fee_recipient", required = true) private Bytes20 feeRecipient; + @JsonCreator + Config(@JsonProperty(value = "fee_recipient", required = true) final Bytes20 feeRecipient) { + this.feeRecipient = feeRecipient; + } + public Bytes20 getFeeRecipient() { return feeRecipient; } diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfigService.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfigService.java deleted file mode 100644 index c6abed978cb..00000000000 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfigService.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2022 ConsenSys AG. - * - * 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.validator.client; - -import java.time.Duration; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import tech.pegasys.teku.infrastructure.async.AsyncRunner; -import tech.pegasys.teku.infrastructure.async.Cancellable; -import tech.pegasys.teku.infrastructure.async.SafeFuture; -import tech.pegasys.teku.service.serviceutils.Service; - -public class ProposerConfigService extends Service { - private static final Logger LOG = LogManager.getLogger(); - static final Duration DEFAULT_REFRESH_RATE = Duration.ofMinutes(1); - - private final Duration refreshRate; - private final AsyncRunner asyncRunner; - private Optional cancellable = Optional.empty(); - private final AtomicBoolean running = new AtomicBoolean(false); - - public ProposerConfigService(final AsyncRunner asyncRunner) { - this.asyncRunner = asyncRunner; - this.refreshRate = DEFAULT_REFRESH_RATE; - } - - @Override - protected SafeFuture doStart() { - cancellable = - Optional.of( - asyncRunner.runWithFixedDelay( - this::loadProposerConfig, - refreshRate, - error -> LOG.error("Failed to refresh proposer configuration", error))); - // Run immediately on start - loadProposerConfig(); - return SafeFuture.COMPLETE; - } - - @Override - protected SafeFuture doStop() { - cancellable.ifPresent(Cancellable::cancel); - cancellable = Optional.empty(); - return SafeFuture.COMPLETE; - } - - private void loadProposerConfig() { - if (running.compareAndSet(false, true)) {} - } -} diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ValidatorClientService.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ValidatorClientService.java index fd4b0dcb7ce..b89708945e4 100644 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ValidatorClientService.java +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ValidatorClientService.java @@ -48,6 +48,7 @@ import tech.pegasys.teku.validator.client.loader.OwnedValidators; import tech.pegasys.teku.validator.client.loader.PublicKeyLoader; import tech.pegasys.teku.validator.client.loader.ValidatorLoader; +import tech.pegasys.teku.validator.client.proposerconfig.ProposerConfigProvider; import tech.pegasys.teku.validator.client.restapi.ValidatorRestApi; import tech.pegasys.teku.validator.client.restapi.ValidatorRestApiConfig; import tech.pegasys.teku.validator.eventadapter.InProcessBeaconNodeApi; @@ -65,6 +66,7 @@ public class ValidatorClientService extends Service { private final List validatorTimingChannels = new ArrayList<>(); private ValidatorStatusLogger validatorStatusLogger; private ValidatorIndexProvider validatorIndexProvider; + private ProposerConfigProvider proposerConfigProvider; private final SafeFuture initializationComplete = new SafeFuture<>(); @@ -180,6 +182,13 @@ private void initializeValidators( AsyncRunner asyncRunner) { validatorLoader.loadValidators(); final OwnedValidators validators = validatorLoader.getOwnedValidators(); + + this.proposerConfigProvider = + ProposerConfigProvider.create( + asyncRunner, + config.getValidatorConfig().getRefreshProposerConfigFromSource(), + Optional.of(config.getValidatorConfig().getProposerConfigSource())); + this.validatorIndexProvider = new ValidatorIndexProvider(validators, validatorApiChannel, asyncRunner); final BlockDutyFactory blockDutyFactory = @@ -238,8 +247,8 @@ private void initializeValidators( new BeaconProposerPreparer( validatorApiChannel, validatorIndexProvider, + proposerConfigProvider, config.getValidatorConfig().getProposerDefaultFeeRecipient(), - validators, spec)); } addValidatorCountMetric(metricsSystem, validators); diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ValidatorIndexProvider.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ValidatorIndexProvider.java index b540895b62a..8f7371e1f69 100644 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ValidatorIndexProvider.java +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ValidatorIndexProvider.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.validator.client; +import static java.util.Collections.unmodifiableMap; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -106,4 +107,8 @@ public SafeFuture> getValidatorIndices( __ -> publicKeys.stream().flatMap(key -> getValidatorIndex(key).stream()).collect(toList())); } + + public SafeFuture> getValidatorIndexesByPublicKey() { + return firstSuccessfulRequest.thenApply(__ -> unmodifiableMap(validatorIndexesByPublicKey)); + } } diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/AbstractProposerConfigProvider.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/AbstractProposerConfigProvider.java new file mode 100644 index 00000000000..66b20f8918d --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/AbstractProposerConfigProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright 2022 ConsenSys AG. + * + * 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.validator.client.proposerconfig; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.provider.JsonProvider; +import tech.pegasys.teku.validator.client.ProposerConfig; +import tech.pegasys.teku.validator.client.proposerconfig.loader.ProposerConfigLoader; + +public abstract class AbstractProposerConfigProvider implements ProposerConfigProvider { + private static final Logger LOG = LogManager.getLogger(); + + private final boolean refresh; + private final AsyncRunner asyncRunner; + protected final ProposerConfigLoader proposerConfigLoader = + new ProposerConfigLoader(new JsonProvider().getObjectMapper()); + private Optional lastProposerConfig = Optional.empty(); + private final AtomicBoolean requestInProgress = new AtomicBoolean(false); + + AbstractProposerConfigProvider(final AsyncRunner asyncRunner, final boolean refresh) { + this.asyncRunner = asyncRunner; + this.refresh = refresh; + } + + @Override + public SafeFuture> getProposerConfig() { + if (!requestInProgress.compareAndSet(false, true)) { + if (lastProposerConfig.isPresent()) { + LOG.warn("A proposer config load is already in progress, providing previous config"); + return SafeFuture.completedFuture(lastProposerConfig); + } + return SafeFuture.failedFuture( + new RuntimeException( + "A proposer config load is already in progress and there is no previous config")); + } + + if (lastProposerConfig.isEmpty() || refresh) { + return asyncRunner + .runAsync( + () -> { + lastProposerConfig = Optional.of(internalGetProposerConfig()); + return lastProposerConfig; + }) + .orTimeout(30, TimeUnit.SECONDS) + .exceptionally( + throwable -> { + if (lastProposerConfig.isPresent()) { + LOG.warn("An error occurred while obtaining config, providing previous config"); + return lastProposerConfig; + } + throw new RuntimeException( + "An error occurred while obtaining config and there is no previous config", + throwable); + }) + .alwaysRun(() -> requestInProgress.set(false)); + } + + requestInProgress.set(false); + return SafeFuture.completedFuture(lastProposerConfig); + } + + protected abstract ProposerConfig internalGetProposerConfig(); +} diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/FileProposerConfigProvider.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/FileProposerConfigProvider.java new file mode 100644 index 00000000000..48353144983 --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/FileProposerConfigProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 ConsenSys AG. + * + * 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.validator.client.proposerconfig; + +import java.io.File; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.validator.client.ProposerConfig; + +public class FileProposerConfigProvider extends AbstractProposerConfigProvider { + private final File source; + + FileProposerConfigProvider( + final AsyncRunner asyncRunner, final boolean refresh, final File source) { + super(asyncRunner, refresh); + this.source = source; + } + + @Override + protected ProposerConfig internalGetProposerConfig() { + return proposerConfigLoader.getProposerConfig(source); + } +} diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProvider.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProvider.java new file mode 100644 index 00000000000..9301b021b7a --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 ConsenSys AG. + * + * 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.validator.client.proposerconfig; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Optional; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.validator.client.ProposerConfig; + +public interface ProposerConfigProvider { + ProposerConfigProvider NOOP = () -> SafeFuture.completedFuture(Optional.empty()); + + static ProposerConfigProvider create( + final AsyncRunner asyncRunner, final boolean refresh, final Optional source) { + + if (source.isPresent()) { + try { + URL sourceUrl = new URL(source.get()); + return new UrlProposerConfigProvider(asyncRunner, refresh, sourceUrl); + } catch (MalformedURLException e) { + File sourceFile = new File(source.get()); + return new FileProposerConfigProvider(asyncRunner, refresh, sourceFile); + } + } + return NOOP; + } + + SafeFuture> getProposerConfig(); +} diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/UrlProposerConfigProvider.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/UrlProposerConfigProvider.java new file mode 100644 index 00000000000..29c49e2369e --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/UrlProposerConfigProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 ConsenSys AG. + * + * 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.validator.client.proposerconfig; + +import java.net.URL; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.validator.client.ProposerConfig; + +public class UrlProposerConfigProvider extends AbstractProposerConfigProvider { + private final URL source; + + UrlProposerConfigProvider( + final AsyncRunner asyncRunner, final boolean refresh, final URL source) { + super(asyncRunner, refresh); + this.source = source; + } + + @Override + protected ProposerConfig internalGetProposerConfig() { + return proposerConfigLoader.getProposerConfig(source); + } +} diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/loader/ProposerConfigLoader.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoader.java similarity index 65% rename from validator/client/src/main/java/tech/pegasys/teku/validator/client/loader/ProposerConfigLoader.java rename to validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoader.java index b16485bf8a1..63a5c3cb32a 100644 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/loader/ProposerConfigLoader.java +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoader.java @@ -11,43 +11,42 @@ * specific language governing permissions and limitations under the License. */ -package tech.pegasys.teku.validator.client.loader; +package tech.pegasys.teku.validator.client.proposerconfig.loader; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; import java.io.IOException; import java.net.URL; -import java.util.Optional; import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException; import tech.pegasys.teku.provider.JsonProvider; import tech.pegasys.teku.validator.client.ProposerConfig; public class ProposerConfigLoader { - private final JsonProvider jsonProvider = new JsonProvider(); + final ObjectMapper objectMapper; - public ProposerConfig getProposerConfig(final String source) { + public ProposerConfigLoader() { + this(new JsonProvider().getObjectMapper()); + } + + public ProposerConfigLoader(final ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public ProposerConfig getProposerConfig(final File source) { try { - final ProposerConfig proposerConfig = - jsonProvider.getObjectMapper().readValue(source, ProposerConfig.class); + final ProposerConfig proposerConfig = objectMapper.readValue(source, ProposerConfig.class); return proposerConfig; } catch (IOException ex) { - throw new InvalidConfigurationException("Failed to proposer config from URL " + source, ex); + throw new InvalidConfigurationException("Failed to proposer config from File " + source, ex); } } public ProposerConfig getProposerConfig(final URL source) { try { - final ProposerConfig proposerConfig = - jsonProvider.getObjectMapper().readValue(source, ProposerConfig.class); + final ProposerConfig proposerConfig = objectMapper.readValue(source, ProposerConfig.class); return proposerConfig; } catch (IOException ex) { throw new InvalidConfigurationException("Failed to proposer config from URL " + source, ex); } } - - private Optional getUrl(final String source) { - try { - return Optional.of(new URL(source)); - } catch (IOException e) { - return Optional.empty(); - } - } } diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/BeaconProposerPreparerTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/BeaconProposerPreparerTest.java index e81aacbaf37..a387722cb9a 100644 --- a/validator/client/src/test/java/tech/pegasys/teku/validator/client/BeaconProposerPreparerTest.java +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/BeaconProposerPreparerTest.java @@ -25,10 +25,10 @@ import java.util.Collection; import java.util.Map; import java.util.Optional; -import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import org.mockito.ArgumentCaptor; +import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.core.signatures.Signer; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -38,7 +38,7 @@ import tech.pegasys.teku.spec.datastructures.eth1.Eth1Address; import tech.pegasys.teku.spec.datastructures.operations.versions.bellatrix.BeaconPreparableProposer; import tech.pegasys.teku.validator.api.ValidatorApiChannel; -import tech.pegasys.teku.validator.client.loader.OwnedValidators; +import tech.pegasys.teku.validator.client.proposerconfig.ProposerConfigProvider; @TestSpecContext(milestone = SpecMilestone.BELLATRIX) public class BeaconProposerPreparerTest { @@ -46,8 +46,11 @@ public class BeaconProposerPreparerTest { private final int validator2Index = 23; private final ValidatorIndexProvider validatorIndexProvider = mock(ValidatorIndexProvider.class); private final ValidatorApiChannel validatorApiChannel = mock(ValidatorApiChannel.class); + private final ProposerConfigProvider proposerConfigProvider = mock(ProposerConfigProvider.class); private BeaconProposerPreparer beaconProposerPreparer; - private Eth1Address feeRecipient; + private Eth1Address defaultFeeRecipient; + private Eth1Address defaultFeeRecipientConfig; + private Eth1Address validator1FeeRecipientConfig; private long slotsPerEpoch; @@ -64,40 +67,73 @@ void setUp(SpecContext specContext) { mock(Signer.class), Optional::empty); - Set validatorIndices = Set.of(validator1Index, validator2Index); - OwnedValidators validators = - new OwnedValidators( - Map.of(validator1.getPublicKey(), validator1, validator2.getPublicKey(), validator2)); + Map validatorIndexesByPublicKey = + Map.of( + validator1.getPublicKey(), validator1Index, validator2.getPublicKey(), validator2Index); - feeRecipient = specContext.getDataStructureUtil().randomEth1Address(); + defaultFeeRecipient = specContext.getDataStructureUtil().randomEth1Address(); + defaultFeeRecipientConfig = specContext.getDataStructureUtil().randomEth1Address(); + validator1FeeRecipientConfig = specContext.getDataStructureUtil().randomEth1Address(); + + ProposerConfig proposerConfig = + new ProposerConfig( + Map.of( + validator1.getPublicKey().toBytesCompressed(), + new ProposerConfig.Config(validator1FeeRecipientConfig)), + new ProposerConfig.Config(defaultFeeRecipientConfig)); beaconProposerPreparer = new BeaconProposerPreparer( validatorApiChannel, validatorIndexProvider, - Optional.of(feeRecipient), - validators, + proposerConfigProvider, + Optional.of(defaultFeeRecipient), specContext.getSpec()); slotsPerEpoch = specContext.getSpec().getSlotsPerEpoch(UInt64.ZERO); - when(validatorIndexProvider.getValidatorIndices(validators.getPublicKeys())) - .thenReturn(SafeFuture.completedFuture(validatorIndices)); + when(validatorIndexProvider.getValidatorIndexesByPublicKey()) + .thenReturn(SafeFuture.completedFuture(validatorIndexesByPublicKey)); + when(proposerConfigProvider.getProposerConfig()) + .thenReturn(SafeFuture.completedFuture(Optional.of(proposerConfig))); } @TestTemplate - void should_callPrepareBeaconProposerAtBeginningOfEpoch(SpecContext specContext) { - beaconProposerPreparer.onSlot(UInt64.valueOf(slotsPerEpoch * 2)); + void should_callPrepareBeaconProposerAtBeginningOfEpoch() { + ArgumentCaptor> captor = doCall(); - @SuppressWarnings("unchecked") - final ArgumentCaptor> captor = - ArgumentCaptor.forClass(Collection.class); - verify(validatorApiChannel).prepareBeaconProposer(captor.capture()); + assertThat(captor.getValue()) + .containsExactlyInAnyOrder( + new BeaconPreparableProposer( + UInt64.valueOf(validator1Index), validator1FeeRecipientConfig), + new BeaconPreparableProposer( + UInt64.valueOf(validator2Index), defaultFeeRecipientConfig)); + } + + @TestTemplate + void should_useDefaultFeeRecipientWhenNoConfig() { + when(proposerConfigProvider.getProposerConfig()) + .thenReturn(SafeFuture.completedFuture(Optional.empty())); + + ArgumentCaptor> captor = doCall(); + + assertThat(captor.getValue()) + .containsExactlyInAnyOrder( + new BeaconPreparableProposer(UInt64.valueOf(validator1Index), defaultFeeRecipient), + new BeaconPreparableProposer(UInt64.valueOf(validator2Index), defaultFeeRecipient)); + } + + @TestTemplate + void should_useDefaultFeeRecipientWhenExceptionInConfigProvider() { + when(proposerConfigProvider.getProposerConfig()) + .thenReturn(SafeFuture.failedFuture(new RuntimeException("error"))); + + ArgumentCaptor> captor = doCall(); assertThat(captor.getValue()) .containsExactlyInAnyOrder( - new BeaconPreparableProposer(UInt64.valueOf(validator1Index), feeRecipient), - new BeaconPreparableProposer(UInt64.valueOf(validator2Index), feeRecipient)); + new BeaconPreparableProposer(UInt64.valueOf(validator1Index), defaultFeeRecipient), + new BeaconPreparableProposer(UInt64.valueOf(validator2Index), defaultFeeRecipient)); } @TestTemplate @@ -116,4 +152,15 @@ void should_catchApiExceptions() { beaconProposerPreparer.onSlot(UInt64.ZERO); verify(validatorApiChannel, times(1)).prepareBeaconProposer(any()); } + + private ArgumentCaptor> doCall() { + beaconProposerPreparer.onSlot(UInt64.valueOf(slotsPerEpoch * 2)); + + @SuppressWarnings("unchecked") + final ArgumentCaptor> captor = + ArgumentCaptor.forClass(Collection.class); + verify(validatorApiChannel).prepareBeaconProposer(captor.capture()); + + return captor; + } } diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/loader/ProposerConfigLoaderTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/loader/ProposerConfigLoaderTest.java index 8969684285f..e6408b1767c 100644 --- a/validator/client/src/test/java/tech/pegasys/teku/validator/client/loader/ProposerConfigLoaderTest.java +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/loader/ProposerConfigLoaderTest.java @@ -22,6 +22,7 @@ import tech.pegasys.teku.infrastructure.ssz.type.Bytes20; import tech.pegasys.teku.validator.client.ProposerConfig; import tech.pegasys.teku.validator.client.ProposerConfig.Config; +import tech.pegasys.teku.validator.client.proposerconfig.loader.ProposerConfigLoader; public class ProposerConfigLoaderTest { private final ProposerConfigLoader loader = new ProposerConfigLoader();