Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public interface SpecConfigGloas extends SpecConfigFulu, NetworkingSpecConfigGlo
UInt64 BUILDER_PAYMENT_THRESHOLD_DENOMINATOR = UInt64.valueOf(10);
int PAYLOAD_BUILDER_VERSION = 0;
// builder-specs
UInt64 MAX_EXECUTION_PAYMENT = UInt64.MAX_VALUE; // 2**64 - 1
long MAX_BUILDER_AUTH_DATA_SIZE = 4096;

static SpecConfigGloas required(final SpecConfig specConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

package tech.pegasys.teku.cli.options;

import static com.google.common.base.Strings.isNullOrEmpty;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
Expand Down Expand Up @@ -57,6 +59,20 @@ Percentage multiplier applied to a bid (p2p or builder): `0` prefers the local
converter = UInt64Converter.class)
private UInt64 builderBoostFactor = ValidatorConfig.DEFAULT_BUILDER_BOOST_FACTOR;

@CommandLine.Option(
names = {"--Xbuilder-max-execution-payment"},
paramLabel = "<uint64>",
showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
description =
"""
The maximum execution layer payment (in Gwei) that counts when a builder's bid is valued.
`0` is the default which counts no execution payment, leaving the bid valued at its trustless value alone.
This parameter may be adjusted based on the level of trust in the builder's reliability and reputation.""",
arity = "1",
hidden = true,
converter = UInt64Converter.class)
private UInt64 builderMaxExecutionPayment = ValidatorConfig.DEFAULT_BUILDER_MAX_EXECUTION_PAYMENT;

@CommandLine.Option(
names = {"--Xbuilder-urls"},
paramLabel = "<url>",
Expand All @@ -72,6 +88,7 @@ public void configure(final TekuConfiguration.Builder builder) {
config
.builderMinBid(builderMinBid)
.builderBoostFactor(builderBoostFactor)
.builderMaxExecutionPayment(builderMaxExecutionPayment)
.builderUrls(parseBuilderUrls()));
}

