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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
### Breaking Changes

### Additions and Improvements
- Added `--p2p-static-peers-url` option to read static peers from a URL or file

### Bug Fixes
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,13 @@ public TekuNodeConfigBuilder withPeers(final TekuBeaconNode... nodes) {
return this;
}

public TekuNodeConfigBuilder withPeersUrl(final String peersUrl) {
mustBe(NodeType.BEACON_NODE);
LOG.debug("p2p-static-peers-url={}", peersUrl);
configMap.put("p2p-static-peers-url", peersUrl);
return this;
}

public TekuNodeConfigBuilder withExternalSignerUrl(final String externalSignerUrl) {
LOG.debug("validators-external-signer-url={}", externalSignerUrl);
configMap.put("validators-external-signer-url", externalSignerUrl);
Expand Down
55 changes: 51 additions & 4 deletions teku/src/main/java/tech/pegasys/teku/cli/options/P2POptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,23 @@
import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig.DEFAULT_P2P_PEERS_UPPER_BOUND_ALL_SUBNETS;
import static tech.pegasys.teku.validator.api.ValidatorConfig.DEFAULT_EXECUTOR_MAX_QUEUE_SIZE_ALL_SUBNETS;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.stream.Collectors;
import picocli.CommandLine.Help.Visibility;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;
import tech.pegasys.teku.beacon.sync.SyncConfig;
import tech.pegasys.teku.cli.converter.OptionalIntConverter;
import tech.pegasys.teku.config.TekuConfiguration;
import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException;
import tech.pegasys.teku.infrastructure.io.resource.ResourceLoader;
import tech.pegasys.teku.networking.eth2.P2PConfig;
import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig;
import tech.pegasys.teku.networking.p2p.gossip.config.GossipConfig;
Expand Down Expand Up @@ -209,14 +216,22 @@ The network interface(s) on which the node listens for P2P communication.
hidden = true)
private Integer minimumRandomlySelectedPeerCount;

@Option(
names = {"--p2p-static-peers-url"},
paramLabel = "<URL>",
description =
"Specifies a URL or file containing a list of 'static' peers (one per line) with which to establish and maintain connections. Accepts multiaddr format.",
arity = "1")
private String p2pStaticPeersUrl;

@Option(
names = {"--p2p-static-peers"},
paramLabel = "<PEER_ADDRESSES>",
description =
"Specifies a list of 'static' peers with which to establish and maintain connections",
"Specifies a comma-separated list of 'static' peers with which to establish and maintain connections. Accepts multiaddr format.",
split = ",",
arity = "0..*")
private List<String> p2pStaticPeers = new ArrayList<>();
arity = "1")
private final List<String> p2pStaticPeers = new ArrayList<>();

