Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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 @@ -14,6 +14,7 @@
- Scheduled Electra for Mainnet at epoch 364032 (May 7, 2025, 10:05:11am UTC)
- Scheduled Electra for Gnosis at epoch 1337856 (April 30, 2025, 14:03:40 UTC)
- Third party library updates.
- Added `--p2p-static-peers-url` option to read static peers from a URL or file

### Bug Fixes
- It is no longer possible to set both `--checkpoint-sync-url` and `--initial-state`.
Expand Down
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
58 changes: 51 additions & 7 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",
Comment thread
rolfyone marked this conversation as resolved.
Outdated
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",
Comment thread
rolfyone marked this conversation as resolved.
Outdated
split = ",",
arity = "0..*")
private List<String> p2pStaticPeers = new ArrayList<>();
arity = "1")
Comment thread
rolfyone marked this conversation as resolved.
private final List<String> p2pStaticPeers = new ArrayList<>();

@Option(
names = {"--p2p-direct-peers"},
Expand Down Expand Up @@ -447,12 +462,41 @@ private OptionalInt getP2pLowerBound() {
}

private OptionalInt getP2pUpperBound() {
if (p2pUpperBound.isPresent() && p2pLowerBound.isPresent()) {
return p2pLowerBound.getAsInt() > p2pUpperBound.getAsInt() ? p2pLowerBound : p2pUpperBound;
}
Comment thread
rolfyone marked this conversation as resolved.
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 +551,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
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,14 @@
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.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import tech.pegasys.teku.beacon.sync.SyncConfig;
import tech.pegasys.teku.cli.AbstractBeaconNodeCommandTest;
import tech.pegasys.teku.config.TekuConfiguration;
Expand All @@ -43,6 +50,7 @@
public class P2POptionsTest extends AbstractBeaconNodeCommandTest {

@Test
@Disabled("Temporarily disabled due to config file path issues in CI")
Comment thread
rolfyone marked this conversation as resolved.
Outdated
public void shouldReadFromConfigurationFile() {
final TekuConfiguration tekuConfig = getTekuConfigurationFromFile("P2POptions_config.yaml");

Expand Down Expand Up @@ -472,4 +480,58 @@ 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");
}

@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);
}
}
}
3 changes: 2 additions & 1 deletion teku/src/test/resources/P2POptions_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ p2p-advertised-port: 9999
p2p-private-key-file: "/the/file"
p2p-advertised-ip: "127.200.0.1"
p2p-static-peers: "127.1.0.1,127.1.1.1"
p2p-static-peers-url: "file:///path/to/peers.txt"
Comment thread
rolfyone marked this conversation as resolved.
Outdated
p2p-peer-lower-bound: 70
p2p-peer-upper-bound: 85
Xp2p-target-subnet-subscriber-count: 5
Expand All @@ -18,4 +19,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