diff --git a/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/events/SyncPreImportBlockChannel.java b/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/events/SyncPreImportBlockChannel.java new file mode 100644 index 00000000000..655661edf18 --- /dev/null +++ b/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/events/SyncPreImportBlockChannel.java @@ -0,0 +1,23 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.beacon.sync.events; + +import java.util.Collection; +import tech.pegasys.teku.infrastructure.events.VoidReturningChannelInterface; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; + +public interface SyncPreImportBlockChannel extends VoidReturningChannelInterface { + + void onNewPreImportBlocks(Collection blocks); +} diff --git a/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/SyncSourceFactory.java b/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/SyncSourceFactory.java index 098266b0f86..e58588a725e 100644 --- a/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/SyncSourceFactory.java +++ b/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/SyncSourceFactory.java @@ -53,6 +53,9 @@ public SyncSource getOrCreateSyncSource(final Eth2Peer peer, final Spec spec) { final Optional maybeMaxBlobSidecarsPerMinute = maybeMaxBlobsPerBlock.map( maxBlobsPerBlock -> this.maxBlobSidecarsPerMinute - (batchSize * maxBlobsPerBlock) - 1); + final Optional maxDataColumnSidecarsPerMinute = + spec.getNumberOfDataColumns() + .map(dataColumnsPerBlock -> maxBlocksPerMinute * dataColumnsPerBlock.intValue()); return syncSourcesByPeer.computeIfAbsent( peer, source -> @@ -62,7 +65,8 @@ public SyncSource getOrCreateSyncSource(final Eth2Peer peer, final Spec spec) { source, maxBlocksPerMinute, maybeMaxBlobsPerBlock, - maybeMaxBlobSidecarsPerMinute)); + maybeMaxBlobSidecarsPerMinute, + maxDataColumnSidecarsPerMinute)); } public void onPeerDisconnected(final Eth2Peer peer) { diff --git a/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/ThrottlingSyncSource.java b/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/ThrottlingSyncSource.java index b7ed8e566df..72c7b63b856 100644 --- a/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/ThrottlingSyncSource.java +++ b/beacon/sync/src/main/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/ThrottlingSyncSource.java @@ -15,6 +15,7 @@ import com.google.common.annotations.VisibleForTesting; import java.time.Duration; +import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import org.apache.logging.log4j.LogManager; @@ -29,6 +30,7 @@ import tech.pegasys.teku.networking.p2p.reputation.ReputationAdjustment; import tech.pegasys.teku.networking.p2p.rpc.RpcResponseListener; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; public class ThrottlingSyncSource implements SyncSource { @@ -43,6 +45,7 @@ public class ThrottlingSyncSource implements SyncSource { private final RateTracker blocksRateTracker; private final Optional maybeMaxBlobsPerBlock; private final RateTracker blobSidecarsRateTracker; + private final RateTracker dataColumnSidecarsRateTracker; public ThrottlingSyncSource( final AsyncRunner asyncRunner, @@ -50,16 +53,32 @@ public ThrottlingSyncSource( final SyncSource delegate, final int maxBlocksPerMinute, final Optional maybeMaxBlobsPerBlock, - final Optional maybeMaxBlobSidecarsPerMinute) { + final Optional maybeMaxBlobSidecarsPerMinute, + final Optional maybeMaxDataColumnSidecarsPerMinute) { this.asyncRunner = asyncRunner; this.delegate = delegate; - this.blocksRateTracker = RateTracker.create(maxBlocksPerMinute, TIMEOUT_SECONDS, timeProvider); + this.blocksRateTracker = + RateTracker.create(maxBlocksPerMinute, TIMEOUT_SECONDS, timeProvider, "throttling-blocks"); this.maybeMaxBlobsPerBlock = maybeMaxBlobsPerBlock; this.blobSidecarsRateTracker = maybeMaxBlobSidecarsPerMinute .map( maxBlobSidecarsPerMinute -> - RateTracker.create(maxBlobSidecarsPerMinute, TIMEOUT_SECONDS, timeProvider)) + RateTracker.create( + maxBlobSidecarsPerMinute, + TIMEOUT_SECONDS, + timeProvider, + "throttling-blobs")) + .orElse(RateTracker.NOOP); + this.dataColumnSidecarsRateTracker = + maybeMaxDataColumnSidecarsPerMinute + .map( + maxDataColumnSidecarsPerMinute -> + RateTracker.create( + maxDataColumnSidecarsPerMinute, + TIMEOUT_SECONDS, + timeProvider, + "throttling-dataColumn")) .orElse(RateTracker.NOOP); } @@ -125,6 +144,23 @@ public SafeFuture requestBlobSidecarsByRange( }); } + @Override + public SafeFuture requestDataColumnSidecarsByRange( + final UInt64 startSlot, + final UInt64 count, + final List columns, + final RpcResponseListener listener) { + final long maxColumnsSidecarsCount = count.times(columns.size()).longValue(); + if (dataColumnSidecarsRateTracker.approveObjectsRequest(maxColumnsSidecarsCount).isPresent()) { + LOG.debug("Sending request for {} data column sidecars on {} columns", count, columns.size()); + return delegate.requestDataColumnSidecarsByRange(startSlot, count, columns, listener); + } else { + return asyncRunner.runAfterDelay( + () -> requestDataColumnSidecarsByRange(startSlot, count, columns, listener), + PEER_REQUEST_DELAY); + } + } + @Override public SafeFuture disconnectCleanly(final DisconnectReason reason) { return delegate.disconnectCleanly(reason); diff --git a/beacon/sync/src/test/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/ThrottlingSyncSourceTest.java b/beacon/sync/src/test/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/ThrottlingSyncSourceTest.java index 6367d487239..3a742bba343 100644 --- a/beacon/sync/src/test/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/ThrottlingSyncSourceTest.java +++ b/beacon/sync/src/test/java/tech/pegasys/teku/beacon/sync/forward/multipeer/chains/ThrottlingSyncSourceTest.java @@ -60,7 +60,8 @@ class ThrottlingSyncSourceTest { delegate, MAX_BLOCKS_PER_MINUTE, Optional.of(MAX_BLOBS_PER_BLOCK), - Optional.of(MAX_BLOB_SIDECARS_PER_MINUTE)); + Optional.of(MAX_BLOB_SIDECARS_PER_MINUTE), + Optional.empty()); @BeforeEach void setup() { diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/networking/libp2p/rpc/DataColumnSidecarsByRootRequestMessage.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/networking/libp2p/rpc/DataColumnSidecarsByRootRequestMessage.java index f26d9ec24db..dec0c251aa1 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/networking/libp2p/rpc/DataColumnSidecarsByRootRequestMessage.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/networking/libp2p/rpc/DataColumnSidecarsByRootRequestMessage.java @@ -1,5 +1,5 @@ /* - * Copyright Consensys Software Inc., 2025 + * 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 diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractDasResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractDasResponseLogger.java new file mode 100644 index 00000000000..9d1aacb1a6f --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractDasResponseLogger.java @@ -0,0 +1,134 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.List; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.infrastructure.logging.LogFormatter; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.util.StringifyUtil; + +abstract class AbstractDasResponseLogger + extends AbstractResponseLogger { + private static final Logger LOG = LogManager.getLogger(DasReqRespLogger.class); + + protected static final UInt64 UNKNOWN_SLOT = UInt64.MAX_VALUE; + + private final int columnCount = 128; + private final int maxResponseLongStringLength = 512; + + public AbstractDasResponseLogger( + final TimeProvider timeProvider, + final Direction direction, + final LoggingPeerId peerId, + final TRequest request) { + super(timeProvider, direction, peerId, request, DataColumnSlotAndIdentifier::fromDataColumn); + } + + protected abstract int requestedMaxCount(); + + @Override + protected Logger getLogger() { + return LOG; + } + + protected String responseString( + final List responses, final Optional result) { + final String responsesString; + if (responses.isEmpty()) { + responsesString = ""; + } else if (responses.size() == requestedMaxCount()) { + responsesString = ""; + } else { + responsesString = columnIdsToString(responses); + } + + if (result.isEmpty()) { + return responsesString; + } else if (responses.isEmpty()) { + return "error: " + result.get(); + } else { + return responsesString + ", error: " + result.get(); + } + } + + protected String columnIdsToString(final List responses) { + final String longString = columnIdsToStringLong(responses); + if (longString.length() <= maxResponseLongStringLength) { + return longString; + } else { + return columnIdsToStringShorter(responses); + } + } + + protected String columnIdsToStringLong(final List responses) { + return responses.size() + + " columns: " + + mapGroupingByBlock( + responses, + (blockId, columns) -> + blockIdString(blockId) + " colIdxs: " + blockResponsesToString(columns)) + .collect(Collectors.joining(", ")); + } + + protected String columnIdsToStringShorter(final List responses) { + + return mapGroupingByBlock( + responses, (blockId, columns) -> blockIdString(blockId) + ": " + columns.size()) + .collect(Collectors.joining(", ")); + } + + protected Stream mapGroupingByBlock( + final List responses, + final BiFunction, R> mapper) { + SortedMap> responsesByBlock = + new TreeMap<>( + responses.stream() + .collect(Collectors.groupingBy(AbstractDasResponseLogger::blockIdFromColumnId))); + return responsesByBlock.entrySet().stream() + .map(entry -> mapper.apply(entry.getKey(), entry.getValue())); + } + + protected String blockResponsesToString(final List responses) { + return StringifyUtil.columnIndexesToString( + responses.stream().map(it -> it.columnIndex().intValue()).toList(), columnCount); + } + + private static String blockIdString(final SlotAndBlockRoot blockId) { + if (blockId.getSlot().equals(UNKNOWN_SLOT)) { + return blockId.getBlockRoot().toHexString(); + } else { + return "#" + + blockId.getSlot() + + " (0x" + + LogFormatter.formatAbbreviatedHashRoot(blockId.getBlockRoot()) + + ")"; + } + } + + private static SlotAndBlockRoot blockIdFromColumnId(final DataColumnSlotAndIdentifier columnId) { + return new SlotAndBlockRoot(columnId.slot(), columnId.blockRoot()); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractResponseLogger.java new file mode 100644 index 00000000000..7bb00b0f36f --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractResponseLogger.java @@ -0,0 +1,107 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.infrastructure.time.TimeProvider; + +abstract class AbstractResponseLogger + implements ReqRespResponseLogger { + + enum Direction { + INBOUND, + OUTBOUND; + + @Override + public String toString() { + return name().toLowerCase(Locale.US); + } + } + + protected record Timestamped(long time, T value) {} + + protected final TimeProvider timeProvider; + protected final Direction direction; + protected final LoggingPeerId peerId; + protected final TRequest request; + private final Function responseSummarizer; + protected final long requestTime; + + private final List> responseSummaries = new ArrayList<>(); + private volatile boolean done = false; + + public AbstractResponseLogger( + final TimeProvider timeProvider, + final Direction direction, + final LoggingPeerId peerId, + final TRequest request, + final Function responseSummarizer) { + this.timeProvider = timeProvider; + this.direction = direction; + this.peerId = peerId; + this.request = request; + this.responseSummarizer = responseSummarizer; + this.requestTime = timeProvider.getTimeInMillis().longValue(); + } + + protected abstract Logger getLogger(); + + protected abstract void responseComplete( + List> responseSummaries, Optional result); + + @Override + public synchronized void onNextItem(final TResponse responseItem) { + if (getLogger().isDebugEnabled()) { + final TResponseSummary responseSummary = responseSummarizer.apply(responseItem); + if (done) { + getLogger().debug("ERROR: Extra onNextItem: " + responseSummary); + return; + } + responseSummaries.add( + new Timestamped<>(timeProvider.getTimeInMillis().longValue(), responseSummary)); + } + } + + @Override + public void onComplete() { + if (getLogger().isDebugEnabled()) { + if (done) { + getLogger().debug("ERROR: Extra onComplete"); + return; + } + finalize(Optional.empty()); + } + } + + @Override + public void onError(final Throwable error) { + if (getLogger().isDebugEnabled()) { + if (done) { + getLogger().debug("ERROR: Extra onError: " + error); + return; + } + finalize(Optional.ofNullable(error)); + } + } + + private void finalize(final Optional result) { + done = true; + responseComplete(responseSummaries, result); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRangeResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRangeResponseLogger.java new file mode 100644 index 00000000000..f1c2577a397 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRangeResponseLogger.java @@ -0,0 +1,70 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.List; +import java.util.Optional; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.util.StringifyUtil; + +class DasByRangeResponseLogger extends AbstractDasResponseLogger { + public DasByRangeResponseLogger( + final TimeProvider timeProvider, + final Direction direction, + final LoggingPeerId peerId, + final DasReqRespLogger.ByRangeRequest request) { + super(timeProvider, direction, peerId, request); + } + + @Override + protected void responseComplete( + final List> responseSummaries, + final Optional result) { + + final List responseSummariesUnboxed = + responseSummaries.stream().map(Timestamped::value).toList(); + final long curTime = timeProvider.getTimeInMillis().longValue(); + + getLogger() + .debug( + "ReqResp {} {}, columns: {}/{} in {} ms{}, peer {}: request: {}, response: {}", + direction, + "data_column_sidecars_by_range", + responseSummaries.size(), + requestedMaxCount(), + curTime - requestTime, + result.isEmpty() ? "" : " with ERROR", + peerId, + requestToString(), + responseString(responseSummariesUnboxed, result)); + } + + @Override + protected int requestedMaxCount() { + return request.slotCount() * request.columnIndexes().size(); + } + + private String requestToString() { + return "[startSlot = " + + request.startSlot() + + ", count = " + + request.slotCount() + + ", columns = " + + StringifyUtil.toIntRangeString( + request.columnIndexes().stream().map(UInt64::intValue).toList()) + + "]"; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRootResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRootResponseLogger.java new file mode 100644 index 00000000000..8d6040130d9 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRootResponseLogger.java @@ -0,0 +1,87 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; + +class DasByRootResponseLogger extends AbstractDasResponseLogger> { + + public DasByRootResponseLogger( + final TimeProvider timeProvider, + final Direction direction, + final LoggingPeerId peerId, + final List dataColumnIdentifiers) { + super(timeProvider, direction, peerId, dataColumnIdentifiers); + } + + @Override + protected void responseComplete( + final List> responseSummaries, + final Optional result) { + + final List responseSummariesUnboxed = + responseSummaries.stream().map(Timestamped::value).toList(); + final long curTime = timeProvider.getTimeInMillis().longValue(); + + getLogger() + .debug( + "ReqResp {} {}, columns: {}/{} in {} ms{}, peer {}: request: {}, response: {}", + direction, + "data_column_sidecars_by_root", + responseSummaries.size(), + requestedMaxCount(), + curTime - requestTime, + result.isEmpty() ? "" : " with ERROR", + peerId, + requestToString(responseSummariesUnboxed), + responseString(responseSummariesUnboxed, result)); + } + + @Override + protected int requestedMaxCount() { + return request.size(); + } + + protected String requestToString(final List responses) { + final Map blockRootToSlot = + responses.stream() + .collect( + Collectors.toMap( + DataColumnSlotAndIdentifier::blockRoot, + DataColumnSlotAndIdentifier::slot, + (s1, s2) -> s1)); + final List idsWithMaybeSlot = + request.stream() + .flatMap( + it -> + it.getColumns().stream() + .map( + column -> + new DataColumnSlotAndIdentifier( + blockRootToSlot.getOrDefault(it.getBlockRoot(), UNKNOWN_SLOT), + it.getBlockRoot(), + column))) + .toList(); + + return columnIdsToString(idsWithMaybeSlot); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLogger.java new file mode 100644 index 00000000000..57494eb3380 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLogger.java @@ -0,0 +1,49 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.List; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; + +public interface DasReqRespLogger { + + record ByRangeRequest(UInt64 startSlot, int slotCount, List columnIndexes) {} + + static DasReqRespLogger create(final TimeProvider timeProvider) { + return new DasReqRespLoggerImpl(timeProvider); + } + + DasReqRespLogger NOOP = + new DasReqRespLogger() { + @Override + public ReqRespMethodLogger, DataColumnSidecar> + getDataColumnSidecarsByRootLogger() { + return new NoopReqRespMethodLogger<>(); + } + + @Override + public ReqRespMethodLogger + getDataColumnSidecarsByRangeLogger() { + return new NoopReqRespMethodLogger<>(); + } + }; + + ReqRespMethodLogger, DataColumnSidecar> + getDataColumnSidecarsByRootLogger(); + + ReqRespMethodLogger getDataColumnSidecarsByRangeLogger(); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLoggerImpl.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLoggerImpl.java new file mode 100644 index 00000000000..7a038a2ec14 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLoggerImpl.java @@ -0,0 +1,75 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.List; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; + +class DasReqRespLoggerImpl implements DasReqRespLogger { + + private final TimeProvider timeProvider; + + private final ReqRespMethodLogger, DataColumnSidecar> + byRootMethodLogger = + new ReqRespMethodLogger<>() { + @Override + public ReqRespResponseLogger onInboundRequest( + final LoggingPeerId fromPeer, final List request) { + return new DasByRootResponseLogger( + timeProvider, AbstractResponseLogger.Direction.INBOUND, fromPeer, request); + } + + @Override + public ReqRespResponseLogger onOutboundRequest( + final LoggingPeerId toPeer, final List request) { + return new DasByRootResponseLogger( + timeProvider, AbstractResponseLogger.Direction.OUTBOUND, toPeer, request); + } + }; + + private final ReqRespMethodLogger byRangeMethodLogger = + new ReqRespMethodLogger<>() { + @Override + public ReqRespResponseLogger onInboundRequest( + final LoggingPeerId fromPeer, final ByRangeRequest request) { + return new DasByRangeResponseLogger( + timeProvider, AbstractResponseLogger.Direction.INBOUND, fromPeer, request); + } + + @Override + public ReqRespResponseLogger onOutboundRequest( + final LoggingPeerId toPeer, final ByRangeRequest request) { + return new DasByRangeResponseLogger( + timeProvider, AbstractResponseLogger.Direction.OUTBOUND, toPeer, request); + } + }; + + public DasReqRespLoggerImpl(final TimeProvider timeProvider) { + this.timeProvider = timeProvider; + } + + @Override + public ReqRespMethodLogger, DataColumnSidecar> + getDataColumnSidecarsByRootLogger() { + return byRootMethodLogger; + } + + @Override + public ReqRespMethodLogger + getDataColumnSidecarsByRangeLogger() { + return byRangeMethodLogger; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRangeReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRangeReqResp.java new file mode 100644 index 00000000000..e6b6e8c8571 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRangeReqResp.java @@ -0,0 +1,55 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.List; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.statetransition.datacolumns.retriever.BatchDataColumnsByRangeReqResp; + +public class LoggingBatchDataColumnsByRangeReqResp implements BatchDataColumnsByRangeReqResp { + + private final BatchDataColumnsByRangeReqResp delegate; + private final DasReqRespLogger logger; + + public LoggingBatchDataColumnsByRangeReqResp( + final BatchDataColumnsByRangeReqResp delegate, final DasReqRespLogger logger) { + this.delegate = delegate; + this.logger = logger; + } + + @Override + public AsyncStream requestDataColumnSidecarsByRange( + final UInt256 nodeId, + final UInt64 startSlot, + final int slotCount, + final List columnIndexes) { + final ReqRespResponseLogger responseLogger = + logger + .getDataColumnSidecarsByRangeLogger() + .onOutboundRequest( + LoggingPeerId.fromNodeId(nodeId), + new DasReqRespLogger.ByRangeRequest(startSlot, slotCount, columnIndexes)); + return delegate + .requestDataColumnSidecarsByRange(nodeId, startSlot, slotCount, columnIndexes) + .peek(responseLogger.asAsyncStreamVisitor()); + } + + @Override + public int getCurrentRequestLimit(final UInt256 nodeId) { + return delegate.getCurrentRequestLimit(nodeId); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRootReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRootReqResp.java new file mode 100644 index 00000000000..9990cb48c93 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRootReqResp.java @@ -0,0 +1,49 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.List; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.retriever.BatchDataColumnsByRootReqResp; + +public class LoggingBatchDataColumnsByRootReqResp implements BatchDataColumnsByRootReqResp { + private final BatchDataColumnsByRootReqResp delegate; + private final DasReqRespLogger logger; + + public LoggingBatchDataColumnsByRootReqResp( + final BatchDataColumnsByRootReqResp delegate, final DasReqRespLogger logger) { + this.delegate = delegate; + this.logger = logger; + } + + @Override + public AsyncStream requestDataColumnSidecarsByRoot( + final UInt256 nodeId, final List columnIdentifiers) { + final ReqRespResponseLogger responseLogger = + logger + .getDataColumnSidecarsByRootLogger() + .onOutboundRequest(LoggingPeerId.fromNodeId(nodeId), columnIdentifiers); + return delegate + .requestDataColumnSidecarsByRoot(nodeId, columnIdentifiers) + .peek(responseLogger.asAsyncStreamVisitor()); + } + + @Override + public int getCurrentRequestLimit(final UInt256 nodeId) { + return delegate.getCurrentRequestLimit(nodeId); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingPeerId.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingPeerId.java new file mode 100644 index 00000000000..5aa8b573a6c --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingPeerId.java @@ -0,0 +1,43 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import java.util.Optional; +import org.apache.tuweni.units.bigints.UInt256; + +public class LoggingPeerId { + public static LoggingPeerId fromNodeId(final UInt256 nodeId) { + return new LoggingPeerId(nodeId, Optional.empty()); + } + + public static LoggingPeerId fromPeerAndNodeId(final String base58PeerId, final UInt256 nodeId) { + return new LoggingPeerId(nodeId, Optional.of(base58PeerId)); + } + + private final UInt256 nodeId; + private final Optional base58PeerId; + + public LoggingPeerId(final UInt256 nodeId, final Optional base58PeerId) { + this.nodeId = nodeId; + this.base58PeerId = base58PeerId; + } + + @Override + public String toString() { + final String sNodeId = nodeId.toHexString(); + final String sShortNodeId = + sNodeId.substring(0, 10) + "..." + sNodeId.substring(sNodeId.length() - 8); + return base58PeerId.map(s -> s + " (nodeId = " + sShortNodeId + ")").orElse(sShortNodeId); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/NoopReqRespMethodLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/NoopReqRespMethodLogger.java new file mode 100644 index 00000000000..e7b547750eb --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/NoopReqRespMethodLogger.java @@ -0,0 +1,43 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +class NoopReqRespMethodLogger + implements ReqRespMethodLogger { + + @Override + public ReqRespResponseLogger onInboundRequest( + final LoggingPeerId fromPeer, final TRequest request) { + return noopResponseLogger(); + } + + @Override + public ReqRespResponseLogger onOutboundRequest( + final LoggingPeerId toPeer, final TRequest request) { + return noopResponseLogger(); + } + + static ReqRespResponseLogger noopResponseLogger() { + return new ReqRespResponseLogger<>() { + @Override + public void onNextItem(final TResponse s) {} + + @Override + public void onComplete() {} + + @Override + public void onError(final Throwable error) {} + }; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespMethodLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespMethodLogger.java new file mode 100644 index 00000000000..f52221d16e2 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespMethodLogger.java @@ -0,0 +1,21 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +public interface ReqRespMethodLogger { + + ReqRespResponseLogger onInboundRequest(LoggingPeerId fromPeer, TRequest request); + + ReqRespResponseLogger onOutboundRequest(LoggingPeerId toPeer, TRequest request); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespResponseLogger.java new file mode 100644 index 00000000000..a7a457f2906 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespResponseLogger.java @@ -0,0 +1,44 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.log.rpc; + +import tech.pegasys.teku.infrastructure.async.stream.AsyncStreamVisitor; + +public interface ReqRespResponseLogger { + + void onNextItem(TResponse s); + + void onComplete(); + + void onError(Throwable error); + + default AsyncStreamVisitor asAsyncStreamVisitor() { + return new AsyncStreamVisitor<>() { + @Override + public void onNext(TResponse tResponse) { + ReqRespResponseLogger.this.onNextItem(tResponse); + } + + @Override + public void onComplete() { + ReqRespResponseLogger.this.onComplete(); + } + + @Override + public void onError(Throwable t) { + ReqRespResponseLogger.this.onError(t); + } + }; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRangeReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRangeReqResp.java new file mode 100644 index 00000000000..8681d68de8a --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRangeReqResp.java @@ -0,0 +1,29 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.retriever; + +import java.util.List; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; + +// TODO-fulu Bring concrete implementation +public interface BatchDataColumnsByRangeReqResp { + + AsyncStream requestDataColumnSidecarsByRange( + UInt256 nodeId, UInt64 startSlot, int slotCount, List columnIndexes); + + int getCurrentRequestLimit(UInt256 nodeId); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRootReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRootReqResp.java new file mode 100644 index 00000000000..76772cacd6b --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRootReqResp.java @@ -0,0 +1,29 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.retriever; + +import java.util.List; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; + +// TODO-fulu Bring concrete implementation +public interface BatchDataColumnsByRootReqResp { + + AsyncStream requestDataColumnSidecarsByRoot( + UInt256 nodeId, List byRootIdentifiers); + + int getCurrentRequestLimit(UInt256 nodeId); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqResp.java new file mode 100644 index 00000000000..81611c8628d --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqResp.java @@ -0,0 +1,35 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.retriever; + +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.util.DataColumnIdentifier; + +public interface DataColumnReqResp { + + SafeFuture requestDataColumnSidecar( + UInt256 nodeId, DataColumnIdentifier columnIdentifier); + + void flush(); + + int getCurrentRequestLimit(UInt256 nodeId); + + class DataColumnReqRespException extends RuntimeException {} + + class DasColumnNotAvailableException extends DataColumnReqRespException {} + + class DasPeerDisconnectedException extends DataColumnReqRespException {} +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java new file mode 100644 index 00000000000..bf50942c275 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java @@ -0,0 +1,132 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.retriever; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStreamHandler; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifierSchema; +import tech.pegasys.teku.spec.datastructures.util.DataColumnIdentifier; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsFulu; + +public class DataColumnReqRespBatchingImpl implements DataColumnReqResp { + private final BatchDataColumnsByRootReqResp batchRpc; + private final DataColumnsByRootIdentifierSchema byRootIdentifierSchema; + + public DataColumnReqRespBatchingImpl( + final BatchDataColumnsByRootReqResp batchRpc, + final SchemaDefinitionsFulu schemaDefinitionsFulu) { + this.batchRpc = batchRpc; + this.byRootIdentifierSchema = schemaDefinitionsFulu.getDataColumnsByRootIdentifierSchema(); + } + + private record RequestEntry( + UInt256 nodeId, + DataColumnIdentifier columnIdentifier, + SafeFuture promise) {} + + private final ConcurrentLinkedQueue bufferedRequests = + new ConcurrentLinkedQueue<>(); + + @Override + public SafeFuture requestDataColumnSidecar( + final UInt256 nodeId, final DataColumnIdentifier columnIdentifier) { + final RequestEntry entry = new RequestEntry(nodeId, columnIdentifier, new SafeFuture<>()); + bufferedRequests.add(entry); + return entry.promise(); + } + + @Override + public void flush() { + final Map> byNodes = new HashMap<>(); + RequestEntry request; + while ((request = bufferedRequests.poll()) != null) { + byNodes.computeIfAbsent(request.nodeId, __ -> new ArrayList<>()).add(request); + } + for (Map.Entry> entry : byNodes.entrySet()) { + flushForNode(entry.getKey(), entry.getValue()); + } + } + + private void flushForNode(final UInt256 nodeId, final List nodeRequests) { + final Map> byRootMap = + nodeRequests.stream() + .map(e -> e.columnIdentifier) + .collect(Collectors.groupingBy(DataColumnIdentifier::blockRoot)); + final List dataColumnsByRootIdentifiers = + byRootMap.entrySet().stream() + .map( + entry -> + byRootIdentifierSchema.create( + entry.getKey(), + entry.getValue().stream().map(DataColumnIdentifier::columnIndex).toList())) + .toList(); + final AsyncStream response = + batchRpc.requestDataColumnSidecarsByRoot(nodeId, dataColumnsByRootIdentifiers); + + response.consume( + new AsyncStreamHandler<>() { + private final AtomicInteger count = new AtomicInteger(); + private final Map requestsNyColumnId = + nodeRequests.stream() + .collect(Collectors.toMap(RequestEntry::columnIdentifier, req -> req)); + + @Override + public SafeFuture onNext(final DataColumnSidecar dataColumnSidecar) { + final DataColumnIdentifier dataColumnIdentifier = + DataColumnIdentifier.createFromSidecar(dataColumnSidecar); + final RequestEntry request = requestsNyColumnId.get(dataColumnIdentifier); + if (request == null) { + return SafeFuture.failedFuture( + new IllegalArgumentException( + "Responded data column was not requested: " + dataColumnIdentifier)); + } else { + request.promise().complete(dataColumnSidecar); + count.incrementAndGet(); + return TRUE_FUTURE; + } + } + + @Override + public void onComplete() { + nodeRequests.stream() + .filter(req -> !req.promise().isDone()) + .forEach( + req -> + req.promise().completeExceptionally(new DasColumnNotAvailableException())); + } + + @Override + public void onError(final Throwable err) { + nodeRequests.forEach(e -> e.promise().completeExceptionally(err)); + } + }); + } + + @Override + public int getCurrentRequestLimit(final UInt256 nodeId) { + return batchRpc.getCurrentRequestLimit(nodeId); + } +} diff --git a/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/TestPeer.java b/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/TestPeer.java new file mode 100644 index 00000000000..23e957c298c --- /dev/null +++ b/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/TestPeer.java @@ -0,0 +1,98 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.statetransition.datacolumns.retriever; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.util.DataColumnIdentifier; + +public class TestPeer { + + public record Request( + DataColumnIdentifier dataColumnIdentifier, SafeFuture response) {} + + private final AsyncRunner asyncRunner; + private final UInt256 nodeId; + private final Duration latency; + + private final Map availableSidecars = new HashMap<>(); + private final List requests = new ArrayList<>(); + private int currentRequestLimit = 1000; + + public TestPeer(final AsyncRunner asyncRunner, final UInt256 nodeId, final Duration latency) { + this.asyncRunner = asyncRunner; + this.nodeId = nodeId; + this.latency = latency; + } + + public void addSidecar(final DataColumnSidecar sidecar) { + availableSidecars.put(DataColumnIdentifier.createFromSidecar(sidecar), sidecar); + } + + public UInt256 getNodeId() { + return nodeId; + } + + public void onDisconnect() { + requests.stream() + .filter(r -> !r.response.isDone()) + .forEach( + r -> + r.response.completeExceptionally( + new DataColumnReqResp.DasPeerDisconnectedException())); + } + + public SafeFuture requestSidecar( + final DataColumnIdentifier dataColumnIdentifier) { + final SafeFuture promise = new SafeFuture<>(); + final Request request = new Request(dataColumnIdentifier, promise); + requests.add(request); + asyncRunner + .runAfterDelay( + () -> { + if (!promise.isDone()) { + DataColumnSidecar maybeSidecar = availableSidecars.get(dataColumnIdentifier); + if (maybeSidecar != null) { + promise.complete(maybeSidecar); + } else { + promise.completeExceptionally( + new DataColumnReqResp.DasColumnNotAvailableException()); + } + } + }, + latency) + .ifExceptionGetsHereRaiseABug(); + return promise; + } + + public List getRequests() { + return requests; + } + + public int getCurrentRequestLimit() { + return currentRequestLimit; + } + + public TestPeer currentRequestLimit(final int currentRequestLimit) { + this.currentRequestLimit = currentRequestLimit; + return this; + } +} diff --git a/networking/eth2/src/integration-test/java/tech/pegasys/teku/networking/eth2/GetMetadataIntegrationTest.java b/networking/eth2/src/integration-test/java/tech/pegasys/teku/networking/eth2/GetMetadataIntegrationTest.java index f0ff615963b..b0e5bff03a0 100644 --- a/networking/eth2/src/integration-test/java/tech/pegasys/teku/networking/eth2/GetMetadataIntegrationTest.java +++ b/networking/eth2/src/integration-test/java/tech/pegasys/teku/networking/eth2/GetMetadataIntegrationTest.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -28,6 +29,7 @@ import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.versions.altair.MetadataMessageAltair; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.versions.fulu.MetadataMessageFulu; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.versions.phase0.MetadataMessagePhase0; public class GetMetadataIntegrationTest extends AbstractRpcMethodIntegrationTest { @@ -71,25 +73,23 @@ public void requestMetadata_shouldSendLatestSyncnets( peerAndNetwork.network().subscribeToSyncCommitteeSubnetId(1); peerAndNetwork.network().subscribeToSyncCommitteeSubnetId(2); MetadataMessage md3 = peer.requestMetadata().get(10, TimeUnit.SECONDS); - assertThat(md3).isInstanceOf(MetadataMessageAltair.class); - final MetadataMessageAltair altairMetadata = (MetadataMessageAltair) md3; + assertThat(md3).isInstanceOfAny(MetadataMessageAltair.class, MetadataMessageFulu.class); // Check metadata - assertThat(altairMetadata.getSeqNumber()).isGreaterThan(md2.getSeqNumber()); - assertThat(altairMetadata.getSyncnets().getBitCount()).isEqualTo(2); - assertThat(altairMetadata.getSyncnets().getBit(1)).isTrue(); - assertThat(altairMetadata.getSyncnets().getBit(2)).isTrue(); + assertThat(md3.getSeqNumber()).isGreaterThan(md2.getSeqNumber()); + assertThat(md3.getOptionalSyncnets().orElseThrow().getBitCount()).isEqualTo(2); + assertThat(md3.getOptionalSyncnets().orElseThrow().getBit(1)).isTrue(); + assertThat(md3.getOptionalSyncnets().orElseThrow().getBit(2)).isTrue(); // Unsubscribe from sync committee subnet peerAndNetwork.network().unsubscribeFromSyncCommitteeSubnetId(2); MetadataMessage md4 = peer.requestMetadata().get(10, TimeUnit.SECONDS); - assertThat(md4).isInstanceOf(MetadataMessageAltair.class); - final MetadataMessageAltair altairMetadata2 = (MetadataMessageAltair) md4; + assertThat(md4).isInstanceOfAny(MetadataMessageAltair.class, MetadataMessageFulu.class); // Check metadata - assertThat(altairMetadata2.getSeqNumber()).isGreaterThan(altairMetadata.getSeqNumber()); - assertThat(altairMetadata2.getSyncnets().getBitCount()).isEqualTo(1); - assertThat(altairMetadata2.getSyncnets().getBit(1)).isTrue(); + assertThat(md4.getSeqNumber()).isGreaterThan(md3.getSeqNumber()); + assertThat(md4.getOptionalSyncnets().orElseThrow().getBitCount()).isEqualTo(1); + assertThat(md4.getOptionalSyncnets().orElseThrow().getBit(1)).isTrue(); } @ParameterizedTest(name = "{0}->{1}") @@ -109,16 +109,28 @@ public void requestMetadata_shouldSendLatestAttnetsAndSyncnets( peerAndNetwork.network().subscribeToSyncCommitteeSubnetId(1); peerAndNetwork.network().setLongTermAttestationSubnetSubscriptions(List.of(0, 1, 8)); MetadataMessage md3 = peer.requestMetadata().get(10, TimeUnit.SECONDS); - assertThat(md3).isInstanceOf(MetadataMessageAltair.class); - final MetadataMessageAltair altairMetadata = (MetadataMessageAltair) md3; - - assertThat(altairMetadata.getSeqNumber()).isGreaterThan(md2.getSeqNumber()); - assertThat(altairMetadata.getSyncnets().getBitCount()).isEqualTo(1); - assertThat(altairMetadata.getSyncnets().getBit(1)).isTrue(); - assertThat(altairMetadata.getAttnets().getBitCount()).isEqualTo(3); - assertThat(altairMetadata.getAttnets().getBit(0)).isTrue(); - assertThat(altairMetadata.getAttnets().getBit(1)).isTrue(); - assertThat(altairMetadata.getAttnets().getBit(8)).isTrue(); + assertThat(md3).isInstanceOfAny(MetadataMessageAltair.class, MetadataMessageFulu.class); + + assertThat(md3.getSeqNumber()).isGreaterThan(md2.getSeqNumber()); + assertThat(md3.getOptionalSyncnets().orElseThrow().getBitCount()).isEqualTo(1); + assertThat(md3.getOptionalSyncnets().orElseThrow().getBit(1)).isTrue(); + assertThat(md3.getAttnets().getBitCount()).isEqualTo(3); + assertThat(md3.getAttnets().getBit(0)).isTrue(); + assertThat(md3.getAttnets().getBit(1)).isTrue(); + assertThat(md3.getAttnets().getBit(8)).isTrue(); + } + + @ParameterizedTest(name = "{0}->{1}") + @MethodSource("generateSpecTransition") + public void requestMetadata_shouldIncludeCustodySubnetCount( + final SpecMilestone baseMilestone, final SpecMilestone nextMilestone) throws Exception { + setUp(baseMilestone, Optional.of(nextMilestone)); + final PeerAndNetwork peerAndNetwork = createRemotePeerAndNetwork(true, true); + final Eth2Peer peer = peerAndNetwork.peer(); + MetadataMessage md1 = peer.requestMetadata().get(10, TimeUnit.SECONDS); + + Assumptions.assumeTrue(md1 instanceof MetadataMessageFulu, "Milestone skipped"); + assertThat(((MetadataMessageFulu) md1).getCustodyGroupCount().isGreaterThan(0)).isTrue(); } @ParameterizedTest(name = "{0} => {1}, nextSpecEnabledLocally={2}, nextSpecEnabledRemotely={3}") @@ -141,13 +153,17 @@ public void requestMetadata_withDisparateVersionsEnabled( assertThat(res).isCompleted(); final MetadataMessage metadata = safeJoin(res); assertThat(metadata).isInstanceOf(expectedType); - assertThat(metadata.getSeqNumber()).isEqualTo(UInt64.ZERO); + // There will be update of custody_group_count in this case + if (!(nextMilestone == SpecMilestone.FULU && nextSpecEnabledRemotely)) { + assertThat(metadata.getSeqNumber()).isEqualTo(UInt64.ZERO); + } } private static Class milestoneToMetadataClass(final SpecMilestone milestone) { return switch (milestone) { case PHASE0 -> MetadataMessagePhase0.class; - default -> MetadataMessageAltair.class; + case ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA -> MetadataMessageAltair.class; + case FULU -> MetadataMessageFulu.class; }; } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java index 4c7bb0db118..107eb485d8b 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java @@ -92,6 +92,7 @@ import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.statetransition.util.DebugDataDumper; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.store.KeyValueStore; @@ -143,8 +144,7 @@ public class Eth2P2PNetworkBuilder { protected boolean recordMessageArrival; protected DebugDataDumper debugDataDumper; private DasGossipLogger dasGossipLogger; - - // private DasReqRespLogger dasReqRespLogger; + private DasReqRespLogger dasReqRespLogger; protected Eth2P2PNetworkBuilder() {} @@ -197,7 +197,8 @@ public Eth2P2PNetwork build() { spec, kzg, discoveryNodeIdExtractor, - dasTotalCustodyGroupCount); + dasTotalCustodyGroupCount, + dasReqRespLogger); final Collection> eth2RpcMethods = eth2PeerManager.getBeaconChainMethods().all(); rpcMethods.addAll(eth2RpcMethods); @@ -727,4 +728,9 @@ public Eth2P2PNetworkBuilder gossipDasLogger(final DasGossipLogger dasGossipLogg this.dasGossipLogger = dasGossipLogger; return this; } + + public Eth2P2PNetworkBuilder reqRespDasLogger(final DasReqRespLogger dasReqRespLogger) { + this.dasReqRespLogger = dasReqRespLogger; + return this; + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DefaultEth2Peer.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DefaultEth2Peer.java index f11a56d6256..1ca829ecfeb 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DefaultEth2Peer.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DefaultEth2Peer.java @@ -28,10 +28,12 @@ import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; +import org.hyperledger.besu.plugin.services.MetricsSystem; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.ssz.SszData; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; import tech.pegasys.teku.infrastructure.subscribers.Subscribers; +import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.kzg.KZG; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.BeaconChainMethods; @@ -39,6 +41,8 @@ import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.BlobSidecarsByRootListenerValidatingProxy; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.BlobSidecarsByRootValidator; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.BlocksByRangeListenerWrapper; +import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.DataColumnSidecarsByRangeListenerValidatingProxy; +import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.DataColumnSidecarsByRootListenerValidatingProxy; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.MetadataMessagesFactory; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.StatusMessageFactory; import tech.pegasys.teku.networking.eth2.rpc.core.Eth2RpcResponseHandler; @@ -52,7 +56,9 @@ import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.config.SpecConfigDeneb; +import tech.pegasys.teku.spec.config.SpecConfigFulu; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BeaconBlocksByRangeRequestMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BeaconBlocksByRootRequestMessage; @@ -60,6 +66,10 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobSidecarsByRangeRequestMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobSidecarsByRootRequestMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobSidecarsByRootRequestMessageSchema; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRangeRequestMessage; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRootRequestMessage; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRootRequestMessageSchema; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.EmptyMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.GoodbyeMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.PingMessage; @@ -68,6 +78,7 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessage; import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsDeneb; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsFulu; class DefaultEth2Peer extends DelegatingPeer implements Eth2Peer { private static final Logger LOG = LogManager.getLogger(); @@ -79,19 +90,29 @@ class DefaultEth2Peer extends DelegatingPeer implements Eth2Peer { private final MetadataMessagesFactory metadataMessagesFactory; private final PeerChainValidator peerChainValidator; private volatile Optional remoteStatus = Optional.empty(); - private volatile Optional remoteMetadataSeqNumber = Optional.empty(); - private volatile Optional remoteAttSubnets = Optional.empty(); + private volatile Optional remoteMetadata = Optional.empty(); private final SafeFuture initialStatus = new SafeFuture<>(); private final Subscribers statusSubscribers = Subscribers.create(true); + private final Subscribers metadataSubscribers = + Subscribers.create(true); private final AtomicInteger outstandingRequests = new AtomicInteger(0); private final AtomicInteger unansweredPings = new AtomicInteger(); private final RateTracker blockRequestTracker; private final RateTracker blobSidecarsRequestTracker; + private final RateTracker dataColumnSidecarsRequestTracker; private final RateTracker requestTracker; private final KZG kzg; + private final MetricsSystem metricsSystem; + private final TimeProvider timeProvider; private final Supplier firstSlotSupportingBlobSidecarsByRange; + private final Supplier firstSlotSupportingDataColumnSidecarsByRange; private final Supplier blobSidecarsByRootRequestMessageSchema; + private final Supplier + dataColumnSidecarsByRootRequestMessageSchema; + private final Supplier< + DataColumnSidecarsByRangeRequestMessage.DataColumnSidecarsByRangeRequestMessageSchema> + dataColumnSidecarsByRangeRequestMessageSchema; DefaultEth2Peer( final Spec spec, @@ -103,8 +124,11 @@ class DefaultEth2Peer extends DelegatingPeer implements Eth2Peer { final PeerChainValidator peerChainValidator, final RateTracker blockRequestTracker, final RateTracker blobSidecarsRequestTracker, + final RateTracker dataColumnSidecarsRequestTracker, final RateTracker requestTracker, - final KZG kzg) { + final KZG kzg, + final MetricsSystem metricsSystem, + final TimeProvider timeProvider) { super(peer); this.spec = spec; this.discoveryNodeId = discoveryNodeId; @@ -114,8 +138,11 @@ class DefaultEth2Peer extends DelegatingPeer implements Eth2Peer { this.peerChainValidator = peerChainValidator; this.blockRequestTracker = blockRequestTracker; this.blobSidecarsRequestTracker = blobSidecarsRequestTracker; + this.dataColumnSidecarsRequestTracker = dataColumnSidecarsRequestTracker; this.requestTracker = requestTracker; this.kzg = kzg; + this.metricsSystem = metricsSystem; + this.timeProvider = timeProvider; this.firstSlotSupportingBlobSidecarsByRange = Suppliers.memoize( () -> { @@ -128,6 +155,24 @@ class DefaultEth2Peer extends DelegatingPeer implements Eth2Peer { SchemaDefinitionsDeneb.required( spec.forMilestone(SpecMilestone.DENEB).getSchemaDefinitions()) .getBlobSidecarsByRootRequestMessageSchema()); + this.firstSlotSupportingDataColumnSidecarsByRange = + Suppliers.memoize( + () -> { + final UInt64 fuluForkEpoch = getSpecConfigFulu().getFuluForkEpoch(); + return spec.computeStartSlotAtEpoch(fuluForkEpoch); + }); + this.dataColumnSidecarsByRootRequestMessageSchema = + Suppliers.memoize( + () -> + SchemaDefinitionsFulu.required( + spec.forMilestone(SpecMilestone.FULU).getSchemaDefinitions()) + .getDataColumnSidecarsByRootRequestMessageSchema()); + this.dataColumnSidecarsByRangeRequestMessageSchema = + Suppliers.memoize( + () -> + SchemaDefinitionsFulu.required( + spec.forMilestone(SpecMilestone.FULU).getSchemaDefinitions()) + .getDataColumnSidecarsByRangeRequestMessageSchema()); } @Override @@ -157,8 +202,7 @@ public void updateStatus(final PeerStatus status) { @Override public void updateMetadataSeqNumber(final UInt64 seqNumber) { - Optional curValue = this.remoteMetadataSeqNumber; - remoteMetadataSeqNumber = Optional.of(seqNumber); + Optional curValue = this.remoteMetadata.map(MetadataMessage::getSeqNumber); if (curValue.isEmpty() || seqNumber.compareTo(curValue.get()) > 0) { requestMetadata() .finish( @@ -167,8 +211,8 @@ public void updateMetadataSeqNumber(final UInt64 seqNumber) { } private void updateMetadata(final MetadataMessage metadataMessage) { - remoteMetadataSeqNumber = Optional.of(metadataMessage.getSeqNumber()); - remoteAttSubnets = Optional.of(metadataMessage.getAttnets()); + this.remoteMetadata = Optional.ofNullable(metadataMessage); + metadataSubscribers.forEach(s -> s.onPeerMetadataUpdate(this, metadataMessage)); } @Override @@ -182,6 +226,12 @@ public void subscribeStatusUpdates(final PeerStatusSubscriber subscriber) { statusSubscribers.subscribe(subscriber); } + @Override + public void subscribeMetadataUpdates(final PeerMetadataUpdateSubscriber subscriber) { + metadataSubscribers.subscribe(subscriber); + remoteMetadata.ifPresent(metadata -> subscriber.onPeerMetadataUpdate(this, metadata)); + } + @Override public PeerStatus getStatus() { return remoteStatus.orElseThrow(); @@ -189,7 +239,7 @@ public PeerStatus getStatus() { @Override public Optional getRemoteAttestationSubnets() { - return remoteAttSubnets; + return remoteMetadata.map(MetadataMessage::getAttnets); } @Override @@ -260,6 +310,29 @@ public SafeFuture requestBlobSidecarsByRoot( .orElse(failWithUnsupportedMethodException("BlobSidecarsByRoot")); } + @Override + public SafeFuture requestDataColumnSidecarsByRoot( + final List dataColumnIdentifiers, + final RpcResponseListener listener) { + return rpcMethods + .dataColumnSidecarsByRoot() + .map( + method -> + requestStream( + method, + new DataColumnSidecarsByRootRequestMessage( + dataColumnSidecarsByRootRequestMessageSchema.get(), dataColumnIdentifiers), + new DataColumnSidecarsByRootListenerValidatingProxy( + this, + spec, + listener, + kzg, + metricsSystem, + timeProvider, + dataColumnIdentifiers))) + .orElse(failWithUnsupportedMethodException("DataColumnSidecarsByRoot")); + } + @Override public SafeFuture> requestBlockBySlot(final UInt64 slot) { final Eth2RpcMethod blocksByRange = @@ -328,7 +401,7 @@ public SafeFuture requestBlobSidecarsByRange( method -> { final UInt64 firstSupportedSlot = firstSlotSupportingBlobSidecarsByRange.get(); final BlobSidecarsByRangeRequestMessage request; - final int maxBlobsPerBlock = getMaxBlobsPerBlock(startSlot.plus(count)); + final int maxBlobsPerBlock = calculateMaxBlobsPerBlock(startSlot.plus(count)); if (startSlot.isLessThan(firstSupportedSlot)) { LOG.debug( @@ -361,8 +434,58 @@ public SafeFuture requestBlobSidecarsByRange( .orElse(failWithUnsupportedMethodException("BlobSidecarsByRange")); } - private int getMaxBlobsPerBlock(final UInt64 slot) { - return spec.getMaxBlobsPerBlockAtSlot(slot).orElseThrow(); + private int calculateMaxBlobsPerBlock(final UInt64 endSlot) { + return SpecConfigDeneb.required(spec.atSlot(endSlot).getConfig()).getMaxBlobsPerBlock(); + } + + @Override + public SafeFuture requestDataColumnSidecarsByRange( + final UInt64 startSlot, + final UInt64 count, + final List columns, + final RpcResponseListener listener) { + return rpcMethods + .getDataColumnSidecarsByRange() + .map( + method -> { + final UInt64 firstSupportedSlot = firstSlotSupportingDataColumnSidecarsByRange.get(); + final DataColumnSidecarsByRangeRequestMessage request; + + if (startSlot.isLessThan(firstSupportedSlot)) { + LOG.debug( + "Requesting data column sidecars from slot {} instead of slot {} because the request is spanning the Deneb fork transition", + firstSupportedSlot, + startSlot); + final UInt64 updatedCount = + count.minusMinZero(firstSupportedSlot.minusMinZero(startSlot)); + if (updatedCount.isZero()) { + return SafeFuture.COMPLETE; + } + request = + dataColumnSidecarsByRangeRequestMessageSchema + .get() + .create(firstSupportedSlot, updatedCount, columns); + } else { + request = + dataColumnSidecarsByRangeRequestMessageSchema + .get() + .create(startSlot, count, columns); + } + return requestStream( + method, + request, + new DataColumnSidecarsByRangeListenerValidatingProxy( + spec, + this, + listener, + kzg, + metricsSystem, + timeProvider, + request.getStartSlot(), + request.getCount(), + request.getColumns())); + }) + .orElse(failWithUnsupportedMethodException("DataColumnSidecarsByRange")); } @Override @@ -396,10 +519,34 @@ public void adjustBlobSidecarsRequest( blobSidecarsRequestTracker, blobSidecarsRequest, returnedBlobSidecarsCount); } + @Override + public long getAvailableDataColumnSidecarsRequestCount() { + return dataColumnSidecarsRequestTracker.getAvailableObjectCount(); + } + + @Override + public Optional approveDataColumnSidecarsRequest( + final ResponseCallback callback, final long dataColumnSidecarsCount) { + return approveObjectsRequest( + "data column sidecars", + dataColumnSidecarsRequestTracker, + dataColumnSidecarsCount, + callback); + } + + @Override + public void adjustDataColumnSidecarsRequest( + final RequestApproval dataColumnSidecarsRequest, final long returnedDataColumnSidecarsCount) { + adjustObjectsRequest( + dataColumnSidecarsRequestTracker, + dataColumnSidecarsRequest, + returnedDataColumnSidecarsCount); + } + @Override public boolean approveRequest() { if (requestTracker.approveObjectsRequest(1L).isEmpty()) { - LOG.debug("Peer {} disconnected due to request rate limits", getId()); + LOG.info("Peer {} disconnected due to request rate limits for {}", getId(), requestTracker); disconnectCleanly(DisconnectReason.RATE_LIMITING).ifExceptionGetsHereRaiseABug(); return false; } @@ -444,7 +591,7 @@ private Optional approveObjectsRequest( final Optional requestApproval = requestTracker.approveObjectsRequest(objectsCount); if (requestApproval.isEmpty()) { - LOG.debug("Peer {} disconnected due to {} rate limits", getId(), requestType); + LOG.info("Peer {} disconnected due to {} rate limits", getId(), requestType); callback.completeWithErrorResponse( new RpcException(INVALID_REQUEST_CODE, "Peer has been rate limited")); disconnectCleanly(DisconnectReason.RATE_LIMITING).ifExceptionGetsHereRaiseABug(); @@ -487,6 +634,10 @@ private SpecConfigDeneb getSpecConfigDeneb() { return SpecConfigDeneb.required(spec.forMilestone(SpecMilestone.DENEB).getConfig()); } + private SpecConfigFulu getSpecConfigFulu() { + return SpecConfigFulu.required(spec.forMilestone(SpecMilestone.FULU).getConfig()); + } + private SafeFuture failWithUnsupportedMethodException(final String method) { return SafeFuture.failedFuture( new UnsupportedOperationException(method + " method is not supported")); diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2Peer.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2Peer.java index 342490a75fe..f5cc5ee60df 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2Peer.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2Peer.java @@ -17,9 +17,11 @@ import java.util.Optional; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; +import org.hyperledger.besu.plugin.services.MetricsSystem; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.ssz.SszData; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; +import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.kzg.KZG; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.BeaconChainMethods; @@ -32,8 +34,10 @@ import tech.pegasys.teku.networking.p2p.rpc.RpcResponseListener; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobIdentifier; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.RpcRequest; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessage; import tech.pegasys.teku.spec.datastructures.state.Checkpoint; @@ -49,8 +53,11 @@ static Eth2Peer create( final PeerChainValidator peerChainValidator, final RateTracker blockRequestTracker, final RateTracker blobSidecarsRequestTracker, + final RateTracker dataColumnSidecarsRequestTracker, final RateTracker requestTracker, - final KZG kzg) { + final KZG kzg, + final MetricsSystem metricsSystem, + final TimeProvider timeProvider) { return new DefaultEth2Peer( spec, peer, @@ -61,8 +68,11 @@ static Eth2Peer create( peerChainValidator, blockRequestTracker, blobSidecarsRequestTracker, + dataColumnSidecarsRequestTracker, requestTracker, - kzg); + kzg, + metricsSystem, + timeProvider); } void updateStatus(PeerStatus status); @@ -73,6 +83,8 @@ static Eth2Peer create( void subscribeStatusUpdates(PeerStatusSubscriber subscriber); + void subscribeMetadataUpdates(PeerMetadataUpdateSubscriber subscriber); + PeerStatus getStatus(); Optional getRemoteAttestationSubnets(); @@ -96,6 +108,10 @@ SafeFuture requestBlocksByRoot( SafeFuture requestBlobSidecarsByRoot( List blobIdentifiers, RpcResponseListener listener); + SafeFuture requestDataColumnSidecarsByRoot( + List dataColumnIdentifiers, + RpcResponseListener listener); + SafeFuture> requestBlockBySlot(UInt64 slot); SafeFuture> requestBlockByRoot(Bytes32 blockRoot); @@ -118,6 +134,14 @@ Optional approveBlobSidecarsRequest( void adjustBlobSidecarsRequest( RequestApproval blobSidecarsRequest, long returnedBlobSidecarsCount); + long getAvailableDataColumnSidecarsRequestCount(); + + Optional approveDataColumnSidecarsRequest( + ResponseCallback callback, long dataColumnSidecarsCount); + + void adjustDataColumnSidecarsRequest( + RequestApproval dataColumnSidecarsRequest, long returnedDataColumnSidecarsCount); + boolean approveRequest(); SafeFuture sendPing(); @@ -129,4 +153,14 @@ void adjustBlobSidecarsRequest( interface PeerStatusSubscriber { void onPeerStatus(final PeerStatus initialStatus); } + + @FunctionalInterface + interface PeerMetadataUpdateSubscriber { + + /** + * Sends the current peer metadata upon subscription if metadata has been received already. Then + * calls the method any time the peer metadata is updated + */ + void onPeerMetadataUpdate(Eth2Peer peer, MetadataMessage metadata); + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerFactory.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerFactory.java index 142b88c9354..059b9a2f5bc 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerFactory.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerFactory.java @@ -77,9 +77,16 @@ public Eth2Peer create(final Peer peer, final BeaconChainMethods rpcMethods) { statusMessageFactory, metadataMessagesFactory, PeerChainValidator.create(spec, metricsSystem, chainDataClient, requiredCheckpoint), - RateTracker.create(peerBlocksRateLimit, TIME_OUT, timeProvider), - RateTracker.create(peerBlobSidecarsRateLimit, TIME_OUT, timeProvider), - RateTracker.create(peerRequestLimit, TIME_OUT, timeProvider), - kzg); + RateTracker.create(peerBlocksRateLimit, TIME_OUT, timeProvider, "blocks"), + RateTracker.create(peerBlobSidecarsRateLimit, TIME_OUT, timeProvider, "blobSidecars"), + RateTracker.create( + peerBlocksRateLimit * spec.getNumberOfDataColumns().orElse(1), + TIME_OUT, + timeProvider, + "dataColumns"), + RateTracker.create(peerRequestLimit, TIME_OUT, timeProvider, "requestTracker"), + kzg, + metricsSystem, + timeProvider); } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManager.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManager.java index b5c6322f01e..3650e666dc7 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManager.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManager.java @@ -47,6 +47,7 @@ import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -85,7 +86,8 @@ public class Eth2PeerManager implements PeerLookup, PeerHandler { final RpcEncoding rpcEncoding, final Duration eth2RpcPingInterval, final int eth2RpcOutstandingPingThreshold, - final Duration eth2StatusUpdateInterval) { + final Duration eth2StatusUpdateInterval, + final DasReqRespLogger dasLogger) { this.asyncRunner = asyncRunner; this.recentChainData = recentChainData; this.eth2PeerFactory = eth2PeerFactory; @@ -96,11 +98,14 @@ public class Eth2PeerManager implements PeerLookup, PeerHandler { asyncRunner, this, combinedChainDataClient, + dataColumnSidecarCustody, + custodyGroupCountManager, recentChainData, metricsSystem, statusMessageFactory, metadataMessagesFactory, - rpcEncoding); + rpcEncoding, + dasLogger); this.eth2RpcPingInterval = eth2RpcPingInterval; this.eth2RpcOutstandingPingThreshold = eth2RpcOutstandingPingThreshold; this.eth2StatusUpdateInterval = eth2StatusUpdateInterval; @@ -128,7 +133,8 @@ public static Eth2PeerManager create( final Spec spec, final KZG kzg, final DiscoveryNodeIdExtractor discoveryNodeIdExtractor, - final Optional custodyGroupCount) { + final Optional custodyGroupCount, + final DasReqRespLogger dasLogger) { // FIXME: we have no guarantee here that it's synced already custodyGroupCount.ifPresent(metadataMessagesFactory::updateCustodyGroupCount); @@ -163,7 +169,8 @@ public static Eth2PeerManager create( rpcEncoding, eth2RpcPingInterval, eth2RpcOutstandingPingThreshold, - eth2StatusUpdateInterval); + eth2StatusUpdateInterval, + dasLogger); } public MetadataMessage getMetadataMessage() { diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTracker.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTracker.java index 04de9fdecab..8b6836ebb97 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTracker.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTracker.java @@ -28,6 +28,11 @@ public Optional approveObjectsRequest(long objectsCount) { public void adjustObjectsRequest( RequestApproval requestApproval, long returnedObjectsCount) {} + @Override + public long getAvailableObjectCount() { + return 0; + } + @Override public void pruneRequests() {} }; @@ -36,12 +41,17 @@ public void pruneRequests() {} // they can have the objects they request otherwise they get none. Optional approveObjectsRequest(long objectsCount); + long getAvailableObjectCount(); + void adjustObjectsRequest(RequestApproval requestApproval, long returnedObjectsCount); void pruneRequests(); static RateTracker create( - final int peerRateLimit, final long timeoutSeconds, final TimeProvider timeProvider) { - return new RateTrackerImpl(peerRateLimit, timeoutSeconds, timeProvider); + final int peerRateLimit, + final long timeoutSeconds, + final TimeProvider timeProvider, + final String name) { + return new RateTrackerImpl(peerRateLimit, timeoutSeconds, timeProvider, name); } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerImpl.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerImpl.java index 14057214832..c2b452f41f2 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerImpl.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerImpl.java @@ -27,12 +27,16 @@ public class RateTrackerImpl implements RateTracker { private final int peerRateLimit; private final long timeoutSeconds; private final TimeProvider timeProvider; + private final String name; private long objectsWithinWindow = 0L; private int newRequestId = 0; public RateTrackerImpl( - final int peerRateLimit, final long timeoutSeconds, final TimeProvider timeProvider) { + final int peerRateLimit, + final long timeoutSeconds, + final TimeProvider timeProvider, + final String name) { Preconditions.checkArgument( peerRateLimit > 0, "peerRateLimit should be a positive number but it was %s", @@ -40,6 +44,7 @@ public RateTrackerImpl( this.peerRateLimit = peerRateLimit; this.timeoutSeconds = timeoutSeconds; this.timeProvider = timeProvider; + this.name = name; } // boundary: if a request comes in and remaining capacity is at least 1, then @@ -62,6 +67,12 @@ public synchronized Optional approveObjectsRequest(final long o return Optional.of(requestApproval); } + @Override + public long getAvailableObjectCount() { + pruneRequests(); + return peerRateLimit - objectsWithinWindow; + } + @Override public synchronized void adjustObjectsRequest( final RequestApproval requestApproval, final long returnedObjectsCount) { @@ -84,4 +95,17 @@ public void pruneRequests() { headMap.values().forEach(value -> objectsWithinWindow -= value); headMap.clear(); } + + @Override + public String toString() { + return "RateTrackerImpl{" + + "peerRateLimit=" + + peerRateLimit + + ", objectsWithinWindow=" + + objectsWithinWindow + + ", name='" + + name + + '\'' + + '}'; + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/SyncSource.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/SyncSource.java index 7af58c868a4..4f5c510f819 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/SyncSource.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/SyncSource.java @@ -13,12 +13,14 @@ package tech.pegasys.teku.networking.eth2.peers; +import java.util.List; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.networking.p2p.peer.DisconnectReason; import tech.pegasys.teku.networking.p2p.reputation.ReputationAdjustment; import tech.pegasys.teku.networking.p2p.rpc.RpcResponseListener; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; /** @@ -32,6 +34,12 @@ SafeFuture requestBlocksByRange( SafeFuture requestBlobSidecarsByRange( UInt64 startSlot, UInt64 count, RpcResponseListener listener); + SafeFuture requestDataColumnSidecarsByRange( + UInt64 startSlot, + UInt64 count, + List columns, + RpcResponseListener listener); + void adjustReputation(final ReputationAdjustment adjustment); SafeFuture disconnectCleanly(DisconnectReason reason); diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodIds.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodIds.java index c48915b3735..88173cfe5b6 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodIds.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodIds.java @@ -26,6 +26,11 @@ public class BeaconChainMethodIds { static final String BLOB_SIDECARS_BY_ROOT = "/eth2/beacon_chain/req/blob_sidecars_by_root"; static final String BLOB_SIDECARS_BY_RANGE = "/eth2/beacon_chain/req/blob_sidecars_by_range"; + static final String DATA_COLUMN_SIDECARS_BY_ROOT = + "/eth2/beacon_chain/req/data_column_sidecars_by_root"; + static final String DATA_COLUMN_SIDECARS_BY_RANGE = + "/eth2/beacon_chain/req/data_column_sidecars_by_range"; + static final String GET_METADATA = "/eth2/beacon_chain/req/metadata"; static final String PING = "/eth2/beacon_chain/req/ping"; @@ -52,6 +57,11 @@ public static String getBlobSidecarsByRangeMethodId( return getMethodId(BLOB_SIDECARS_BY_RANGE, version, encoding); } + public static String getDataColumnSidecarsByRootMethodId( + final int version, final RpcEncoding encoding) { + return getMethodId(DATA_COLUMN_SIDECARS_BY_ROOT, version, encoding); + } + public static String getStatusMethodId(final int version, final RpcEncoding encoding) { return getMethodId(STATUS, version, encoding); } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethods.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethods.java index 0f44a97307e..e2bc35d467f 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethods.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethods.java @@ -27,6 +27,8 @@ import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.BeaconBlocksByRootMessageHandler; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.BlobSidecarsByRangeMessageHandler; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.BlobSidecarsByRootMessageHandler; +import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.DataColumnSidecarsByRangeMessageHandler; +import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.DataColumnSidecarsByRootMessageHandler; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.GoodbyeMessageHandler; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.MetadataMessageHandler; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.MetadataMessagesFactory; @@ -42,7 +44,9 @@ import tech.pegasys.teku.networking.p2p.rpc.RpcMethod; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.config.SpecConfigFulu; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BeaconBlocksByRangeRequestMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BeaconBlocksByRangeRequestMessage.BeaconBlocksByRangeRequestMessageSchema; @@ -51,6 +55,9 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobSidecarsByRangeRequestMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobSidecarsByRootRequestMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobSidecarsByRootRequestMessageSchema; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRangeRequestMessage; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRootRequestMessage; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRootRequestMessageSchema; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.EmptyMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.EmptyMessage.EmptyMessageSchema; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.GoodbyeMessage; @@ -58,6 +65,10 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.StatusMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessage; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsDeneb; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsFulu; +import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; +import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -73,6 +84,10 @@ public class BeaconChainMethods { blobSidecarsByRoot; private final Optional> blobSidecarsByRange; + private final Optional> + dataColumnSidecarsByRoot; + private final Optional> + dataColumnSidecarsByRange; private final Eth2RpcMethod getMetadata; private final Eth2RpcMethod ping; @@ -87,6 +102,10 @@ private BeaconChainMethods( blobSidecarsByRoot, final Optional> blobSidecarsByRange, + final Optional> + dataColumnSidecarsByRoot, + final Optional> + dataColumnSidecarsByRange, final Eth2RpcMethod getMetadata, final Eth2RpcMethod ping) { this.status = status; @@ -95,6 +114,8 @@ private BeaconChainMethods( this.beaconBlocksByRange = beaconBlocksByRange; this.blobSidecarsByRoot = blobSidecarsByRoot; this.blobSidecarsByRange = blobSidecarsByRange; + this.dataColumnSidecarsByRoot = dataColumnSidecarsByRoot; + this.dataColumnSidecarsByRange = dataColumnSidecarsByRange; this.getMetadata = getMetadata; this.ping = ping; this.allMethods = @@ -102,6 +123,8 @@ private BeaconChainMethods( List.of(status, goodBye, beaconBlocksByRoot, beaconBlocksByRange, getMetadata, ping)); blobSidecarsByRoot.ifPresent(allMethods::add); blobSidecarsByRange.ifPresent(allMethods::add); + dataColumnSidecarsByRoot.ifPresent(allMethods::add); + dataColumnSidecarsByRange.ifPresent(allMethods::add); } public static BeaconChainMethods create( @@ -109,11 +132,14 @@ public static BeaconChainMethods create( final AsyncRunner asyncRunner, final PeerLookup peerLookup, final CombinedChainDataClient combinedChainDataClient, + final DataColumnSidecarByRootCustody dataColumnSidecarCustody, + final CustodyGroupCountManager custodyGroupCountManager, final RecentChainData recentChainData, final MetricsSystem metricsSystem, final StatusMessageFactory statusMessageFactory, final MetadataMessagesFactory metadataMessagesFactory, - final RpcEncoding rpcEncoding) { + final RpcEncoding rpcEncoding, + final DasReqRespLogger dasLogger) { return new BeaconChainMethods( createStatus(asyncRunner, statusMessageFactory, peerLookup, rpcEncoding), createGoodBye(asyncRunner, metricsSystem, peerLookup, rpcEncoding), @@ -143,6 +169,26 @@ public static BeaconChainMethods create( peerLookup, rpcEncoding, recentChainData), + createDataColumnSidecarsByRoot( + spec, + metricsSystem, + asyncRunner, + combinedChainDataClient, + dataColumnSidecarCustody, + custodyGroupCountManager, + peerLookup, + rpcEncoding, + recentChainData, + dasLogger), + createDataColumnsSidecarsByRange( + spec, + metricsSystem, + asyncRunner, + combinedChainDataClient, + peerLookup, + rpcEncoding, + recentChainData, + dasLogger), createMetadata(spec, asyncRunner, metadataMessagesFactory, peerLookup, rpcEncoding), createPing(asyncRunner, metadataMessagesFactory, peerLookup, rpcEncoding)); } @@ -332,6 +378,95 @@ private static Eth2RpcMethod createGoodBye( peerLookup)); } + private static Optional> + createDataColumnSidecarsByRoot( + final Spec spec, + final MetricsSystem metricsSystem, + final AsyncRunner asyncRunner, + final CombinedChainDataClient combinedChainDataClient, + final DataColumnSidecarByRootCustody dataColumnSidecarCustody, + final CustodyGroupCountManager custodyGroupCountManager, + final PeerLookup peerLookup, + final RpcEncoding rpcEncoding, + final RecentChainData recentChainData, + final DasReqRespLogger dasLogger) { + if (!spec.isMilestoneSupported(SpecMilestone.FULU)) { + return Optional.empty(); + } + + final RpcContextCodec forkDigestContextCodec = + RpcContextCodec.forkDigest( + spec, recentChainData, ForkDigestPayloadContext.DATA_COLUMN_SIDECAR); + + final DataColumnSidecarsByRootMessageHandler dataColumnSidecarsByRootMessageHandler = + new DataColumnSidecarsByRootMessageHandler( + spec, + metricsSystem, + combinedChainDataClient, + dataColumnSidecarCustody, + custodyGroupCountManager, + dasLogger); + final DataColumnSidecarsByRootRequestMessageSchema + dataColumnSidecarsByRootRequestMessageSchema = + SchemaDefinitionsFulu.required( + spec.forMilestone(SpecMilestone.FULU).getSchemaDefinitions()) + .getDataColumnSidecarsByRootRequestMessageSchema(); + + return Optional.of( + new SingleProtocolEth2RpcMethod<>( + asyncRunner, + BeaconChainMethodIds.DATA_COLUMN_SIDECARS_BY_ROOT, + 1, + rpcEncoding, + dataColumnSidecarsByRootRequestMessageSchema, + true, + forkDigestContextCodec, + dataColumnSidecarsByRootMessageHandler, + peerLookup)); + } + + private static Optional> + createDataColumnsSidecarsByRange( + final Spec spec, + final MetricsSystem metricsSystem, + final AsyncRunner asyncRunner, + final CombinedChainDataClient combinedChainDataClient, + final PeerLookup peerLookup, + final RpcEncoding rpcEncoding, + final RecentChainData recentChainData, + final DasReqRespLogger dasLogger) { + + if (!spec.isMilestoneSupported(SpecMilestone.FULU)) { + return Optional.empty(); + } + + final DataColumnSidecarsByRangeRequestMessage.DataColumnSidecarsByRangeRequestMessageSchema + requestType = + SchemaDefinitionsFulu.required( + spec.forMilestone(SpecMilestone.FULU).getSchemaDefinitions()) + .getDataColumnSidecarsByRangeRequestMessageSchema(); + + final RpcContextCodec forkDigestContextCodec = + RpcContextCodec.forkDigest( + spec, recentChainData, ForkDigestPayloadContext.DATA_COLUMN_SIDECAR); + + final DataColumnSidecarsByRangeMessageHandler dataColumnSidecarsByRangeMessageHandler = + new DataColumnSidecarsByRangeMessageHandler( + spec, getSpecConfigFulu(spec), metricsSystem, combinedChainDataClient, dasLogger); + + return Optional.of( + new SingleProtocolEth2RpcMethod<>( + asyncRunner, + BeaconChainMethodIds.DATA_COLUMN_SIDECARS_BY_RANGE, + 1, + rpcEncoding, + requestType, + true, + forkDigestContextCodec, + dataColumnSidecarsByRangeMessageHandler, + peerLookup)); + } + private static Eth2RpcMethod createMetadata( final Spec spec, final AsyncRunner asyncRunner, @@ -363,6 +498,33 @@ private static Eth2RpcMethod createMetadata( messageHandler, peerLookup); + final List> versionedMethods = + new ArrayList<>(); + + if (spec.isMilestoneSupported(SpecMilestone.FULU)) { + final SszSchema fuluMetadataSchema = + SszSchema.as( + MetadataMessage.class, + SchemaDefinitionsFulu.required( + spec.forMilestone(SpecMilestone.FULU).getSchemaDefinitions()) + .getMetadataMessageSchema()); + final RpcContextCodec fuluContextCodec = + RpcContextCodec.noop(fuluMetadataSchema); + + final SingleProtocolEth2RpcMethod v3Method = + new SingleProtocolEth2RpcMethod<>( + asyncRunner, + BeaconChainMethodIds.GET_METADATA, + 3, + rpcEncoding, + requestType, + expectResponse, + fuluContextCodec, + messageHandler, + peerLookup); + versionedMethods.add(v3Method); + } + if (spec.isMilestoneSupported(SpecMilestone.ALTAIR)) { final SszSchema altairMetadataSchema = SszSchema.as( @@ -384,8 +546,10 @@ private static Eth2RpcMethod createMetadata( altairContextCodec, messageHandler, peerLookup); + versionedMethods.add(v2Method); + versionedMethods.add(v1Method); return VersionedEth2RpcMethod.create( - rpcEncoding, requestType, expectResponse, List.of(v2Method, v1Method)); + rpcEncoding, requestType, expectResponse, versionedMethods); } else { return v1Method; } @@ -411,6 +575,10 @@ private static Eth2RpcMethod createPing( peerLookup); } + private static SpecConfigFulu getSpecConfigFulu(final Spec spec) { + return SpecConfigFulu.required(spec.forMilestone(SpecMilestone.FULU).getConfig()); + } + public Collection> all() { return Collections.unmodifiableCollection(allMethods); } @@ -436,6 +604,16 @@ public Eth2RpcMethod beaco return blobSidecarsByRoot; } + public Optional> + dataColumnSidecarsByRoot() { + return dataColumnSidecarsByRoot; + } + + public Optional> + getDataColumnSidecarsByRange() { + return dataColumnSidecarsByRange; + } + public Optional> blobSidecarsByRange() { return blobSidecarsByRange; diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/AbstractDataColumnSidecarValidator.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/AbstractDataColumnSidecarValidator.java new file mode 100644 index 00000000000..25d806fdfa3 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/AbstractDataColumnSidecarValidator.java @@ -0,0 +1,78 @@ +/* + * 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 tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods; + +import static tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.kzg.KZG; +import tech.pegasys.teku.networking.p2p.peer.Peer; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; + +public abstract class AbstractDataColumnSidecarValidator { + + private static final Logger LOG = LogManager.getLogger(); + + private final Spec spec; + private final KZG kzg; + final Peer peer; + + public AbstractDataColumnSidecarValidator(final Peer peer, final Spec spec, final KZG kzg) { + this.peer = peer; + this.spec = spec; + this.kzg = kzg; + } + + void verifyKzgProof(final DataColumnSidecar dataColumnSidecar) { + if (!verifyDataColumnSidecarKzgProof(dataColumnSidecar)) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, InvalidResponseType.DATA_COLUMN_SIDECAR_KZG_VERIFICATION_FAILED); + } + } + + private boolean verifyDataColumnSidecarKzgProof(final DataColumnSidecar dataColumnSidecar) { + try { + return MiscHelpersFulu.required(spec.atSlot(dataColumnSidecar.getSlot()).miscHelpers()) + .verifyDataColumnSidecarKzgProof(kzg, dataColumnSidecar); + } catch (final Exception ex) { + LOG.debug( + "KZG verification failed for DataColumnSidecar {}", dataColumnSidecar.toLogString()); + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, InvalidResponseType.DATA_COLUMN_SIDECAR_KZG_VERIFICATION_FAILED, ex); + } + } + + void verifyInclusionProof(final DataColumnSidecar dataColumnSidecar) { + if (!verifyDataColumnSidecarInclusionProof(dataColumnSidecar)) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, InvalidResponseType.DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_FAILED); + } + } + + private boolean verifyDataColumnSidecarInclusionProof(final DataColumnSidecar dataColumnSidecar) { + try { + return MiscHelpersFulu.required(spec.atSlot(dataColumnSidecar.getSlot()).miscHelpers()) + .verifyDataColumnSidecarInclusionProof(dataColumnSidecar); + } catch (final Exception ex) { + LOG.debug( + "Inclusion proof verification failed for DataColumnSidecar {}", + dataColumnSidecar.toLogString()); + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, InvalidResponseType.DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_FAILED, ex); + } + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeListenerValidatingProxy.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeListenerValidatingProxy.java new file mode 100644 index 00000000000..faca4ca77d3 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeListenerValidatingProxy.java @@ -0,0 +1,111 @@ +/* + * 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 tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods; + +import static tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType.DATA_COLUMN_SIDECAR_SLOT_NOT_IN_RANGE; +import static tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType.DATA_COLUMN_SIDECAR_UNEXPECTED_IDENTIFIER; +import static tech.pegasys.teku.statetransition.validation.DataColumnSidecarGossipValidator.DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_HISTOGRAM; +import static tech.pegasys.teku.statetransition.validation.DataColumnSidecarGossipValidator.DATA_COLUMN_SIDECAR_KZG_BATCH_VERIFICATION_HISTOGRAM; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.metrics.MetricsHistogram; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.kzg.KZG; +import tech.pegasys.teku.networking.p2p.peer.Peer; +import tech.pegasys.teku.networking.p2p.rpc.RpcResponseListener; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; + +public class DataColumnSidecarsByRangeListenerValidatingProxy + extends AbstractDataColumnSidecarValidator implements RpcResponseListener { + private final RpcResponseListener dataColumnSidecarResponseListener; + + private final UInt64 startSlot; + private final UInt64 endSlot; + + private final Set columns; + private final MetricsHistogram dataColumnSidecarInclusionProofVerificationTimeSeconds; + private final MetricsHistogram dataColumnSidecarKzgBatchVerificationTimeSeconds; + + public DataColumnSidecarsByRangeListenerValidatingProxy( + final Spec spec, + final Peer peer, + final RpcResponseListener dataColumnSidecarResponseListener, + final KZG kzg, + final MetricsSystem metricsSystem, + final TimeProvider timeProvider, + final UInt64 startSlot, + final UInt64 count, + final List columns) { + super(peer, spec, kzg); + this.dataColumnSidecarResponseListener = dataColumnSidecarResponseListener; + this.startSlot = startSlot; + this.endSlot = startSlot.plus(count).minusMinZero(1); + this.columns = new HashSet<>(columns); + this.dataColumnSidecarInclusionProofVerificationTimeSeconds = + DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_HISTOGRAM.apply( + metricsSystem, timeProvider); + this.dataColumnSidecarKzgBatchVerificationTimeSeconds = + DATA_COLUMN_SIDECAR_KZG_BATCH_VERIFICATION_HISTOGRAM.apply(metricsSystem, timeProvider); + } + + @Override + public SafeFuture onResponse(final DataColumnSidecar dataColumnSidecar) { + return SafeFuture.of( + () -> { + final UInt64 dataColumnSidecarSlot = dataColumnSidecar.getSlot(); + if (!dataColumnSidecarSlotIsInRange(dataColumnSidecarSlot)) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, DATA_COLUMN_SIDECAR_SLOT_NOT_IN_RANGE); + } + + if (!columns.contains(dataColumnSidecar.getIndex())) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, DATA_COLUMN_SIDECAR_UNEXPECTED_IDENTIFIER); + } + + try (MetricsHistogram.Timer ignored = + dataColumnSidecarInclusionProofVerificationTimeSeconds.startTimer()) { + verifyInclusionProof(dataColumnSidecar); + } catch (final IOException ioException) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, + DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType + .DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_FAILED); + } + try (MetricsHistogram.Timer ignored = + dataColumnSidecarKzgBatchVerificationTimeSeconds.startTimer()) { + verifyKzgProof(dataColumnSidecar); + } catch (final IOException ioException) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, + DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType + .DATA_COLUMN_SIDECAR_KZG_VERIFICATION_FAILED); + } + + return dataColumnSidecarResponseListener.onResponse(dataColumnSidecar); + }); + } + + private boolean dataColumnSidecarSlotIsInRange(final UInt64 dataColumnSidecarSlot) { + return dataColumnSidecarSlot.isGreaterThanOrEqualTo(startSlot) + && dataColumnSidecarSlot.isLessThanOrEqualTo(endSlot); + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeMessageHandler.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeMessageHandler.java new file mode 100644 index 00000000000..4d57d268ac5 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeMessageHandler.java @@ -0,0 +1,347 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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.networking.eth2.rpc.beaconchain.methods; + +import static tech.pegasys.teku.networking.eth2.rpc.core.RpcResponseStatus.INVALID_REQUEST_CODE; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSortedMap; +import java.nio.channels.ClosedChannelException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.networking.eth2.peers.Eth2Peer; +import tech.pegasys.teku.networking.eth2.peers.RequestApproval; +import tech.pegasys.teku.networking.eth2.rpc.core.PeerRequiredLocalMessageHandler; +import tech.pegasys.teku.networking.eth2.rpc.core.ResponseCallback; +import tech.pegasys.teku.networking.eth2.rpc.core.RpcException; +import tech.pegasys.teku.networking.eth2.rpc.core.RpcException.ResourceUnavailableException; +import tech.pegasys.teku.networking.p2p.rpc.StreamClosedException; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.config.SpecConfigFulu; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRangeRequestMessage; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.LoggingPeerId; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.ReqRespResponseLogger; +import tech.pegasys.teku.storage.client.CombinedChainDataClient; + +/** + * DataColumnSidecarsByRange + * v1 + */ +public class DataColumnSidecarsByRangeMessageHandler + extends PeerRequiredLocalMessageHandler< + DataColumnSidecarsByRangeRequestMessage, DataColumnSidecar> { + + private static final Logger LOG = LogManager.getLogger(); + + private final Spec spec; + private final SpecConfigFulu specConfigFulu; + private final CombinedChainDataClient combinedChainDataClient; + private final LabelledMetric requestCounter; + private final Counter totalDataColumnSidecarsRequestedCounter; + private final DasReqRespLogger dasLogger; + + public DataColumnSidecarsByRangeMessageHandler( + final Spec spec, + final SpecConfigFulu specConfigFulu, + final MetricsSystem metricsSystem, + final CombinedChainDataClient combinedChainDataClient, + final DasReqRespLogger dasLogger) { + this.spec = spec; + this.specConfigFulu = specConfigFulu; + this.combinedChainDataClient = combinedChainDataClient; + requestCounter = + metricsSystem.createLabelledCounter( + TekuMetricCategory.NETWORK, + "rpc_data_column_sidecars_by_range_requests_total", + "Total number of data column sidecars by range requests received", + "status"); + totalDataColumnSidecarsRequestedCounter = + metricsSystem.createCounter( + TekuMetricCategory.NETWORK, + "rpc_data_column_sidecars_by_range_requested_sidecars_total", + "Total number of data column sidecars requested in accepted blob sidecars by range requests from peers"); + this.dasLogger = dasLogger; + } + + @Override + public void onIncomingMessage( + final String protocolId, + final Eth2Peer peer, + final DataColumnSidecarsByRangeRequestMessage message, + final ResponseCallback responseCallback) { + final UInt64 startSlot = message.getStartSlot(); + final UInt64 endSlot = message.getMaxSlot(); + final List columns = message.getColumns(); + + final ReqRespResponseLogger responseLogger = + dasLogger + .getDataColumnSidecarsByRangeLogger() + .onInboundRequest( + LoggingPeerId.fromPeerAndNodeId( + peer.getId().toBase58(), peer.getDiscoveryNodeId().orElseThrow()), + new DasReqRespLogger.ByRangeRequest( + message.getStartSlot(), message.getCount().intValue(), message.getColumns())); + final LoggingResponseCallback responseCallbackWithLogging = + new LoggingResponseCallback<>(responseCallback, responseLogger); + + final int requestedCount = message.getMaximumResponseChunks(); + + if (requestedCount > specConfigFulu.getMaxRequestDataColumnSidecars()) { + requestCounter.labels("count_too_big").inc(); + responseCallbackWithLogging.completeWithErrorResponse( + new RpcException( + INVALID_REQUEST_CODE, + String.format( + "Only a maximum of %s blob sidecars can be requested per request. Requested: %s", + specConfigFulu.getMaxRequestDataColumnSidecars(), requestedCount))); + return; + } + + final Optional dataColumnSidecarsRequestApproval = + peer.approveDataColumnSidecarsRequest(responseCallbackWithLogging, requestedCount); + + if (!peer.approveRequest() || dataColumnSidecarsRequestApproval.isEmpty()) { + requestCounter.labels("rate_limited").inc(); + return; + } + + requestCounter.labels("ok").inc(); + totalDataColumnSidecarsRequestedCounter.inc(message.getCount().longValue()); + final SafeFuture> earliestDataColumnSidecarSlotFuture = + combinedChainDataClient.getEarliestDataColumnSidecarSlot(); + final SafeFuture> firstIncompleteSlotFuture = + combinedChainDataClient.getFirstCustodyIncompleteSlot(); + SafeFuture.collectAll(earliestDataColumnSidecarSlotFuture, firstIncompleteSlotFuture) + .thenCompose( + slotOptionals -> { + final Optional earliestSidecarSlot = slotOptionals.get(0); + final Optional firstIncompleteSlot = slotOptionals.get(1); + final UInt64 requestEpoch = spec.computeEpochAtSlot(startSlot); + if (spec.isAvailabilityOfDataColumnSidecarsRequiredAtEpoch( + combinedChainDataClient.getStore(), requestEpoch) + && !checkDataColumnSidecarsAreAvailable( + earliestSidecarSlot, firstIncompleteSlot, endSlot)) { + return SafeFuture.failedFuture( + new ResourceUnavailableException( + "Requested data column sidecars are not available.")); + } + + UInt64 finalizedSlot = + combinedChainDataClient.getFinalizedBlockSlot().orElse(UInt64.ZERO); + + final SortedMap canonicalHotRoots; + if (endSlot.isGreaterThan(finalizedSlot)) { + final UInt64 hotSlotsCount = endSlot.increment().minusMinZero(startSlot); + + canonicalHotRoots = + combinedChainDataClient.getAncestorRoots(startSlot, UInt64.ONE, hotSlotsCount); + + // refresh finalized slot to avoid race condition that can occur if we finalize just + // before getting hot canonical roots + finalizedSlot = combinedChainDataClient.getFinalizedBlockSlot().orElse(UInt64.ZERO); + } else { + canonicalHotRoots = ImmutableSortedMap.of(); + } + + final RequestState initialState = + new RequestState( + responseCallbackWithLogging, + specConfigFulu.getMaxRequestDataColumnSidecars(), + startSlot, + endSlot, + columns, + canonicalHotRoots, + finalizedSlot); + if (initialState.isComplete()) { + return SafeFuture.completedFuture(initialState); + } + return sendDataColumnSidecars(initialState); + }) + .finish( + requestState -> { + final int sentDataColumnSidecars = requestState.sentDataColumnSidecars.get(); + if (sentDataColumnSidecars != requestedCount) { + peer.adjustDataColumnSidecarsRequest( + dataColumnSidecarsRequestApproval.get(), sentDataColumnSidecars); + } + responseCallbackWithLogging.completeSuccessfully(); + }, + error -> { + peer.adjustDataColumnSidecarsRequest(dataColumnSidecarsRequestApproval.get(), 0); + handleProcessingRequestError(error, responseCallbackWithLogging); + }); + ; + } + + private boolean checkDataColumnSidecarsAreAvailable( + final Optional earliestAvailableSidecarSlotOptional, + final Optional firstIncompleteSlotOptional, + final UInt64 requestSlot) { + if (earliestAvailableSidecarSlotOptional.isPresent()) { + if (earliestAvailableSidecarSlotOptional.get().isLessThanOrEqualTo(requestSlot)) { + return true; + } + return firstIncompleteSlotOptional + .map(firstIncompleteSlot -> firstIncompleteSlot.isGreaterThan(requestSlot)) + .orElse(false); + } else { + return false; + } + } + + private SafeFuture sendDataColumnSidecars(final RequestState requestState) { + return requestState + .loadNextDataColumnSidecar() + .thenCompose( + maybeDataColumnSidecar -> + maybeDataColumnSidecar + .map(requestState::sendDataColumnSidecar) + .orElse(SafeFuture.COMPLETE)) + .thenCompose( + __ -> { + if (requestState.isComplete()) { + return SafeFuture.completedFuture(requestState); + } else { + return sendDataColumnSidecars(requestState); + } + }); + } + + private void handleProcessingRequestError( + final Throwable error, final ResponseCallback callback) { + final Throwable rootCause = Throwables.getRootCause(error); + if (rootCause instanceof RpcException) { + LOG.trace("Rejecting data column sidecars by range request", error); + callback.completeWithErrorResponse((RpcException) rootCause); + } else { + if (rootCause instanceof StreamClosedException + || rootCause instanceof ClosedChannelException) { + LOG.trace("Stream closed while sending requested data column sidecars", error); + } else { + LOG.error("Failed to process data column sidecars request", error); + } + callback.completeWithUnexpectedError(error); + } + } + + @VisibleForTesting + class RequestState { + + private final AtomicInteger sentDataColumnSidecars = new AtomicInteger(0); + private final ResponseCallback callback; + private final UInt64 maxRequestDataColumnSidecars; + private final UInt64 startSlot; + private final UInt64 endSlot; + private final List columns; + private final UInt64 finalizedSlot; + private final Map canonicalHotRoots; + + // since our storage stores hot and finalized data columns sidecar on the same "table", this + // iterator can span + // over hot and finalized data column sidecar + private Optional> dataColumnSidecarKeysIterator = + Optional.empty(); + + RequestState( + final ResponseCallback callback, + final int maxRequestDataColumnSidecars, + final UInt64 startSlot, + final UInt64 endSlot, + final List columns, + final Map canonicalHotRoots, + final UInt64 finalizedSlot) { + this.callback = callback; + this.maxRequestDataColumnSidecars = UInt64.valueOf(maxRequestDataColumnSidecars); + this.startSlot = startSlot; + this.endSlot = endSlot; + this.columns = columns; + this.finalizedSlot = finalizedSlot; + this.canonicalHotRoots = canonicalHotRoots; + } + + SafeFuture sendDataColumnSidecar(final DataColumnSidecar dataColumnSidecar) { + return callback.respond(dataColumnSidecar).thenRun(sentDataColumnSidecars::incrementAndGet); + } + + SafeFuture> loadNextDataColumnSidecar() { + if (dataColumnSidecarKeysIterator.isEmpty()) { + return combinedChainDataClient + .getDataColumnIdentifiers(startSlot, endSlot, maxRequestDataColumnSidecars) + .thenCompose( + keys -> { + dataColumnSidecarKeysIterator = + Optional.of( + keys.stream() + .filter(key -> columns.contains(key.columnIndex())) + .iterator()); + return getNextDataColumnSidecar(dataColumnSidecarKeysIterator.get()); + }); + } else { + return getNextDataColumnSidecar(dataColumnSidecarKeysIterator.get()); + } + } + + private SafeFuture> getNextDataColumnSidecar( + final Iterator dataColumnSidecarIdentifiers) { + if (dataColumnSidecarIdentifiers.hasNext()) { + final DataColumnSlotAndIdentifier columnSlotAndIdentifier = + dataColumnSidecarIdentifiers.next(); + + if (finalizedSlot.isGreaterThanOrEqualTo(columnSlotAndIdentifier.slot())) { + return combinedChainDataClient.getSidecar(columnSlotAndIdentifier); + } + + // not finalized, let's check if it is on canonical chain + if (isCanonicalHotDataColumnSidecar(columnSlotAndIdentifier)) { + return combinedChainDataClient.getSidecar(columnSlotAndIdentifier); + } + + // non-canonical, try next one + return getNextDataColumnSidecar(dataColumnSidecarIdentifiers); + } + + return SafeFuture.completedFuture(Optional.empty()); + } + + private boolean isCanonicalHotDataColumnSidecar( + final DataColumnSlotAndIdentifier columnSlotAndIdentifier) { + return Optional.ofNullable(canonicalHotRoots.get(columnSlotAndIdentifier.slot())) + .map(blockRoot -> blockRoot.equals(columnSlotAndIdentifier.blockRoot())) + .orElse(false); + } + + boolean isComplete() { + return endSlot.isLessThan(startSlot) + || dataColumnSidecarKeysIterator.map(iterator -> !iterator.hasNext()).orElse(false); + } + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootListenerValidatingProxy.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootListenerValidatingProxy.java new file mode 100644 index 00000000000..45f360c4e6d --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootListenerValidatingProxy.java @@ -0,0 +1,66 @@ +/* + * 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 tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods; + +import java.util.List; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.kzg.KZG; +import tech.pegasys.teku.networking.p2p.peer.Peer; +import tech.pegasys.teku.networking.p2p.rpc.RpcResponseListener; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; +import tech.pegasys.teku.spec.datastructures.util.DataColumnIdentifier; + +public class DataColumnSidecarsByRootListenerValidatingProxy + extends DataColumnSidecarsByRootValidator implements RpcResponseListener { + + private final RpcResponseListener listener; + + public DataColumnSidecarsByRootListenerValidatingProxy( + final Peer peer, + final Spec spec, + final RpcResponseListener listener, + final KZG kzg, + final MetricsSystem metricsSystem, + final TimeProvider timeProvider, + final List expectedByRootIdentifiers) { + super( + peer, + spec, + kzg, + metricsSystem, + timeProvider, + expectedByRootIdentifiers.stream() + .flatMap( + byRootIdentifier -> + byRootIdentifier.getColumns().stream() + .map( + column -> + new DataColumnIdentifier(byRootIdentifier.getBlockRoot(), column))) + .toList()); + this.listener = listener; + } + + @Override + public SafeFuture onResponse(final DataColumnSidecar dataColumnSidecar) { + return SafeFuture.of( + () -> { + validate(dataColumnSidecar); + return listener.onResponse(dataColumnSidecar); + }); + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootMessageHandler.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootMessageHandler.java new file mode 100644 index 00000000000..9d0308d1506 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootMessageHandler.java @@ -0,0 +1,275 @@ +/* + * 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 tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods; + +import static tech.pegasys.teku.networking.eth2.rpc.core.RpcResponseStatus.INVALID_REQUEST_CODE; + +import com.google.common.base.Throwables; +import java.nio.channels.ClosedChannelException; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.networking.eth2.peers.Eth2Peer; +import tech.pegasys.teku.networking.eth2.peers.RequestApproval; +import tech.pegasys.teku.networking.eth2.rpc.core.PeerRequiredLocalMessageHandler; +import tech.pegasys.teku.networking.eth2.rpc.core.ResponseCallback; +import tech.pegasys.teku.networking.eth2.rpc.core.RpcException; +import tech.pegasys.teku.networking.p2p.rpc.StreamClosedException; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRootRequestMessage; +import tech.pegasys.teku.spec.datastructures.util.DataColumnIdentifier; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; +import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.LoggingPeerId; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.ReqRespResponseLogger; +import tech.pegasys.teku.storage.client.CombinedChainDataClient; + +/** + * DataColumnSidecarsByRoot + * v1 + */ +public class DataColumnSidecarsByRootMessageHandler + extends PeerRequiredLocalMessageHandler< + DataColumnSidecarsByRootRequestMessage, DataColumnSidecar> { + + private static final Logger LOG = LogManager.getLogger(); + + private final Spec spec; + private final CombinedChainDataClient combinedChainDataClient; + private final DataColumnSidecarByRootCustody dataColumnSidecarCustody; + private final CustodyGroupCountManager custodyGroupCountManager; + + private final LabelledMetric requestCounter; + private final Counter totalDataColumnSidecarsRequestedCounter; + private final DasReqRespLogger dasLogger; + + public DataColumnSidecarsByRootMessageHandler( + final Spec spec, + final MetricsSystem metricsSystem, + final CombinedChainDataClient combinedChainDataClient, + final DataColumnSidecarByRootCustody dataColumnSidecarCustody, + final CustodyGroupCountManager custodyGroupCountManager, + final DasReqRespLogger dasLogger) { + this.spec = spec; + this.combinedChainDataClient = combinedChainDataClient; + this.custodyGroupCountManager = custodyGroupCountManager; + this.dataColumnSidecarCustody = dataColumnSidecarCustody; + this.dasLogger = dasLogger; + this.requestCounter = + metricsSystem.createLabelledCounter( + TekuMetricCategory.NETWORK, + "rpc_data_column_sidecars_by_root_requests_total", + "Total number of data column sidecars by root requests received", + "status"); + this.totalDataColumnSidecarsRequestedCounter = + metricsSystem.createCounter( + TekuMetricCategory.NETWORK, + "rpc_data_column_sidecars_by_root_requested_blob_sidecars_total", + "Total number of data column sidecars requested in accepted data column sidecars by root requests from peers"); + } + + private SafeFuture validateAndSendMaybeRespond( + final DataColumnIdentifier identifier, + final Optional maybeSidecar, + final UInt64 finalizedEpoch, + final ResponseCallback callback) { + return validateMinimumRequestEpoch(identifier, maybeSidecar, finalizedEpoch) + .thenCompose( + __ -> + maybeSidecar + .map(sideCar -> callback.respond(sideCar).thenApply(___ -> true)) + .orElse(SafeFuture.completedFuture(false))); + } + + @Override + public void onIncomingMessage( + final String protocolId, + final Eth2Peer peer, + final DataColumnSidecarsByRootRequestMessage message, + final ResponseCallback responseCallback) { + + final ReqRespResponseLogger responseLogger = + dasLogger + .getDataColumnSidecarsByRootLogger() + .onInboundRequest( + LoggingPeerId.fromPeerAndNodeId( + peer.getId().toBase58(), peer.getDiscoveryNodeId().orElseThrow()), + message.asList()); + + final LoggingResponseCallback responseCallbackWithLogging = + new LoggingResponseCallback<>(responseCallback, responseLogger); + + final Optional dataColumnSidecarsRequestApproval = + peer.approveDataColumnSidecarsRequest(responseCallbackWithLogging, message.size()); + + if (!peer.approveRequest() || dataColumnSidecarsRequestApproval.isEmpty()) { + requestCounter.labels("rate_limited").inc(); + return; + } + + requestCounter.labels("ok").inc(); + totalDataColumnSidecarsRequestedCounter.inc(message.size()); + + final UInt64 finalizedEpoch = getFinalizedEpoch(); + + final Set myCustodyColumns = + new HashSet<>(custodyGroupCountManager.getCustodyColumnIndices()); + final Stream> responseStream = + message.stream() + .flatMap( + byRootIdentifier -> + byRootIdentifier.getColumns().stream() + .filter(myCustodyColumns::contains) + .map( + column -> + new DataColumnIdentifier(byRootIdentifier.getBlockRoot(), column))) + .map( + dataColumnIdentifier -> + retrieveDataColumnSidecar(dataColumnIdentifier) + .thenCompose( + maybeSidecar -> + validateAndSendMaybeRespond( + dataColumnIdentifier, + maybeSidecar, + finalizedEpoch, + responseCallbackWithLogging))); + + final SafeFuture> listOfResponses = SafeFuture.collectAll(responseStream); + + listOfResponses + .thenApply(list -> list.stream().filter(isSent -> isSent).count()) + .thenAccept( + sentDataColumnSidecarsCount -> { + if (sentDataColumnSidecarsCount != message.size()) { + peer.adjustDataColumnSidecarsRequest( + dataColumnSidecarsRequestApproval.get(), sentDataColumnSidecarsCount); + } + responseCallbackWithLogging.completeSuccessfully(); + }) + .finish( + err -> { + peer.adjustDataColumnSidecarsRequest(dataColumnSidecarsRequestApproval.get(), 0); + handleError(responseCallbackWithLogging, err); + }); + } + + private SafeFuture> getNonCanonicalDataColumnSidecar( + final DataColumnIdentifier identifier) { + return combinedChainDataClient + .getBlockByBlockRoot(identifier.blockRoot()) + .thenApply(maybeBlock -> maybeBlock.map(SignedBeaconBlock::getSlot)) + .thenCompose( + maybeSlot -> { + if (maybeSlot.isPresent()) { + return combinedChainDataClient.getNonCanonicalSidecar( + new DataColumnSlotAndIdentifier( + maybeSlot.get(), identifier.blockRoot(), identifier.columnIndex())); + } else { + return SafeFuture.completedFuture(Optional.empty()); + } + }); + } + + private UInt64 getFinalizedEpoch() { + return combinedChainDataClient + .getFinalizedBlock() + .map(SignedBeaconBlock::getSlot) + .map(spec::computeEpochAtSlot) + .orElse(UInt64.ZERO); + } + + /** + * Validations: + * + *
    + *
  • The block root references a block greater than or equal to the minimum_request_epoch + *
+ */ + @SuppressWarnings("unused") + private SafeFuture validateMinimumRequestEpoch( + final DataColumnIdentifier identifier, + final Optional maybeSidecar, + final UInt64 finalizedEpoch) { + return maybeSidecar + .map(sidecar -> SafeFuture.completedFuture(Optional.of(sidecar.getSlot()))) + .orElse( + combinedChainDataClient + .getBlockByBlockRoot(identifier.blockRoot()) + .thenApply(maybeBlock -> maybeBlock.map(SignedBeaconBlock::getSlot))) + .thenAcceptChecked( + maybeSlot -> { + if (maybeSlot.isEmpty()) { + return; + } + final UInt64 requestedEpoch = spec.computeEpochAtSlot(maybeSlot.get()); + if (!spec.isAvailabilityOfDataColumnSidecarsRequiredAtEpoch( + combinedChainDataClient.getStore(), requestedEpoch) + // TODO uncomment when sync by range is ready + // https://github.com/Consensys/teku/issues/9448 + /* || requestedEpoch.isLessThan(finalizedEpoch)*/ ) { + throw new RpcException( + INVALID_REQUEST_CODE, + String.format( + "Block root (%s) references a block earlier than the minimum_request_epoch", + identifier.blockRoot())); + } + }); + } + + private SafeFuture> retrieveDataColumnSidecar( + final DataColumnIdentifier identifier) { + return dataColumnSidecarCustody + .getCustodyDataColumnSidecarByRoot(identifier) + .thenCompose( + maybeSidecar -> { + if (maybeSidecar.isPresent()) { + return SafeFuture.completedFuture(maybeSidecar); + } + // Fallback to non-canonical sidecar if the canonical one is not found + return getNonCanonicalDataColumnSidecar(identifier); + }); + } + + private void handleError( + final ResponseCallback callback, final Throwable error) { + final Throwable rootCause = Throwables.getRootCause(error); + if (rootCause instanceof RpcException) { + LOG.trace("Rejecting data column sidecars by root request", error); + callback.completeWithErrorResponse((RpcException) rootCause); + } else { + if (rootCause instanceof StreamClosedException + || rootCause instanceof ClosedChannelException) { + LOG.trace("Stream closed while sending requested data column sidecars", error); + } else { + LOG.error("Failed to process data column sidecars by root request", error); + } + callback.completeWithUnexpectedError(error); + } + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootValidator.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootValidator.java new file mode 100644 index 00000000000..8e259244116 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootValidator.java @@ -0,0 +1,78 @@ +/* + * 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 tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods; + +import static tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType; +import static tech.pegasys.teku.statetransition.validation.DataColumnSidecarGossipValidator.DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_HISTOGRAM; +import static tech.pegasys.teku.statetransition.validation.DataColumnSidecarGossipValidator.DATA_COLUMN_SIDECAR_KZG_BATCH_VERIFICATION_HISTOGRAM; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import tech.pegasys.teku.infrastructure.metrics.MetricsHistogram; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.kzg.KZG; +import tech.pegasys.teku.networking.p2p.peer.Peer; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.util.DataColumnIdentifier; + +public class DataColumnSidecarsByRootValidator extends AbstractDataColumnSidecarValidator { + private final Set expectedDataColumnIdentifiers; + private final MetricsHistogram dataColumnSidecarInclusionProofVerificationTimeSeconds; + private final MetricsHistogram dataColumnSidecarKzgBatchVerificationTimeSeconds; + + public DataColumnSidecarsByRootValidator( + final Peer peer, + final Spec spec, + final KZG kzg, + final MetricsSystem metricsSystem, + final TimeProvider timeProvider, + final List expectedDataColumnIdentifiers) { + super(peer, spec, kzg); + this.expectedDataColumnIdentifiers = ConcurrentHashMap.newKeySet(); + this.expectedDataColumnIdentifiers.addAll(expectedDataColumnIdentifiers); + this.dataColumnSidecarInclusionProofVerificationTimeSeconds = + DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_HISTOGRAM.apply( + metricsSystem, timeProvider); + this.dataColumnSidecarKzgBatchVerificationTimeSeconds = + DATA_COLUMN_SIDECAR_KZG_BATCH_VERIFICATION_HISTOGRAM.apply(metricsSystem, timeProvider); + } + + public void validate(final DataColumnSidecar dataColumnSidecar) { + final DataColumnIdentifier dataColumnIdentifier = + DataColumnIdentifier.createFromSidecar(dataColumnSidecar); + if (!expectedDataColumnIdentifiers.remove(dataColumnIdentifier)) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, InvalidResponseType.DATA_COLUMN_SIDECAR_UNEXPECTED_IDENTIFIER); + } + + try (MetricsHistogram.Timer ignored = + dataColumnSidecarInclusionProofVerificationTimeSeconds.startTimer()) { + verifyInclusionProof(dataColumnSidecar); + } catch (final IOException ioException) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, InvalidResponseType.DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_FAILED); + } + try (MetricsHistogram.Timer ignored = + dataColumnSidecarKzgBatchVerificationTimeSeconds.startTimer()) { + verifyKzgProof(dataColumnSidecar); + } catch (final IOException ioException) { + throw new DataColumnSidecarsResponseInvalidResponseException( + peer, InvalidResponseType.DATA_COLUMN_SIDECAR_KZG_VERIFICATION_FAILED); + } + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsResponseInvalidResponseException.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsResponseInvalidResponseException.java new file mode 100644 index 00000000000..84d678428ee --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsResponseInvalidResponseException.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 tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods; + +import tech.pegasys.teku.networking.eth2.rpc.core.InvalidResponseException; +import tech.pegasys.teku.networking.p2p.peer.Peer; + +public class DataColumnSidecarsResponseInvalidResponseException extends InvalidResponseException { + + public DataColumnSidecarsResponseInvalidResponseException( + final Peer peer, final InvalidResponseType invalidResponseType) { + super( + String.format( + "Received invalid response from peer %s: %s", peer, invalidResponseType.describe())); + } + + public DataColumnSidecarsResponseInvalidResponseException( + final Peer peer, final InvalidResponseType invalidResponseType, final Exception cause) { + super( + String.format( + "Received invalid response from peer %s: %s", peer, invalidResponseType.describe()), + cause); + } + + public enum InvalidResponseType { + DATA_COLUMN_SIDECAR_KZG_VERIFICATION_FAILED( + "KZG verification for DataColumnSidecar has failed"), + DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_FAILED( + "Inclusion verification for DataColumnSidecar has failed"), + DATA_COLUMN_SIDECAR_SLOT_NOT_IN_RANGE("DataColumnSidecar's slot is not within requested range"), + DATA_COLUMN_SIDECAR_UNEXPECTED_IDENTIFIER( + "DataColumnSidecar is not within requested identifiers"); + + private final String description; + + InvalidResponseType(final String description) { + this.description = description; + } + + public String describe() { + return description; + } + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/LoggingResponseCallback.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/LoggingResponseCallback.java new file mode 100644 index 00000000000..c25f4d94fca --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/LoggingResponseCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.networking.eth2.rpc.beaconchain.methods; + +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.networking.eth2.rpc.core.ResponseCallback; +import tech.pegasys.teku.networking.eth2.rpc.core.RpcException; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.ReqRespResponseLogger; + +public record LoggingResponseCallback( + ResponseCallback callback, ReqRespResponseLogger logger) implements ResponseCallback { + + @Override + public SafeFuture respond(final T data) { + logger.onNextItem(data); + return callback.respond(data); + } + + @Override + public void respondAndCompleteSuccessfully(final T data) { + logger.onNextItem(data); + logger.onComplete(); + callback.respondAndCompleteSuccessfully(data); + } + + @Override + public void completeSuccessfully() { + logger.onComplete(); + callback.completeSuccessfully(); + } + + @Override + public void completeWithErrorResponse(final RpcException error) { + logger.onError(error); + callback.completeWithErrorResponse(error); + } + + @Override + public void completeWithUnexpectedError(final Throwable error) { + logger.onError(error); + callback.completeWithUnexpectedError(error); + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/MetadataMessageHandler.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/MetadataMessageHandler.java index 4a95fe8b3db..daa919e3263 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/MetadataMessageHandler.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/MetadataMessageHandler.java @@ -46,7 +46,13 @@ public void onIncomingMessage( final int protocolVersion = BeaconChainMethodIds.extractGetMetadataVersion(protocolId); final SpecMilestone milestone = - protocolVersion == 1 ? SpecMilestone.PHASE0 : SpecMilestone.ALTAIR; + switch (protocolVersion) { + case 1 -> SpecMilestone.PHASE0; + case 2 -> SpecMilestone.ALTAIR; + case 3 -> SpecMilestone.FULU; + default -> + throw new IllegalStateException("Unexpected protocol version: " + protocolVersion); + }; final MetadataMessageSchema schema = spec.forMilestone(milestone).getSchemaDefinitions().getMetadataMessageSchema(); diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/core/encodings/context/ForkDigestPayloadContext.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/core/encodings/context/ForkDigestPayloadContext.java index 6cda052f8b2..283e8988d44 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/core/encodings/context/ForkDigestPayloadContext.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/core/encodings/context/ForkDigestPayloadContext.java @@ -17,8 +17,10 @@ import tech.pegasys.teku.infrastructure.ssz.schema.SszSchema; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.schemas.SchemaDefinitions; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsFulu; public interface ForkDigestPayloadContext { @@ -50,6 +52,20 @@ public SszSchema getSchemaFromSchemaDefinitions( } }; + ForkDigestPayloadContext DATA_COLUMN_SIDECAR = + new ForkDigestPayloadContext<>() { + @Override + public UInt64 getSlotFromPayload(final DataColumnSidecar responsePayload) { + return responsePayload.getSlot(); + } + + @Override + public SszSchema getSchemaFromSchemaDefinitions( + final SchemaDefinitions schemaDefinitions) { + return SchemaDefinitionsFulu.required(schemaDefinitions).getDataColumnSidecarSchema(); + } + }; + UInt64 getSlotFromPayload(final TPayload responsePayload); SszSchema getSchemaFromSchemaDefinitions(final SchemaDefinitions schemaDefinitions); diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManagerTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManagerTest.java index c0161c2df47..ffbd4ac31ef 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManagerTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManagerTest.java @@ -47,6 +47,7 @@ import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -81,7 +82,8 @@ public class Eth2PeerManagerTest { rpcEncoding, Eth2P2PNetworkBuilder.DEFAULT_ETH2_RPC_PING_INTERVAL, Eth2P2PNetworkBuilder.DEFAULT_ETH2_RPC_OUTSTANDING_PING_THRESHOLD, - Eth2P2PNetworkBuilder.DEFAULT_ETH2_STATUS_UPDATE_INTERVAL); + Eth2P2PNetworkBuilder.DEFAULT_ETH2_STATUS_UPDATE_INTERVAL, + DasReqRespLogger.NOOP); @Test public void subscribeConnect_singleListener() { diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerTest.java index 57fa698a95a..0e17e529cc4 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerTest.java @@ -25,9 +25,11 @@ import java.util.List; import java.util.Optional; import java.util.stream.IntStream; +import org.hyperledger.besu.plugin.services.MetricsSystem; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.kzg.KZG; import tech.pegasys.teku.networking.eth2.peers.Eth2Peer.PeerStatusSubscriber; @@ -62,8 +64,11 @@ class Eth2PeerTest { private final PeerChainValidator peerChainValidator = mock(PeerChainValidator.class); private final RateTracker blockRateTracker = mock(RateTracker.class); private final RateTracker blobSidecarsRateTracker = mock(RateTracker.class); + private final RateTracker dataColumnSidecarsRateTracker = mock(RateTracker.class); private final RateTracker rateTracker = mock(RateTracker.class); private final KZG kzg = mock(KZG.class); + private final MetricsSystem metricsSystem = mock(MetricsSystem.class); + private final TimeProvider timeProvider = mock(TimeProvider.class); private final PeerStatus randomPeerStatus = randomPeerStatus(); @@ -78,8 +83,11 @@ class Eth2PeerTest { peerChainValidator, blockRateTracker, blobSidecarsRateTracker, + dataColumnSidecarsRateTracker, rateTracker, - kzg); + kzg, + metricsSystem, + timeProvider); @Test void updateStatus_shouldNotUpdateUntilValidationPasses() { diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerTest.java index 30136f70b6d..1ab1ce64365 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerTest.java @@ -33,7 +33,7 @@ public void setUp() { @Test public void shouldAllowAddingItemsWithinLimit() { - final RateTracker tracker = RateTracker.create(1, 1, timeProvider); + final RateTracker tracker = RateTracker.create(1, 1, timeProvider, ""); final long objectsCount = 1; final Optional objectRequests = tracker.approveObjectsRequest(objectsCount); assertRequestsAllowed(objectRequests, objectsCount, timeProvider); @@ -41,7 +41,7 @@ public void shouldAllowAddingItemsWithinLimit() { @Test public void shouldNotUnderflowWhenTimeWindowGreaterThanCurrentTime() { - final RateTracker tracker = RateTracker.create(1, 15000, timeProvider); + final RateTracker tracker = RateTracker.create(1, 15000, timeProvider, ""); final long objectsCount = 1; final Optional objectRequests = tracker.approveObjectsRequest(objectsCount); assertRequestsAllowed(objectRequests, objectsCount, timeProvider); @@ -49,7 +49,7 @@ public void shouldNotUnderflowWhenTimeWindowGreaterThanCurrentTime() { @Test public void shouldAllowAddingItemsAfterTimeoutPasses() { - final RateTracker tracker = RateTracker.create(1, 1, timeProvider); + final RateTracker tracker = RateTracker.create(1, 1, timeProvider, ""); long objectsCount = 1; Optional objectRequests = tracker.approveObjectsRequest(objectsCount); @@ -63,7 +63,7 @@ public void shouldAllowAddingItemsAfterTimeoutPasses() { @Test public void shouldReturnFalseIfCacheFull() { - final RateTracker tracker = RateTracker.create(1, 1, timeProvider); + final RateTracker tracker = RateTracker.create(1, 1, timeProvider, ""); final long objectsCount = 1; Optional objectRequests = tracker.approveObjectsRequest(objectsCount); assertRequestsAllowed(objectRequests, objectsCount, timeProvider); @@ -74,7 +74,7 @@ public void shouldReturnFalseIfCacheFull() { @Test public void shouldAddMultipleValuesToCache() throws InterruptedException { - final RateTracker tracker = RateTracker.create(10, 1, timeProvider); + final RateTracker tracker = RateTracker.create(10, 1, timeProvider, ""); long objectsCount = 10; Optional objectRequests = tracker.approveObjectsRequest(objectsCount); assertRequestsAllowed(objectRequests, objectsCount, timeProvider); @@ -91,7 +91,7 @@ public void shouldAddMultipleValuesToCache() throws InterruptedException { @Test public void shouldMaintainCounterOverTime() { - final RateTracker tracker = RateTracker.create(10, 2, timeProvider); + final RateTracker tracker = RateTracker.create(10, 2, timeProvider, ""); long objectsCount = 9; Optional objectRequests = tracker.approveObjectsRequest(objectsCount); @@ -156,7 +156,7 @@ public void shouldMaintainCounterOverTime() { @Test public void shouldAdjustObjectsCount() { - final RateTracker tracker = RateTracker.create(10, 2, timeProvider); + final RateTracker tracker = RateTracker.create(10, 2, timeProvider, ""); // time: 1000, tracker count: 0, limit: 10, remaining: 10 Optional objectRequests = tracker.approveObjectsRequest(10); diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodsTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodsTest.java index b3919fe78d0..746373b4303 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodsTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodsTest.java @@ -35,6 +35,9 @@ import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.StatusMessage; +import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; +import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -150,10 +153,13 @@ private BeaconChainMethods getMethods(final Spec spec) { asyncRunner, peerLookup, combinedChainDataClient, + DataColumnSidecarByRootCustody.NOOP, + CustodyGroupCountManager.NOOP, recentChainData, metricsSystem, statusMessageFactory, metadataMessagesFactory, - RpcEncoding.createSszSnappyEncoding(spec.getNetworkingConfig().getMaxPayloadSize())); + RpcEncoding.createSszSnappyEncoding(spec.getNetworkingConfig().getMaxPayloadSize()), + DasReqRespLogger.NOOP); } } diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootListenerValidatingProxyTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootListenerValidatingProxyTest.java new file mode 100644 index 00000000000..b72c0337286 --- /dev/null +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootListenerValidatingProxyTest.java @@ -0,0 +1,216 @@ +/* + * 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 tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ONE; +import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ZERO; + +import java.util.List; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.metrics.StubMetricsSystem; +import tech.pegasys.teku.infrastructure.time.StubTimeProvider; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.kzg.KZG; +import tech.pegasys.teku.networking.eth2.peers.Eth2Peer; +import tech.pegasys.teku.networking.p2p.rpc.RpcResponseListener; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockHeader; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlockHeader; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifierSchema; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsElectra; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsFulu; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +@SuppressWarnings("JavaCase") +public class DataColumnSidecarsByRootListenerValidatingProxyTest { + private final Spec spec = TestSpecFactory.createMainnetFulu(); + final DataColumnsByRootIdentifierSchema schema = + SchemaDefinitionsFulu.required(spec.forMilestone(SpecMilestone.FULU).getSchemaDefinitions()) + .getDataColumnsByRootIdentifierSchema(); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private DataColumnSidecarsByRootListenerValidatingProxy listenerWrapper; + private final Eth2Peer peer = mock(Eth2Peer.class); + private final KZG kzg = mock(KZG.class); + private final MetricsSystem metricsSystem = new StubMetricsSystem(); + private final TimeProvider timeProvider = StubTimeProvider.withTimeInMillis(ZERO); + + @SuppressWarnings("unchecked") + private final RpcResponseListener listener = mock(RpcResponseListener.class); + + @BeforeEach + void setUp() { + when(listener.onResponse(any())).thenReturn(SafeFuture.completedFuture(null)); + when(kzg.verifyCellProofBatch(any(), any(), any())).thenReturn(true); + } + + @Test + void dataColumnSidecarsAreCorrect() { + final SignedBeaconBlock block1 = dataStructureUtil.randomSignedBeaconBlock(ONE); + final SignedBeaconBlock block2 = dataStructureUtil.randomSignedBeaconBlock(UInt64.valueOf(2)); + final SignedBeaconBlock block3 = dataStructureUtil.randomSignedBeaconBlock(UInt64.valueOf(3)); + final SignedBeaconBlock block4 = dataStructureUtil.randomSignedBeaconBlock(UInt64.valueOf(4)); + final List dataColumnIdentifiers = + List.of( + schema.create(block1.getRoot(), List.of(ZERO, ONE)), + schema.create( + block2.getRoot(), List.of(ZERO, ONE)), // ONE will be missed, shouldn't be fatal + schema.create(block3.getRoot(), ZERO), + schema.create(block4.getRoot(), ZERO)); + listenerWrapper = + new DataColumnSidecarsByRootListenerValidatingProxy( + peer, spec, listener, kzg, metricsSystem, timeProvider, dataColumnIdentifiers); + + final DataColumnSidecar dataColumnSidecar1_0 = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof(block1, ZERO); + final DataColumnSidecar dataColumnSidecar1_1 = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof(block1, ONE); + final DataColumnSidecar dataColumnSidecar2_0 = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof(block2, ZERO); + final DataColumnSidecar dataColumnSidecar3_0 = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof(block3, ZERO); + final DataColumnSidecar dataColumnSidecar4_0 = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof(block4, ZERO); + + assertDoesNotThrow(() -> listenerWrapper.onResponse(dataColumnSidecar1_0).join()); + assertDoesNotThrow(() -> listenerWrapper.onResponse(dataColumnSidecar1_1).join()); + assertDoesNotThrow(() -> listenerWrapper.onResponse(dataColumnSidecar2_0).join()); + assertDoesNotThrow(() -> listenerWrapper.onResponse(dataColumnSidecar3_0).join()); + assertDoesNotThrow(() -> listenerWrapper.onResponse(dataColumnSidecar4_0).join()); + } + + @Test + void blobSidecarIdentifierNotRequested() { + final SignedBeaconBlock block1 = dataStructureUtil.randomSignedBeaconBlock(ONE); + final SignedBeaconBlock block2 = dataStructureUtil.randomSignedBeaconBlock(UInt64.valueOf(2)); + final List dataColumnIdentifiers = + List.of(schema.create(block1.getRoot(), List.of(ZERO, ONE))); + listenerWrapper = + new DataColumnSidecarsByRootListenerValidatingProxy( + peer, spec, listener, kzg, metricsSystem, timeProvider, dataColumnIdentifiers); + + final DataColumnSidecar datColumnSidecar1_0 = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof(block1, ZERO); + final DataColumnSidecar datColumnSidecar1_1 = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof(block1, ONE); + final DataColumnSidecar datColumnSidecar2_0 = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof(block2, ZERO); + + assertDoesNotThrow(() -> listenerWrapper.onResponse(datColumnSidecar1_0).join()); + assertDoesNotThrow(() -> listenerWrapper.onResponse(datColumnSidecar1_1).join()); + final SafeFuture result = listenerWrapper.onResponse(datColumnSidecar2_0); + assertThat(result).isCompletedExceptionally(); + assertThatThrownBy(result::get) + .hasCauseExactlyInstanceOf(DataColumnSidecarsResponseInvalidResponseException.class); + assertThatThrownBy(result::get) + .hasMessageContaining( + DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType + .DATA_COLUMN_SIDECAR_UNEXPECTED_IDENTIFIER + .describe()); + } + + @Test + void dataColumnSidecarFailsKzgVerification() { + when(kzg.verifyCellProofBatch(any(), any(), any())).thenReturn(false); + final SignedBeaconBlock block1 = dataStructureUtil.randomSignedBeaconBlock(ONE); + final DataColumnsByRootIdentifier dataColumnIdentifier = schema.create(block1.getRoot(), ZERO); + listenerWrapper = + new DataColumnSidecarsByRootListenerValidatingProxy( + peer, spec, listener, kzg, metricsSystem, timeProvider, List.of(dataColumnIdentifier)); + + final DataColumnSidecar dataColumnSidecar = + dataStructureUtil.randomDataColumnSidecarWithInclusionProof( + block1, dataColumnIdentifier.getColumns().getFirst()); + + final SafeFuture result = listenerWrapper.onResponse(dataColumnSidecar); + assertThat(result).isCompletedExceptionally(); + assertThatThrownBy(result::get) + .hasCauseExactlyInstanceOf(DataColumnSidecarsResponseInvalidResponseException.class); + assertThatThrownBy(result::get) + .hasMessageContaining( + DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType + .DATA_COLUMN_SIDECAR_KZG_VERIFICATION_FAILED + .describe()); + } + + @Test + void dataColumnSidecarFailsInclusionProofVerification() { + final SchemaDefinitionsElectra schemaDefinitionsElectra = + SchemaDefinitionsElectra.required(spec.getGenesisSchemaDefinitions()); + final DataColumnSidecar dataColumnSidecar = + SchemaDefinitionsFulu.required(schemaDefinitionsElectra) + .getDataColumnSidecarSchema() + .create( + ZERO, + SchemaDefinitionsFulu.required(schemaDefinitionsElectra) + .getDataColumnSchema() + .create(List.of()), + List.of(), + List.of(), + new SignedBeaconBlockHeader( + new BeaconBlockHeader( + UInt64.valueOf(37), + UInt64.valueOf(3426), + Bytes32.fromHexString( + "0x6d3091dae0e2a0251cc2c0d9fef846e1c6e685f18fc8a2c7734f25750c22da36"), + Bytes32.fromHexString( + "0x715f24108254c3fcbef60c739fe702aed3ee692cb223c884b3db6e041c56c2a6"), + Bytes32.fromHexString( + "0xbea87258cde49915c8c929b6b91fbbcde004aeaaa08a3ccdc3248dc62b0e682f")), + BLSSignature.fromBytesCompressed( + Bytes.fromHexString( + "0xb4c313365edbc7cfa9319c54ecba0a8dc54c8537752c72a86c762eb0a81b3ad1eda43f0f3b19a9c9523a6a42450c1d070556e0a443d4733922765764ef5850b41d20b4f6af6cc93a70eb1023cc63473f111de772315a2726406be9dc6cb24e67"))), + List.of( + Bytes32.fromHexString( + "0x792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535"), + Bytes32.fromHexString( + "0xcd581849371d5f91b7d02a366b23402397007b50180069584f2bd4e14397540b"), + Bytes32.fromHexString( + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"), + Bytes32.fromHexString( + "0x9535c3eb42aaf182b13b18aacbcbc1df6593ecafd0bf7d5e94fb727b2dc1f265"))); + final DataColumnsByRootIdentifier dataColumnIdentifier = + schema.create(dataColumnSidecar.getBlockRoot(), ZERO); + listenerWrapper = + new DataColumnSidecarsByRootListenerValidatingProxy( + peer, spec, listener, kzg, metricsSystem, timeProvider, List.of(dataColumnIdentifier)); + + final SafeFuture result = listenerWrapper.onResponse(dataColumnSidecar); + assertThat(result).isCompletedExceptionally(); + assertThatThrownBy(result::get) + .hasCauseExactlyInstanceOf(DataColumnSidecarsResponseInvalidResponseException.class); + assertThatThrownBy(result::get) + .hasMessageContaining( + DataColumnSidecarsResponseInvalidResponseException.InvalidResponseType + .DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_FAILED + .describe()); + } +} diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/core/AbstractRequestHandlerTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/core/AbstractRequestHandlerTest.java index 9ddeb56a1b8..03334367886 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/core/AbstractRequestHandlerTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/core/AbstractRequestHandlerTest.java @@ -37,6 +37,9 @@ import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.util.DataStructureUtil; +import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; +import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -64,11 +67,14 @@ public void setup() { asyncRunner, peerLookup, combinedChainDataClient, + DataColumnSidecarByRootCustody.NOOP, + CustodyGroupCountManager.NOOP, recentChainData, new NoOpMetricsSystem(), new StatusMessageFactory(recentChainData), new MetadataMessagesFactory(), - getRpcEncoding()); + getRpcEncoding(), + DasReqRespLogger.NOOP); reqHandler = createRequestHandler(beaconChainMethods); diff --git a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java index 638df14f048..aca4f5d3ab3 100644 --- a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java +++ b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java @@ -110,6 +110,7 @@ import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.statetransition.util.DebugDataDumper; import tech.pegasys.teku.storage.api.StorageQueryChannel; import tech.pegasys.teku.storage.api.StubStorageQueryChannel; @@ -258,7 +259,8 @@ protected Eth2P2PNetwork buildNetwork(final P2PConfig config) { spec, NoOpKZG.INSTANCE, (__) -> Optional.empty(), - dasTotalCustodySubnetCount); + dasTotalCustodySubnetCount, + DasReqRespLogger.NOOP); List> rpcMethods = eth2PeerManager.getBeaconChainMethods().all().stream() diff --git a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/peers/RespondingEth2Peer.java b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/peers/RespondingEth2Peer.java index 4d9851c407a..053a5c9f86a 100644 --- a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/peers/RespondingEth2Peer.java +++ b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/peers/RespondingEth2Peer.java @@ -50,10 +50,12 @@ import tech.pegasys.teku.networking.p2p.rpc.RpcStreamController; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockAndState; import tech.pegasys.teku.spec.datastructures.blocks.StateAndBlockSummary; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobIdentifier; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnsByRootIdentifier; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.RpcRequest; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessage; import tech.pegasys.teku.spec.datastructures.state.Checkpoint; @@ -159,6 +161,9 @@ public void subscribeStatusUpdates(final PeerStatusSubscriber subscriber) { statusSubscribers.subscribe(subscriber); } + @Override + public void subscribeMetadataUpdates(final PeerMetadataUpdateSubscriber subscriber) {} + @Override public PeerStatus getStatus() { return status; @@ -258,6 +263,24 @@ public SafeFuture requestBlobSidecarsByRoot( return createPendingBlobSidecarRequest(handler); } + @Override + public SafeFuture requestDataColumnSidecarsByRoot( + final List dataColumnIdentifiers, + final RpcResponseListener listener) { + // TODO-fulu + return SafeFuture.COMPLETE; + } + + @Override + public SafeFuture requestDataColumnSidecarsByRange( + final UInt64 startSlot, + final UInt64 count, + final List columns, + final RpcResponseListener listener) { + // TODO-fulu + return SafeFuture.COMPLETE; + } + @Override public SafeFuture> requestBlockBySlot(final UInt64 slot) { final PendingRequestHandler, SignedBeaconBlock> handler = @@ -341,6 +364,23 @@ public Optional approveBlobSidecarsRequest( public void adjustBlobSidecarsRequest( final RequestApproval blobSidecarRequests, final long returnedBlobSidecarsCount) {} + @Override + public long getAvailableDataColumnSidecarsRequestCount() { + return 0; + } + + @Override + public Optional approveDataColumnSidecarsRequest( + final ResponseCallback callback, final long dataColumnSidecarsCount) { + return Optional.of( + new RequestApproval.RequestApprovalBuilder().timeSeconds(ZERO).objectsCount(0).build()); + } + + @Override + public void adjustDataColumnSidecarsRequest( + final RequestApproval dataColumnSidecarRequests, + final long returnedDataColumnSidecarsCount) {} + @Override public boolean approveRequest() { return true; diff --git a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/peers/StubSyncSource.java b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/peers/StubSyncSource.java index 91c384f3d88..83ef7d49d62 100644 --- a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/peers/StubSyncSource.java +++ b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/peers/StubSyncSource.java @@ -27,6 +27,7 @@ import tech.pegasys.teku.networking.p2p.reputation.ReputationAdjustment; import tech.pegasys.teku.networking.p2p.rpc.RpcResponseListener; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; public class StubSyncSource implements SyncSource { @@ -81,6 +82,16 @@ public SafeFuture requestBlobSidecarsByRange( return request; } + @Override + public SafeFuture requestDataColumnSidecarsByRange( + final UInt64 startSlot, + final UInt64 count, + final List columns, + final RpcResponseListener listener) { + // TODO + return SafeFuture.COMPLETE; + } + @Override public SafeFuture disconnectCleanly(final DisconnectReason reason) { return SafeFuture.COMPLETE;