@Option(
names = {"--p2p-direct-peers"},
Expand Down Expand Up @@ -453,6 +468,38 @@ private OptionalInt getP2pUpperBound() {
return p2pUpperBound;
}

private List<String> getStaticPeersList() {
final List<String> staticPeers = new ArrayList<>(p2pStaticPeers);

if (p2pStaticPeersUrl != null) {
try {
final Optional<InputStream> maybeStream =
ResourceLoader.urlOrFile().load(p2pStaticPeersUrl);
Comment thread
rolfyone marked this conversation as resolved.
if (maybeStream.isPresent()) {
try (final BufferedReader reader =
new BufferedReader(
new InputStreamReader(maybeStream.get(), StandardCharsets.UTF_8))) {
final List<String> peersFromUrl =
reader
.lines()
.map(String::trim)
.filter(line -> !line.isEmpty() && !line.startsWith("#"))
.collect(Collectors.toList());
staticPeers.addAll(peersFromUrl);
}
} else {
throw new InvalidConfigurationException(
String.format("Static peers URL not found: %s", p2pStaticPeersUrl));
}
} catch (Exception e) {
throw new InvalidConfigurationException(
String.format("Failed to read static peers from URL: %s", p2pStaticPeersUrl), e);
}
}

return staticPeers;
}

public void configure(final TekuConfiguration.Builder builder) {
// From a discovery configuration perspective, direct peers are static peers
p2pStaticPeers.addAll(p2pDirectPeers);
Expand Down Expand Up @@ -507,7 +554,7 @@ public void configure(final TekuConfiguration.Builder builder) {
d.advertisedUdpPortIpv6(OptionalInt.of(p2pAdvertisedPortIpv6));
}
d.isDiscoveryEnabled(p2pDiscoveryEnabled)
.staticPeers(p2pStaticPeers)
.staticPeers(getStaticPeersList())
.siteLocalAddressesEnabled(siteLocalAddressesEnabled);
})
.network(
Expand Down
120 changes: 120 additions & 0 deletions teku/src/test/java/tech/pegasys/teku/cli/options/P2POptionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static tech.pegasys.teku.infrastructure.async.AsyncRunnerFactory.DEFAULT_MAX_QUEUE_SIZE_ALL_SUBNETS;
import static tech.pegasys.teku.networking.eth2.P2PConfig.DEFAULT_GOSSIP_BLOBS_AFTER_BLOCK_ENABLED;
Expand All @@ -27,8 +28,18 @@
import static tech.pegasys.teku.validator.api.ValidatorConfig.DEFAULT_EXECUTOR_MAX_QUEUE_SIZE_ALL_SUBNETS;

import com.google.common.base.Supplier;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import tech.pegasys.teku.beacon.sync.SyncConfig;
import tech.pegasys.teku.cli.AbstractBeaconNodeCommandTest;
import tech.pegasys.teku.config.TekuConfiguration;
Expand Down Expand Up @@ -79,6 +90,23 @@ public void shouldReadFromConfigurationFile() {
assertThat(syncConfig.getForwardSyncMaxBlobSidecarsPerMinute()).isEqualTo(400);
}

@Test
public void shouldReadUrlFromConfigurationFile(@TempDir final Path tempDir) throws Exception {
final Path peersFile = tempDir.resolve("peers.txt");
final Path configPath = tempDir.resolve("config.yaml");
Files.writeString(peersFile, "\n\n127.0.1.1\n127.1.1.1\n", StandardCharsets.UTF_8);
Files.writeString(
configPath,
String.format("p2p-static-peers-url: \"%s\"", peersFile.toAbsolutePath()),
StandardCharsets.UTF_8);

final TekuConfiguration tekuConfig =
getTekuConfigurationFromArguments("--config-file", configPath.toAbsolutePath().toString());

final DiscoveryConfig discoConfig = tekuConfig.discovery();
assertThat(discoConfig.getStaticPeers()).isEqualTo(List.of("127.0.1.1", "127.1.1.1"));
}

@Test
public void p2pEnabled_shouldNotRequireAValue() {
final TekuConfiguration config = getTekuConfigurationFromArguments("--p2p-enabled");
Expand Down Expand Up @@ -472,4 +500,96 @@ public void defaultPortsAreSetCorrectly() {
assertThat(networkConfig.getAdvertisedPort()).isEqualTo(DEFAULT_P2P_PORT);
assertThat(networkConfig.getAdvertisedPortIpv6()).isEqualTo(DEFAULT_P2P_PORT_IPV6);
}

@Test
public void staticPeersUrl_shouldReadPeersFromUrl(@TempDir final Path tempDir) throws Exception {
// Create a test file with peers
final Path peersFile = tempDir.resolve("static-peers.txt");
Files.writeString(peersFile, "peer1\npeer2\n#comment\n\npeer3");
Comment thread
rolfyone marked this conversation as resolved.

final TekuConfiguration tekuConfiguration =
getTekuConfigurationFromArguments(
"--p2p-static-peers-url", peersFile.toAbsolutePath().toString());

assertThat(tekuConfiguration.discovery().getStaticPeers())
.containsExactlyInAnyOrder("peer1", "peer2", "peer3");
}

@Test
public void staticPeersUrl_shouldCombineWithCommandLinePeers(@TempDir final Path tempDir)
throws Exception {
// Create a test file with peers
final Path peersFile = tempDir.resolve("static-peers.txt");
Files.writeString(peersFile, "peer1\npeer2");

final TekuConfiguration tekuConfiguration =
getTekuConfigurationFromArguments(
"--p2p-static-peers-url",
peersFile.toAbsolutePath().toString(),
"--p2p-static-peers",
"peer3,peer4");

assertThat(tekuConfiguration.discovery().getStaticPeers())
.containsExactlyInAnyOrder("peer1", "peer2", "peer3", "peer4");
}

@ParameterizedTest
@MethodSource("peerBoundsTestParameters")
public void shouldSmartDefaultPeerBounds(
final String lowerIn,
final String upperIn,
final int lowerExpected,
final int upperExpected) {
final TekuConfiguration tekuConfiguration =
getTekuConfigurationFromArguments(
"--p2p-peer-upper-bound", upperIn, "--p2p-peer-lower-bound", lowerIn);

assertThat(tekuConfiguration.discovery().getMinPeers()).isEqualTo(lowerExpected);
assertThat(tekuConfiguration.discovery().getMaxPeers()).isEqualTo(upperExpected);
}

private static Stream<Arguments> peerBoundsTestParameters() {
return Stream.of(
Arguments.of("100", "200", 100, 200),
Arguments.of("100", "100", 100, 100),
Arguments.of("0", "0", 0, 0),
Arguments.of("1024000", "1024000", 1024000, 1024000),
Arguments.of("200", "100", 100, 200));
}

@Test
public void peersLowerBound_mustNotBeNegative() {
assertThatThrownBy(() -> getTekuConfigurationFromArguments("--p2p-peer-lower-bound", "-1"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Invalid minPeers: -1");
}

@Test
public void peersUpperBound_mustNotBeNegative() {
assertThatThrownBy(() -> getTekuConfigurationFromArguments("--p2p-peer-upper-bound", "-1"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Invalid maxPeers: -1");
}

@Test
public void staticPeersUrl_shouldThrowIfUrlDoesNotExist() {
// Create a dummy instance of P2POptions
final P2POptions p2pOptions = new P2POptions();

// Use reflection to access a private field and set its value
try {
final Field field = P2POptions.class.getDeclaredField("p2pStaticPeersUrl");
field.setAccessible(true);
field.set(p2pOptions, "/non/existent/file.txt");

// Use reflection to call a private method getStaticPeersList
final Method method = P2POptions.class.getDeclaredMethod("getStaticPeersList");
method.setAccessible(true);

assertThatThrownBy(() -> method.invoke(p2pOptions))
.hasCauseInstanceOf(InvalidConfigurationException.class);
} catch (Exception e) {
fail("Test setup failed: " + e.getMessage(), e);
}
}
}
2 changes: 1 addition & 1 deletion teku/src/test/resources/P2POptions_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ Xp2p-historical-sync-batch-size: 102
Xp2p-sync-batch-size: 103
Xp2p-sync-max-pending-batches: 8
Xp2p-sync-blocks-rate-limit: 100
Xp2p-sync-blob-sidecars-rate-limit: 400
Xp2p-sync-blob-sidecars-rate-limit: 400