From 6992eb4261d4582cf12eac46e215c89468613113 Mon Sep 17 00:00:00 2001 From: Enrico Del Fante Date: Tue, 1 Feb 2022 09:16:46 +0100 Subject: [PATCH] Add support for multiple fee recipients (#4894) Add support for multiple fee recipients --- .../teku/provider/Bytes48KeyDeserializer.java | 26 ++++ .../pegasys/teku/provider/JsonProvider.java | 3 + .../cli/options/ValidatorProposerOptions.java | 27 +++- .../teku/validator/api/ValidatorConfig.java | 42 +++++- .../validator/api/ValidatorConfigTest.java | 65 +++++---- .../validator/client/AbstractDutyLoader.java | 2 +- .../client/BeaconProposerPreparer.java | 85 ++++++++--- .../teku/validator/client/ProposerConfig.java | 103 +++++++++++++ .../client/ValidatorClientService.java | 55 ++++--- .../client/ValidatorIndexProvider.java | 26 +++- .../AbstractProposerConfigProvider.java | 86 +++++++++++ .../ProposerConfigProvider.java | 52 +++++++ .../UrlProposerConfigProvider.java | 37 +++++ .../loader/ProposerConfigLoader.java | 46 ++++++ .../client/AbstractDutySchedulerTest.java | 2 +- .../client/AttestationDutyLoaderTest.java | 2 +- .../client/BeaconProposerPreparerTest.java | 99 ++++++++++--- .../client/SyncCommitteeDutyLoaderTest.java | 2 +- .../client/ValidatorIndexProviderTest.java | 2 +- .../ProposerConfigProviderTest.java | 136 ++++++++++++++++++ .../loader/ProposerConfigLoaderTest.java | 104 ++++++++++++++ .../resources/proposerConfigInvalid1.json | 10 ++ .../resources/proposerConfigInvalid2.json | 10 ++ .../resources/proposerConfigInvalid3.json | 10 ++ .../resources/proposerConfigInvalid4.json | 9 ++ .../resources/proposerConfigInvalid5.json | 7 + .../test/resources/proposerConfigValid1.json | 11 ++ .../test/resources/proposerConfigValid2.json | 6 + 28 files changed, 961 insertions(+), 104 deletions(-) create mode 100644 data/serializer/src/main/java/tech/pegasys/teku/provider/Bytes48KeyDeserializer.java create mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfig.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/ProposerConfigProvider.java create mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/UrlProposerConfigProvider.java create mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoader.java create mode 100644 validator/client/src/test/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProviderTest.java create mode 100644 validator/client/src/test/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoaderTest.java create mode 100644 validator/client/src/test/resources/proposerConfigInvalid1.json create mode 100644 validator/client/src/test/resources/proposerConfigInvalid2.json create mode 100644 validator/client/src/test/resources/proposerConfigInvalid3.json create mode 100644 validator/client/src/test/resources/proposerConfigInvalid4.json create mode 100644 validator/client/src/test/resources/proposerConfigInvalid5.json create mode 100644 validator/client/src/test/resources/proposerConfigValid1.json create mode 100644 validator/client/src/test/resources/proposerConfigValid2.json diff --git a/data/serializer/src/main/java/tech/pegasys/teku/provider/Bytes48KeyDeserializer.java b/data/serializer/src/main/java/tech/pegasys/teku/provider/Bytes48KeyDeserializer.java new file mode 100644 index 00000000000..b2a8754bd01 --- /dev/null +++ b/data/serializer/src/main/java/tech/pegasys/teku/provider/Bytes48KeyDeserializer.java @@ -0,0 +1,26 @@ +/* + * 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.provider; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.KeyDeserializer; +import org.apache.tuweni.bytes.Bytes48; + +public class Bytes48KeyDeserializer extends KeyDeserializer { + + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) { + return Bytes48.fromHexStringStrict(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 a2c16832f2c..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; @@ -45,6 +46,8 @@ private void addTekuMappers() { module.addDeserializer(BLSSignature.class, new BLSSignatureDeserializer()); module.addSerializer(BLSSignature.class, new BLSSignatureSerializer()); + module.addKeyDeserializer(Bytes48.class, new Bytes48KeyDeserializer()); + module.addDeserializer(Bytes32.class, new Bytes32Deserializer()); module.addDeserializer(Bytes4.class, new Bytes4Deserializer()); module.addSerializer(Bytes4.class, new Bytes4Serializer()); 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 614d79d170d..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 @@ -15,6 +15,7 @@ import picocli.CommandLine.Option; import tech.pegasys.teku.config.TekuConfiguration; +import tech.pegasys.teku.validator.api.ValidatorConfig; public class ValidatorProposerOptions { @Option( @@ -26,7 +27,31 @@ public class ValidatorProposerOptions { hidden = true) private String proposerDefaultFeeRecipient = null; + @Option( + names = {"--Xvalidators-proposer-config"}, + paramLabel = "", + description = "remote URL or local file path to load proposer configuration from", + arity = "0..1", + hidden = true) + private String proposerConfig = null; + + @Option( + names = {"--Xvalidators-proposer-config-refresh-enabled"}, + paramLabel = "", + description = + "Enable the proposer configuration reload on every proposer preparation (once per epoch)", + arity = "0..1", + fallbackValue = "true", + hidden = true) + private boolean proposerConfigRefreshEnabled = + ValidatorConfig.DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_ENABLED; + public void configure(TekuConfiguration.Builder builder) { - builder.validator(config -> config.proposerDefaultFeeRecipient(proposerDefaultFeeRecipient)); + builder.validator( + config -> + config + .proposerDefaultFeeRecipient(proposerDefaultFeeRecipient) + .proposerConfigSource(proposerConfig) + .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 c6f2b5c9de6..e37729b212d 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,6 +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 boolean DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_ENABLED = false; private final List validatorKeys; private final List validatorExternalSignerPublicKeySources; @@ -58,6 +59,8 @@ public class ValidatorConfig { private final boolean useDependentRoots; private final boolean generateEarlyAttestations; private final Optional proposerDefaultFeeRecipient; + private final Optional proposerConfigSource; + private final boolean refreshProposerConfigFromSource; private ValidatorConfig( final List validatorKeys, @@ -76,7 +79,9 @@ private ValidatorConfig( final int validatorExternalSignerConcurrentRequestLimit, final boolean useDependentRoots, final boolean generateEarlyAttestations, - final Optional proposerDefaultFeeRecipient) { + final Optional proposerDefaultFeeRecipient, + final Optional proposerConfigSource, + final boolean refreshProposerConfigFromSource) { this.validatorKeys = validatorKeys; this.validatorExternalSignerPublicKeySources = validatorExternalSignerPublicKeySources; this.validatorExternalSignerUrl = validatorExternalSignerUrl; @@ -97,6 +102,8 @@ private ValidatorConfig( this.useDependentRoots = useDependentRoots; this.generateEarlyAttestations = generateEarlyAttestations; this.proposerDefaultFeeRecipient = proposerDefaultFeeRecipient; + this.proposerConfigSource = proposerConfigSource; + this.refreshProposerConfigFromSource = refreshProposerConfigFromSource; } public static Builder builder() { @@ -161,15 +168,25 @@ public boolean useDependentRoots() { } public Optional getProposerDefaultFeeRecipient() { - validateProposerDefaultFeeRecipient(); + validateProposerDefaultFeeRecipientOrProposerConfigSource(); return proposerDefaultFeeRecipient; } - private void validateProposerDefaultFeeRecipient() { + public Optional getProposerConfigSource() { + validateProposerDefaultFeeRecipientOrProposerConfigSource(); + return proposerConfigSource; + } + + public boolean getRefreshProposerConfigFromSource() { + return refreshProposerConfigFromSource; + } + + private void validateProposerDefaultFeeRecipientOrProposerConfigSource() { if (proposerDefaultFeeRecipient.isEmpty() + && proposerConfigSource.isEmpty() && !(validatorKeys.isEmpty() && validatorExternalSignerPublicKeySources.isEmpty())) { throw new InvalidConfigurationException( - "Invalid configuration. --Xvalidators-proposer-default-fee-recipient must be specified when Bellatrix milestone is active"); + "Invalid configuration. --Xvalidators-proposer-default-fee-recipient or --Xvalidators-proposer-config must be specified when Bellatrix milestone is active"); } } @@ -195,6 +212,9 @@ public static final class Builder { private boolean useDependentRoots = DEFAULT_USE_DEPENDENT_ROOTS; private boolean generateEarlyAttestations = DEFAULT_GENERATE_EARLY_ATTESTATIONS; private Optional proposerDefaultFeeRecipient = Optional.empty(); + private Optional proposerConfigSource = Optional.empty(); + private boolean refreshProposerConfigFromSource = + DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_ENABLED; private Builder() {} @@ -304,6 +324,16 @@ public Builder proposerDefaultFeeRecipient(final String proposerDefaultFeeRecipi return this; } + public Builder proposerConfigSource(final String proposerConfigSource) { + this.proposerConfigSource = Optional.ofNullable(proposerConfigSource); + return this; + } + + public Builder refreshProposerConfigFromSource(final boolean refreshProposerConfigFromSource) { + this.refreshProposerConfigFromSource = refreshProposerConfigFromSource; + return this; + } + public ValidatorConfig build() { validateExternalSignerUrlAndPublicKeys(); validateExternalSignerKeystoreAndPasswordFileConfig(); @@ -326,7 +356,9 @@ public ValidatorConfig build() { validatorExternalSignerConcurrentRequestLimit, useDependentRoots, generateEarlyAttestations, - proposerDefaultFeeRecipient); + proposerDefaultFeeRecipient, + proposerConfigSource, + refreshProposerConfigFromSource); } private void validateExternalSignerUrlAndPublicKeys() { diff --git a/validator/api/src/test/java/tech/pegasys/teku/validator/api/ValidatorConfigTest.java b/validator/api/src/test/java/tech/pegasys/teku/validator/api/ValidatorConfigTest.java index 364fd8166a8..b7b6f3c9df1 100644 --- a/validator/api/src/test/java/tech/pegasys/teku/validator/api/ValidatorConfigTest.java +++ b/validator/api/src/test/java/tech/pegasys/teku/validator/api/ValidatorConfigTest.java @@ -21,14 +21,13 @@ import org.junit.jupiter.api.Test; import tech.pegasys.teku.bls.BLSTestUtil; import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException; -import tech.pegasys.teku.spec.datastructures.eth1.Eth1Address; class ValidatorConfigTest { private final ValidatorConfig.Builder configBuilder = ValidatorConfig.builder(); @Test - public void shouldThrowExceptionIfExternalPublicKeysAreSpecifiedWithoutExternalSignerUrl() { + public void shouldThrowIfExternalPublicKeysAreSpecifiedWithoutExternalSignerUrl() { final ValidatorConfig.Builder builder = configBuilder.validatorExternalSignerPublicKeySources( List.of(BLSTestUtil.randomKeyPair(0).getPublicKey().toString())); @@ -39,7 +38,7 @@ public void shouldThrowExceptionIfExternalPublicKeysAreSpecifiedWithoutExternalS } @Test - public void noExceptionThrownIfExternalSignerUrlIsSpecifiedWithoutExternalPublicKeys() + public void shouldNotThrowIfExternalSignerUrlIsSpecifiedWithoutExternalPublicKeys() throws MalformedURLException { final ValidatorConfig.Builder builder = configBuilder.validatorExternalSignerUrl(URI.create("http://localhost:9000").toURL()); @@ -47,7 +46,7 @@ public void noExceptionThrownIfExternalSignerUrlIsSpecifiedWithoutExternalPublic } @Test - public void noExceptionThrownIfBothExternalSignerUrlAndPublicKeysAreSpecified() + public void shouldNotThrowIfBothExternalSignerUrlAndPublicKeysAreSpecified() throws MalformedURLException { final ValidatorConfig.Builder builder = configBuilder @@ -59,7 +58,7 @@ public void noExceptionThrownIfBothExternalSignerUrlAndPublicKeysAreSpecified() } @Test - public void shouldThrowExceptionIfExternalSignerKeystoreSpecifiedWithoutPasswordFile() { + public void shouldThrowIfExternalSignerKeystoreSpecifiedWithoutPasswordFile() { final ValidatorConfig.Builder builder = configBuilder.validatorExternalSignerKeystore(Path.of("somepath")); Assertions.assertThatExceptionOfType(InvalidConfigurationException.class) @@ -69,7 +68,7 @@ public void shouldThrowExceptionIfExternalSignerKeystoreSpecifiedWithoutPassword } @Test - public void shouldThrowExceptionIfExternalSignerKeystorePasswordFileIsSpecifiedWithoutKeystore() { + public void shouldThrowIfExternalSignerKeystorePasswordFileIsSpecifiedWithoutKeystore() { final ValidatorConfig.Builder builder = configBuilder.validatorExternalSignerKeystorePasswordFile(Path.of("somepath")); Assertions.assertThatExceptionOfType(InvalidConfigurationException.class) @@ -79,7 +78,7 @@ public void shouldThrowExceptionIfExternalSignerKeystorePasswordFileIsSpecifiedW } @Test - public void noExceptionThrownIfBothExternalSignerKeystoreAndPasswordFileAreSpecified() { + public void shouldNotThrowIfBothExternalSignerKeystoreAndPasswordFileAreSpecified() { final ValidatorConfig.Builder builder = configBuilder .validatorExternalSignerKeystore(Path.of("somepath")) @@ -89,7 +88,7 @@ public void noExceptionThrownIfBothExternalSignerKeystoreAndPasswordFileAreSpeci } @Test - public void shouldThrowExceptionIfExternalSignerTruststoreSpecifiedWithoutPasswordFile() { + public void shouldThrowIfExternalSignerTruststoreSpecifiedWithoutPasswordFile() { final ValidatorConfig.Builder builder = configBuilder.validatorExternalSignerTruststore(Path.of("somepath")); Assertions.assertThatExceptionOfType(InvalidConfigurationException.class) @@ -99,8 +98,7 @@ public void shouldThrowExceptionIfExternalSignerTruststoreSpecifiedWithoutPasswo } @Test - public void - shouldThrowExceptionIfExternalSignerTruststorePasswordFileIsSpecifiedWithoutTruststore() { + public void shouldThrowIfExternalSignerTruststorePasswordFileIsSpecifiedWithoutTruststore() { final ValidatorConfig.Builder builder = configBuilder.validatorExternalSignerTruststorePasswordFile(Path.of("somepath")); Assertions.assertThatExceptionOfType(InvalidConfigurationException.class) @@ -110,7 +108,7 @@ public void shouldThrowExceptionIfExternalSignerTruststoreSpecifiedWithoutPasswo } @Test - public void noExceptionThrownIfBothExternalSignerTruststoreAndPasswordFileAreSpecified() { + public void shouldNotThrowIfBothExternalSignerTruststoreAndPasswordFileAreSpecified() { final ValidatorConfig.Builder builder = configBuilder .validatorExternalSignerTruststore(Path.of("somepath")) @@ -120,7 +118,7 @@ public void noExceptionThrownIfBothExternalSignerTruststoreAndPasswordFileAreSpe } @Test - public void bellatrix_shouldThrowExceptionIfExternalSignerPublicKeySourcesIsSpecified() + public void bellatrix_shouldThrowIfExternalSignerPublicKeySourcesIsSpecified() throws MalformedURLException { final ValidatorConfig config = configBuilder @@ -129,24 +127,18 @@ public void bellatrix_shouldThrowExceptionIfExternalSignerPublicKeySourcesIsSpec .validatorExternalSignerUrl(URI.create("http://localhost:9000").toURL()) .build(); - Assertions.assertThatExceptionOfType(InvalidConfigurationException.class) - .isThrownBy(config::getProposerDefaultFeeRecipient) - .withMessageContaining( - "Invalid configuration. --Xvalidators-proposer-default-fee-recipient must be specified when Bellatrix milestone is active"); + verifyProposerConfigOrProposerDefaultFeeRecipientThrow(config); } @Test - public void bellatrix_shouldThrowExceptionIfValidatorKeysAreSpecified() { + public void bellatrix_shouldThrowIfValidatorKeysAreSpecified() { final ValidatorConfig config = configBuilder.validatorKeys(List.of("some string")).build(); - Assertions.assertThatExceptionOfType(InvalidConfigurationException.class) - .isThrownBy(config::getProposerDefaultFeeRecipient) - .withMessageContaining( - "Invalid configuration. --Xvalidators-proposer-default-fee-recipient must be specified when Bellatrix milestone is active"); + verifyProposerConfigOrProposerDefaultFeeRecipientThrow(config); } @Test - public void bellatrix_noExceptionThrownIfIfExternalSignerPublicKeySourcesIsSpecified() + public void bellatrix_shouldNotThrowIfValidationIsActiveAndDefaultFeeRecipientIsSpecified() throws MalformedURLException { final ValidatorConfig config = configBuilder @@ -156,18 +148,37 @@ public void bellatrix_noExceptionThrownIfIfExternalSignerPublicKeySourcesIsSpeci .proposerDefaultFeeRecipient("0x0000000000000000000000000000000000000000") .build(); - Assertions.assertThatCode(config::getProposerDefaultFeeRecipient).doesNotThrowAnyException(); + verifyProposerConfigOrProposerDefaultFeeRecipientNotThrow(config); } @Test - public void bellatrix_noExceptionThrownIfIfValidatorKeysAreSpecified() { + public void bellatrix_shouldNotThrowIfValidationIsActiveAndProposerConfigSourceIsSpecified() + throws MalformedURLException { final ValidatorConfig config = configBuilder - .validatorKeys(List.of("some string")) - .proposerDefaultFeeRecipient( - Eth1Address.fromHexString("0x0000000000000000000000000000000000000000")) + .validatorExternalSignerPublicKeySources( + List.of(BLSTestUtil.randomKeyPair(0).getPublicKey().toString())) + .validatorExternalSignerUrl(URI.create("http://localhost:9000").toURL()) + .proposerConfigSource("some path") .build(); + verifyProposerConfigOrProposerDefaultFeeRecipientNotThrow(config); + } + + void verifyProposerConfigOrProposerDefaultFeeRecipientNotThrow(final ValidatorConfig config) { Assertions.assertThatCode(config::getProposerDefaultFeeRecipient).doesNotThrowAnyException(); + Assertions.assertThatCode(config::getProposerConfigSource).doesNotThrowAnyException(); + } + + void verifyProposerConfigOrProposerDefaultFeeRecipientThrow(final ValidatorConfig config) { + verifyProposerConfigOrProposerDefaultFeeRecipientThrow(config::getProposerDefaultFeeRecipient); + verifyProposerConfigOrProposerDefaultFeeRecipientThrow(config::getProposerConfigSource); + } + + void verifyProposerConfigOrProposerDefaultFeeRecipientThrow(final Runnable task) { + Assertions.assertThatExceptionOfType(InvalidConfigurationException.class) + .isThrownBy(task::run) + .withMessageContaining( + "Invalid configuration. --Xvalidators-proposer-default-fee-recipient or --Xvalidators-proposer-config must be specified when Bellatrix milestone is active"); } } diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/AbstractDutyLoader.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/AbstractDutyLoader.java index 15dd5180246..100365bfebf 100644 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/AbstractDutyLoader.java +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/AbstractDutyLoader.java @@ -38,7 +38,7 @@ protected AbstractDutyLoader( public SafeFuture> loadDutiesForEpoch(final UInt64 epoch) { LOG.trace("Requesting duties for epoch {}", epoch); return validatorIndexProvider - .getValidatorIndices(validators.getPublicKeys()) + .getValidatorIndices() .thenCompose( validatorIndices -> { if (validatorIndices.isEmpty()) { 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..94b0bfb91de 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,60 @@ 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()) - .thenApply( - integers -> - integers.stream() - .map( - index -> - new BeaconPreparableProposer( - UInt64.valueOf(index), getFeeRecipient())) - .collect(Collectors.toList())) + .getValidatorIndexesByPublicKey() + .thenCompose( + publicKeyToIndex -> + proposerConfigFuture + .thenApply( + proposerConfig -> + buildBeaconPreparableProposerList(proposerConfig, publicKeyToIndex)) + .exceptionally( + throwable -> { + LOG.warn( + "An error occurred while obtaining proposer config", throwable); + return buildBeaconPreparableProposerList( + Optional.empty(), publicKeyToIndex); + })) .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> blsPublicKeyToIndexMap) { + return blsPublicKeyToIndexMap.entrySet().stream() + .filter(blsPublicKeyOptionalEntry -> blsPublicKeyOptionalEntry.getValue().isPresent()) + .map( + blsPublicKeyIntegerEntry -> + new BeaconPreparableProposer( + UInt64.valueOf(blsPublicKeyIntegerEntry.getValue().get()), + 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 new file mode 100644 index 00000000000..83246e30048 --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerConfig.java @@ -0,0 +1,103 @@ +/* + * 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +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") + private Map proposerConfig; + + @JsonProperty(value = "default_config") + private Config defaultConfig; + + @JsonCreator + public ProposerConfig( + @JsonProperty(value = "proposer_config") final Map proposerConfig, + @JsonProperty(value = "default_config") final Config defaultConfig) { + checkNotNull(defaultConfig, "default_config is required"); + this.proposerConfig = proposerConfig == null ? ImmutableMap.of() : proposerConfig; + this.defaultConfig = defaultConfig; + } + + public Optional getConfigForPubKey(final String pubKey) { + return getConfigForPubKey(Bytes48.fromHexString(pubKey)); + } + + public Optional getConfigForPubKey(final BLSPublicKey pubKey) { + return getConfigForPubKey(pubKey.toBytesCompressed()); + } + + public Optional getConfigForPubKey(final Bytes48 pubKey) { + return Optional.ofNullable(proposerConfig.get(pubKey)); + } + + public Optional getDefaultConfig() { + return Optional.ofNullable(defaultConfig); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ProposerConfig that = (ProposerConfig) o; + return Objects.equals(proposerConfig, that.proposerConfig) + && Objects.equals(defaultConfig, that.defaultConfig); + } + + @Override + public int hashCode() { + return Objects.hash(proposerConfig, defaultConfig); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Config { + @JsonProperty(value = "fee_recipient") + private Bytes20 feeRecipient; + + @JsonCreator + public Config(@JsonProperty(value = "fee_recipient") final Bytes20 feeRecipient) { + checkNotNull(feeRecipient, "fee_recipient is required"); + this.feeRecipient = feeRecipient; + } + + public Bytes20 getFeeRecipient() { + return feeRecipient; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Config that = (Config) o; + return Objects.equals(feeRecipient, that.feeRecipient); + } + + @Override + public int hashCode() { + return Objects.hash(feeRecipient); + } + } +} 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 ad44fc5a087..a4178b173c0 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 @@ -30,6 +30,7 @@ import tech.pegasys.teku.infrastructure.io.SystemSignalListener; import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; import tech.pegasys.teku.infrastructure.restapi.RestApi; +import tech.pegasys.teku.provider.JsonProvider; import tech.pegasys.teku.service.serviceutils.Service; import tech.pegasys.teku.service.serviceutils.ServiceConfig; import tech.pegasys.teku.service.serviceutils.layout.DataDirLayout; @@ -48,6 +49,8 @@ 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.proposerconfig.loader.ProposerConfigLoader; 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 +68,7 @@ public class ValidatorClientService extends Service { private final List validatorTimingChannels = new ArrayList<>(); private ValidatorStatusLogger validatorStatusLogger; private ValidatorIndexProvider validatorIndexProvider; + private Optional proposerConfigProvider; private final SafeFuture initializationComplete = new SafeFuture<>(); @@ -181,6 +185,7 @@ private void initializeValidators( AsyncRunner asyncRunner) { validatorLoader.loadValidators(); final OwnedValidators validators = validatorLoader.getOwnedValidators(); + this.validatorIndexProvider = new ValidatorIndexProvider(validators, validatorApiChannel, asyncRunner); final BlockDutyFactory blockDutyFactory = @@ -235,13 +240,23 @@ private void initializeValidators( } if (spec.isMilestoneSupported(SpecMilestone.BELLATRIX)) { + proposerConfigProvider = + Optional.of( + ProposerConfigProvider.create( + asyncRunner, + config.getValidatorConfig().getRefreshProposerConfigFromSource(), + new ProposerConfigLoader(new JsonProvider().getObjectMapper()), + config.getValidatorConfig().getProposerConfigSource())); + validatorTimingChannels.add( new BeaconProposerPreparer( validatorApiChannel, validatorIndexProvider, + proposerConfigProvider.get(), config.getValidatorConfig().getProposerDefaultFeeRecipient(), - validators, spec)); + } else { + proposerConfigProvider = Optional.empty(); } addValidatorCountMetric(metricsSystem, validators); this.validatorStatusLogger = @@ -280,22 +295,28 @@ private static void addValidatorCountMetric( @Override protected SafeFuture doStart() { - return initializationComplete.thenCompose( - (__) -> { - validatorRestApi.ifPresent(restApi -> restApi.start().reportExceptions()); - SystemSignalListener.registerReloadConfigListener(validatorLoader::loadValidators); - validatorIndexProvider.lookupValidators(); - eventChannels.subscribe( - ValidatorTimingChannel.class, - new ValidatorTimingActions( - validatorStatusLogger, - validatorIndexProvider, - validatorTimingChannels, - spec, - metricsSystem)); - validatorStatusLogger.printInitialValidatorStatuses().reportExceptions(); - return beaconNodeApi.subscribeToEvents(); - }); + return initializationComplete + .thenCompose( + __ -> + proposerConfigProvider + .map(ProposerConfigProvider::getProposerConfig) + .orElse(SafeFuture.completedFuture(Optional.empty()))) + .thenCompose( + __ -> { + validatorRestApi.ifPresent(restApi -> restApi.start().reportExceptions()); + SystemSignalListener.registerReloadConfigListener(validatorLoader::loadValidators); + validatorIndexProvider.lookupValidators(); + eventChannels.subscribe( + ValidatorTimingChannel.class, + new ValidatorTimingActions( + validatorStatusLogger, + validatorIndexProvider, + validatorTimingChannels, + spec, + metricsSystem)); + validatorStatusLogger.printInitialValidatorStatuses().reportExceptions(); + return beaconNodeApi.subscribeToEvents(); + }); } @Override 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..0c19990fe4f 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 @@ -24,6 +24,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import tech.pegasys.teku.bls.BLSPublicKey; @@ -36,7 +38,7 @@ public class ValidatorIndexProvider { private static final Logger LOG = LogManager.getLogger(); public static final Duration RETRY_DELAY = Duration.ofSeconds(5); - private final OwnedValidators validators; + private final OwnedValidators ownedValidators; private final ValidatorApiChannel validatorApiChannel; private final AsyncRunner asyncRunner; private final Map validatorIndexesByPublicKey = new ConcurrentHashMap<>(); @@ -45,10 +47,10 @@ public class ValidatorIndexProvider { private final SafeFuture firstSuccessfulRequest = new SafeFuture<>(); public ValidatorIndexProvider( - final OwnedValidators validators, + final OwnedValidators ownedValidators, final ValidatorApiChannel validatorApiChannel, final AsyncRunner asyncRunner) { - this.validators = validators; + this.ownedValidators = ownedValidators; this.validatorApiChannel = validatorApiChannel; this.asyncRunner = asyncRunner; } @@ -81,7 +83,7 @@ public void lookupValidators() { } private Collection getUnknownValidators() { - return Sets.difference(validators.getPublicKeys(), validatorIndexesByPublicKey.keySet()); + return Sets.difference(ownedValidators.getPublicKeys(), validatorIndexesByPublicKey.keySet()); } private void logNewValidatorIndices(final Map knownValidators) { @@ -99,11 +101,21 @@ public Optional getValidatorIndex(final BLSPublicKey publicKey) { return Optional.ofNullable(validatorIndexesByPublicKey.get(publicKey)); } - public SafeFuture> getValidatorIndices( - final Collection publicKeys) { + public SafeFuture> getValidatorIndices() { // Wait for at least one successful load of validator indices before attempting to read return firstSuccessfulRequest.thenApply( __ -> - publicKeys.stream().flatMap(key -> getValidatorIndex(key).stream()).collect(toList())); + ownedValidators.getActiveValidators().stream() + .flatMap(validator -> getValidatorIndex(validator.getPublicKey()).stream()) + .collect(toList())); + } + + public SafeFuture>> getValidatorIndexesByPublicKey() { + // Wait for at least one successful load of validator indices before attempting to read + return firstSuccessfulRequest.thenApply( + __ -> + ownedValidators.getActiveValidators().stream() + .map(Validator::getPublicKey) + .collect(Collectors.toMap(Function.identity(), this::getValidatorIndex))); } } 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..eba32d4dec8 --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/AbstractProposerConfigProvider.java @@ -0,0 +1,86 @@ +/* + * 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 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.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; + private Optional lastProposerConfig = Optional.empty(); + private Optional>> futureProposerConfig = Optional.empty(); + + AbstractProposerConfigProvider( + final AsyncRunner asyncRunner, + final boolean refresh, + final ProposerConfigLoader proposerConfigLoader) { + this.asyncRunner = asyncRunner; + this.refresh = refresh; + this.proposerConfigLoader = proposerConfigLoader; + } + + @Override + public synchronized SafeFuture> getProposerConfig() { + if (lastProposerConfig.isPresent() && !refresh) { + return SafeFuture.completedFuture(lastProposerConfig); + } + + if (futureProposerConfig.isPresent()) { + LOG.warn( + "A proposer config load is already progress, waiting it instead of generating a new request"); + return futureProposerConfig.get(); + } + futureProposerConfig = + Optional.of( + 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 last loaded config", + throwable); + return lastProposerConfig; + } + throw new RuntimeException( + "An error occurred while obtaining config and there is no previously loaded config", + throwable); + }) + .thenPeek(__ -> LOG.info("proposer config successfully loaded")) + .alwaysRun( + () -> { + synchronized (this) { + futureProposerConfig = Optional.empty(); + } + })); + return futureProposerConfig.get(); + } + + protected abstract ProposerConfig internalGetProposerConfig(); +} 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..6c82a5d2534 --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProvider.java @@ -0,0 +1,52 @@ +/* + * 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; +import tech.pegasys.teku.validator.client.proposerconfig.loader.ProposerConfigLoader; + +public interface ProposerConfigProvider { + ProposerConfigProvider NOOP = () -> SafeFuture.completedFuture(Optional.empty()); + + static ProposerConfigProvider create( + final AsyncRunner asyncRunner, + final boolean refresh, + final ProposerConfigLoader proposerConfigLoader, + final Optional source) { + + if (source.isPresent()) { + URL sourceUrl; + try { + sourceUrl = new URL(source.get()); + return new UrlProposerConfigProvider(asyncRunner, refresh, proposerConfigLoader, sourceUrl); + } catch (MalformedURLException e1) { + try { + sourceUrl = new File(source.get()).toURI().toURL(); + } catch (MalformedURLException e2) { + throw new RuntimeException("Unable to translate file to URL", e2); + } + } + return new UrlProposerConfigProvider(asyncRunner, refresh, proposerConfigLoader, sourceUrl); + } + 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..1bf70f04c1f --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/UrlProposerConfigProvider.java @@ -0,0 +1,37 @@ +/* + * 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; +import tech.pegasys.teku.validator.client.proposerconfig.loader.ProposerConfigLoader; + +public class UrlProposerConfigProvider extends AbstractProposerConfigProvider { + private final URL source; + + UrlProposerConfigProvider( + final AsyncRunner asyncRunner, + final boolean refresh, + final ProposerConfigLoader proposerConfigLoader, + final URL source) { + super(asyncRunner, refresh, proposerConfigLoader); + 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/loader/ProposerConfigLoader.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoader.java new file mode 100644 index 00000000000..54ca67f4202 --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoader.java @@ -0,0 +1,46 @@ +/* + * 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.loader; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.net.URL; +import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException; +import tech.pegasys.teku.infrastructure.http.UrlSanitizer; +import tech.pegasys.teku.provider.JsonProvider; +import tech.pegasys.teku.validator.client.ProposerConfig; + +public class ProposerConfigLoader { + final ObjectMapper objectMapper; + + public ProposerConfigLoader() { + this(new JsonProvider().getObjectMapper()); + } + + public ProposerConfigLoader(final ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public ProposerConfig getProposerConfig(final URL source) { + try { + return objectMapper.readValue(source, ProposerConfig.class); + } catch (IOException ex) { + + throw new InvalidConfigurationException( + "Failed to load proposer config from: " + + UrlSanitizer.sanitizePotentialUrl(source.toString()), + ex); + } + } +} diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/AbstractDutySchedulerTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/AbstractDutySchedulerTest.java index 31ebaab0027..c2720bf02f9 100644 --- a/validator/client/src/test/java/tech/pegasys/teku/validator/client/AbstractDutySchedulerTest.java +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/AbstractDutySchedulerTest.java @@ -56,7 +56,7 @@ public abstract class AbstractDutySchedulerTest { @BeforeEach public void setUp() { - when(validatorIndexProvider.getValidatorIndices(VALIDATOR_KEYS)) + when(validatorIndexProvider.getValidatorIndices()) .thenReturn(SafeFuture.completedFuture(VALIDATOR_INDICES)); final SafeFuture rejectAggregationSignature = SafeFuture.failedFuture(new UnsupportedOperationException("This test ignores aggregation")); diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/AttestationDutyLoaderTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/AttestationDutyLoaderTest.java index ff46a0a6ff9..4a12b8501b4 100644 --- a/validator/client/src/test/java/tech/pegasys/teku/validator/client/AttestationDutyLoaderTest.java +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/AttestationDutyLoaderTest.java @@ -79,7 +79,7 @@ class AttestationDutyLoaderTest { @BeforeEach void setUp() { - when(validatorIndexProvider.getValidatorIndices(any())) + when(validatorIndexProvider.getValidatorIndices()) .thenReturn(SafeFuture.completedFuture(VALIDATOR_INDICES)); when(forkProvider.getForkInfo(any())).thenReturn(SafeFuture.completedFuture(forkInfo)); } 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..b9dad0e0cde 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; @@ -63,41 +66,84 @@ void setUp(SpecContext specContext) { specContext.getDataStructureUtil().randomPublicKey(), mock(Signer.class), Optional::empty); + Validator validatorWithoutIndex = + new Validator( + specContext.getDataStructureUtil().randomPublicKey(), + mock(Signer.class), + Optional::empty); - Set validatorIndices = Set.of(validator1Index, validator2Index); - OwnedValidators validators = - new OwnedValidators( - Map.of(validator1.getPublicKey(), validator1, validator2.getPublicKey(), validator2)); - - feeRecipient = specContext.getDataStructureUtil().randomEth1Address(); + Map> validatorIndexesByPublicKey = + Map.of( + validator1.getPublicKey(), + Optional.of(validator1Index), + validator2.getPublicKey(), + Optional.of(validator2Index), + validatorWithoutIndex.getPublicKey(), + Optional.empty()); + + 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), feeRecipient), - new BeaconPreparableProposer(UInt64.valueOf(validator2Index), feeRecipient)); + 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), defaultFeeRecipient), + new BeaconPreparableProposer(UInt64.valueOf(validator2Index), defaultFeeRecipient)); } @TestTemplate @@ -116,4 +162,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/SyncCommitteeDutyLoaderTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/SyncCommitteeDutyLoaderTest.java index 6dc6efc49fb..9e1119fd714 100644 --- a/validator/client/src/test/java/tech/pegasys/teku/validator/client/SyncCommitteeDutyLoaderTest.java +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/SyncCommitteeDutyLoaderTest.java @@ -69,7 +69,7 @@ class SyncCommitteeDutyLoaderTest { @BeforeEach void setUp() { - when(validatorIndexProvider.getValidatorIndices(validators.getPublicKeys())) + when(validatorIndexProvider.getValidatorIndices()) .thenReturn(SafeFuture.completedFuture(validatorIndices)); } diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/ValidatorIndexProviderTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/ValidatorIndexProviderTest.java index fd7e83d8f3b..ee53a4363d7 100644 --- a/validator/client/src/test/java/tech/pegasys/teku/validator/client/ValidatorIndexProviderTest.java +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/ValidatorIndexProviderTest.java @@ -161,7 +161,7 @@ void shouldWaitForFirstSuccessfulRequestBeforeLookingUpValidatorIndices() { final SafeFuture> requestResult = new SafeFuture<>(); when(validatorApiChannel.getValidatorIndices(Set.of(key1))).thenReturn(requestResult); - final SafeFuture> result = provider.getValidatorIndices(List.of(key1)); + final SafeFuture> result = provider.getValidatorIndices(); assertThat(result).isNotDone(); provider.lookupValidators(); diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProviderTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProviderTest.java new file mode 100644 index 00000000000..76aa9938148 --- /dev/null +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/proposerconfig/ProposerConfigProviderTest.java @@ -0,0 +1,136 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.async.StubAsyncRunner; +import tech.pegasys.teku.infrastructure.ssz.type.Bytes20; +import tech.pegasys.teku.validator.client.ProposerConfig; +import tech.pegasys.teku.validator.client.proposerconfig.loader.ProposerConfigLoader; + +public class ProposerConfigProviderTest { + private static final String SOURCE = "some/file/path"; + + private final StubAsyncRunner asyncRunner = new StubAsyncRunner(); + private final ProposerConfigLoader proposerConfigLoader = mock(ProposerConfigLoader.class); + + private final ProposerConfig proposerConfigA = + new ProposerConfig(ImmutableMap.of(), new ProposerConfig.Config(Bytes20.ZERO)); + + private final ProposerConfig proposerConfigB = + new ProposerConfig( + ImmutableMap.of(), + new ProposerConfig.Config( + Bytes20.fromHexString("0x6e35733c5af9B61374A128e6F85f553aF09ff89A"))); + + private final ProposerConfigProvider proposerConfigProvider = + ProposerConfigProvider.create(asyncRunner, true, proposerConfigLoader, Optional.of(SOURCE)); + private final URL sourceUrl; + + public ProposerConfigProviderTest() throws MalformedURLException { + sourceUrl = new File(SOURCE).toURI().toURL(); + } + + @Test + void getProposerConfig_shouldReturnConfig() { + SafeFuture> futureMaybeConfig = + proposerConfigProvider.getProposerConfig(); + + assertThat(futureMaybeConfig).isNotCompleted(); + + when(proposerConfigLoader.getProposerConfig(sourceUrl)).thenReturn(proposerConfigA); + asyncRunner.executeQueuedActions(); + + assertThat(futureMaybeConfig).isCompletedWithValue(Optional.of(proposerConfigA)); + } + + @Test + void getProposerConfig_onErrorShouldThrowWhenNoLastConfigAvailable() { + SafeFuture> futureMaybeConfig = + proposerConfigProvider.getProposerConfig(); + + when(proposerConfigLoader.getProposerConfig(sourceUrl)) + .thenThrow(new RuntimeException("error")); + asyncRunner.executeQueuedActions(); + + assertThat(futureMaybeConfig).isCompletedExceptionally(); + } + + @Test + void getProposerConfig_onErrorShouldReturnLastConfigWhenLastConfigAvailable() { + SafeFuture> futureMaybeConfig = + proposerConfigProvider.getProposerConfig(); + + when(proposerConfigLoader.getProposerConfig(sourceUrl)).thenReturn(proposerConfigA); + asyncRunner.executeQueuedActions(); + + assertThat(futureMaybeConfig).isCompletedWithValue(Optional.of(proposerConfigA)); + + futureMaybeConfig = proposerConfigProvider.getProposerConfig(); + + when(proposerConfigLoader.getProposerConfig(sourceUrl)) + .thenThrow(new RuntimeException("error")); + asyncRunner.executeQueuedActions(); + + assertThat(futureMaybeConfig).isCompletedWithValue(Optional.of(proposerConfigA)); + } + + @Test + void getProposerConfig_onConcurrentCallsShouldMergeFutures() { + SafeFuture> futureMaybeConfig = + proposerConfigProvider.getProposerConfig(); + + SafeFuture> futureMaybeConfig2 = + proposerConfigProvider.getProposerConfig(); + assertThat(futureMaybeConfig2).isEqualTo(futureMaybeConfig); + assertThat(futureMaybeConfig2).isNotCompleted(); + + when(proposerConfigLoader.getProposerConfig(sourceUrl)).thenReturn(proposerConfigA); + asyncRunner.executeQueuedActions(); + + assertThat(futureMaybeConfig).isCompletedWithValue(Optional.of(proposerConfigA)); + } + + @Test + void getProposerConfig_shouldAlwaysReturnFirstValidConfigWhenRefreshIsFalse() { + final ProposerConfigProvider proposerConfigProvider = + ProposerConfigProvider.create( + asyncRunner, false, proposerConfigLoader, Optional.of(SOURCE)); + + SafeFuture> futureMaybeConfig = + proposerConfigProvider.getProposerConfig(); + + assertThat(futureMaybeConfig).isNotCompleted(); + + when(proposerConfigLoader.getProposerConfig(sourceUrl)).thenReturn(proposerConfigA); + asyncRunner.executeQueuedActions(); + + assertThat(futureMaybeConfig).isCompletedWithValue(Optional.of(proposerConfigA)); + + when(proposerConfigLoader.getProposerConfig(sourceUrl)).thenReturn(proposerConfigB); + + futureMaybeConfig = proposerConfigProvider.getProposerConfig(); + assertThat(futureMaybeConfig).isCompletedWithValue(Optional.of(proposerConfigA)); + } +} diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoaderTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoaderTest.java new file mode 100644 index 00000000000..07a241229a0 --- /dev/null +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/proposerconfig/loader/ProposerConfigLoaderTest.java @@ -0,0 +1,104 @@ +/* + * 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.loader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.io.Resources; +import java.net.URL; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.infrastructure.ssz.type.Bytes20; +import tech.pegasys.teku.validator.client.ProposerConfig; +import tech.pegasys.teku.validator.client.ProposerConfig.Config; + +public class ProposerConfigLoaderTest { + private final ProposerConfigLoader loader = new ProposerConfigLoader(); + + @Test + void shouldLoadValidConfigFromUrl() { + final URL resource = Resources.getResource("proposerConfigValid1.json"); + + validateContent1(loader.getProposerConfig(resource)); + } + + @Test + void shouldLoadConfigWithEmptyProposerConfig() { + final URL resource = Resources.getResource("proposerConfigValid2.json"); + + validateContent2(loader.getProposerConfig(resource)); + } + + @Test + void shouldNotLoadInvalidPubKey() { + final URL resource = Resources.getResource("proposerConfigInvalid1.json"); + + assertThatThrownBy(() -> loader.getProposerConfig(resource)); + } + + @Test + void shouldNotLoadNullFeeRecipient() { + final URL resource = Resources.getResource("proposerConfigInvalid2.json"); + + assertThatThrownBy(() -> loader.getProposerConfig(resource)); + } + + @Test + void shouldNotLoadInvalidFeeRecipient() { + final URL resource = Resources.getResource("proposerConfigInvalid2.json"); + + assertThatThrownBy(() -> loader.getProposerConfig(resource)); + } + + @Test + void shouldNotLoadMissingFeeRecipient() { + final URL resource = Resources.getResource("proposerConfigInvalid4.json"); + + assertThatThrownBy(() -> loader.getProposerConfig(resource)); + } + + @Test + void shouldNotLoadMissingDefault() { + final URL resource = Resources.getResource("proposerConfigInvalid5.json"); + + assertThatThrownBy(() -> loader.getProposerConfig(resource)); + } + + private void validateContent1(ProposerConfig config) { + Optional theConfig = + config.getConfigForPubKey( + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a"); + assertThat(theConfig).isPresent(); + assertThat(theConfig.get().getFeeRecipient()) + .isEqualTo(Bytes20.fromHexString("0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3")); + + Optional defaultConfig = config.getDefaultConfig(); + assertThat(defaultConfig).isPresent(); + assertThat(defaultConfig.get().getFeeRecipient()) + .isEqualTo(Bytes20.fromHexString("0x6e35733c5af9B61374A128e6F85f553aF09ff89A")); + } + + private void validateContent2(ProposerConfig config) { + Optional theConfig = + config.getConfigForPubKey( + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a"); + assertThat(theConfig).isEmpty(); + + Optional defaultConfig = config.getDefaultConfig(); + assertThat(defaultConfig).isPresent(); + assertThat(defaultConfig.get().getFeeRecipient()) + .isEqualTo(Bytes20.fromHexString("0x6e35733c5af9B61374A128e6F85f553aF09ff89A")); + } +} diff --git a/validator/client/src/test/resources/proposerConfigInvalid1.json b/validator/client/src/test/resources/proposerConfigInvalid1.json new file mode 100644 index 00000000000..58e2ea9f666 --- /dev/null +++ b/validator/client/src/test/resources/proposerConfigInvalid1.json @@ -0,0 +1,10 @@ +{ + "proposer_config": { + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd": { + "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3" + } + }, + "default_config": { + "fee_recipient": "0x6e35733c5af9B61374A128e6F85f553aF09ff89A" + } +} \ No newline at end of file diff --git a/validator/client/src/test/resources/proposerConfigInvalid2.json b/validator/client/src/test/resources/proposerConfigInvalid2.json new file mode 100644 index 00000000000..9ffef435310 --- /dev/null +++ b/validator/client/src/test/resources/proposerConfigInvalid2.json @@ -0,0 +1,10 @@ +{ + "proposer_config": { + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": { + "fee_recipient": null + } + }, + "default_config": { + "fee_recipient": "0x6e35733c5af9B61374A128e6F85f553aF09ff89A" + } +} \ No newline at end of file diff --git a/validator/client/src/test/resources/proposerConfigInvalid3.json b/validator/client/src/test/resources/proposerConfigInvalid3.json new file mode 100644 index 00000000000..665e07a2acd --- /dev/null +++ b/validator/client/src/test/resources/proposerConfigInvalid3.json @@ -0,0 +1,10 @@ +{ + "proposer_config": { + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": { + "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeF" + } + }, + "default_config": { + "fee_recipient": "0x6e35733c5af9B61374A128e6F85f553aF09ff89A" + } +} \ No newline at end of file diff --git a/validator/client/src/test/resources/proposerConfigInvalid4.json b/validator/client/src/test/resources/proposerConfigInvalid4.json new file mode 100644 index 00000000000..a7f21253019 --- /dev/null +++ b/validator/client/src/test/resources/proposerConfigInvalid4.json @@ -0,0 +1,9 @@ +{ + "proposer_config": { + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": { + "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3" + } + }, + "default_config": { + } +} \ No newline at end of file diff --git a/validator/client/src/test/resources/proposerConfigInvalid5.json b/validator/client/src/test/resources/proposerConfigInvalid5.json new file mode 100644 index 00000000000..26c2adbcdf7 --- /dev/null +++ b/validator/client/src/test/resources/proposerConfigInvalid5.json @@ -0,0 +1,7 @@ +{ + "proposer_config": { + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": { + "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3" + } + } +} \ No newline at end of file diff --git a/validator/client/src/test/resources/proposerConfigValid1.json b/validator/client/src/test/resources/proposerConfigValid1.json new file mode 100644 index 00000000000..902c4c2c805 --- /dev/null +++ b/validator/client/src/test/resources/proposerConfigValid1.json @@ -0,0 +1,11 @@ +{ + "proposer_config": { + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": { + "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3", + "future_param": "future value" + } + }, + "default_config": { + "fee_recipient": "0x6e35733c5af9B61374A128e6F85f553aF09ff89A" + } +} \ No newline at end of file diff --git a/validator/client/src/test/resources/proposerConfigValid2.json b/validator/client/src/test/resources/proposerConfigValid2.json new file mode 100644 index 00000000000..7338aeb662c --- /dev/null +++ b/validator/client/src/test/resources/proposerConfigValid2.json @@ -0,0 +1,6 @@ +{ + "default_config": { + "fee_recipient": "0x6e35733c5af9B61374A128e6F85f553aF09ff89A", + "future_param": "future value" + } +} \ No newline at end of file