diff --git a/hildr-node/build.gradle b/hildr-node/build.gradle index 8d894b94..eeca33cf 100644 --- a/hildr-node/build.gradle +++ b/hildr-node/build.gradle @@ -27,6 +27,18 @@ version '0.1.0' repositories { // Use Maven Central for resolving dependencies. mavenCentral() + maven { + url "https://artifacts.consensys.net/public/teku/maven/" + } + maven { + url "https://dl.cloudsmith.io/public/libp2p/jvm-libp2p/maven/" + } + maven { + url "https://hyperledger.jfrog.io/artifactory/besu-maven/" + } + maven { + url "https://artifacts.consensys.net/public/maven/maven/" + } } @@ -49,15 +61,6 @@ tasks.withType(JavaExec).configureEach { } dependencies { - // Use JUnit Jupiter for testing. - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' - // https://mvnrepository.com/artifact/org.junit.platform/junit-platform-suite-api - testImplementation 'org.junit.platform:junit-platform-suite-api:1.9.1' - // https://mvnrepository.com/artifact/org.junit.platform/junit-platform-suite-engine - testRuntimeOnly 'org.junit.platform:junit-platform-suite-engine:1.9.1' - - testRuntimeOnly 'org.junit.platform:junit-platform-reporting:1.9.1' // This dependency is used by the application. implementation 'com.google.guava:guava:31.1-jre' implementation 'com.github.rholder:guava-retrying:2.0.0' @@ -117,6 +120,108 @@ dependencies { errorprone("com.google.errorprone:error_prone_core:2.18.0") +// implementation 'io.tmio:tuweni-devp2p:2.4.2' +// implementation 'io.tmio:tuweni-ssz:2.4.2' + implementation('tech.pegasys.teku.internal:p2p:23.6.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('tech.pegasys.teku.internal:metrics:23.6.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('tech.pegasys.teku.internal:async:23.6.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('tech.pegasys.teku.internal:storage:23.6.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('tech.pegasys.teku.internal:time:23.6.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('tech.pegasys.teku.internal:spec:23.6.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('tech.pegasys.teku.internal:serviceutils:23.6.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('tech.pegasys.teku.internal:unsigned:23.6.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('org.hyperledger.besu.internal:metrics-core:22.10.4'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('org.hyperledger.besu:plugin-api:22.10.4'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation('io.libp2p:jvm-libp2p-minimal:0.10.0-RELEASE'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } + implementation 'io.tmio:tuweni-units:2.4.2' + implementation('io.tmio:tuweni-crypto:2.4.2'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + } + implementation 'io.tmio:tuweni-rlp:2.4.2' + implementation('tech.pegasys.discovery:discovery:22.12.0'){ + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.apache.tuweni', module: 'tuweni-bytes' + exclude group: 'org.apache.tuweni', module: 'tuweni-units' + exclude group: 'org.apache.tuweni', module: 'tuweni-rlp' + exclude group: 'org.apache.tuweni', module: 'tuweni-crypto' + } +// implementation fileTree(dir: '../lib', include: '*.jar') + // Use JUnit Jupiter for testing. + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + // https://mvnrepository.com/artifact/org.junit.platform/junit-platform-suite-api + testImplementation 'org.junit.platform:junit-platform-suite-api:1.9.1' + // https://mvnrepository.com/artifact/org.junit.platform/junit-platform-suite-engine + testRuntimeOnly 'org.junit.platform:junit-platform-suite-engine:1.9.1' + + testRuntimeOnly 'org.junit.platform:junit-platform-reporting:1.9.1' + testImplementation 'org.mockito:mockito-junit-jupiter:2.19.0' testImplementation("com.squareup.okhttp3:mockwebserver:5.0.0-alpha.2") } diff --git a/hildr-node/src/main/java/io/optimism/network/DiscV5Service.java b/hildr-node/src/main/java/io/optimism/network/DiscV5Service.java new file mode 100644 index 00000000..bef80b0a --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/DiscV5Service.java @@ -0,0 +1,271 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.network; + +import static java.util.stream.Collectors.toList; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.crypto.SECP256K1.SecretKey; +import org.apache.tuweni.units.bigints.UInt64; +import org.ethereum.beacon.discovery.AddressAccessPolicy; +import org.ethereum.beacon.discovery.DiscoverySystem; +import org.ethereum.beacon.discovery.DiscoverySystemBuilder; +import org.ethereum.beacon.discovery.schema.EnrField; +import org.ethereum.beacon.discovery.schema.NodeRecord; +import org.ethereum.beacon.discovery.schema.NodeRecordBuilder; +import org.ethereum.beacon.discovery.schema.NodeRecordFactory; +import org.ethereum.beacon.discovery.storage.NewAddressHandler; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.Cancellable; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryService; +import tech.pegasys.teku.networking.p2p.discovery.discv5.SecretKeyParser; +import tech.pegasys.teku.networking.p2p.libp2p.MultiaddrUtil; +import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; +import tech.pegasys.teku.service.serviceutils.Service; +import tech.pegasys.teku.storage.store.KeyValueStore; + +/** + * DiscV5Service is a discovery service that uses the DiscV5 protocol to discover and connect to + * peers. + * + * @author grapebaba + * @since 0.1.1 + */ +public class DiscV5Service extends Service implements DiscoveryService { + private static final Logger LOGGER = LoggerFactory.getLogger(DiscV5Service.class); + private static final String SEQ_NO_STORE_KEY = "local-enr-seqno"; + + private static final String OP_STACK = "opstack"; + + private static final Duration BOOTNODE_REFRESH_DELAY = Duration.ofMinutes(2); + + /** + * Create default discovery system builder discovery system builder. + * + * @return the discovery system builder + */ + public static DiscoverySystemBuilder createDefaultDiscoverySystemBuilder() { + return new DiscoverySystemBuilder(); + } + + private final AsyncRunner asyncRunner; + private final SecretKey localNodePrivateKey; + // private final SchemaDefinitionsSupplier currentSchemaDefinitionsSupplier; + private final NodeRecordConverter nodeRecordConverter; + + private final DiscoverySystem discoverySystem; + private final KeyValueStore kvStore; + private final List bootnodes; + private volatile Cancellable bootnodeRefreshTask; + + private final UInt64 chainId; + + /** + * Instantiates a new Disc v 5 service. + * + * @param metricsSystem the metrics system + * @param asyncRunner the async runner + * @param discoConfig the disco config + * @param p2pConfig the p 2 p config + * @param kvStore the kv store + * @param privateKey the private key + * @param discoverySystemBuilder the discovery system builder + * @param chainId the chain id + * @param nodeRecordConverter the node record converter + */ + public DiscV5Service( + final MetricsSystem metricsSystem, + final AsyncRunner asyncRunner, + final DiscoveryConfig discoConfig, + final NetworkConfig p2pConfig, + final KeyValueStore kvStore, + final Bytes privateKey, + final DiscoverySystemBuilder discoverySystemBuilder, + final UInt64 chainId, + final NodeRecordConverter nodeRecordConverter) { + this.chainId = chainId; + this.asyncRunner = asyncRunner; + this.localNodePrivateKey = SecretKeyParser.fromLibP2pPrivKey(privateKey); + // this.currentSchemaDefinitionsSupplier = currentSchemaDefinitionsSupplier; + this.nodeRecordConverter = nodeRecordConverter; + final String listenAddress = p2pConfig.getNetworkInterface(); + final int listenUdpPort = discoConfig.getListenUdpPort(); + final String advertisedAddress = p2pConfig.getAdvertisedIp(); + final int advertisedTcpPort = p2pConfig.getAdvertisedPort(); + final int advertisedUdpPort = discoConfig.getAdvertisedUdpPort(); + final UInt64 seqNo = + kvStore.get(SEQ_NO_STORE_KEY).map(UInt64::fromBytes).orElse(UInt64.ZERO).add(1); + final NewAddressHandler maybeUpdateNodeRecordHandler = + maybeUpdateNodeRecord(p2pConfig.hasUserExplicitlySetAdvertisedIp(), advertisedTcpPort); + this.bootnodes = + discoConfig.getBootnodes().stream() + .map(NodeRecordFactory.DEFAULT::fromEnr) + .collect(toList()); + final OpStackEnrData opStackEnrData = new OpStackEnrData(chainId, UInt64.ZERO); + final NodeRecordBuilder nodeRecordBuilder = + new NodeRecordBuilder() + .secretKey(localNodePrivateKey) + .seq(seqNo) + .customField(OP_STACK, opStackEnrData.encode()); + if (p2pConfig.hasUserExplicitlySetAdvertisedIp()) { + nodeRecordBuilder.address(advertisedAddress, advertisedUdpPort, advertisedTcpPort); + } + final NodeRecord localNodeRecord = nodeRecordBuilder.build(); + this.discoverySystem = + discoverySystemBuilder + .listen(listenAddress, listenUdpPort) + .secretKey(localNodePrivateKey) + .bootnodes(bootnodes) + .localNodeRecord(localNodeRecord) + .newAddressHandler(maybeUpdateNodeRecordHandler) + .localNodeRecordListener(this::localNodeRecordUpdated) + .addressAccessPolicy( + discoConfig.areSiteLocalAddressesEnabled() + ? AddressAccessPolicy.ALLOW_ALL + : address -> !address.getAddress().isSiteLocalAddress()) + .build(); + this.kvStore = kvStore; + metricsSystem.createIntegerGauge( + TekuMetricCategory.DISCOVERY, + "live_nodes_current", + "Current number of live nodes tracked by the discovery system", + () -> discoverySystem.getBucketStats().getTotalLiveNodeCount()); + } + + private NewAddressHandler maybeUpdateNodeRecord( + boolean userExplicitlySetAdvertisedIpOrPort, final int advertisedTcpPort) { + if (userExplicitlySetAdvertisedIpOrPort) { + return (oldRecord, newAddress) -> Optional.of(oldRecord); + } else { + return (oldRecord, newAddress) -> + Optional.of( + oldRecord.withNewAddress( + newAddress, Optional.of(advertisedTcpPort), localNodePrivateKey)); + } + } + + private void localNodeRecordUpdated(NodeRecord oldRecord, NodeRecord newRecord) { + kvStore.put(SEQ_NO_STORE_KEY, newRecord.getSeq().toBytes()); + } + + @Override + protected SafeFuture doStart() { + return SafeFuture.of(discoverySystem.start()) + .thenRun( + () -> + this.bootnodeRefreshTask = + asyncRunner.runWithFixedDelay( + this::pingBootnodes, + BOOTNODE_REFRESH_DELAY, + error -> LOGGER.error("Failed to contact discovery bootnodes", error))); + } + + private void pingBootnodes() { + bootnodes.forEach( + bootnode -> + SafeFuture.of(discoverySystem.ping(bootnode)) + .finish(error -> LOGGER.debug("Bootnode {} is unresponsive", bootnode))); + } + + @Override + protected SafeFuture doStop() { + final Cancellable refreshTask = this.bootnodeRefreshTask; + this.bootnodeRefreshTask = null; + if (refreshTask != null) { + refreshTask.cancel(); + } + discoverySystem.stop(); + return SafeFuture.completedFuture(null); + } + + @Override + public Stream streamKnownPeers() { + return activeNodes().flatMap(node -> nodeRecordConverter.convertToDiscoveryPeer(node).stream()); + } + + @Override + public SafeFuture> searchForPeers() { + return SafeFuture.of(discoverySystem.searchForNewPeers()) + // Current version of discovery doesn't return the found peers but next version will + .thenApply(this::filterByOpStackDataEnr) + .thenApply(this::convertToDiscoveryPeers); + } + + private List filterByOpStackDataEnr(final Collection nodeRecords) { + return nodeRecords.stream() + .filter( + nodeRecord -> + nodeRecord.containsKey(OP_STACK) + && OpStackEnrData.decode((Bytes) nodeRecord.get(OP_STACK)) + .getChainId() + .equals(chainId) + && OpStackEnrData.decode((Bytes) nodeRecord.get(OP_STACK)) + .getVersion() + .isZero()) + .collect(toList()); + } + + private List convertToDiscoveryPeers(final Collection foundNodes) { + LOGGER.debug("Found {} nodes prior to filtering", foundNodes.size()); + return foundNodes.stream() + .flatMap(nodeRecord -> nodeRecordConverter.convertToDiscoveryPeer(nodeRecord).stream()) + .collect(toList()); + } + + @Override + public Optional getEnr() { + return Optional.of(discoverySystem.getLocalNodeRecord().asEnr()); + } + + @Override + public Optional getDiscoveryAddress() { + final NodeRecord nodeRecord = discoverySystem.getLocalNodeRecord(); + if (nodeRecord.getUdpAddress().isEmpty()) { + return Optional.empty(); + } + final DiscoveryPeer discoveryPeer = + new DiscoveryPeer( + (Bytes) nodeRecord.get(EnrField.PKEY_SECP256K1), + nodeRecord.getUdpAddress().get(), + Optional.empty(), + null, + null); + + return Optional.of(MultiaddrUtil.fromDiscoveryPeerAsUdp(discoveryPeer).toString()); + } + + @Override + public void updateCustomENRField(String fieldName, Bytes value) { + discoverySystem.updateCustomFieldValue(fieldName, value); + } + + private Stream activeNodes() { + return discoverySystem.streamLiveNodes(); + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/NodeRecordConverter.java b/hildr-node/src/main/java/io/optimism/network/NodeRecordConverter.java new file mode 100644 index 00000000..b844b434 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/NodeRecordConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright ConsenSys Software Inc., 2022 + * + * 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 io.optimism.network; + +import java.net.InetSocketAddress; +import java.util.Optional; +import org.apache.tuweni.bytes.Bytes; +import org.ethereum.beacon.discovery.schema.EnrField; +import org.ethereum.beacon.discovery.schema.NodeRecord; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; + +/** + * The type NodeRecordConverter. + * + * @author grapebaba + * @since 0.1.1 + */ +public class NodeRecordConverter { + + /** Instantiates a new Node record converter. */ + public NodeRecordConverter() {} + + /** + * Convert to discovery peer optional. + * + * @param nodeRecord the node record + * @return the optional + */ + public Optional convertToDiscoveryPeer(final NodeRecord nodeRecord) { + return nodeRecord + .getTcpAddress() + .map(address -> socketAddressToDiscoveryPeer(nodeRecord, address)); + } + + private static DiscoveryPeer socketAddressToDiscoveryPeer( + final NodeRecord nodeRecord, final InetSocketAddress address) { + + return new DiscoveryPeer( + ((Bytes) nodeRecord.get(EnrField.PKEY_SECP256K1)), address, Optional.empty(), null, null); + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/OpStackEnrData.java b/hildr-node/src/main/java/io/optimism/network/OpStackEnrData.java new file mode 100644 index 00000000..e914966a --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/OpStackEnrData.java @@ -0,0 +1,132 @@ +/* + * Copyright 2023 281165273grape@gmail.com + * + * 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 io.optimism.network; + +import com.google.common.base.Objects; +import io.libp2p.etc.types.ByteBufExtKt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt64; + +/** + * The type OpStackEnrData. + * + * @author grapebaba + * @since 0.1.1 + */ +public class OpStackEnrData { + + private UInt64 chainId; + + private UInt64 version; + + /** + * Instantiates a new Op stack enr data. + * + * @param chainId the chain id + * @param version the version + */ + public OpStackEnrData(UInt64 chainId, UInt64 version) { + this.chainId = chainId; + this.version = version; + } + + /** + * Gets chain id. + * + * @return the chain id + */ + public UInt64 getChainId() { + return chainId; + } + + /** + * Sets chain id. + * + * @param chainId the chain id + */ + public void setChainId(UInt64 chainId) { + this.chainId = chainId; + } + + /** + * Gets version. + * + * @return the version + */ + public UInt64 getVersion() { + return version; + } + + /** + * Sets version. + * + * @param version the version + */ + public void setVersion(UInt64 version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OpStackEnrData)) { + return false; + } + OpStackEnrData that = (OpStackEnrData) o; + return Objects.equal(chainId, that.chainId) && Objects.equal(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hashCode(chainId, version); + } + + @Override + public String toString() { + return "OpStackEnrData{" + "chainId=" + chainId + ", version=" + version + '}'; + } + + /** + * Encode bytes. + * + * @return the bytes + */ + public Bytes encode() { + ByteBuf buffer = Unpooled.buffer(20); + ByteBufExtKt.writeUvarint(buffer, chainId.toLong()); + ByteBufExtKt.writeUvarint(buffer, version.toLong()); + return Bytes.wrap(ByteBufUtil.getBytes(buffer)); + } + + /** + * Decode op stack enr data. + * + * @param value the value + * @return the op stack enr data + */ + public static OpStackEnrData decode(Bytes value) { + ByteBuf buffer = Unpooled.wrappedBuffer(value.toArray()); + UInt64 chainId = UInt64.valueOf(ByteBufExtKt.readUvarint(buffer)); + UInt64 version = UInt64.valueOf(ByteBufExtKt.readUvarint(buffer)); + return new OpStackEnrData(chainId, version); + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/OpStackP2PNetwork.java b/hildr-node/src/main/java/io/optimism/network/OpStackP2PNetwork.java new file mode 100644 index 00000000..18e56c4f --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/OpStackP2PNetwork.java @@ -0,0 +1,207 @@ +/* + * Copyright ConsenSys Software Inc., 2022 + * + * 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 io.optimism.network; + +import static com.google.common.base.Preconditions.checkNotNull; + +import io.optimism.config.Config; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt64; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.AsyncRunnerFactory; +import tech.pegasys.teku.infrastructure.async.MetricTrackingExecutorFactory; +import tech.pegasys.teku.infrastructure.time.SystemTimeProvider; +import tech.pegasys.teku.networking.p2p.connection.PeerPools; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryService; +import tech.pegasys.teku.networking.p2p.discovery.noop.NoOpDiscoveryService; +import tech.pegasys.teku.networking.p2p.libp2p.LibP2PNetworkBuilder; +import tech.pegasys.teku.networking.p2p.network.P2PNetwork; +import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; +import tech.pegasys.teku.networking.p2p.reputation.DefaultReputationManager; +import tech.pegasys.teku.networking.p2p.reputation.ReputationManager; +import tech.pegasys.teku.storage.store.KeyValueStore; +import tech.pegasys.teku.storage.store.MemKeyValueStore; + +/** + * The type OpStackP2PNetwork. + * + * @author grapebaba + * @since 0.1.1 + */ +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class OpStackP2PNetwork { + + private static final Logger LOGGER = LoggerFactory.getLogger(OpStackP2PNetwork.class); + + private final DiscoveryService discoveryService; + + private final UInt64 chainId; + + /** + * Instantiates a new Op stack p 2 p network. + * + * @param config the config + */ + public OpStackP2PNetwork(Config.ChainConfig config) { + this.chainId = UInt64.valueOf(config.l2ChainId()); + MetricsSystem metricsSystem = new NoOpMetricsSystem(); + final PeerPools peerPools = new PeerPools(); + final ReputationManager reputationManager = + new DefaultReputationManager(metricsSystem, new SystemTimeProvider(), 1024, peerPools); + final DiscoveryConfig discoveryConfig = + DiscoveryConfig.builder() + .listenUdpPort(7777) + .bootnodes( + List.of( + "enr:-J64QBbwPjPLZ6IOOToOLsSjtFUjjzN66qmBZdUexpO32Klrc" + + "458Q24kbty2PdRaLacHM5z-cZQr8mjeQu3pik6jPS" + + "OGAYYFIqBfgmlkgnY0gmlwhDaRWFWHb3BzdGFja4SzlAUAi" + + "XNlY3AyNTZrMaECmeSnJh7zjKrDSPoNMGXoopeDF4" + + "hhpj5I0OsQUUt4u8uDdGNwgiQGg3VkcIIkBg", + "enr:-J64QAlTCDa188Hl1OGv5_2Kj2nWCsvxMVc_rEnLtw7RPFbOf" + + "qUOV6khXT_PH6cC603I2ynY31rSQ8sI9gLeJbfFGa" + + "WGAYYFIrpdgmlkgnY0gmlwhANWgzCHb3BzdGFja4SzlAUAi" + + "XNlY3AyNTZrMaECkySjcg-2v0uWAsFsZZu43qNHpp" + + "Gr2D5F913Qqs5jDCGDdGNwgiQGg3VkcIIkBg", + "enr:-J24QGEzN4mJgLWNTUNwj7riVJ2ZjRLenOFccl2dbRFxHHOC" + + "CZx8SXWzgf-sLzrGs6QgqSFCvGXVgGPBkRkfOWlT1-" + + "iGAYe6Cu93gmlkgnY0gmlwhCJBEUSHb3BzdGFja4OkAwC" + + "Jc2VjcDI1NmsxoQLuYIwaYOHg3CUQhCkS-RsSHmUd1b" + + "_x93-9yQ5ItS6udIN0Y3CCIyuDdWRwgiMr")) + .build(); + final NetworkConfig p2pConfig = NetworkConfig.builder().build(); + final AsyncRunner asyncRunner = + AsyncRunnerFactory.createDefault(new MetricTrackingExecutorFactory(metricsSystem)) + .create("op_stack", 20); + final KeyValueStore kvStore = new MemKeyValueStore<>(); + final P2PNetwork p2pNetwork = + LibP2PNetworkBuilder.create() + .metricsSystem(metricsSystem) + .asyncRunner( + AsyncRunnerFactory.createDefault(new MetricTrackingExecutorFactory(metricsSystem)) + .create("op_stack_p2p", 20)) + .config(p2pConfig) + // .privateKeyProvider(new LibP2PPrivateKeyLoader(new + // FileKeyValueStore(Path.of("/.hildr-node").resolve("kvstore")), Optional.empty())) + .privateKeyProvider(PrivateKeyGenerator::generate) + .reputationManager(reputationManager) + .rpcMethods(Collections.emptyList()) + .peerHandlers(Collections.emptyList()) + .preparedGossipMessageFactory( + (topic, payload, networkingSpecConfig) -> { + throw new UnsupportedOperationException(); + }) + .gossipTopicFilter(topic -> true) + .build(); + if (discoveryConfig.isDiscoveryEnabled()) { + checkNotNull(metricsSystem); + checkNotNull(asyncRunner); + checkNotNull(p2pConfig); + checkNotNull(kvStore); + checkNotNull(p2pNetwork); + + discoveryService = + new DiscV5Service( + metricsSystem, + asyncRunner, + discoveryConfig, + p2pConfig, + kvStore, + p2pNetwork.getPrivateKey(), + DiscV5Service.createDefaultDiscoverySystemBuilder(), + chainId, + new NodeRecordConverter()); + + } else { + discoveryService = new NoOpDiscoveryService(); + } + + // network = DiscoveryNetworkBuilder.create() + // .metricsSystem(metricsSystem) + // .asyncRunner(AsyncRunnerFactory.createDefault(new + // MetricTrackingExecutorFactory(metricsSystem)).create("op_stack_disc", 20)) + // .kvStore(new MemKeyValueStore<>()) + // .p2pNetwork( + // LibP2PNetworkBuilder.create() + // .metricsSystem(metricsSystem) + // .asyncRunner(AsyncRunnerFactory.createDefault(new + // MetricTrackingExecutorFactory(metricsSystem)).create("op_stack_p2p", 20)) + // .config(p2pConfig) + //// .privateKeyProvider(new LibP2PPrivateKeyLoader(new + // FileKeyValueStore(Path.of("/.hildr-node").resolve("kvstore")), Optional.empty())) + // .privateKeyProvider(PrivateKeyGenerator::generate) + // .reputationManager(reputationManager) + // .rpcMethods(Collections.emptyList()) + // .peerHandlers(Collections.emptyList()) + // .preparedGossipMessageFactory( + // (topic, payload, networkingSpecConfig) -> { + // throw new UnsupportedOperationException(); + // }) + // .gossipTopicFilter(topic -> true) + // .build()) + // .peerPools(peerPools) + // .peerSelectionStrategy(new SimplePeerSelectionStrategy(new + // TargetPeerRange(DiscoveryConfig.DEFAULT_P2P_PEERS_LOWER_BOUND, DEFAULT_P2P_PEERS_UPPER_BOUND, + // Math.max(1, DEFAULT_P2P_PEERS_LOWER_BOUND * 2 / 10)))) + // .discoveryConfig(discoveryConfig) + // .p2pConfig(p2pConfig) + // .spec(spec) + // .currentSchemaDefinitionsSupplier(spec::getGenesisSchemaDefinitions) + // .build(); + + } + + /** Start. */ + public void start() { + try { + discoveryService.start().get(30, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } + + /** + * Search peers list. + * + * @return the list + * @throws ExecutionException the execution exception + * @throws InterruptedException the interrupted exception + */ + public List searchPeers() throws ExecutionException, InterruptedException { + return discoveryService.searchForPeers().get().stream().toList(); + } + + /** Stop. */ + public void stop() { + try { + discoveryService.stop().get(30, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/PrivateKeyGenerator.java b/hildr-node/src/main/java/io/optimism/network/PrivateKeyGenerator.java new file mode 100644 index 00000000..cfcc3476 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/PrivateKeyGenerator.java @@ -0,0 +1,41 @@ +/* + * Copyright ConsenSys Software Inc., 2022 + * + * 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 io.optimism.network; + +import io.libp2p.core.crypto.KEY_TYPE; +import io.libp2p.core.crypto.KeyKt; +import io.libp2p.core.crypto.PrivKey; + +/** + * The type PrivateKeyGenerator. + * + * @author grapebaba + * @since 0.1.1 + */ +public class PrivateKeyGenerator { + /** Instantiates a new Private key generator. */ + public PrivateKeyGenerator() {} + + /** + * Generate priv key. + * + * @return the priv key + */ + public static PrivKey generate() { + return KeyKt.generateKeyPair(KEY_TYPE.SECP256K1).component1(); + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/SimplePeerSelectionStrategy.java b/hildr-node/src/main/java/io/optimism/network/SimplePeerSelectionStrategy.java new file mode 100644 index 00000000..0277b922 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/SimplePeerSelectionStrategy.java @@ -0,0 +1,72 @@ +/* + * Copyright ConsenSys Software Inc., 2022 + * + * 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 io.optimism.network; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +import static tech.pegasys.teku.networking.p2p.connection.PeerConnectionType.STATIC; + +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; +import tech.pegasys.teku.networking.p2p.connection.PeerPools; +import tech.pegasys.teku.networking.p2p.connection.PeerSelectionStrategy; +import tech.pegasys.teku.networking.p2p.connection.TargetPeerRange; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +import tech.pegasys.teku.networking.p2p.network.P2PNetwork; +import tech.pegasys.teku.networking.p2p.network.PeerAddress; +import tech.pegasys.teku.networking.p2p.peer.Peer; + +/** The type Simple peer selection strategy. */ +public class SimplePeerSelectionStrategy implements PeerSelectionStrategy { + private final TargetPeerRange targetPeerRange; + + /** + * Instantiates a new Simple peer selection strategy. + * + * @param targetPeerRange the target peer range + */ + public SimplePeerSelectionStrategy(final TargetPeerRange targetPeerRange) { + this.targetPeerRange = targetPeerRange; + } + + @Override + public List selectPeersToConnect( + final P2PNetwork network, + final PeerPools peerPools, + final Supplier> candidates) { + final int peersToAdd = targetPeerRange.getPeersToAdd(network.getPeerCount()); + if (peersToAdd == 0) { + return emptyList(); + } + return candidates.get().stream() + .map(network::createPeerAddress) + .limit(peersToAdd) + .collect(toList()); + } + + @Override + public List selectPeersToDisconnect( + final P2PNetwork network, final PeerPools peerPools) { + final int peersToDrop = targetPeerRange.getPeersToDrop(network.getPeerCount()); + return network + .streamPeers() + .filter(peer -> peerPools.getPeerConnectionType(peer.getId()) != STATIC) + .limit(peersToDrop) + .collect(toList()); + } +} diff --git a/hildr-node/src/test/java/io/optimism/config/ConfigTest.java b/hildr-node/src/test/java/io/optimism/config/ConfigTest.java index 4f7167a6..cdc6adf3 100644 --- a/hildr-node/src/test/java/io/optimism/config/ConfigTest.java +++ b/hildr-node/src/test/java/io/optimism/config/ConfigTest.java @@ -16,7 +16,9 @@ package io.optimism.config; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; import io.optimism.config.Config.ChainConfig; @@ -138,11 +140,13 @@ void readExternalChainFromJson() { "{\n" + "\"genesis\": {\n" + " \"l1\": {\n" - + " \"hash\": \"0xdb52a58e7341447d1a9525d248ea07dbca7dfa0e105721dee1aa5a86163c088d\",\n" + + " \"hash\": \"0xdb52a58e7341447d1a9525d248ea" + + "07dbca7dfa0e105721dee1aa5a86163c088d\",\n" + " \"number\": 0\n" + " },\n" + " \"l2\": {\n" - + " \"hash\": \"0xf85bca315a08237644b06a8350cda3bc0de1593745a91be93daeadb28fb3a32e\",\n" + + " \"hash\": \"0xf85bca315a08237644b06a8350cda3" + + "bc0de1593745a91be93daeadb28fb3a32e\",\n" + " \"number\": 0\n" + " },\n" + " \"l2_time\": 1685710775,\n" @@ -150,7 +154,8 @@ void readExternalChainFromJson() { + " \"batcherAddr\": \"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc\",\n" + " \"overhead\":\n" + " \"0x0000000000000000000000000000000000000000000000000000000000000834\",\n" - + " \"scalar\": \"0x00000000000000000000000000000000000000000000000000000000000f4240\",\n" + + " \"scalar\": \"0x000000000000000000000000000000" + + "00000000000000000000000000000f4240\",\n" + " \"gasLimit\": 30000000\n" + " }\n" + "},\n" diff --git a/hildr-node/src/test/java/io/optimism/network/OpStackP2PNetworkTest.java b/hildr-node/src/test/java/io/optimism/network/OpStackP2PNetworkTest.java new file mode 100644 index 00000000..397a8049 --- /dev/null +++ b/hildr-node/src/test/java/io/optimism/network/OpStackP2PNetworkTest.java @@ -0,0 +1,31 @@ +package io.optimism.network; + +import static java.lang.Thread.sleep; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.optimism.config.Config; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; + +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +class OpStackP2PNetworkTest { + + @Test + void start() throws InterruptedException, ExecutionException { + OpStackP2PNetwork opStackP2PNetwork = + new OpStackP2PNetwork(Config.ChainConfig.optimismGoerli()); + opStackP2PNetwork.start(); + + int sum = 0; + for (var i = 0; i < 5; i++) { + List peers = opStackP2PNetwork.searchPeers(); + peers.forEach(peer -> System.out.println(peer.getNodeAddress())); + sum += peers.size(); + sleep(1000); + } + assertTrue(sum > 0); + opStackP2PNetwork.stop(); + } +}