Skip to content

Commit

Permalink
Add support for multiple fee recipients (#4894)
Browse files Browse the repository at this point in the history
Add support for multiple fee recipients
  • Loading branch information
tbenr authored Feb 1, 2022
1 parent 44b73e9 commit 6992eb4
Show file tree
Hide file tree
Showing 28 changed files with 961 additions and 104 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -26,7 +27,31 @@ public class ValidatorProposerOptions {
hidden = true)
private String proposerDefaultFeeRecipient = null;

@Option(
names = {"--Xvalidators-proposer-config"},
paramLabel = "<STRING>",
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 = "<BOOLEAN>",
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bytes32> DEFAULT_GRAFFITI = Optional.empty();
public static final boolean DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_ENABLED = false;

private final List<String> validatorKeys;
private final List<String> validatorExternalSignerPublicKeySources;
Expand All @@ -58,6 +59,8 @@ public class ValidatorConfig {
private final boolean useDependentRoots;
private final boolean generateEarlyAttestations;
private final Optional<Eth1Address> proposerDefaultFeeRecipient;
private final Optional<String> proposerConfigSource;
private final boolean refreshProposerConfigFromSource;

private ValidatorConfig(
final List<String> validatorKeys,
Expand All @@ -76,7 +79,9 @@ private ValidatorConfig(
final int validatorExternalSignerConcurrentRequestLimit,
final boolean useDependentRoots,
final boolean generateEarlyAttestations,
final Optional<Eth1Address> proposerDefaultFeeRecipient) {
final Optional<Eth1Address> proposerDefaultFeeRecipient,
final Optional<String> proposerConfigSource,
final boolean refreshProposerConfigFromSource) {
this.validatorKeys = validatorKeys;
this.validatorExternalSignerPublicKeySources = validatorExternalSignerPublicKeySources;
this.validatorExternalSignerUrl = validatorExternalSignerUrl;
Expand All @@ -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() {
Expand Down Expand Up @@ -161,15 +168,25 @@ public boolean useDependentRoots() {
}

public Optional<Eth1Address> getProposerDefaultFeeRecipient() {
validateProposerDefaultFeeRecipient();
validateProposerDefaultFeeRecipientOrProposerConfigSource();
return proposerDefaultFeeRecipient;
}

private void validateProposerDefaultFeeRecipient() {
public Optional<String> 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");
}
}

Expand All @@ -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<Eth1Address> proposerDefaultFeeRecipient = Optional.empty();
private Optional<String> proposerConfigSource = Optional.empty();
private boolean refreshProposerConfigFromSource =
DEFAULT_VALIDATOR_PROPOSER_CONFIG_REFRESH_ENABLED;

private Builder() {}

Expand Down Expand Up @@ -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();
Expand All @@ -326,7 +356,9 @@ public ValidatorConfig build() {
validatorExternalSignerConcurrentRequestLimit,
useDependentRoots,
generateEarlyAttestations,
proposerDefaultFeeRecipient);
proposerDefaultFeeRecipient,
proposerConfigSource,
refreshProposerConfigFromSource);
}

private void validateExternalSignerUrlAndPublicKeys() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand All @@ -39,15 +38,15 @@ public void shouldThrowExceptionIfExternalPublicKeysAreSpecifiedWithoutExternalS
}

@Test
public void noExceptionThrownIfExternalSignerUrlIsSpecifiedWithoutExternalPublicKeys()
public void shouldNotThrowIfExternalSignerUrlIsSpecifiedWithoutExternalPublicKeys()
throws MalformedURLException {
final ValidatorConfig.Builder builder =
configBuilder.validatorExternalSignerUrl(URI.create("http://localhost:9000").toURL());
Assertions.assertThatCode(builder::build).doesNotThrowAnyException();
}

@Test
public void noExceptionThrownIfBothExternalSignerUrlAndPublicKeysAreSpecified()
public void shouldNotThrowIfBothExternalSignerUrlAndPublicKeysAreSpecified()
throws MalformedURLException {
final ValidatorConfig.Builder builder =
configBuilder
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -79,7 +78,7 @@ public void shouldThrowExceptionIfExternalSignerKeystorePasswordFileIsSpecifiedW
}

@Test
public void noExceptionThrownIfBothExternalSignerKeystoreAndPasswordFileAreSpecified() {
public void shouldNotThrowIfBothExternalSignerKeystoreAndPasswordFileAreSpecified() {
final ValidatorConfig.Builder builder =
configBuilder
.validatorExternalSignerKeystore(Path.of("somepath"))
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -110,7 +108,7 @@ public void shouldThrowExceptionIfExternalSignerTruststoreSpecifiedWithoutPasswo
}

@Test
public void noExceptionThrownIfBothExternalSignerTruststoreAndPasswordFileAreSpecified() {
public void shouldNotThrowIfBothExternalSignerTruststoreAndPasswordFileAreSpecified() {
final ValidatorConfig.Builder builder =
configBuilder
.validatorExternalSignerTruststore(Path.of("somepath"))
Expand All @@ -120,7 +118,7 @@ public void noExceptionThrownIfBothExternalSignerTruststoreAndPasswordFileAreSpe
}

@Test
public void bellatrix_shouldThrowExceptionIfExternalSignerPublicKeySourcesIsSpecified()
public void bellatrix_shouldThrowIfExternalSignerPublicKeySourcesIsSpecified()
throws MalformedURLException {
final ValidatorConfig config =
configBuilder
Expand All @@ -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
Expand All @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ protected AbstractDutyLoader(
public SafeFuture<Optional<S>> loadDutiesForEpoch(final UInt64 epoch) {
LOG.trace("Requesting duties for epoch {}", epoch);
return validatorIndexProvider
.getValidatorIndices(validators.getPublicKeys())
.getValidatorIndices()
.thenCompose(
validatorIndices -> {
if (validatorIndices.isEmpty()) {
Expand Down
Loading

0 comments on commit 6992eb4

Please sign in to comment.