Expand All @@ -81,7 +98,12 @@ private List<URL> parseBuilderUrls() {

private URL parseBuilderUrl(final String builderUrl) {
try {
return URI.create(builderUrl).toURL();
final URL parsedBuilderUrl = URI.create(builderUrl).toURL();
if (isNullOrEmpty(parsedBuilderUrl.getHost())) {
throw new InvalidConfigurationException(
"Invalid configuration. Builder URL (" + builderUrl + ") must contain a valid host");
}
return parsedBuilderUrl;
} catch (IllegalArgumentException | MalformedURLException e) {
throw new InvalidConfigurationException(
"Invalid configuration. Builder URL (" + builderUrl + ") has invalid syntax", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ public void shouldParseBuilderOptions() {
"42",
"--Xbuilder-boost-factor",
"80",
"--Xbuilder-max-execution-payment",
"67",
"--Xbuilder-urls",
"https://foobar.com,https://plataberget.com"
};
Expand All @@ -495,6 +497,7 @@ public void shouldParseBuilderOptions() {
beaconNodeCommand.tekuConfiguration().validatorClient().getValidatorConfig();
assertThat(validatorConfig.getBuilderMinBid()).isEqualTo(UInt64.valueOf(42));
assertThat(validatorConfig.getBuilderBoostFactor()).isEqualTo(UInt64.valueOf(80));
assertThat(validatorConfig.getBuilderMaxExecutionPayment()).isEqualTo(UInt64.valueOf(67));
assertThat(validatorConfig.getBuilderUrls()).hasSize(2);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public class ValidatorConfig {
// Builder default options
public static final UInt64 DEFAULT_BUILDER_MIN_BID = UInt64.ZERO;
public static final UInt64 DEFAULT_BUILDER_BOOST_FACTOR = UInt64.valueOf(90);
// leave the bid valued at its trustless value alone
public static final UInt64 DEFAULT_BUILDER_MAX_EXECUTION_PAYMENT = UInt64.ZERO;

private final List<String> validatorKeys;
private final List<String> validatorExternalSignerPublicKeySources;
Expand Down Expand Up @@ -123,6 +125,7 @@ public class ValidatorConfig {
// Builder options
private final UInt64 builderMinBid;
private final UInt64 builderBoostFactor;
private final UInt64 builderMaxExecutionPayment;
private final List<URL> builderUrls;

private ValidatorConfig(
Expand Down Expand Up @@ -168,6 +171,7 @@ private ValidatorConfig(
final boolean attestationsV2ApisEnabled,
final UInt64 builderMinBid,
final UInt64 builderBoostFactor,
final UInt64 builderMaxExecutionPayment,
final List<URL> builderUrls) {
this.validatorKeys = validatorKeys;
this.validatorExternalSignerPublicKeySources = validatorExternalSignerPublicKeySources;
Expand Down Expand Up @@ -216,6 +220,7 @@ private ValidatorConfig(
this.attestationsV2ApisEnabled = attestationsV2ApisEnabled;
this.builderMinBid = builderMinBid;
this.builderBoostFactor = builderBoostFactor;
this.builderMaxExecutionPayment = builderMaxExecutionPayment;
this.builderUrls = builderUrls;

LOG.debug(
Expand Down Expand Up @@ -407,6 +412,10 @@ public UInt64 getBuilderBoostFactor() {
return builderBoostFactor;
}

public UInt64 getBuilderMaxExecutionPayment() {
return builderMaxExecutionPayment;
}

public List<URL> getBuilderUrls() {
return builderUrls;
}
Expand Down Expand Up @@ -467,6 +476,7 @@ public static final class Builder {
private boolean attestationsV2ApisEnabled = DEFAULT_ATTESTATIONS_V2_APIS_ENABLED;
private UInt64 builderMinBid = DEFAULT_BUILDER_MIN_BID;
private UInt64 builderBoostFactor = DEFAULT_BUILDER_BOOST_FACTOR;
private UInt64 builderMaxExecutionPayment = DEFAULT_BUILDER_MAX_EXECUTION_PAYMENT;
private List<URL> builderUrls = new ArrayList<>();

private Builder() {}
Expand Down Expand Up @@ -751,6 +761,11 @@ public Builder builderBoostFactor(final UInt64 builderBoostFactor) {
return this;
}

public Builder builderMaxExecutionPayment(final UInt64 builderMaxExecutionPayment) {
this.builderMaxExecutionPayment = builderMaxExecutionPayment;
return this;
}

public Builder builderUrls(final List<URL> builderUrls) {
this.builderUrls = builderUrls;
return this;
Expand Down Expand Up @@ -804,6 +819,7 @@ public ValidatorConfig build() {
attestationsV2ApisEnabled,
builderMinBid,
builderBoostFactor,
builderMaxExecutionPayment,
builderUrls);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@

package tech.pegasys.teku.validator.client;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.InetAddresses;
import java.net.IDN;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.tuweni.bytes.Bytes;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.collections.cache.LRUCache;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.config.SpecConfigGloas;
import tech.pegasys.teku.spec.datastructures.builder.versions.gloas.BuilderConfig;
import tech.pegasys.teku.spec.datastructures.builder.versions.gloas.BuilderConfigSchema;
import tech.pegasys.teku.spec.datastructures.builder.versions.gloas.BuilderEntry;
import tech.pegasys.teku.spec.datastructures.builder.versions.gloas.BuilderRequestAuth;
import tech.pegasys.teku.spec.datastructures.builder.versions.gloas.SignedBuilderRequestAuth;
Expand All @@ -32,6 +38,9 @@

public class BuilderConfigProvider {

private final LRUCache<String, Bytes> cachedDefaultAuthDataByHost =
LRUCache.create((int) BuilderConfigSchema.MAX_BUILDER_ENTRIES);

private final Spec spec;
private final ValidatorConfig validatorConfig;

Expand All @@ -47,13 +56,10 @@ public SafeFuture<Optional<BuilderConfig>> getBuilderConfig(
}
final Stream<SafeFuture<BuilderEntry>> builderEntriesFutures =
validatorConfig.getBuilderUrls().stream()
.map(url -> Bytes.of(url.toExternalForm().getBytes(StandardCharsets.UTF_8)))
.map(
builderUrl -> {
final BuilderRequestAuth auth =
ApiSchemas.BUILDER_REQUEST_AUTH_SCHEMA.create(
// default to the UTF-8 bytes of the builder's own advertised URL
builderUrl, slot);
ApiSchemas.BUILDER_REQUEST_AUTH_SCHEMA.create(getAuthData(builderUrl), slot);
// Authenticates bid requests to the builder
return validator
.getSigner()
Expand All @@ -64,10 +70,10 @@ public SafeFuture<Optional<BuilderConfig>> getBuilderConfig(
ApiSchemas.SIGNED_BUILDER_REQUEST_AUTH_SCHEMA.create(
auth, signature);
return ApiSchemas.BUILDER_ENTRY_SCHEMA.create(
builderUrl,
Bytes.of(builderUrl.toString().getBytes(StandardCharsets.UTF_8)),
signedAuth,
List.of(),
SpecConfigGloas.MAX_EXECUTION_PAYMENT,
validatorConfig.getBuilderMaxExecutionPayment(),
validatorConfig.getBuilderMinBid(),
validatorConfig.getBuilderBoostFactor());
});
Expand All @@ -85,4 +91,23 @@ public SafeFuture<Optional<BuilderConfig>> getBuilderConfig(
private boolean isBuilderConfigRequired(final UInt64 slot) {
return spec.atSlot(slot).getMilestone().isGreaterThanOrEqualTo(SpecMilestone.GLOAS);
}

private Bytes getAuthData(final URL builderUrl) {
return cachedDefaultAuthDataByHost.get(
builderUrl.getHost(),
__ -> Bytes.of(getDefaultAuthData(builderUrl).getBytes(StandardCharsets.US_ASCII)));
}

// Spec: hostname lowercased, ASCII, IPv6 in compressed form (RFC 5952) inside brackets
@VisibleForTesting
String getDefaultAuthData(final URL builderUrl) {
final String host = builderUrl.getHost();
// Handle IPv6 inside brackets
if (host.startsWith("[") && host.endsWith("]")) {
final String ipv6 = host.substring(1, host.length() - 1);
// toAddrString from Guava follows RFC 5952
return "[" + InetAddresses.toAddrString(InetAddresses.forString(ipv6)) + "]";
}
return IDN.toASCII(host).toLowerCase(Locale.ROOT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static tech.pegasys.teku.infrastructure.async.SafeFuture.completedFuture;
import static tech.pegasys.teku.spec.config.SpecConfigGloas.MAX_EXECUTION_PAYMENT;

import java.net.MalformedURLException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import tech.pegasys.teku.bls.BLSSignature;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
Expand Down Expand Up @@ -66,18 +69,20 @@ void shouldReturnBuilderConfigForGloas() throws MalformedURLException {
final UInt64 slot = UInt64.valueOf(42);
final UInt64 minBid = UInt64.valueOf(100);
final UInt64 boostFactor = UInt64.valueOf(80);
final UInt64 maxExecutionPayment = UInt64.valueOf(67);
final BLSSignature signature = dataStructureUtil.randomSignature();

final ValidatorConfig config =
ValidatorConfig.builder()
.builderUrls(List.of(URI.create(builderUrl).toURL()))
.builderMinBid(minBid)
.builderBoostFactor(boostFactor)
.builderMaxExecutionPayment(maxExecutionPayment)
.build();

final BuilderRequestAuth expectedAuth =
ApiSchemas.BUILDER_REQUEST_AUTH_SCHEMA.create(
Bytes.of(builderUrl.getBytes(StandardCharsets.UTF_8)), slot);
Bytes.of("builder.example.com".getBytes(StandardCharsets.US_ASCII)), slot);
when(signer.signBuilderRequestAuth(expectedAuth)).thenReturn(completedFuture(signature));

final BuilderConfigProvider provider = new BuilderConfigProvider(spec, config);
Expand All @@ -95,6 +100,29 @@ void shouldReturnBuilderConfigForGloas() throws MalformedURLException {
assertThat(entry.getAuth().getSignature()).isEqualTo(signature);
assertThat(entry.getMinBid()).isEqualTo(minBid);
assertThat(entry.getBuilderBoostFactor()).isEqualTo(boostFactor);
assertThat(entry.getMaxExecutionPayment()).isEqualTo(MAX_EXECUTION_PAYMENT);
assertThat(entry.getMaxExecutionPayment()).isEqualTo(maxExecutionPayment);
}

@ParameterizedTest
@MethodSource("authDataTestCases")
void shouldGetDefaultAuthData(final String builderUrl, final String expectedAuthData)
throws MalformedURLException {
final BuilderConfigProvider provider =
new BuilderConfigProvider(spec, ValidatorConfig.builder().build());
assertThat(provider.getDefaultAuthData(URI.create(builderUrl).toURL()))
.isEqualTo(expectedAuthData);
}

static Stream<Arguments> authDataTestCases() {
return Stream.of(
Arguments.of("https://builder.example.com/", "builder.example.com"),
Arguments.of("HTTPS://Builder.Example.com:443/bids?x=1", "builder.example.com"),
Arguments.of("https://builder.example.com:8080", "builder.example.com"),
Arguments.of("https://user:pw@builder.example.com/", "builder.example.com"),
Arguments.of("https://10.0.0.5:18550/eth/v1/builder", "10.0.0.5"),
Arguments.of("https://[0:0:0:0:0:0:0:1]:8443/", "[::1]"),
Arguments.of("https://[2001:db8::192.0.2.1]/", "[2001:db8::c000:201]"),
Arguments.of("https://bücher.example.org/bids", "xn--bcher-kva.example.org"),
Arguments.of("https://platåberget.dev", "xn--platberget-45a.dev"));
}
}
Loading