diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/networking/libp2p/rpc/GoodbyeMessage.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/networking/libp2p/rpc/GoodbyeMessage.java index 64872e2fd36..238e6764432 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/networking/libp2p/rpc/GoodbyeMessage.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/networking/libp2p/rpc/GoodbyeMessage.java @@ -48,6 +48,8 @@ public GoodbyeMessage createFromBackingNode(final TreeNode node) { public static final UInt64 REASON_UNABLE_TO_VERIFY_NETWORK = UInt64.valueOf(128); public static final UInt64 REASON_TOO_MANY_PEERS = UInt64.valueOf(129); public static final UInt64 REASON_RATE_LIMITING = UInt64.valueOf(130); + public static final UInt64 REASON_BAD_SCORE = UInt64.valueOf(250); + public static final UInt64 REASON_BANNED = UInt64.valueOf(251); private GoodbyeMessage(final GoodbyeMessageSchema type, final TreeNode backingNode) { super(type, backingNode); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/CustodyGroupCountChannel.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/CustodyGroupCountChannel.java new file mode 100644 index 00000000000..31a799774c4 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/CustodyGroupCountChannel.java @@ -0,0 +1,32 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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; + +import tech.pegasys.teku.infrastructure.events.VoidReturningChannelInterface; + +public interface CustodyGroupCountChannel extends VoidReturningChannelInterface { + + CustodyGroupCountChannel NOOP = + new CustodyGroupCountChannel() { + @Override + public void onCustodyGroupCountUpdate(final int groupCount) {} + + @Override + public void onCustodyGroupCountSynced(final int groupCount) {} + }; + + void onCustodyGroupCountUpdate(int groupCount); + + void onCustodyGroupCountSynced(int groupCount); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/CustodyGroupCountManager.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/CustodyGroupCountManager.java new file mode 100644 index 00000000000..0c94b2eda03 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/CustodyGroupCountManager.java @@ -0,0 +1,48 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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; + +import java.util.List; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +public interface CustodyGroupCountManager { + CustodyGroupCountManager NOOP = + new CustodyGroupCountManager() { + @Override + public int getCustodyGroupCount() { + return 0; + } + + @Override + public List getCustodyColumnIndices() { + return List.of(); + } + + @Override + public int getCustodyGroupSyncedCount() { + return 0; + } + + @Override + public void setCustodyGroupSyncedCount(int custodyGroupSyncedCount) {} + }; + + int getCustodyGroupCount(); + + List getCustodyColumnIndices(); + + int getCustodyGroupSyncedCount(); + + void setCustodyGroupSyncedCount(int custodyGroupSyncedCount); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarByRootCustody.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarByRootCustody.java new file mode 100644 index 00000000000..434fe01c389 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarByRootCustody.java @@ -0,0 +1,59 @@ +/* + * 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; + +import java.util.Optional; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.util.DataColumnIdentifier; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; + +public interface DataColumnSidecarByRootCustody extends DataColumnSidecarCustody { + + DataColumnSidecarByRootCustody NOOP = + new DataColumnSidecarByRootCustody() { + @Override + public SafeFuture> getCustodyDataColumnSidecarByRoot( + DataColumnIdentifier columnId) { + return SafeFuture.completedFuture(Optional.empty()); + } + + @Override + public SafeFuture hasCustodyDataColumnSidecar( + DataColumnSlotAndIdentifier columnId) { + return SafeFuture.completedFuture(false); + } + + @Override + public SafeFuture> getCustodyDataColumnSidecar( + DataColumnSlotAndIdentifier columnId) { + return SafeFuture.completedFuture(Optional.empty()); + } + + @Override + public SafeFuture onNewValidatedDataColumnSidecar( + DataColumnSidecar dataColumnSidecar) { + return SafeFuture.COMPLETE; + } + + @Override + public AsyncStream retrieveMissingColumns() { + return AsyncStream.empty(); + } + }; + + SafeFuture> getCustodyDataColumnSidecarByRoot( + DataColumnIdentifier columnId); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarCustody.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarCustody.java new file mode 100644 index 00000000000..93ab8d58b6a --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarCustody.java @@ -0,0 +1,32 @@ +/* + * 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; + +import java.util.Optional; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; + +public interface DataColumnSidecarCustody { + + SafeFuture> getCustodyDataColumnSidecar( + DataColumnSlotAndIdentifier columnId); + + SafeFuture hasCustodyDataColumnSidecar(DataColumnSlotAndIdentifier columnId); + + SafeFuture onNewValidatedDataColumnSidecar(DataColumnSidecar dataColumnSidecar); + + AsyncStream retrieveMissingColumns(); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/DataColumnSidecarGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/DataColumnSidecarGossipValidator.java new file mode 100644 index 00000000000..6256583c4fa --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/DataColumnSidecarGossipValidator.java @@ -0,0 +1,458 @@ +/* + * 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.statetransition.validation; + +import static tech.pegasys.teku.infrastructure.async.SafeFuture.completedFuture; +import static tech.pegasys.teku.spec.config.Constants.BEST_CASE_NON_FINALIZED_EPOCHS; +import static tech.pegasys.teku.spec.config.Constants.VALID_BLOCK_SET_SIZE; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ACCEPT; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ignore; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.reject; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Counter; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.collections.LimitedSet; +import tech.pegasys.teku.infrastructure.metrics.MetricsHistogram; +import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.kzg.KZG; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.constants.Domain; +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.SignedBeaconBlockHeader; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.logic.common.statetransition.results.BlockImportResult; +import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; + +/** + * This class supposed to implement gossip validation rules as per spec + */ +public class DataColumnSidecarGossipValidator { + private static final Logger LOG = LogManager.getLogger(); + public static final BiFunction + DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_HISTOGRAM = + (metricsSystem, timeProvider) -> + new MetricsHistogram( + metricsSystem, + timeProvider, + TekuMetricCategory.BEACON, + "data_column_sidecar_inclusion_proof_verification_seconds", + "Time taken to verify data column sidecar inclusion proof", + 0.001, + 0.002, + 0.003, + 0.004, + 0.005, + 0.01, + 0.015, + 0.02, + 0.025, + 0.03, + 0.04, + 0.05, + 0.1, + 0.5, + 1.0); + public static final BiFunction + DATA_COLUMN_SIDECAR_KZG_BATCH_VERIFICATION_HISTOGRAM = + (metricsSystem, timeProvider) -> + new MetricsHistogram( + metricsSystem, + timeProvider, + TekuMetricCategory.BEACON, + "kzg_verification_data_column_batch_seconds", + "Runtime of batched data column kzg verification", + 0.005, + 0.01, + 0.025, + 0.05, + 0.075, + 0.1, + 0.25, + 0.5, + 0.75, + 1.0, + 1.25, + 1.5, + 1.75, + 2.0); + + private final Spec spec; + private final Set receivedValidDataColumnSidecarInfoSet; + private final Set validInclusionProofInfoSet; + private final Set validSignedBlockHeaders; + private final GossipValidationHelper gossipValidationHelper; + private final Map invalidBlockRoots; + private final MiscHelpersFulu miscHelpersFulu; + private final KZG kzg; + private final Counter totalDataColumnSidecarsProcessingRequestsCounter; + private final Counter totalDataColumnSidecarsProcessingSuccessesCounter; + private final MetricsHistogram dataColumnSidecarInclusionProofVerificationTimeSeconds; + private final MetricsHistogram dataColumnSidecarKzgBatchVerificationTimeSeconds; + + public static DataColumnSidecarGossipValidator create( + final Spec spec, + final Map invalidBlockRoots, + final GossipValidationHelper validationHelper, + final MiscHelpersFulu miscHelpersFulu, + final KZG kzg, + final MetricsSystem metricsSystem, + final TimeProvider timeProvider) { + + final Optional maybeNumberOfColumns = spec.getNumberOfDataColumns(); + + final int validInfoSize = VALID_BLOCK_SET_SIZE * maybeNumberOfColumns.orElse(1); + // It's not fatal if we miss something and we don't need finalized data + final int validSignedBlockHeadersSize = + spec.getGenesisSpec().getSlotsPerEpoch() * BEST_CASE_NON_FINALIZED_EPOCHS; + + return new DataColumnSidecarGossipValidator( + spec, + invalidBlockRoots, + validationHelper, + miscHelpersFulu, + kzg, + metricsSystem, + timeProvider, + LimitedSet.createSynchronized(validInfoSize), + LimitedSet.createSynchronized(validSignedBlockHeadersSize), + LimitedSet.createSynchronized(validSignedBlockHeadersSize)); + } + + @VisibleForTesting + public Set getReceivedValidDataColumnSidecarInfoSet() { + return receivedValidDataColumnSidecarInfoSet; + } + + private DataColumnSidecarGossipValidator( + final Spec spec, + final Map invalidBlockRoots, + final GossipValidationHelper gossipValidationHelper, + final MiscHelpersFulu miscHelpersFulu, + final KZG kzg, + final MetricsSystem metricsSystem, + final TimeProvider timeProvider, + final Set receivedValidDataColumnSidecarInfoSet, + final Set validInclusionProofInfoSet, + final Set validSignedBlockHeaders) { + this.spec = spec; + this.invalidBlockRoots = invalidBlockRoots; + this.gossipValidationHelper = gossipValidationHelper; + this.miscHelpersFulu = miscHelpersFulu; + this.kzg = kzg; + this.receivedValidDataColumnSidecarInfoSet = receivedValidDataColumnSidecarInfoSet; + this.totalDataColumnSidecarsProcessingRequestsCounter = + metricsSystem.createCounter( + TekuMetricCategory.BEACON, + "data_column_sidecar_processing_requests_total", + "Total number of data column sidecars submitted for processing"); + this.totalDataColumnSidecarsProcessingSuccessesCounter = + metricsSystem.createCounter( + TekuMetricCategory.BEACON, + "data_column_sidecar_processing_successes_total", + "Total number of data column sidecars verified for gossip"); + this.dataColumnSidecarInclusionProofVerificationTimeSeconds = + DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION_HISTOGRAM.apply( + metricsSystem, timeProvider); + this.dataColumnSidecarKzgBatchVerificationTimeSeconds = + DATA_COLUMN_SIDECAR_KZG_BATCH_VERIFICATION_HISTOGRAM.apply(metricsSystem, timeProvider); + + this.validInclusionProofInfoSet = validInclusionProofInfoSet; + this.validSignedBlockHeaders = validSignedBlockHeaders; + } + + public SafeFuture validate(final DataColumnSidecar dataColumnSidecar) { + final BeaconBlockHeader blockHeader = + dataColumnSidecar.getSignedBeaconBlockHeader().getMessage(); + + totalDataColumnSidecarsProcessingRequestsCounter.inc(); + + /* + * [IGNORE] The sidecar is the first sidecar for the tuple (block_header.slot, block_header.proposer_index, sidecar.index) with valid header signature, sidecar inclusion proof, and kzg proof. + */ + if (!isFirstValidForSlotProposerIndexAndColumnIndex(dataColumnSidecar, blockHeader)) { + LOG.trace( + "DataColumnSidecar is not the first valid for its slot and index. It will be dropped"); + return completedFuture(InternalValidationResult.IGNORE); + } + + /* + * [REJECT] The sidecar's index is consistent with NUMBER_OF_COLUMNS -- i.e. sidecar.index < NUMBER_OF_COLUMNS. + */ + final Optional maybeNumberOfColumns = spec.getNumberOfDataColumns(); + if (maybeNumberOfColumns.isEmpty()) { + return completedFuture(reject("DataColumnSidecar's slot is pre-Fulu")); + } + if (!dataColumnSidecar.getIndex().isLessThan(maybeNumberOfColumns.get())) { + return completedFuture(reject("DataColumnSidecar index not less than number of columns.")); + } + + /* + * [REJECT] The sidecar is for the correct subnet -- i.e. compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id. + * Already done in {@link tech.pegasys.teku.networking.eth2.gossip.topics.topichandlers.DataColumnSidecarTopicHandler.TopicSubnetIdAwareOperationProcessor#process( + * DataColumnSidecar, Optional)} + */ + + /* + * [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. validate that block_header.slot <= current_slot (a client MAY queue future sidecars for processing at the appropriate slot). + */ + if (gossipValidationHelper.isSlotFromFuture(blockHeader.getSlot())) { + LOG.trace("DataColumnSidecar is from the future. It will be saved for future processing"); + return completedFuture(InternalValidationResult.SAVE_FOR_FUTURE); + } + + /* + * [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that block_header.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) + */ + if (gossipValidationHelper.isSlotFinalized(blockHeader.getSlot())) { + LOG.trace("DataColumnSidecar is too old (slot already finalized)"); + return completedFuture(InternalValidationResult.IGNORE); + } + + // Optimization: If we have already completely verified DataColumnSidecar with the same + // SignedBlockHeader, we can skip most steps and jump to shortened validation + if (validSignedBlockHeaders.contains( + dataColumnSidecar.getSignedBeaconBlockHeader().hashTreeRoot())) { + return validateDataColumnSidecarWithKnownValidHeader(dataColumnSidecar, blockHeader); + } + + /* + * [REJECT] The proposer signature of sidecar.signed_block_header, is valid with respect to the block_header.proposer_index pubkey. + * + * Verified later after all checks not involving state are passed + */ + + /* + * [IGNORE] The sidecar's block's parent (defined by block_header.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue sidecars for processing once the parent block is retrieved). + */ + if (!gossipValidationHelper.isBlockAvailable(blockHeader.getParentRoot())) { + LOG.trace( + "DataColumnSidecar block header parent block is not available. It will be saved for future processing"); + return completedFuture(InternalValidationResult.SAVE_FOR_FUTURE); + } + final Optional maybeParentBlockSlot = + gossipValidationHelper.getSlotForBlockRoot(blockHeader.getParentRoot()); + if (maybeParentBlockSlot.isEmpty()) { + LOG.trace( + "DataColumnSidecar block header parent block does not exist. It will be saved for future processing"); + return completedFuture(InternalValidationResult.SAVE_FOR_FUTURE); + } + final UInt64 parentBlockSlot = maybeParentBlockSlot.get(); + + /* + * [REJECT] The sidecar's block's parent (defined by block_header.parent_root) passes validation. + */ + if (invalidBlockRoots.containsKey(blockHeader.getParentRoot())) { + return completedFuture(reject("DataColumnSidecar block header has an invalid parent root")); + } + + /* + * [REJECT] The sidecar is from a higher slot than the sidecar's block's parent (defined by block_header.parent_root). + */ + if (!blockHeader.getSlot().isGreaterThan(parentBlockSlot)) { + return completedFuture(reject("Parent block is after DataColumnSidecar slot.")); + } + + /* + * [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block -- i.e. get_checkpoint_block(store, block_header.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root. + */ + if (!gossipValidationHelper.currentFinalizedCheckpointIsAncestorOfBlock( + blockHeader.getSlot(), blockHeader.getParentRoot())) { + return completedFuture( + reject("DataColumnSidecar block header does not descend from finalized checkpoint")); + } + + /* + * [REJECT] The sidecar's kzg_commitments field inclusion proof is valid as verified by verify_data_column_sidecar_inclusion_proof(sidecar). + */ + if (!verifyDataColumnSidecarInclusionProof(dataColumnSidecar)) { + return completedFuture(reject("DataColumnSidecar inclusion proof validation failed")); + } + + /* + * [REJECT] The sidecar's column data is valid as verified by verify_data_column_sidecar_kzg_proofs(sidecar). + */ + try (MetricsHistogram.Timer ignored = + dataColumnSidecarKzgBatchVerificationTimeSeconds.startTimer()) { + if (!miscHelpersFulu.verifyDataColumnSidecarKzgProof(kzg, dataColumnSidecar)) { + return completedFuture(reject("DataColumnSidecar does not pass kzg validation")); + } + } catch (final Throwable t) { + return completedFuture(reject("DataColumnSidecar does not pass kzg validation")); + } + + return gossipValidationHelper + .getParentStateInBlockEpoch( + parentBlockSlot, blockHeader.getParentRoot(), blockHeader.getSlot()) + .thenApply( + maybePostState -> { + /* + * [REJECT] [REJECT] The sidecar is proposed by the expected proposer_index for the block's slot in the context of the current shuffling (defined by block_header.parent_root/block_header.slot). + * + * If the proposer_index cannot immediately be verified against the expected shuffling, the sidecar MAY be queued for later processing while proposers for the block's branch are calculated -- in such a case do not REJECT, instead IGNORE this message. + */ + if (maybePostState.isEmpty()) { + LOG.trace( + "DataColumnSidecar block header state wasn't available. Must have been pruned by finalized."); + return InternalValidationResult.IGNORE; + } + final BeaconState postState = maybePostState.get(); + if (!gossipValidationHelper.isProposerTheExpectedProposer( + blockHeader.getProposerIndex(), blockHeader.getSlot(), postState)) { + return reject( + "DataColumnSidecar block header proposed by incorrect proposer (%s).", + blockHeader.getProposerIndex()); + } + + /* + * [REJECT] The proposer signature of sidecar.signed_block_header, is valid with respect to the block_header.proposer_index pubkey. + */ + if (!verifyBlockHeaderSignature( + postState, dataColumnSidecar.getSignedBeaconBlockHeader())) { + return reject("DataColumnSidecar block header signature is invalid."); + } + + /* + * Checking it again at the very end because whole method is not synchronized + * + * [IGNORE] The sidecar is the first sidecar for the tuple (block_header.slot, block_header.proposer_index, sidecar.index) with valid header signature, sidecar inclusion proof, and kzg proof. + */ + if (!receivedValidDataColumnSidecarInfoSet.add( + new SlotProposerIndexAndColumnIndex( + blockHeader.getSlot(), + blockHeader.getProposerIndex(), + dataColumnSidecar.getIndex()))) { + return ignore( + "DataColumnSidecar is not the first valid for its slot and index. It will be dropped."); + } + + validSignedBlockHeaders.add( + dataColumnSidecar.getSignedBeaconBlockHeader().hashTreeRoot()); + validInclusionProofInfoSet.add( + new InclusionProofInfo( + dataColumnSidecar.getSszKZGCommitments().hashTreeRoot(), + dataColumnSidecar.getKzgCommitmentsInclusionProof().hashTreeRoot(), + dataColumnSidecar.getBlockBodyRoot())); + + totalDataColumnSidecarsProcessingSuccessesCounter.inc(); + return ACCEPT; + }); + } + + private SafeFuture validateDataColumnSidecarWithKnownValidHeader( + final DataColumnSidecar dataColumnSidecar, final BeaconBlockHeader blockHeader) { + + /* + * [REJECT] The sidecar's kzg_commitments field inclusion proof is valid as verified by verify_data_column_sidecar_inclusion_proof(sidecar). + */ + if (!verifyDataColumnSidecarInclusionProof(dataColumnSidecar)) { + return completedFuture(reject("DataColumnSidecar inclusion proof validation failed")); + } + + /* + * [REJECT] The sidecar's column data is valid as verified by verify_data_column_sidecar_kzg_proofs(sidecar). + */ + try (MetricsHistogram.Timer ignored = + dataColumnSidecarKzgBatchVerificationTimeSeconds.startTimer()) { + if (!miscHelpersFulu.verifyDataColumnSidecarKzgProof(kzg, dataColumnSidecar)) { + return completedFuture(reject("DataColumnSidecar does not pass kzg validation")); + } + } catch (final Throwable t) { + return completedFuture(reject("DataColumnSidecar does not pass kzg validation")); + } + + // This can be changed between two received DataColumnSidecars from one block, so checking + /* + * [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block -- i.e. get_checkpoint_block(store, block_header.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root. + */ + if (!gossipValidationHelper.currentFinalizedCheckpointIsAncestorOfBlock( + blockHeader.getSlot(), blockHeader.getParentRoot())) { + return completedFuture( + reject("DataColumnSidecar block header does not descend from finalized checkpoint")); + } + + /* + * [IGNORE] The sidecar is the first sidecar for the tuple (block_header.slot, block_header.proposer_index, sidecar.index) with valid header signature, sidecar inclusion proof, and kzg proof. + */ + if (!receivedValidDataColumnSidecarInfoSet.add( + new SlotProposerIndexAndColumnIndex( + blockHeader.getSlot(), blockHeader.getProposerIndex(), dataColumnSidecar.getIndex()))) { + return SafeFuture.completedFuture( + ignore( + "DataColumnSidecar is not the first valid for its slot and index. It will be dropped.")); + } + + totalDataColumnSidecarsProcessingSuccessesCounter.inc(); + + return SafeFuture.completedFuture(ACCEPT); + } + + private boolean verifyDataColumnSidecarInclusionProof(final DataColumnSidecar dataColumnSidecar) { + if (validInclusionProofInfoSet.contains( + new InclusionProofInfo( + dataColumnSidecar.getSszKZGCommitments().hashTreeRoot(), + dataColumnSidecar.getKzgCommitmentsInclusionProof().hashTreeRoot(), + dataColumnSidecar.getBlockBodyRoot()))) { + return true; + } + try (MetricsHistogram.Timer ignored = + dataColumnSidecarInclusionProofVerificationTimeSeconds.startTimer()) { + return miscHelpersFulu.verifyDataColumnSidecarInclusionProof(dataColumnSidecar); + } catch (final Throwable t) { + return false; + } + } + + private boolean verifyBlockHeaderSignature( + final BeaconState state, final SignedBeaconBlockHeader signedBlockHeader) { + final Bytes32 domain = + spec.getDomain( + Domain.BEACON_PROPOSER, + spec.getCurrentEpoch(state), + state.getFork(), + state.getGenesisValidatorsRoot()); + final Bytes signingRoot = spec.computeSigningRoot(signedBlockHeader.getMessage(), domain); + + return gossipValidationHelper.isSignatureValidWithRespectToProposerIndex( + signingRoot, + signedBlockHeader.getMessage().getProposerIndex(), + signedBlockHeader.getSignature(), + state); + } + + private boolean isFirstValidForSlotProposerIndexAndColumnIndex( + final DataColumnSidecar dataColumnSidecar, final BeaconBlockHeader blockHeader) { + return !receivedValidDataColumnSidecarInfoSet.contains( + new SlotProposerIndexAndColumnIndex( + blockHeader.getSlot(), blockHeader.getProposerIndex(), dataColumnSidecar.getIndex())); + } + + record SlotProposerIndexAndColumnIndex(UInt64 slot, UInt64 proposerIndex, UInt64 columnIndex) {} + + record InclusionProofInfo( + Bytes32 commitmentsRoot, Bytes32 inclusionProofRoot, Bytes32 bodyRoot) {} +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/signatures/AggregatingSignatureVerificationService.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/signatures/AggregatingSignatureVerificationService.java index b9bbaebeeb6..cff980df828 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/signatures/AggregatingSignatureVerificationService.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/signatures/AggregatingSignatureVerificationService.java @@ -34,7 +34,7 @@ import tech.pegasys.teku.infrastructure.async.AsyncRunner; import tech.pegasys.teku.infrastructure.async.AsyncRunnerFactory; import tech.pegasys.teku.infrastructure.async.SafeFuture; -import tech.pegasys.teku.infrastructure.metrics.MetricsHistogram; +import tech.pegasys.teku.infrastructure.metrics.MetricsQuantileHistogram; import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; import tech.pegasys.teku.service.serviceutils.ServiceCapacityExceededException; @@ -53,7 +53,7 @@ public class AggregatingSignatureVerificationService extends SignatureVerificati private final AsyncRunner asyncRunner; private final Counter batchCounter; private final Counter taskCounter; - private final MetricsHistogram batchSizeHistogram; + private final MetricsQuantileHistogram batchSizeHistogram; @VisibleForTesting AggregatingSignatureVerificationService( @@ -89,7 +89,7 @@ public class AggregatingSignatureVerificationService extends SignatureVerificati "signature_verifications_task_count_total", "Reports the number of individual verification tasks processed"); batchSizeHistogram = - MetricsHistogram.create( + MetricsQuantileHistogram.create( TekuMetricCategory.EXECUTOR, metricsSystem, "signature_verifications_batch_size", diff --git a/infrastructure/metrics/build.gradle b/infrastructure/metrics/build.gradle index 2db54e7b187..9b04d549b37 100644 --- a/infrastructure/metrics/build.gradle +++ b/infrastructure/metrics/build.gradle @@ -3,6 +3,7 @@ dependencies { implementation project(':infrastructure:exceptions') implementation project(':infrastructure:io') + implementation project(':infrastructure:time') implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'io.vertx:vertx-core' diff --git a/infrastructure/metrics/src/main/java/tech/pegasys/teku/infrastructure/metrics/MetricsHistogram.java b/infrastructure/metrics/src/main/java/tech/pegasys/teku/infrastructure/metrics/MetricsHistogram.java index 37011df889b..5ede2c6579c 100644 --- a/infrastructure/metrics/src/main/java/tech/pegasys/teku/infrastructure/metrics/MetricsHistogram.java +++ b/infrastructure/metrics/src/main/java/tech/pegasys/teku/infrastructure/metrics/MetricsHistogram.java @@ -13,172 +13,81 @@ package tech.pegasys.teku.infrastructure.metrics; -import static com.google.common.base.Preconditions.checkArgument; - -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; -import org.HdrHistogram.SynchronizedHistogram; -import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem; +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; import org.hyperledger.besu.plugin.services.MetricsSystem; -import org.hyperledger.besu.plugin.services.metrics.ExternalSummary; +import org.hyperledger.besu.plugin.services.metrics.Histogram; import org.hyperledger.besu.plugin.services.metrics.MetricCategory; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; -/** - * A histogram metric that automatically selects bucket sizes based on simple configuration and the - * values actually received. Only records values when the metrics system is a {@link - * PrometheusMetricsSystem}. - * - *

Backing is an HdrHistogram. - * - * @see HdrHistogram docs - */ public class MetricsHistogram { - static final String QUANTILE_LABEL = "quantile"; - static final double LABEL_50 = 0.5; - static final double LABEL_95 = 0.95; - static final double LABEL_99 = 0.99; - static final double LABEL_1 = 1; - - private final Map, SynchronizedHistogram> histogramMap = new ConcurrentHashMap<>(); - private final List labels; - private final Optional highestTrackableValue; - private final int numberOfSignificantValueDigits; + private static final double[] DEFAULT_BUCKETS = + new double[] { + 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0 + }; - protected MetricsHistogram( - final int numberOfSignificantValueDigits, - final Optional highestTrackableValue, - final List customLabelsNames) { - this.numberOfSignificantValueDigits = numberOfSignificantValueDigits; - this.highestTrackableValue = highestTrackableValue; - this.labels = Stream.concat(customLabelsNames.stream(), Stream.of(QUANTILE_LABEL)).toList(); - } + private final Histogram histogram; + private final TimeProvider timeProvider; - /** - * Create a new histogram metric which auto-resizes to fit any values supplied and maintains at - * least {@code numberOfSignificantValueDigits} of precision. - * - * @param category the metrics category - * @param metricsSystem the metrics system to register with - * @param name the name of the metric - * @param help the help text describing the metric - * @param numberOfSignificantValueDigits the number of digits of precision to preserve - * @return the new metric - */ - public static MetricsHistogram create( - final MetricCategory category, + public MetricsHistogram( final MetricsSystem metricsSystem, + final TimeProvider timeProvider, + final MetricCategory category, final String name, final String help, - final int numberOfSignificantValueDigits, - final List customLabelsNames) { - - return createMetric( - category, - metricsSystem, - name, - help, - numberOfSignificantValueDigits, - Optional.empty(), - customLabelsNames); + final double... buckets) { + this.histogram = + metricsSystem.createHistogram( + category, name, help, buckets.length > 0 ? buckets : DEFAULT_BUCKETS); + this.timeProvider = timeProvider; } - /** - * Create a new histogram metric with a fixed maximum value, maintaining at least {@code - * numberOfSignificantValueDigits} of precision. - * - *

Values above the specified highestTrackableValue will be recorded as being equal to that - * value. - * - * @param category the metrics category - * @param metricsSystem the metrics system to register with - * @param name the name of the metric - * @param help the help text describing the metric - * @param numberOfSignificantValueDigits the number of digits of precision to preserve - * @param highestTrackableValue the highest value that can be recorded by this histogram - * @return the new metric - */ - public static MetricsHistogram create( - final MetricCategory category, + public MetricsHistogram( final MetricsSystem metricsSystem, + final TimeProvider timeProvider, + final MetricCategory category, final String name, final String help, - final int numberOfSignificantValueDigits, - final long highestTrackableValue, - final List customLabelsNames) { - - return createMetric( - category, - metricsSystem, - name, - help, - numberOfSignificantValueDigits, - Optional.of(highestTrackableValue), - customLabelsNames); + final double[] buckets, + final String... labels) { + this.histogram = + metricsSystem + .createLabelledHistogram( + category, name, help, buckets.length > 0 ? buckets : DEFAULT_BUCKETS) + .labels(labels.length > 0 ? labels : new String[0]); + this.timeProvider = timeProvider; } - private static MetricsHistogram createMetric( - final MetricCategory category, - final MetricsSystem metricsSystem, - final String name, - final String help, - final int numberOfSignificantValueDigits, - final Optional highestTrackableValue, - final List customLabelsNames) { + public static class Timer implements Closeable { + private final Histogram histogram; + private final TimeProvider timeProvider; + private final UInt64 start; - final MetricsHistogram histogram = - new MetricsHistogram( - numberOfSignificantValueDigits, highestTrackableValue, customLabelsNames); - if (metricsSystem instanceof PrometheusMetricsSystem) { - final String summaryMetricName = category.toString().toLowerCase(Locale.ROOT) + "_" + name; - metricsSystem.createSummary( - category, summaryMetricName, help, histogram::histogramToCollector); + public Timer(final Histogram histogram, final TimeProvider timeProvider) { + this.histogram = histogram; + this.timeProvider = timeProvider; + start = timeProvider.getTimeInMillis(); } - return histogram; - } - - public void recordValue(final long value, final String... customLabelValues) { - checkArgument( - labels.size() == customLabelValues.length + 1, - "customLabelsNames and customLabelsValues must have the same size"); - final SynchronizedHistogram histogram = - histogramMap.computeIfAbsent( - Arrays.asList(customLabelValues), - __ -> - highestTrackableValue - .map(aLong -> new SynchronizedHistogram(aLong, numberOfSignificantValueDigits)) - .orElseGet(() -> new SynchronizedHistogram(numberOfSignificantValueDigits))); + @Override + public void close() throws IOException { + histogram.observe(timeProvider.getTimeInMillis().minus(start).doubleValue() / 1000); + } - if (histogram.isAutoResize()) { - histogram.recordValue(value); - } else { - histogram.recordValue(Math.min(histogram.getHighestTrackableValue(), value)); + public Runnable closeUnchecked() { + return () -> { + try { + close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; } } - protected ExternalSummary histogramToCollector() { - - final List quantiles = - histogramMap.entrySet().stream() - .flatMap( - entry -> - Stream.of( - new ExternalSummary.Quantile( - LABEL_50, entry.getValue().getValueAtPercentile(50)), - new ExternalSummary.Quantile( - LABEL_95, entry.getValue().getValueAtPercentile(95)), - new ExternalSummary.Quantile( - LABEL_99, entry.getValue().getValueAtPercentile(99)), - new ExternalSummary.Quantile( - LABEL_1, entry.getValue().getValueAtPercentile(100)))) - .toList(); - - // return sum 0 as we currently don't use it - return new ExternalSummary(histogramMap.size(), 0, quantiles); + public Timer startTimer() { + return new Timer(histogram, timeProvider); } } diff --git a/infrastructure/metrics/src/main/java/tech/pegasys/teku/infrastructure/metrics/MetricsQuantileHistogram.java b/infrastructure/metrics/src/main/java/tech/pegasys/teku/infrastructure/metrics/MetricsQuantileHistogram.java new file mode 100644 index 00000000000..3fb22af6932 --- /dev/null +++ b/infrastructure/metrics/src/main/java/tech/pegasys/teku/infrastructure/metrics/MetricsQuantileHistogram.java @@ -0,0 +1,184 @@ +/* + * 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.infrastructure.metrics; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; +import org.HdrHistogram.SynchronizedHistogram; +import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.ExternalSummary; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +/** + * A histogram metric that automatically selects bucket sizes based on simple configuration and the + * values actually received. Only records values when the metrics system is a {@link + * PrometheusMetricsSystem}. + * + *

Backing is an HdrHistogram. + * + * @see HdrHistogram docs + */ +public class MetricsQuantileHistogram { + static final String QUANTILE_LABEL = "quantile"; + static final double LABEL_50 = 0.5; + static final double LABEL_95 = 0.95; + static final double LABEL_99 = 0.99; + static final double LABEL_1 = 1; + + private final Map, SynchronizedHistogram> histogramMap = new ConcurrentHashMap<>(); + private final List labels; + private final Optional highestTrackableValue; + private final int numberOfSignificantValueDigits; + + protected MetricsQuantileHistogram( + final int numberOfSignificantValueDigits, + final Optional highestTrackableValue, + final List customLabelsNames) { + this.numberOfSignificantValueDigits = numberOfSignificantValueDigits; + this.highestTrackableValue = highestTrackableValue; + this.labels = Stream.concat(customLabelsNames.stream(), Stream.of(QUANTILE_LABEL)).toList(); + } + + /** + * Create a new histogram metric which auto-resizes to fit any values supplied and maintains at + * least {@code numberOfSignificantValueDigits} of precision. + * + * @param category the metrics category + * @param metricsSystem the metrics system to register with + * @param name the name of the metric + * @param help the help text describing the metric + * @param numberOfSignificantValueDigits the number of digits of precision to preserve + * @return the new metric + */ + public static MetricsQuantileHistogram create( + final MetricCategory category, + final MetricsSystem metricsSystem, + final String name, + final String help, + final int numberOfSignificantValueDigits, + final List customLabelsNames) { + + return createMetric( + category, + metricsSystem, + name, + help, + numberOfSignificantValueDigits, + Optional.empty(), + customLabelsNames); + } + + /** + * Create a new histogram metric with a fixed maximum value, maintaining at least {@code + * numberOfSignificantValueDigits} of precision. + * + *

Values above the specified highestTrackableValue will be recorded as being equal to that + * value. + * + * @param category the metrics category + * @param metricsSystem the metrics system to register with + * @param name the name of the metric + * @param help the help text describing the metric + * @param numberOfSignificantValueDigits the number of digits of precision to preserve + * @param highestTrackableValue the highest value that can be recorded by this histogram + * @return the new metric + */ + public static MetricsQuantileHistogram create( + final MetricCategory category, + final MetricsSystem metricsSystem, + final String name, + final String help, + final int numberOfSignificantValueDigits, + final long highestTrackableValue, + final List customLabelsNames) { + + return createMetric( + category, + metricsSystem, + name, + help, + numberOfSignificantValueDigits, + Optional.of(highestTrackableValue), + customLabelsNames); + } + + private static MetricsQuantileHistogram createMetric( + final MetricCategory category, + final MetricsSystem metricsSystem, + final String name, + final String help, + final int numberOfSignificantValueDigits, + final Optional highestTrackableValue, + final List customLabelsNames) { + + final MetricsQuantileHistogram histogram = + new MetricsQuantileHistogram( + numberOfSignificantValueDigits, highestTrackableValue, customLabelsNames); + if (metricsSystem instanceof PrometheusMetricsSystem) { + final String summaryMetricName = category.toString().toLowerCase(Locale.ROOT) + "_" + name; + metricsSystem.createSummary( + category, summaryMetricName, help, histogram::histogramToCollector); + } + return histogram; + } + + public void recordValue(final long value, final String... customLabelValues) { + checkArgument( + labels.size() == customLabelValues.length + 1, + "customLabelsNames and customLabelsValues must have the same size"); + + final SynchronizedHistogram histogram = + histogramMap.computeIfAbsent( + Arrays.asList(customLabelValues), + __ -> + highestTrackableValue + .map(aLong -> new SynchronizedHistogram(aLong, numberOfSignificantValueDigits)) + .orElseGet(() -> new SynchronizedHistogram(numberOfSignificantValueDigits))); + + if (histogram.isAutoResize()) { + histogram.recordValue(value); + } else { + histogram.recordValue(Math.min(histogram.getHighestTrackableValue(), value)); + } + } + + protected ExternalSummary histogramToCollector() { + + final List quantiles = + histogramMap.entrySet().stream() + .flatMap( + entry -> + Stream.of( + new ExternalSummary.Quantile( + LABEL_50, entry.getValue().getValueAtPercentile(50)), + new ExternalSummary.Quantile( + LABEL_95, entry.getValue().getValueAtPercentile(95)), + new ExternalSummary.Quantile( + LABEL_99, entry.getValue().getValueAtPercentile(99)), + new ExternalSummary.Quantile( + LABEL_1, entry.getValue().getValueAtPercentile(100)))) + .toList(); + + // return sum 0 as we currently don't use it + return new ExternalSummary(histogramMap.size(), 0, quantiles); + } +} diff --git a/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/MetricsHistogramTest.java b/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/MetricsQuantileHistogramTest.java similarity index 87% rename from infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/MetricsHistogramTest.java rename to infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/MetricsQuantileHistogramTest.java index c7bffe07151..eba1f11e9fe 100644 --- a/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/MetricsHistogramTest.java +++ b/infrastructure/metrics/src/test/java/tech/pegasys/teku/infrastructure/metrics/MetricsQuantileHistogramTest.java @@ -28,7 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class MetricsHistogramTest { +class MetricsQuantileHistogramTest { private static final TekuMetricCategory CATEGORY = TekuMetricCategory.BEACON; private ObservableMetricsSystem metricsSystem; @@ -45,8 +45,8 @@ void shutdown() { @Test void shouldReportValuesWithNoSpecifiedUpperLimit() { - final MetricsHistogram histogram = - MetricsHistogram.create(CATEGORY, metricsSystem, "test", "Test help", 3, List.of()); + final MetricsQuantileHistogram histogram = + MetricsQuantileHistogram.create(CATEGORY, metricsSystem, "test", "Test help", 3, List.of()); for (int i = 1; i <= 100; i++) { histogram.recordValue(i); @@ -66,8 +66,9 @@ void shouldReportValuesWithNoSpecifiedUpperLimit() { @Test void shouldReportValuesWithUpperLimit() { - final MetricsHistogram histogram = - MetricsHistogram.create(CATEGORY, metricsSystem, "test", "Test help", 2, 80, List.of()); + final MetricsQuantileHistogram histogram = + MetricsQuantileHistogram.create( + CATEGORY, metricsSystem, "test", "Test help", 2, 80, List.of()); for (int i = 1; i <= 100; i++) { histogram.recordValue(i); @@ -87,7 +88,7 @@ void shouldReportValuesWithUpperLimit() { private static List key(final List labelValues) { final List key = new ArrayList<>(); - key.add(MetricsHistogram.QUANTILE_LABEL); + key.add(MetricsQuantileHistogram.QUANTILE_LABEL); key.addAll(labelValues); return key; } diff --git a/infrastructure/unsigned/src/main/java/tech/pegasys/teku/infrastructure/unsigned/UInt64.java b/infrastructure/unsigned/src/main/java/tech/pegasys/teku/infrastructure/unsigned/UInt64.java index 6742f5e03de..a5073f6a004 100644 --- a/infrastructure/unsigned/src/main/java/tech/pegasys/teku/infrastructure/unsigned/UInt64.java +++ b/infrastructure/unsigned/src/main/java/tech/pegasys/teku/infrastructure/unsigned/UInt64.java @@ -125,6 +125,19 @@ public UInt64 decrement() { return minus(value, 1); } + /** + * Decrement this value by one and return the result. If trying to decrement from ZERO, still + * returns the ZERO + * + * @return The result of decrementing this value by 1. + */ + public UInt64 safeDecrement() { + if (value == 0L) { + return ZERO; + } + return minus(value, 1); + } + /** * Return the result of adding this value and the specified one. * diff --git a/infrastructure/unsigned/src/test/java/tech/pegasys/teku/infrastructure/unsigned/UInt64Test.java b/infrastructure/unsigned/src/test/java/tech/pegasys/teku/infrastructure/unsigned/UInt64Test.java index 8dd01dc4a91..61d08f4e889 100644 --- a/infrastructure/unsigned/src/test/java/tech/pegasys/teku/infrastructure/unsigned/UInt64Test.java +++ b/infrastructure/unsigned/src/test/java/tech/pegasys/teku/infrastructure/unsigned/UInt64Test.java @@ -343,6 +343,17 @@ void decrement_shouldThrowArithmeticExceptionWhenResultUnderflows() { assertThatThrownBy(UInt64.ZERO::decrement).isInstanceOf(ArithmeticException.class); } + @Test + void safeDecrement() { + assertThat(UInt64.ONE.safeDecrement()).isEqualTo(UInt64.ZERO); + assertThat(UInt64.valueOf(3).safeDecrement().safeDecrement()).isEqualTo(UInt64.ONE); + } + + @Test + void safeDecrement_shouldNotUnderflows() { + assertThat(UInt64.ZERO.safeDecrement()).isEqualTo(UInt64.ZERO); + } + @ParameterizedTest @MethodSource("additionNumbers") void subtract_shouldSubtractWhenNotOverflowing( diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetwork.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetwork.java index fa0383b95fd..9631083b788 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetwork.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetwork.java @@ -29,6 +29,7 @@ import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.networking.eth2.gossip.BlobSidecarGossipChannel; import tech.pegasys.teku.networking.eth2.gossip.BlockGossipChannel; +import tech.pegasys.teku.networking.eth2.gossip.DataColumnSidecarGossipChannel; import tech.pegasys.teku.networking.eth2.gossip.config.Eth2Context; import tech.pegasys.teku.networking.eth2.gossip.config.GossipConfigurator; import tech.pegasys.teku.networking.eth2.gossip.encoding.GossipEncoding; @@ -42,6 +43,7 @@ import tech.pegasys.teku.networking.p2p.peer.NodeId; import tech.pegasys.teku.networking.p2p.peer.PeerConnectedSubscriber; import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessage; import tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing; import tech.pegasys.teku.spec.datastructures.operations.ProposerSlashing; @@ -68,8 +70,10 @@ public class ActiveEth2P2PNetwork extends DelegatingP2PNetwork impleme private final GossipConfigurator gossipConfigurator; private final SubnetSubscriptionService attestationSubnetService; private final SubnetSubscriptionService syncCommitteeSubnetService; + private final SubnetSubscriptionService dataColumnSidecarSubnetService; private final ProcessedAttestationSubscriptionProvider processedAttestationSubscriptionProvider; private final AtomicBoolean gossipStarted = new AtomicBoolean(false); + private final int dasTotalCustodySubnetCount; private final GossipForkManager gossipForkManager; @@ -90,9 +94,11 @@ public ActiveEth2P2PNetwork( final RecentChainData recentChainData, final SubnetSubscriptionService attestationSubnetService, final SubnetSubscriptionService syncCommitteeSubnetService, + final SubnetSubscriptionService dataColumnSidecarSubnetService, final GossipEncoding gossipEncoding, final GossipConfigurator gossipConfigurator, final ProcessedAttestationSubscriptionProvider processedAttestationSubscriptionProvider, + final int dasTotalCustodySubnetCount, final boolean allTopicsFilterEnabled) { super(discoveryNetwork); this.spec = spec; @@ -106,7 +112,9 @@ public ActiveEth2P2PNetwork( this.gossipConfigurator = gossipConfigurator; this.attestationSubnetService = attestationSubnetService; this.syncCommitteeSubnetService = syncCommitteeSubnetService; + this.dataColumnSidecarSubnetService = dataColumnSidecarSubnetService; this.processedAttestationSubscriptionProvider = processedAttestationSubscriptionProvider; + this.dasTotalCustodySubnetCount = dasTotalCustodySubnetCount; this.allTopicsFilterEnabled = allTopicsFilterEnabled; } @@ -130,6 +138,9 @@ private synchronized void startup() { processedAttestationSubscriptionProvider.subscribe(gossipForkManager::publishAttestation); eventChannels.subscribe(BlockGossipChannel.class, gossipForkManager::publishBlock); eventChannels.subscribe(BlobSidecarGossipChannel.class, gossipForkManager::publishBlobSidecar); + eventChannels.subscribe( + DataColumnSidecarGossipChannel.class, + (sidecar, __) -> gossipForkManager.publishDataColumnSidecar(sidecar)); if (recentChainData.isCloseToInSync()) { startGossip(); } @@ -158,6 +169,10 @@ private synchronized void startGossip() { discoveryNetworkSyncCommitteeSubnetsSubscription = syncCommitteeSubnetService.subscribeToUpdates( discoveryNetwork::setSyncCommitteeSubnetSubscriptions); + if (spec.isMilestoneSupported(SpecMilestone.FULU)) { + LOG.info("Using custody sidecar subnets count: {}", dasTotalCustodySubnetCount); + discoveryNetwork.setDASTotalCustodySubnetCount(dasTotalCustodySubnetCount); + } gossipForkManager.configureGossipForEpoch(recentChainData.getCurrentEpoch().orElseThrow()); if (allTopicsFilterEnabled) { @@ -324,6 +339,18 @@ public void unsubscribeFromSyncCommitteeSubnetId(final int subnetId) { syncCommitteeSubnetService.removeSubscription(subnetId); } + @Override + public void subscribeToDataColumnSidecarSubnetId(final int subnetId) { + gossipForkManager.subscribeToDataColumnSidecarSubnetId(subnetId); + dataColumnSidecarSubnetService.addSubscription(subnetId); + } + + @Override + public void unsubscribeFromDataColumnSidecarSubnetId(final int subnetId) { + gossipForkManager.unsubscribeFromDataColumnSidecarSubnetId(subnetId); + dataColumnSidecarSubnetService.removeSubscription(subnetId); + } + @Override public MetadataMessage getMetadata() { return peerManager.getMetadataMessage(); diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetwork.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetwork.java index bb57c425891..8d1b926bb79 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetwork.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetwork.java @@ -40,6 +40,10 @@ public interface Eth2P2PNetwork extends P2PNetwork { void unsubscribeFromSyncCommitteeSubnetId(int subnetId); + void subscribeToDataColumnSidecarSubnetId(int subnetId); + + void unsubscribeFromDataColumnSidecarSubnetId(int subnetId); + MetadataMessage getMetadata(); void publishSyncCommitteeMessage(ValidatableSyncCommitteeMessage message); 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 308f551ad6e..4c7bb0db118 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 @@ -30,6 +30,7 @@ import tech.pegasys.teku.infrastructure.metrics.SettableLabelledGauge; import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; 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.gossip.encoding.GossipEncoding; import tech.pegasys.teku.networking.eth2.gossip.forks.GossipForkManager; @@ -42,6 +43,8 @@ import tech.pegasys.teku.networking.eth2.gossip.forks.versions.GossipForkSubscriptionsFulu; import tech.pegasys.teku.networking.eth2.gossip.forks.versions.GossipForkSubscriptionsPhase0; import tech.pegasys.teku.networking.eth2.gossip.subnets.AttestationSubnetTopicProvider; +import tech.pegasys.teku.networking.eth2.gossip.subnets.DataColumnSidecarSubnetTopicProvider; +import tech.pegasys.teku.networking.eth2.gossip.subnets.NodeIdToDataColumnSidecarSubnetsCalculator; import tech.pegasys.teku.networking.eth2.gossip.subnets.PeerSubnetSubscriptions; import tech.pegasys.teku.networking.eth2.gossip.subnets.SyncCommitteeSubnetTopicProvider; import tech.pegasys.teku.networking.eth2.gossip.topics.Eth2GossipTopicFilter; @@ -51,6 +54,7 @@ import tech.pegasys.teku.networking.eth2.peers.Eth2PeerManager; import tech.pegasys.teku.networking.eth2.peers.Eth2PeerSelectionStrategy; import tech.pegasys.teku.networking.eth2.peers.LibP2PDiscoveryNodeIdExtractor; +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.encodings.RpcEncoding; import tech.pegasys.teku.networking.p2p.connection.PeerPools; @@ -70,9 +74,11 @@ import tech.pegasys.teku.networking.p2p.reputation.ReputationManager; 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.Constants; import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation; 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.operations.AttesterSlashing; import tech.pegasys.teku.spec.datastructures.operations.ProposerSlashing; @@ -83,6 +89,9 @@ import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.spec.datastructures.util.ForkAndSpecMilestone; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsSupplier; +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.util.DebugDataDumper; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.store.KeyValueStore; @@ -100,6 +109,9 @@ public class Eth2P2PNetworkBuilder { protected P2PConfig config; protected EventChannels eventChannels; protected CombinedChainDataClient combinedChainDataClient; + protected DataColumnSidecarByRootCustody dataColumnSidecarCustody; + protected CustodyGroupCountManager custodyGroupCountManager; + protected MetadataMessagesFactory metadataMessagesFactory; protected OperationProcessor gossipedBlockProcessor; protected OperationProcessor gossipedBlobSidecarProcessor; protected OperationProcessor gossipedAttestationConsumer; @@ -125,10 +137,14 @@ public class Eth2P2PNetworkBuilder { gossipedSignedContributionAndProofProcessor; protected OperationProcessor gossipedSyncCommitteeMessageProcessor; + protected OperationProcessor dataColumnSidecarOperationProcessor; protected StatusMessageFactory statusMessageFactory; protected KZG kzg; protected boolean recordMessageArrival; protected DebugDataDumper debugDataDumper; + private DasGossipLogger dasGossipLogger; + + // private DasReqRespLogger dasReqRespLogger; protected Eth2P2PNetworkBuilder() {} @@ -142,17 +158,29 @@ public Eth2P2PNetwork build() { // Setup eth2 handlers final SubnetSubscriptionService attestationSubnetService = new SubnetSubscriptionService(); final SubnetSubscriptionService syncCommitteeSubnetService = new SubnetSubscriptionService(); - + final SubnetSubscriptionService dataColumnSidecarSubnetService = + new SubnetSubscriptionService(); final DiscoveryNodeIdExtractor discoveryNodeIdExtractor = new LibP2PDiscoveryNodeIdExtractor(); final RpcEncoding rpcEncoding = RpcEncoding.createSszSnappyEncoding(spec.getNetworkingConfig().getMaxPayloadSize()); if (statusMessageFactory == null) { statusMessageFactory = new StatusMessageFactory(combinedChainDataClient.getRecentChainData()); } + + final Optional dasTotalCustodyGroupCount = + spec.isMilestoneSupported(SpecMilestone.FULU) + ? Optional.of( + UInt64.valueOf( + config.getTotalCustodyGroupCount(spec.forMilestone(SpecMilestone.FULU)))) + : Optional.empty(); + final Eth2PeerManager eth2PeerManager = Eth2PeerManager.create( asyncRunner, combinedChainDataClient, + dataColumnSidecarCustody, + custodyGroupCountManager, + metadataMessagesFactory, metricsSystem, attestationSubnetService, syncCommitteeSubnetService, @@ -168,7 +196,8 @@ public Eth2P2PNetwork build() { config.getPeerRequestLimit(), spec, kzg, - discoveryNodeIdExtractor); + discoveryNodeIdExtractor, + dasTotalCustodyGroupCount); final Collection> eth2RpcMethods = eth2PeerManager.getBeaconChainMethods().all(); rpcMethods.addAll(eth2RpcMethods); @@ -176,7 +205,8 @@ public Eth2P2PNetwork build() { final GossipEncoding gossipEncoding = config.getGossipEncoding(); // Build core network and inject eth2 handlers - final DiscoveryNetwork network = buildNetwork(gossipEncoding, syncCommitteeSubnetService); + final DiscoveryNetwork network = + buildNetwork(gossipEncoding, syncCommitteeSubnetService, dataColumnSidecarSubnetService); final GossipForkManager gossipForkManager = buildGossipForkManager(gossipEncoding, network); @@ -190,9 +220,11 @@ public Eth2P2PNetwork build() { combinedChainDataClient.getRecentChainData(), attestationSubnetService, syncCommitteeSubnetService, + dataColumnSidecarSubnetService, gossipEncoding, config.getGossipConfigurator(), processedAttestationSubscriptionProvider, + dasTotalCustodyGroupCount.orElse(UInt64.ZERO).intValue(), config.isAllTopicsFilterEnabled()); } @@ -207,6 +239,7 @@ private GossipForkManager buildGossipForkManager( forkAndSpecMilestone -> createSubscriptions(forkAndSpecMilestone, network, gossipEncoding)) .forEach(gossipForkManagerBuilder::fork); + return gossipForkManagerBuilder.build(); } @@ -345,13 +378,16 @@ private GossipForkSubscriptions createSubscriptions( gossipedSignedContributionAndProofProcessor, gossipedSyncCommitteeMessageProcessor, gossipedSignedBlsToExecutionChangeProcessor, - debugDataDumper); + dataColumnSidecarOperationProcessor, + debugDataDumper, + dasGossipLogger); }; } protected DiscoveryNetwork buildNetwork( final GossipEncoding gossipEncoding, - final SubnetSubscriptionService syncCommitteeSubnetService) { + final SubnetSubscriptionService syncCommitteeSubnetService, + final SubnetSubscriptionService dataColumnSidecarSubnetService) { final PeerPools peerPools = new PeerPools(); final ReputationManager reputationManager = new DefaultReputationManager( @@ -388,6 +424,9 @@ protected DiscoveryNetwork buildNetwork( final SyncCommitteeSubnetTopicProvider syncCommitteeSubnetTopicProvider = new SyncCommitteeSubnetTopicProvider( combinedChainDataClient.getRecentChainData(), gossipEncoding); + final DataColumnSidecarSubnetTopicProvider dataColumnSidecarSubnetTopicProvider = + new DataColumnSidecarSubnetTopicProvider( + combinedChainDataClient.getRecentChainData(), gossipEncoding); final TargetPeerRange targetPeerRange = new TargetPeerRange( @@ -396,6 +435,11 @@ protected DiscoveryNetwork buildNetwork( discoConfig.getMinRandomlySelectedPeers()); final SchemaDefinitionsSupplier currentSchemaDefinitions = () -> combinedChainDataClient.getRecentChainData().getCurrentSpec().getSchemaDefinitions(); + + final NodeIdToDataColumnSidecarSubnetsCalculator nodeIdToDataColumnSidecarSubnetsCalculator = + NodeIdToDataColumnSidecarSubnetsCalculator.create( + spec, () -> combinedChainDataClient.getRecentChainData().getCurrentSlot()); + final SettableLabelledGauge subnetPeerCountGauge = SettableLabelledGauge.create( metricsSystem, @@ -414,11 +458,14 @@ protected DiscoveryNetwork buildNetwork( targetPeerRange, network -> PeerSubnetSubscriptions.create( - currentSchemaDefinitions, + combinedChainDataClient.getRecentChainData().getCurrentSpec(), + nodeIdToDataColumnSidecarSubnetsCalculator, network, attestationSubnetTopicProvider, syncCommitteeSubnetTopicProvider, syncCommitteeSubnetService, + dataColumnSidecarSubnetTopicProvider, + dataColumnSidecarSubnetService, config.getTargetSubnetSubscriberCount(), subnetPeerCountGauge), reputationManager, @@ -443,6 +490,9 @@ private void validate() { assertNotNull("eventChannels", eventChannels); assertNotNull("metricsSystem", metricsSystem); assertNotNull("combinedChainDataClient", combinedChainDataClient); + assertNotNull("dataColumnSidecarCustody", dataColumnSidecarCustody); + assertNotNull("custodyGroupCountManager", custodyGroupCountManager); + assertNotNull("metadataMessagesFactory", metadataMessagesFactory); assertNotNull("keyValueStore", keyValueStore); assertNotNull("timeProvider", timeProvider); assertNotNull("gossipedBlockProcessor", gossipedBlockProcessor); @@ -482,6 +532,27 @@ public Eth2P2PNetworkBuilder combinedChainDataClient( return this; } + public Eth2P2PNetworkBuilder dataColumnSidecarCustody( + final DataColumnSidecarByRootCustody dataColumnSidecarCustody) { + checkNotNull(dataColumnSidecarCustody); + this.dataColumnSidecarCustody = dataColumnSidecarCustody; + return this; + } + + public Eth2P2PNetworkBuilder custodyGroupCountManager( + final CustodyGroupCountManager custodyGroupCountManager) { + checkNotNull(custodyGroupCountManager); + this.custodyGroupCountManager = custodyGroupCountManager; + return this; + } + + public Eth2P2PNetworkBuilder metadataMessagesFactory( + final MetadataMessagesFactory metadataMessagesFactory) { + checkNotNull(metadataMessagesFactory); + this.metadataMessagesFactory = metadataMessagesFactory; + return this; + } + public Eth2P2PNetworkBuilder keyValueStore(final KeyValueStore kvStore) { checkNotNull(kvStore); this.keyValueStore = kvStore; @@ -568,6 +639,13 @@ public Eth2P2PNetworkBuilder gossipedSignedBlsToExecutionChangeProcessor( return this; } + public Eth2P2PNetworkBuilder gossipedDataColumnSidecarOperationProcessor( + final OperationProcessor dataColumnSidecarOperationProcessor) { + checkNotNull(dataColumnSidecarOperationProcessor); + this.dataColumnSidecarOperationProcessor = dataColumnSidecarOperationProcessor; + return this; + } + public Eth2P2PNetworkBuilder metricsSystem(final MetricsSystem metricsSystem) { checkNotNull(metricsSystem); this.metricsSystem = metricsSystem; @@ -644,4 +722,9 @@ public Eth2P2PNetworkBuilder p2pDebugDataDumper(final DebugDataDumper debugDataD this.debugDataDumper = debugDataDumper; return this; } + + public Eth2P2PNetworkBuilder gossipDasLogger(final DasGossipLogger dasGossipLogger) { + this.dasGossipLogger = dasGossipLogger; + return this; + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/P2PConfig.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/P2PConfig.java index 33aabeeba2f..e0de990773b 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/P2PConfig.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/P2PConfig.java @@ -26,8 +26,11 @@ import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig; import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecVersion; import tech.pegasys.teku.spec.config.NetworkingSpecConfig; import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.config.SpecConfigFulu; +import tech.pegasys.teku.spec.logic.common.helpers.MathHelpers; public class P2PConfig { @@ -47,6 +50,7 @@ public class P2PConfig { public static final int DEFAULT_BATCH_VERIFY_QUEUE_CAPACITY = 30_000; public static final int DEFAULT_BATCH_VERIFY_MAX_BATCH_SIZE = 250; public static final boolean DEFAULT_BATCH_VERIFY_STRICT_THREAD_LIMIT_ENABLED = false; + public static final int DEFAULT_DAS_EXTRA_CUSTODY_GROUP_COUNT = 0; private final Spec spec; private final NetworkConfig networkConfig; @@ -57,6 +61,7 @@ public class P2PConfig { private final GossipEncoding gossipEncoding; private final int targetSubnetSubscriberCount; private final boolean subscribeAllSubnetsEnabled; + private final int dasExtraCustodyGroupCount; private final int peerBlocksRateLimit; private final int peerBlobSidecarsRateLimit; private final int peerRequestLimit; @@ -75,6 +80,7 @@ private P2PConfig( final GossipEncoding gossipEncoding, final int targetSubnetSubscriberCount, final boolean subscribeAllSubnetsEnabled, + final int dasExtraCustodyGroupCount, final int peerBlocksRateLimit, final int peerBlobSidecarsRateLimit, final int peerRequestLimit, @@ -91,6 +97,7 @@ private P2PConfig( this.gossipEncoding = gossipEncoding; this.targetSubnetSubscriberCount = targetSubnetSubscriberCount; this.subscribeAllSubnetsEnabled = subscribeAllSubnetsEnabled; + this.dasExtraCustodyGroupCount = dasExtraCustodyGroupCount; this.peerBlocksRateLimit = peerBlocksRateLimit; this.peerBlobSidecarsRateLimit = peerBlobSidecarsRateLimit; this.peerRequestLimit = peerRequestLimit; @@ -135,6 +142,15 @@ public boolean isSubscribeAllSubnetsEnabled() { return subscribeAllSubnetsEnabled; } + public int getTotalCustodyGroupCount(final SpecVersion specVersion) { + final SpecConfigFulu specConfig = SpecConfigFulu.required(specVersion.getConfig()); + final int minCustodyGroupRequirement = specConfig.getCustodyRequirement(); + final int maxGroups = specConfig.getNumberOfCustodyGroups(); + return Integer.min( + maxGroups, + MathHelpers.intPlusMaxIntCapped(minCustodyGroupRequirement, dasExtraCustodyGroupCount)); + } + public int getPeerBlocksRateLimit() { return peerBlocksRateLimit; } @@ -184,6 +200,8 @@ public static class Builder { private final GossipEncoding gossipEncoding = GossipEncoding.SSZ_SNAPPY; private Integer targetSubnetSubscriberCount = DEFAULT_P2P_TARGET_SUBNET_SUBSCRIBER_COUNT; private Boolean subscribeAllSubnetsEnabled = DEFAULT_SUBSCRIBE_ALL_SUBNETS_ENABLED; + private Boolean subscribeAllCustodySubnetsEnabled = DEFAULT_SUBSCRIBE_ALL_SUBNETS_ENABLED; + private int dasExtraCustodyGroupCount = DEFAULT_DAS_EXTRA_CUSTODY_GROUP_COUNT; private Integer peerBlocksRateLimit = DEFAULT_PEER_BLOCKS_RATE_LIMIT; private Integer peerBlobSidecarsRateLimit = DEFAULT_PEER_BLOB_SIDECARS_RATE_LIMIT; private Integer peerRequestLimit = DEFAULT_PEER_REQUEST_LIMIT; @@ -228,6 +246,10 @@ public P2PConfig build() { discoveryConfig.advertisedUdpPortIpv6Default( OptionalInt.of(networkConfig.getAdvertisedPortIpv6())); + if (subscribeAllCustodySubnetsEnabled) { + dasExtraCustodyGroupCount = Integer.MAX_VALUE; + } + return new P2PConfig( spec, networkConfig, @@ -236,6 +258,7 @@ public P2PConfig build() { gossipEncoding, targetSubnetSubscriberCount, subscribeAllSubnetsEnabled, + dasExtraCustodyGroupCount, peerBlocksRateLimit, peerBlobSidecarsRateLimit, peerRequestLimit, @@ -289,6 +312,18 @@ public Builder subscribeAllSubnetsEnabled(final Boolean subscribeAllSubnetsEnabl return this; } + public Builder dasExtraCustodyGroupCount(final int dasExtraCustodyGroupCount) { + this.dasExtraCustodyGroupCount = dasExtraCustodyGroupCount; + return this; + } + + public Builder subscribeAllCustodySubnetsEnabled( + final Boolean subscribeAllCustodySubnetsEnabled) { + checkNotNull(subscribeAllCustodySubnetsEnabled); + this.subscribeAllCustodySubnetsEnabled = subscribeAllCustodySubnetsEnabled; + return this; + } + public Builder peerBlocksRateLimit(final Integer peerBlocksRateLimit) { checkNotNull(peerBlocksRateLimit); if (peerBlocksRateLimit < 0) { diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipChannel.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipChannel.java new file mode 100644 index 00000000000..0ec93423258 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipChannel.java @@ -0,0 +1,32 @@ +/* + * 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.gossip; + +import java.util.List; +import tech.pegasys.teku.infrastructure.events.VoidReturningChannelInterface; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.statetransition.blobs.RemoteOrigin; + +public interface DataColumnSidecarGossipChannel extends VoidReturningChannelInterface { + + DataColumnSidecarGossipChannel NOOP = (dataColumnSidecar, origin) -> {}; + + default void publishDataColumnSidecars( + final List dataColumnSidecars, final RemoteOrigin origin) { + dataColumnSidecars.forEach( + dataColumnSidecar -> publishDataColumnSidecar(dataColumnSidecar, origin)); + } + + void publishDataColumnSidecar(DataColumnSidecar dataColumnSidecar, RemoteOrigin remoteOrigin); +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipManager.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipManager.java new file mode 100644 index 00000000000..45e5cf11cd0 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipManager.java @@ -0,0 +1,59 @@ +/* + * 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.gossip; + +import java.util.Optional; +import tech.pegasys.teku.networking.eth2.gossip.subnets.DataColumnSidecarSubnetSubscriptions; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; + +public class DataColumnSidecarGossipManager implements GossipManager { + private final DataColumnSidecarSubnetSubscriptions subnetSubscriptions; + private final DasGossipLogger dasGossipLogger; + + public DataColumnSidecarGossipManager( + final DataColumnSidecarSubnetSubscriptions dataColumnSidecarSubnetSubscriptions, + final DasGossipLogger dasGossipLogger) { + subnetSubscriptions = dataColumnSidecarSubnetSubscriptions; + this.dasGossipLogger = dasGossipLogger; + } + + public void publish(final DataColumnSidecar dataColumnSidecar) { + subnetSubscriptions + .gossip(dataColumnSidecar) + .finish( + __ -> dasGossipLogger.onPublish(dataColumnSidecar, Optional.empty()), + error -> dasGossipLogger.onPublish(dataColumnSidecar, Optional.of(error))); + } + + public void subscribeToSubnetId(final int subnetId) { + subnetSubscriptions.subscribeToSubnetId(subnetId); + dasGossipLogger.onDataColumnSubnetSubscribe(subnetId); + } + + public void unsubscribeFromSubnetId(final int subnetId) { + subnetSubscriptions.unsubscribeFromSubnetId(subnetId); + dasGossipLogger.onDataColumnSubnetUnsubscribe(subnetId); + } + + @Override + public void subscribe() { + subnetSubscriptions.subscribe(); + } + + @Override + public void unsubscribe() { + subnetSubscriptions.unsubscribe(); + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManager.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManager.java index c64a3a027cf..a91b37c2c1e 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManager.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManager.java @@ -36,6 +36,7 @@ import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation; 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.operations.AttesterSlashing; import tech.pegasys.teku.spec.datastructures.operations.ProposerSlashing; @@ -63,6 +64,7 @@ public class GossipForkManager { private final Set activeSubscriptions = new HashSet<>(); private final IntSet currentAttestationSubnets = new IntOpenHashSet(); private final IntSet currentSyncCommitteeSubnets = new IntOpenHashSet(); + private final IntSet currentDataColumnSidecarSubnets = new IntOpenHashSet(); private Optional currentEpoch = Optional.empty(); private boolean isHeadOptimistic; @@ -178,6 +180,14 @@ public SafeFuture publishBlobSidecar(final BlobSidecar blobSidecar) { GossipForkSubscriptions::publishBlobSidecar); } + public synchronized void publishDataColumnSidecar(final DataColumnSidecar dataColumnSidecar) { + publishMessage( + dataColumnSidecar.getSlot(), + dataColumnSidecar, + "data column sidecar", + GossipForkSubscriptions::publishDataColumnSidecar); + } + public void publishSyncCommitteeMessage(final ValidatableSyncCommitteeMessage message) { publishMessage( message.getSlot(), @@ -293,6 +303,20 @@ public synchronized void unsubscribeFromSyncCommitteeSubnetId(final int subnetId } } + public void subscribeToDataColumnSidecarSubnetId(final int subnetId) { + if (currentDataColumnSidecarSubnets.add(subnetId)) { + activeSubscriptions.forEach( + subscription -> subscription.subscribeToDataColumnSidecarSubnet(subnetId)); + } + } + + public void unsubscribeFromDataColumnSidecarSubnetId(final int subnetId) { + if (currentDataColumnSidecarSubnets.remove(subnetId)) { + activeSubscriptions.forEach( + subscription -> subscription.unsubscribeFromDataColumnSidecarSubnet(subnetId)); + } + } + private boolean isActive(final GossipForkSubscriptions subscriptions) { return activeSubscriptions.contains(subscriptions); } @@ -304,6 +328,7 @@ private void startSubscriptions(final GossipForkSubscriptions subscription) { isHeadOptimistic); currentAttestationSubnets.forEach(subscription::subscribeToAttestationSubnetId); currentSyncCommitteeSubnets.forEach(subscription::subscribeToSyncCommitteeSubnet); + currentDataColumnSidecarSubnets.forEach(subscription::subscribeToDataColumnSidecarSubnet); } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkSubscriptions.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkSubscriptions.java index 907fc4aa6ef..d608a6fcf1b 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkSubscriptions.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkSubscriptions.java @@ -18,6 +18,7 @@ import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation; 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.operations.AttesterSlashing; import tech.pegasys.teku.spec.datastructures.operations.ProposerSlashing; @@ -25,6 +26,7 @@ import tech.pegasys.teku.spec.datastructures.operations.SignedVoluntaryExit; import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SignedContributionAndProof; import tech.pegasys.teku.spec.datastructures.operations.versions.altair.ValidatableSyncCommitteeMessage; +import tech.pegasys.teku.spec.datastructures.state.ForkInfo; public interface GossipForkSubscriptions { @@ -48,6 +50,8 @@ default SafeFuture publishBlobSidecar(final BlobSidecar blobSidecar) { void unsubscribeFromAttestationSubnetId(int subnetId); + default void addBlobSidecarGossipManager(final ForkInfo forkInfo) {} + default void publishSyncCommitteeMessage(final ValidatableSyncCommitteeMessage message) { // since Altair } @@ -71,4 +75,16 @@ default void unsubscribeFromSyncCommitteeSubnet(final int subnetId) { } default void publishSignedBlsToExecutionChangeMessage(final SignedBlsToExecutionChange message) {} + + default void publishDataColumnSidecar(final DataColumnSidecar blobSidecar) { + // since Fulu + } + + default void subscribeToDataColumnSidecarSubnet(final int subnetId) { + // since Fulu + } + + default void unsubscribeFromDataColumnSidecarSubnet(final int subnetId) { + // since Fulu + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsDeneb.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsDeneb.java index a1cf1ce5872..56ea7c6e3cb 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsDeneb.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsDeneb.java @@ -90,7 +90,8 @@ protected void addGossipManagers(final ForkInfo forkInfo) { addBlobSidecarGossipManager(forkInfo); } - void addBlobSidecarGossipManager(final ForkInfo forkInfo) { + @Override + public void addBlobSidecarGossipManager(final ForkInfo forkInfo) { blobSidecarGossipManager = BlobSidecarGossipManager.create( recentChainData, diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsFulu.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsFulu.java index 998ecfdf91b..e0f5810f23f 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsFulu.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsFulu.java @@ -15,12 +15,16 @@ import org.hyperledger.besu.plugin.services.MetricsSystem; import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.networking.eth2.gossip.DataColumnSidecarGossipManager; import tech.pegasys.teku.networking.eth2.gossip.encoding.GossipEncoding; +import tech.pegasys.teku.networking.eth2.gossip.subnets.DataColumnSidecarSubnetSubscriptions; import tech.pegasys.teku.networking.eth2.gossip.topics.OperationProcessor; import tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation; 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.operations.AttesterSlashing; import tech.pegasys.teku.spec.datastructures.operations.ProposerSlashing; @@ -29,11 +33,18 @@ import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SignedContributionAndProof; import tech.pegasys.teku.spec.datastructures.operations.versions.altair.ValidatableSyncCommitteeMessage; import tech.pegasys.teku.spec.datastructures.state.Fork; +import tech.pegasys.teku.spec.datastructures.state.ForkInfo; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; import tech.pegasys.teku.statetransition.util.DebugDataDumper; import tech.pegasys.teku.storage.client.RecentChainData; +// Capella because we don't need blobSidecar subscriptions public class GossipForkSubscriptionsFulu extends GossipForkSubscriptionsElectra { + private final OperationProcessor dataColumnSidecarOperationProcessor; + private DataColumnSidecarGossipManager dataColumnSidecarGossipManager; + public DasGossipLogger dasGossipLogger; + public GossipForkSubscriptionsFulu( final Fork fork, final Spec spec, @@ -55,7 +66,9 @@ public GossipForkSubscriptionsFulu( syncCommitteeMessageOperationProcessor, final OperationProcessor signedBlsToExecutionChangeOperationProcessor, - final DebugDataDumper debugDataDumper) { + final OperationProcessor dataColumnSidecarOperationProcessor, + final DebugDataDumper debugDataDumper, + final DasGossipLogger dasGossipLogger) { super( fork, spec, @@ -75,5 +88,57 @@ public GossipForkSubscriptionsFulu( syncCommitteeMessageOperationProcessor, signedBlsToExecutionChangeOperationProcessor, debugDataDumper); + this.dataColumnSidecarOperationProcessor = dataColumnSidecarOperationProcessor; + this.dasGossipLogger = dasGossipLogger; + } + + @Override + protected void addGossipManagers(final ForkInfo forkInfo) { + super.addGossipManagers(forkInfo); + addDataColumnSidecarGossipManager(forkInfo); + } + + void addDataColumnSidecarGossipManager(final ForkInfo forkInfo) { + final DataColumnSidecarSubnetSubscriptions dataColumnSidecarSubnetSubscriptions = + new DataColumnSidecarSubnetSubscriptions( + spec, + asyncRunner, + discoveryNetwork, + gossipEncoding, + recentChainData, + dataColumnSidecarOperationProcessor, + debugDataDumper, + forkInfo); + + this.dataColumnSidecarGossipManager = + new DataColumnSidecarGossipManager(dataColumnSidecarSubnetSubscriptions, dasGossipLogger); + + addGossipManager(dataColumnSidecarGossipManager); + } + + @Override + public void addBlobSidecarGossipManager(final ForkInfo forkInfo) { + // Do nothing + } + + @Override + public SafeFuture publishBlobSidecar(final BlobSidecar blobSidecar) { + // Do nothing + return SafeFuture.COMPLETE; + } + + @Override + public void publishDataColumnSidecar(final DataColumnSidecar blobSidecar) { + dataColumnSidecarGossipManager.publish(blobSidecar); + } + + @Override + public void subscribeToDataColumnSidecarSubnet(final int subnetId) { + dataColumnSidecarGossipManager.subscribeToSubnetId(subnetId); + } + + @Override + public void unsubscribeFromDataColumnSidecarSubnet(final int subnetId) { + dataColumnSidecarGossipManager.unsubscribeFromSubnetId(subnetId); } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetBackboneSubscriber.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetBackboneSubscriber.java new file mode 100644 index 00000000000..32d1b860a79 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetBackboneSubscriber.java @@ -0,0 +1,97 @@ +/* + * 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.gossip.subnets; + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.ethereum.events.SlotEventsChannel; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.networking.eth2.Eth2P2PNetwork; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.statetransition.CustodyGroupCountChannel; + +public class DataColumnSidecarSubnetBackboneSubscriber + implements SlotEventsChannel, CustodyGroupCountChannel { + private final Eth2P2PNetwork eth2P2PNetwork; + private final UInt256 nodeId; + private final AtomicInteger totalGroupCount; + private final Spec spec; + + private IntSet currentSubscribedSubnets = IntSet.of(); + private UInt64 lastEpoch = UInt64.MAX_VALUE; + + public DataColumnSidecarSubnetBackboneSubscriber( + final Spec spec, + final Eth2P2PNetwork eth2P2PNetwork, + final UInt256 nodeId, + final int totalGroupCount) { + this.spec = spec; + this.eth2P2PNetwork = eth2P2PNetwork; + this.nodeId = nodeId; + this.totalGroupCount = new AtomicInteger(totalGroupCount); + } + + @Override + public void onCustodyGroupCountUpdate(final int groupCount) { + totalGroupCount.set(groupCount); + } + + @Override + public void onCustodyGroupCountSynced(final int groupCount) {} + + private void subscribeToSubnets(final Collection newSubscriptions) { + + IntOpenHashSet newSubscriptionsSet = new IntOpenHashSet(newSubscriptions); + + for (int oldSubnet : currentSubscribedSubnets) { + if (!newSubscriptionsSet.contains(oldSubnet)) { + eth2P2PNetwork.unsubscribeFromDataColumnSidecarSubnetId(oldSubnet); + } + } + + for (int newSubnet : newSubscriptionsSet) { + if (!currentSubscribedSubnets.contains(newSubnet)) { + eth2P2PNetwork.subscribeToDataColumnSidecarSubnetId(newSubnet); + } + } + + currentSubscribedSubnets = newSubscriptionsSet; + } + + private void onEpoch(final UInt64 epoch) { + spec.atEpoch(epoch) + .miscHelpers() + .toVersionFulu() + .ifPresent( + miscHelpersFulu -> { + List subnets = + miscHelpersFulu.computeDataColumnSidecarBackboneSubnets( + nodeId, epoch, totalGroupCount.get()); + subscribeToSubnets(subnets.stream().map(UInt64::intValue).toList()); + }); + } + + @Override + public synchronized void onSlot(final UInt64 slot) { + UInt64 epoch = spec.computeEpochAtSlot(slot); + if (!epoch.equals(lastEpoch)) { + lastEpoch = epoch; + onEpoch(epoch); + } + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetSubscriptions.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetSubscriptions.java new file mode 100644 index 00000000000..ee1b74b4648 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetSubscriptions.java @@ -0,0 +1,106 @@ +/* + * 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.gossip.subnets; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Optional; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.networking.eth2.gossip.encoding.GossipEncoding; +import tech.pegasys.teku.networking.eth2.gossip.topics.GossipTopicName; +import tech.pegasys.teku.networking.eth2.gossip.topics.GossipTopics; +import tech.pegasys.teku.networking.eth2.gossip.topics.OperationProcessor; +import tech.pegasys.teku.networking.eth2.gossip.topics.topichandlers.DataColumnSidecarTopicHandler; +import tech.pegasys.teku.networking.eth2.gossip.topics.topichandlers.Eth2TopicHandler; +import tech.pegasys.teku.networking.p2p.gossip.GossipNetwork; +import tech.pegasys.teku.networking.p2p.gossip.TopicChannel; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.SpecVersion; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecarSchema; +import tech.pegasys.teku.spec.datastructures.state.ForkInfo; +import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsFulu; +import tech.pegasys.teku.statetransition.util.DebugDataDumper; +import tech.pegasys.teku.storage.client.RecentChainData; + +public class DataColumnSidecarSubnetSubscriptions extends CommitteeSubnetSubscriptions { + + private final Spec spec; + private final AsyncRunner asyncRunner; + private final RecentChainData recentChainData; + private final OperationProcessor processor; + private final ForkInfo forkInfo; + private final DataColumnSidecarSchema dataColumnSidecarSchema; + private final DebugDataDumper debugDataDumper; + private final MiscHelpersFulu miscHelpersFulu; + + public DataColumnSidecarSubnetSubscriptions( + final Spec spec, + final AsyncRunner asyncRunner, + final GossipNetwork gossipNetwork, + final GossipEncoding gossipEncoding, + final RecentChainData recentChainData, + final OperationProcessor processor, + final DebugDataDumper debugDataDumper, + final ForkInfo forkInfo) { + super(gossipNetwork, gossipEncoding); + this.spec = spec; + this.asyncRunner = asyncRunner; + this.recentChainData = recentChainData; + this.processor = processor; + this.debugDataDumper = debugDataDumper; + this.forkInfo = forkInfo; + final SpecVersion specVersion = spec.forMilestone(SpecMilestone.getHighestMilestone()); + this.dataColumnSidecarSchema = + SchemaDefinitionsFulu.required(specVersion.getSchemaDefinitions()) + .getDataColumnSidecarSchema(); + this.miscHelpersFulu = + MiscHelpersFulu.required(spec.forMilestone(SpecMilestone.FULU).miscHelpers()); + } + + public SafeFuture gossip(final DataColumnSidecar sidecar) { + int subnetId = computeSubnetForSidecar(sidecar); + final String topic = + GossipTopics.getDataColumnSidecarSubnetTopic( + forkInfo.getForkDigest(spec), subnetId, gossipEncoding); + return gossipNetwork.gossip(topic, gossipEncoding.encode(sidecar)); + } + + @VisibleForTesting + Optional getChannel(final DataColumnSidecar sidecar) { + int subnetId = computeSubnetForSidecar(sidecar); + return getChannelForSubnet(subnetId); + } + + @Override + protected Eth2TopicHandler createTopicHandler(final int subnetId) { + final String topicName = GossipTopicName.getDataColumnSidecarSubnetTopicName(subnetId); + return DataColumnSidecarTopicHandler.createHandler( + recentChainData, + asyncRunner, + processor, + gossipEncoding, + debugDataDumper, + forkInfo, + topicName, + dataColumnSidecarSchema, + subnetId); + } + + private int computeSubnetForSidecar(final DataColumnSidecar sidecar) { + return miscHelpersFulu.computeSubnetForDataColumnSidecar(sidecar.getIndex()).intValue(); + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetTopicProvider.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetTopicProvider.java new file mode 100644 index 00000000000..bc55ea5f7ca --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/DataColumnSidecarSubnetTopicProvider.java @@ -0,0 +1,40 @@ +/* + * 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.gossip.subnets; + +import static tech.pegasys.teku.networking.eth2.gossip.topics.GossipTopics.getDataColumnSidecarSubnetTopic; + +import tech.pegasys.teku.infrastructure.bytes.Bytes4; +import tech.pegasys.teku.networking.eth2.gossip.encoding.GossipEncoding; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.storage.client.RecentChainData; + +public class DataColumnSidecarSubnetTopicProvider { + private final Spec spec; + private final RecentChainData recentChainData; + private final GossipEncoding gossipEncoding; + + public DataColumnSidecarSubnetTopicProvider( + final RecentChainData recentChainData, final GossipEncoding gossipEncoding) { + this.spec = recentChainData.getSpec(); + this.recentChainData = recentChainData; + this.gossipEncoding = gossipEncoding; + } + + public String getTopicForSubnet(final int subnetId) { + final Bytes4 forkDigest = + recentChainData.getCurrentForkInfo().orElseThrow().getForkDigest(spec); + return getDataColumnSidecarSubnetTopic(forkDigest, subnetId, gossipEncoding); + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/NodeIdToDataColumnSidecarSubnetsCalculator.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/NodeIdToDataColumnSidecarSubnetsCalculator.java new file mode 100644 index 00000000000..eb5474d6af0 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/NodeIdToDataColumnSidecarSubnetsCalculator.java @@ -0,0 +1,76 @@ +/* + * 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.gossip.subnets; + +import com.google.common.base.Supplier; +import java.util.List; +import java.util.Optional; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; +import tech.pegasys.teku.infrastructure.ssz.schema.collections.SszBitvectorSchema; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.SpecVersion; +import tech.pegasys.teku.spec.config.SpecConfigFulu; +import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; +import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; + +@FunctionalInterface +public interface NodeIdToDataColumnSidecarSubnetsCalculator { + + Optional calculateSubnets(UInt256 nodeId, Optional groupCount); + + NodeIdToDataColumnSidecarSubnetsCalculator NOOP = (nodeId, subnetCount) -> Optional.empty(); + + /** Creates a calculator instance for the specific slot */ + private static NodeIdToDataColumnSidecarSubnetsCalculator createAtSlot( + final SpecConfigFulu config, final MiscHelpers miscHelpers, final UInt64 currentSlot) { + UInt64 currentEpoch = miscHelpers.computeEpochAtSlot(currentSlot); + SszBitvectorSchema bitvectorSchema = + SszBitvectorSchema.create(config.getDataColumnSidecarSubnetCount()); + return (nodeId, groupCount) -> { + List nodeSubnets = + MiscHelpersFulu.required(miscHelpers) + .computeDataColumnSidecarBackboneSubnets( + nodeId, currentEpoch, groupCount.orElse(config.getCustodyRequirement())); + return Optional.of( + bitvectorSchema.ofBits(nodeSubnets.stream().map(UInt64::intValue).toList())); + }; + } + + /** Create an instance base on the current slot */ + static NodeIdToDataColumnSidecarSubnetsCalculator create( + final Spec spec, final Supplier> currentSlotSupplier) { + + return (nodeId, groupCount) -> + currentSlotSupplier + .get() + .flatMap( + slot -> { + final SpecVersion specVersion = spec.atSlot(slot); + final NodeIdToDataColumnSidecarSubnetsCalculator calculatorAtSlot; + if (specVersion.getMilestone().isGreaterThanOrEqualTo(SpecMilestone.FULU)) { + calculatorAtSlot = + createAtSlot( + SpecConfigFulu.required(specVersion.getConfig()), + specVersion.miscHelpers(), + slot); + } else { + calculatorAtSlot = NOOP; + } + return calculatorAtSlot.calculateSubnets(nodeId, groupCount); + }); + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/PeerSubnetSubscriptions.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/PeerSubnetSubscriptions.java index b4bd4168751..b951ec0cf18 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/PeerSubnetSubscriptions.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/PeerSubnetSubscriptions.java @@ -21,10 +21,13 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.OptionalInt; import java.util.function.Consumer; import java.util.stream.IntStream; +import org.apache.tuweni.units.bigints.UInt256; import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException; import tech.pegasys.teku.infrastructure.metrics.SettableLabelledGauge; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; @@ -33,36 +36,58 @@ import tech.pegasys.teku.networking.eth2.peers.PeerScorer; import tech.pegasys.teku.networking.p2p.gossip.GossipNetwork; import tech.pegasys.teku.networking.p2p.peer.NodeId; +import tech.pegasys.teku.spec.SpecVersion; +import tech.pegasys.teku.spec.config.SpecConfigFulu; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsSupplier; public class PeerSubnetSubscriptions { private final SubnetSubscriptions attestationSubnetSubscriptions; private final SubnetSubscriptions syncCommitteeSubnetSubscriptions; + private final SubnetSubscriptions dataColumnSidecarSubnetSubscriptions; + private final NodeIdToDataColumnSidecarSubnetsCalculator + nodeIdToDataColumnSidecarSubnetsCalculator; private final int targetSubnetSubscriberCount; private PeerSubnetSubscriptions( final SubnetSubscriptions attestationSubnetSubscriptions, final SubnetSubscriptions syncCommitteeSubnetSubscriptions, + final SubnetSubscriptions dataColumnSidecarSubnetSubscriptions, + final NodeIdToDataColumnSidecarSubnetsCalculator nodeIdToDataColumnSidecarSubnetsCalculator, final int targetSubnetSubscriberCount) { this.attestationSubnetSubscriptions = attestationSubnetSubscriptions; this.syncCommitteeSubnetSubscriptions = syncCommitteeSubnetSubscriptions; + this.dataColumnSidecarSubnetSubscriptions = dataColumnSidecarSubnetSubscriptions; + this.nodeIdToDataColumnSidecarSubnetsCalculator = nodeIdToDataColumnSidecarSubnetsCalculator; this.targetSubnetSubscriberCount = targetSubnetSubscriberCount; } public static PeerSubnetSubscriptions create( - final SchemaDefinitionsSupplier currentSchemaDefinitions, + final SpecVersion currentVersion, + final NodeIdToDataColumnSidecarSubnetsCalculator nodeIdToDataColumnSidecarSubnetsCalculator, final GossipNetwork network, final AttestationSubnetTopicProvider attestationTopicProvider, final SyncCommitteeSubnetTopicProvider syncCommitteeSubnetTopicProvider, final SubnetSubscriptionService syncCommitteeSubnetService, + final DataColumnSidecarSubnetTopicProvider dataColumnSidecarSubnetTopicProvider, + final SubnetSubscriptionService dataColumnSidecarSubnetService, final int targetSubnetSubscriberCount, final SettableLabelledGauge subnetPeerCountGauge) { final Map> subscribersByTopic = network.getSubscribersByTopic(); + SchemaDefinitionsSupplier currentSchemaDefinitions = currentVersion::getSchemaDefinitions; + Integer dataColumnSidecarSubnetCount = + currentVersion + .getConfig() + .toVersionFulu() + .map(SpecConfigFulu::getDataColumnSidecarSubnetCount) + // SszBitvectorSchema.create will throw with 0 + .orElse(1); + final PeerSubnetSubscriptions subscriptions = - builder(currentSchemaDefinitions) + builder(currentSchemaDefinitions, SszBitvectorSchema.create(dataColumnSidecarSubnetCount)) .targetSubnetSubscriberCount(targetSubnetSubscriberCount) + .nodeIdToDataColumnSidecarSubnetsCalculator(nodeIdToDataColumnSidecarSubnetsCalculator) .attestationSubnetSubscriptions( b -> // Track all attestation subnets @@ -94,6 +119,20 @@ public static PeerSubnetSubscriptions create( subscriber -> b.addSubscriber(syncCommitteeSubnet, subscriber)); })) + .dataColumnSidecarSubnetSubscriptions( + b -> + dataColumnSidecarSubnetService + .getSubnets() + .forEach( + columnSubnet -> { + b.addRelevantSubnet(columnSubnet); + subscribersByTopic + .getOrDefault( + dataColumnSidecarSubnetTopicProvider.getTopicForSubnet( + columnSubnet), + Collections.emptySet()) + .forEach(subscriber -> b.addSubscriber(columnSubnet, subscriber)); + })) .build(); updateMetrics(currentSchemaDefinitions, subnetPeerCountGauge, subscriptions); return subscriptions; @@ -129,14 +168,19 @@ private static IntStream streamAllSyncCommitteeSubnetIds( return IntStream.range(0, currentSchemaDefinitions.getSyncnetsENRFieldSchema().getLength()); } - static Builder builder(final SchemaDefinitionsSupplier currentSchemaDefinitions) { - return new Builder(currentSchemaDefinitions); + static Builder builder( + final SchemaDefinitionsSupplier currentSchemaDefinitions, + final SszBitvectorSchema dataColumnSidecarSubnetBitmaskSchema) { + return new Builder(currentSchemaDefinitions, dataColumnSidecarSubnetBitmaskSchema); } @VisibleForTesting static PeerSubnetSubscriptions createEmpty( - final SchemaDefinitionsSupplier currentSchemaDefinitions) { - return builder(currentSchemaDefinitions).build(); + final SchemaDefinitionsSupplier currentSchemaDefinitions, + final SszBitvectorSchema dataColumnSidecarSubnetBitmaskSchema) { + return builder(currentSchemaDefinitions, dataColumnSidecarSubnetBitmaskSchema) + .nodeIdToDataColumnSidecarSubnetsCalculator(NodeIdToDataColumnSidecarSubnetsCalculator.NOOP) + .build(); } public int getSubscriberCountForAttestationSubnet(final int subnetId) { @@ -147,6 +191,10 @@ public int getSubscriberCountForSyncCommitteeSubnet(final int subnetId) { return syncCommitteeSubnetSubscriptions.getSubscriberCountForSubnet(subnetId); } + public int getSubscriberCountForDataColumnSidecarSubnet(final int subnetId) { + return dataColumnSidecarSubnetSubscriptions.getSubscriberCountForSubnet(subnetId); + } + public SszBitvector getAttestationSubnetSubscriptions(final NodeId peerId) { return attestationSubnetSubscriptions.getSubnetSubscriptions(peerId); } @@ -155,6 +203,17 @@ public SszBitvector getSyncCommitteeSubscriptions(final NodeId peerId) { return syncCommitteeSubnetSubscriptions.getSubnetSubscriptions(peerId); } + public SszBitvector getDataColumnSidecarSubnetSubscriptions(final NodeId peerId) { + return dataColumnSidecarSubnetSubscriptions.getSubnetSubscriptions(peerId); + } + + public SszBitvector getDataColumnSidecarSubnetSubscriptionsByNodeId( + final UInt256 peerId, final Optional custodySubnetCount) { + return nodeIdToDataColumnSidecarSubnetsCalculator + .calculateSubnets(peerId, custodySubnetCount) + .orElse(dataColumnSidecarSubnetSubscriptions.getSubscriptionSchema().getDefault()); + } + public boolean isSyncCommitteeSubnetRelevant(final int subnetId) { return syncCommitteeSubnetSubscriptions.isSubnetRelevant(subnetId); } @@ -163,6 +222,10 @@ public boolean isAttestationSubnetRelevant(final int subnetId) { return attestationSubnetSubscriptions.isSubnetRelevant(subnetId); } + public boolean isDataColumnSidecarSubnetRelevant(final int subnetId) { + return dataColumnSidecarSubnetSubscriptions.isSubnetRelevant(subnetId); + } + public PeerScorer createScorer() { return SubnetScorer.create(this); } @@ -177,20 +240,15 @@ public int getSubscribersRequired() { } private OptionalInt getMinSubscriberCount() { - final OptionalInt minAttestationSubscribers = - attestationSubnetSubscriptions.getMinSubscriberCount(); - final OptionalInt minSyncnetSubscribers = - syncCommitteeSubnetSubscriptions.getMinSubscriberCount(); - if (minAttestationSubscribers.isPresent() && minSyncnetSubscribers.isPresent()) { - return OptionalInt.of( - Math.min(minAttestationSubscribers.getAsInt(), minSyncnetSubscribers.getAsInt())); - } else { - if (minAttestationSubscribers.isPresent()) { - return minAttestationSubscribers; - } else { - return minSyncnetSubscribers; - } - } + return optionalMin( + List.of( + attestationSubnetSubscriptions.getMinSubscriberCount(), + syncCommitteeSubnetSubscriptions.getMinSubscriberCount(), + dataColumnSidecarSubnetSubscriptions.getMinSubscriberCount())); + } + + private static OptionalInt optionalMin(final List optionalInts) { + return optionalInts.stream().flatMapToInt(OptionalInt::stream).min(); } public interface Factory { @@ -250,6 +308,10 @@ public SszBitvector getSubnetSubscriptions(final NodeId peerId) { return subscriptionsByPeer.getOrDefault(peerId, subscriptionSchema.getDefault()); } + public SszBitvectorSchema getSubscriptionSchema() { + return subscriptionSchema; + } + public static class Builder { private final SszBitvectorSchema subscriptionSchema; @@ -288,19 +350,27 @@ public SubnetSubscriptions build() { public static class Builder { private final SubnetSubscriptions.Builder attestationSubnetSubscriptions; private final SubnetSubscriptions.Builder syncCommitteeSubnetSubscriptions; + private final SubnetSubscriptions.Builder dataColumnSidecarSubnetSubscriptions; + private NodeIdToDataColumnSidecarSubnetsCalculator nodeIdToDataColumnSidecarSubnetsCalculator; private int targetSubnetSubscriberCount = 2; - private Builder(final SchemaDefinitionsSupplier currentSchemaDefinitions) { + private Builder( + final SchemaDefinitionsSupplier currentSchemaDefinitions, + final SszBitvectorSchema dataColumnSidecarSubnetBitmaskSchema) { attestationSubnetSubscriptions = SubnetSubscriptions.builder(currentSchemaDefinitions.getAttnetsENRFieldSchema()); syncCommitteeSubnetSubscriptions = SubnetSubscriptions.builder(currentSchemaDefinitions.getSyncnetsENRFieldSchema()); + dataColumnSidecarSubnetSubscriptions = + SubnetSubscriptions.builder(dataColumnSidecarSubnetBitmaskSchema); } public PeerSubnetSubscriptions build() { return new PeerSubnetSubscriptions( attestationSubnetSubscriptions.build(), syncCommitteeSubnetSubscriptions.build(), + dataColumnSidecarSubnetSubscriptions.build(), + nodeIdToDataColumnSidecarSubnetsCalculator, targetSubnetSubscriberCount); } @@ -324,5 +394,18 @@ public Builder syncCommitteeSubnetSubscriptions( consumer.accept(syncCommitteeSubnetSubscriptions); return this; } + + public Builder dataColumnSidecarSubnetSubscriptions( + final Consumer consumer) { + consumer.accept(dataColumnSidecarSubnetSubscriptions); + return this; + } + + public Builder nodeIdToDataColumnSidecarSubnetsCalculator( + final NodeIdToDataColumnSidecarSubnetsCalculator + nodeIdToDataColumnSidecarSubnetsCalculator) { + this.nodeIdToDataColumnSidecarSubnetsCalculator = nodeIdToDataColumnSidecarSubnetsCalculator; + return this; + } } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/GossipTopicName.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/GossipTopicName.java index e2869b10449..fc779f24eb6 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/GossipTopicName.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/GossipTopicName.java @@ -40,4 +40,8 @@ public static String getSyncCommitteeSubnetTopicName(final int subnetId) { public static String getBlobSidecarSubnetTopicName(final int subnetId) { return "blob_sidecar_" + subnetId; } + + public static String getDataColumnSidecarSubnetTopicName(final int subnetId) { + return "data_column_sidecar_" + subnetId; + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/GossipTopics.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/GossipTopics.java index 1dc5b954716..ac23dbc48bc 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/GossipTopics.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/GossipTopics.java @@ -13,8 +13,11 @@ package tech.pegasys.teku.networking.eth2.gossip.topics; +import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.networking.eth2.gossip.encoding.GossipEncoding; import tech.pegasys.teku.spec.Spec; @@ -64,6 +67,24 @@ public static String getBlobSidecarSubnetTopic( forkDigest, GossipTopicName.getBlobSidecarSubnetTopicName(subnetId), gossipEncoding); } + public static String getDataColumnSidecarSubnetTopic( + final Bytes4 forkDigest, final int subnetId, final GossipEncoding gossipEncoding) { + return getTopic( + forkDigest, GossipTopicName.getDataColumnSidecarSubnetTopicName(subnetId), gossipEncoding); + } + + public static Set getAllDataColumnSidecarSubnetTopics( + final GossipEncoding gossipEncoding, final Bytes4 forkDigest, final Spec spec) { + + return spec.getNumberOfDataColumnSubnets() + .map( + subnetCount -> + IntStream.range(0, subnetCount) + .mapToObj(i -> getDataColumnSidecarSubnetTopic(forkDigest, i, gossipEncoding)) + .collect(Collectors.toSet())) + .orElse(Collections.emptySet()); + } + public static Set getAllTopics( final GossipEncoding gossipEncoding, final Bytes4 forkDigest, @@ -77,7 +98,6 @@ public static Set getAllTopics( for (int i = 0; i < NetworkConstants.SYNC_COMMITTEE_SUBNET_COUNT; i++) { topics.add(getSyncCommitteeSubnetTopic(forkDigest, i, gossipEncoding)); } - spec.forMilestone(specMilestone) .getConfig() .toVersionDeneb() @@ -86,6 +106,8 @@ public static Set getAllTopics( addBlobSidecarSubnetTopics( config.getBlobSidecarSubnetCount(), topics, forkDigest, gossipEncoding)); + topics.addAll(getAllDataColumnSidecarSubnetTopics(gossipEncoding, forkDigest, spec)); + for (GossipTopicName topicName : GossipTopicName.values()) { topics.add(GossipTopics.getTopic(forkDigest, topicName, gossipEncoding)); } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/topichandlers/DataColumnSidecarTopicHandler.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/topichandlers/DataColumnSidecarTopicHandler.java new file mode 100644 index 00000000000..8cc32ec0253 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/topics/topichandlers/DataColumnSidecarTopicHandler.java @@ -0,0 +1,94 @@ +/* + * 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.gossip.topics.topichandlers; + +import java.util.Optional; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.networking.eth2.gossip.encoding.GossipEncoding; +import tech.pegasys.teku.networking.eth2.gossip.topics.OperationMilestoneValidator; +import tech.pegasys.teku.networking.eth2.gossip.topics.OperationProcessor; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.blobs.versions.fulu.DataColumnSidecarSchema; +import tech.pegasys.teku.spec.datastructures.state.ForkInfo; +import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; +import tech.pegasys.teku.statetransition.util.DebugDataDumper; +import tech.pegasys.teku.statetransition.validation.InternalValidationResult; +import tech.pegasys.teku.storage.client.RecentChainData; + +public class DataColumnSidecarTopicHandler { + + public static Eth2TopicHandler createHandler( + final RecentChainData recentChainData, + final AsyncRunner asyncRunner, + final OperationProcessor operationProcessor, + final GossipEncoding gossipEncoding, + final DebugDataDumper debugDataDumper, + final ForkInfo forkInfo, + final String topicName, + final DataColumnSidecarSchema dataColumnSidecarSchema, + final int subnetId) { + + final Spec spec = recentChainData.getSpec(); + + return new Eth2TopicHandler<>( + recentChainData, + asyncRunner, + new TopicSubnetIdAwareOperationProcessor(spec, subnetId, operationProcessor), + gossipEncoding, + forkInfo.getForkDigest(spec), + topicName, + new OperationMilestoneValidator<>( + recentChainData.getSpec(), + forkInfo.getFork(), + message -> spec.computeEpochAtSlot(message.getSlot())), + dataColumnSidecarSchema, + spec.getNetworkingConfig(), + debugDataDumper); + } + + private static class TopicSubnetIdAwareOperationProcessor + implements OperationProcessor { + private final int subnetId; + private final OperationProcessor delegate; + private final MiscHelpersFulu miscHelpersFulu; + + TopicSubnetIdAwareOperationProcessor( + final Spec spec, final int subnetId, final OperationProcessor delegate) { + this.subnetId = subnetId; + this.delegate = delegate; + this.miscHelpersFulu = + MiscHelpersFulu.required(spec.forMilestone(SpecMilestone.FULU).miscHelpers()); + } + + @Override + public SafeFuture process( + final DataColumnSidecar dataColumnSidecar, final Optional arrivalTimestamp) { + final int dataColumnSidecarSubnet = + miscHelpersFulu + .computeSubnetForDataColumnSidecar(dataColumnSidecar.getIndex()) + .intValue(); + if (dataColumnSidecarSubnet != subnetId) { + return SafeFuture.completedFuture( + InternalValidationResult.reject( + "DataColumnSidecar with subnet_id %s does not match the topic subnet_id %d", + dataColumnSidecarSubnet, subnetId)); + } + return delegate.process(dataColumnSidecar, arrivalTimestamp); + } + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/mock/NoOpEth2P2PNetwork.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/mock/NoOpEth2P2PNetwork.java index 8fc65aa0e5f..15579a5a28c 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/mock/NoOpEth2P2PNetwork.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/mock/NoOpEth2P2PNetwork.java @@ -54,6 +54,12 @@ public void subscribeToSyncCommitteeSubnetId(final int subnetId) {} @Override public void unsubscribeFromSyncCommitteeSubnetId(final int subnetId) {} + @Override + public void subscribeToDataColumnSidecarSubnetId(final int subnetId) {} + + @Override + public void unsubscribeFromDataColumnSidecarSubnetId(final int subnetId) {} + @Override public MetadataMessage getMetadata() { return spec.getGenesisSchemaDefinitions().getMetadataMessageSchema().createDefault(); 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 2f88c9c36f5..b5c6322f01e 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 @@ -28,6 +28,7 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; 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.SubnetSubscriptionService; import tech.pegasys.teku.networking.eth2.rpc.beaconchain.BeaconChainMethods; @@ -44,6 +45,8 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessageSchema; 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.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -72,6 +75,8 @@ public class Eth2PeerManager implements PeerLookup, PeerHandler { final Spec spec, final AsyncRunner asyncRunner, final CombinedChainDataClient combinedChainDataClient, + final DataColumnSidecarByRootCustody dataColumnSidecarCustody, + final CustodyGroupCountManager custodyGroupCountManager, final RecentChainData recentChainData, final MetricsSystem metricsSystem, final Eth2PeerFactory eth2PeerFactory, @@ -104,6 +109,9 @@ public class Eth2PeerManager implements PeerLookup, PeerHandler { public static Eth2PeerManager create( final AsyncRunner asyncRunner, final CombinedChainDataClient combinedChainDataClient, + final DataColumnSidecarByRootCustody dataColumnSidecarCustody, + final CustodyGroupCountManager custodyGroupCountManager, + final MetadataMessagesFactory metadataMessagesFactory, final MetricsSystem metricsSystem, final SubnetSubscriptionService attestationSubnetService, final SubnetSubscriptionService syncCommitteeSubnetService, @@ -119,9 +127,11 @@ public static Eth2PeerManager create( final int peerRequestLimit, final Spec spec, final KZG kzg, - final DiscoveryNodeIdExtractor discoveryNodeIdExtractor) { + final DiscoveryNodeIdExtractor discoveryNodeIdExtractor, + final Optional custodyGroupCount) { - final MetadataMessagesFactory metadataMessagesFactory = new MetadataMessagesFactory(); + // FIXME: we have no guarantee here that it's synced already + custodyGroupCount.ifPresent(metadataMessagesFactory::updateCustodyGroupCount); attestationSubnetService.subscribeToUpdates( metadataMessagesFactory::updateAttestationSubnetIds); syncCommitteeSubnetService.subscribeToUpdates( @@ -131,6 +141,8 @@ public static Eth2PeerManager create( spec, asyncRunner, combinedChainDataClient, + dataColumnSidecarCustody, + custodyGroupCountManager, combinedChainDataClient.getRecentChainData(), metricsSystem, new Eth2PeerFactory( @@ -234,7 +246,7 @@ private void ensureStatusReceived(final Eth2Peer peer) { if (!peer.hasStatus()) { LOG.trace( "Disconnecting peer {} because initial status was not received", peer.getId()); - peer.disconnectCleanly(DisconnectReason.REMOTE_FAULT) + peer.disconnectCleanly(DisconnectReason.NO_STATUS_RECEIVED) .ifExceptionGetsHereRaiseABug(); } }, @@ -245,7 +257,7 @@ private void ensureStatusReceived(final Eth2Peer peer) { LOG.error( "Error while waiting for peer {} to exchange status. Disconnecting", peer.getId()); - peer.disconnectImmediately(Optional.of(DisconnectReason.REMOTE_FAULT), true); + peer.disconnectImmediately(Optional.of(DisconnectReason.NO_STATUS_RECEIVED), true); }); } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/MetadataMessagesFactory.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/MetadataMessagesFactory.java index da80176571a..4bd501ff10c 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/MetadataMessagesFactory.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/MetadataMessagesFactory.java @@ -22,15 +22,15 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.PingMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessage; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessageSchema; +import tech.pegasys.teku.statetransition.CustodyGroupCountChannel; -public class MetadataMessagesFactory { +public class MetadataMessagesFactory implements CustodyGroupCountChannel { private static final Logger LOG = LogManager.getLogger(); private final AtomicLong seqNumberGenerator = new AtomicLong(0L); private Iterable attestationSubnetIds = Collections.emptyList(); private Iterable syncCommitteeSubnetIds = Collections.emptyList(); - // TODO-fulu update with Fulu networking-related changes (CustodyGroupCountChannel) - private final Optional custodyGroupCount = Optional.empty(); + private Optional custodyGroupCount = Optional.empty(); public synchronized void updateAttestationSubnetIds( final Iterable attestationSubnetIds) { @@ -44,6 +44,21 @@ public synchronized void updateSyncCommitteeSubnetIds( handleUpdate(); } + public synchronized void updateCustodyGroupCount(final UInt64 custodyGroupCount) { + this.custodyGroupCount = Optional.of(custodyGroupCount); + handleUpdate(); + } + + @Override + public void onCustodyGroupCountUpdate(final int groupCount) { + // we don't care until it's synced + } + + @Override + public void onCustodyGroupCountSynced(final int groupCount) { + updateCustodyGroupCount(UInt64.valueOf(groupCount)); + } + private void handleUpdate() { seqNumberGenerator.incrementAndGet(); } diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetworkTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetworkTest.java index 3676777442e..9de65dceea2 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetworkTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetworkTest.java @@ -70,6 +70,8 @@ public class ActiveEth2P2PNetworkTest { new SubnetSubscriptionService(); private final SubnetSubscriptionService syncCommitteeSubnetService = new SubnetSubscriptionService(); + private final SubnetSubscriptionService dataColumnSidecarCommitteeSubnetService = + new SubnetSubscriptionService(); private final RecentChainData recentChainData = storageSystem.recentChainData(); private final GossipEncoding gossipEncoding = GossipEncoding.SSZ_SNAPPY; private final GossipConfigurator gossipConfigurator = GossipConfigurator.NOOP; @@ -310,9 +312,11 @@ ActiveEth2P2PNetwork createNetwork() { recentChainData, attestationSubnetService, syncCommitteeSubnetService, + dataColumnSidecarCommitteeSubnetService, gossipEncoding, gossipConfigurator, processedAttestationSubscriptionProvider, + 0, true); } } diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/subnets/PeerSubnetSubscriptionsTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/subnets/PeerSubnetSubscriptionsTest.java index abf190aa47a..644f4eab9d6 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/subnets/PeerSubnetSubscriptionsTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/subnets/PeerSubnetSubscriptionsTest.java @@ -18,6 +18,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.common.base.Supplier; import com.google.common.collect.ImmutableMap; import it.unimi.dsi.fastutil.ints.IntList; import java.util.ArrayList; @@ -25,17 +26,20 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.pegasys.teku.infrastructure.metrics.SettableLabelledGauge; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.networking.eth2.SubnetSubscriptionService; import tech.pegasys.teku.networking.p2p.gossip.GossipNetwork; import tech.pegasys.teku.networking.p2p.mock.MockNodeId; import tech.pegasys.teku.networking.p2p.peer.NodeId; import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecVersion; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsSupplier; @@ -47,6 +51,8 @@ class PeerSubnetSubscriptionsTest { private final Spec spec = TestSpecFactory.createMinimalAltair(); private final SettableLabelledGauge subnetPeerCountGauge = mock(SettableLabelledGauge.class); + final Supplier currentSpecVersionSupplier = spec::getGenesisSpec; + final Supplier> currentSlotSupplier = Optional::empty; private final SchemaDefinitionsSupplier currentSchemaDefinitions = spec::getGenesisSchemaDefinitions; private final GossipNetwork gossipNetwork = mock(GossipNetwork.class); @@ -54,7 +60,10 @@ class PeerSubnetSubscriptionsTest { mock(AttestationSubnetTopicProvider.class); private final SyncCommitteeSubnetTopicProvider syncCommitteeTopicProvider = mock(SyncCommitteeSubnetTopicProvider.class); + private final DataColumnSidecarSubnetTopicProvider dataColumnSidecarSubnetTopicProvider = + mock(DataColumnSidecarSubnetTopicProvider.class); private final SubnetSubscriptionService syncnetSubscriptions = new SubnetSubscriptionService(); + private final SubnetSubscriptionService dataColumnSubscriptions = new SubnetSubscriptionService(); @BeforeEach public void setUp() { @@ -197,11 +206,14 @@ public void isAttestationSubnetRelevant() { private PeerSubnetSubscriptions createPeerSubnetSubscriptions() { return PeerSubnetSubscriptions.create( - currentSchemaDefinitions, + currentSpecVersionSupplier.get(), + NodeIdToDataColumnSidecarSubnetsCalculator.NOOP, gossipNetwork, attestationTopicProvider, syncCommitteeTopicProvider, syncnetSubscriptions, + dataColumnSidecarSubnetTopicProvider, + dataColumnSubscriptions, TARGET_SUBSCRIBER_COUNT, subnetPeerCountGauge); } diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/subnets/SubnetScorerTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/subnets/SubnetScorerTest.java index 2c1113e95b6..65cc9cb62f0 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/subnets/SubnetScorerTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/subnets/SubnetScorerTest.java @@ -15,18 +15,26 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static tech.pegasys.teku.networking.p2p.discovery.discv5.DiscV5Service.DEFAULT_NODE_RECORD_CONVERTER; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntLists; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Pair; +import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Test; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; +import tech.pegasys.teku.infrastructure.ssz.schema.collections.SszBitvectorSchema; import tech.pegasys.teku.networking.eth2.peers.PeerScorer; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; import tech.pegasys.teku.networking.p2p.mock.MockNodeId; import tech.pegasys.teku.networking.p2p.peer.NodeId; import tech.pegasys.teku.spec.Spec; @@ -36,22 +44,30 @@ class SubnetScorerTest { private final Spec spec = TestSpecFactory.createMinimalAltair(); private final SchemaDefinitions schemaDefinitions = spec.getGenesisSchemaDefinitions(); + private static final int DATA_COLUMN_SIDECAR_SUBNET_COUNT = 128; @Test void shouldScoreCandidatePeerWithNoSubnetsAsZero() { final SubnetScorer scorer = - SubnetScorer.create(PeerSubnetSubscriptions.createEmpty(() -> schemaDefinitions)); + SubnetScorer.create( + PeerSubnetSubscriptions.createEmpty( + () -> schemaDefinitions, + SszBitvectorSchema.create(DATA_COLUMN_SIDECAR_SUBNET_COUNT))); assertThat( scorer.scoreCandidatePeer( - schemaDefinitions.getAttnetsENRFieldSchema().getDefault(), - schemaDefinitions.getSyncnetsENRFieldSchema().getDefault())) + createDiscoveryPeer( + schemaDefinitions.getAttnetsENRFieldSchema().getDefault(), + schemaDefinitions.getSyncnetsENRFieldSchema().getDefault()))) .isZero(); } @Test void shouldScoreExistingPeerWithNoSubnetsAsZero() { final SubnetScorer scorer = - SubnetScorer.create(PeerSubnetSubscriptions.createEmpty(() -> schemaDefinitions)); + SubnetScorer.create( + PeerSubnetSubscriptions.createEmpty( + () -> schemaDefinitions, + SszBitvectorSchema.create(DATA_COLUMN_SIDECAR_SUBNET_COUNT))); assertThat(scorer.scoreExistingPeer(new MockNodeId(1))).isZero(); } @@ -64,7 +80,9 @@ void shouldScoreExistingPeersOnSubnetsWithFewPeersMoreHighly() { final MockNodeId node5 = new MockNodeId(4); final SubnetScorer scorer = SubnetScorer.create( - PeerSubnetSubscriptions.builder(() -> schemaDefinitions) + PeerSubnetSubscriptions.builder( + () -> schemaDefinitions, + SszBitvectorSchema.create(DATA_COLUMN_SIDECAR_SUBNET_COUNT)) .attestationSubnetSubscriptions( b -> b.addRelevantSubnet(1) @@ -113,7 +131,9 @@ void shouldScoreCandidatePeersOnSubnetsWithFewPeersMoreHighly() { final MockNodeId node3 = new MockNodeId(2); final SubnetScorer scorer = SubnetScorer.create( - PeerSubnetSubscriptions.builder(() -> schemaDefinitions) + PeerSubnetSubscriptions.builder( + () -> schemaDefinitions, + SszBitvectorSchema.create(DATA_COLUMN_SIDECAR_SUBNET_COUNT)) .attestationSubnetSubscriptions( b -> b.addRelevantSubnet(1) @@ -139,6 +159,8 @@ void shouldScoreCandidatePeersOnSubnetsWithFewPeersMoreHighly() { .addSubscriber(1, node1) .addSubscriber(1, node2) .addSubscriber(1, node3)) + .nodeIdToDataColumnSidecarSubnetsCalculator( + NodeIdToDataColumnSidecarSubnetsCalculator.NOOP) .build()); assertCandidatePeerScores( @@ -176,10 +198,30 @@ private void assertCandidatePeerScores( Function.identity(), (subscriptions) -> scorer.scoreCandidatePeer( - subscriptions.getLeft(), subscriptions.getRight()))); + createDiscoveryPeer( + subscriptions.getLeft(), subscriptions.getRight())))); assertThat(actual).contains(expected); } + private DiscoveryPeer createDiscoveryPeer( + final SszBitvector attSubnets, final SszBitvector syncSubnets) { + try { + Bytes pubKey = + Bytes.fromHexString( + "0x03B86ED9F747A7FA99963F39E3B176B45E9E863108A2D145EA3A4E76D8D0935194"); + return new DiscoveryPeer( + pubKey, + DEFAULT_NODE_RECORD_CONVERTER.convertPublicKeyToNodeId(pubKey), + new InetSocketAddress(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}), 9000), + Optional.empty(), + attSubnets, + syncSubnets, + Optional.empty()); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + private Pair candidateWithSubnets( final List attnets, final List syncnets) { return Pair.of( 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 c7d0920836e..c0161c2df47 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 @@ -45,6 +45,8 @@ import tech.pegasys.teku.networking.p2p.peer.Peer; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; +import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -69,6 +71,8 @@ public class Eth2PeerManagerTest { spec, asyncRunner, combinedChainDataClient, + DataColumnSidecarByRootCustody.NOOP, + CustodyGroupCountManager.NOOP, recentChainData, new NoOpMetricsSystem(), eth2PeerFactory, @@ -176,7 +180,7 @@ void onConnect_shouldDisconnectIfIncomingPeerDoesNotSendStatusMessage() { asyncRunner.executeQueuedActions(); // Didn't receive a status message in time, so disconnect. - verify(eth2Peer).disconnectCleanly(DisconnectReason.REMOTE_FAULT); + verify(eth2Peer).disconnectCleanly(DisconnectReason.NO_STATUS_RECEIVED); } @Test diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerSelectionStrategyTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerSelectionStrategyTest.java index ecb316323b8..29a2b4cb84c 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerSelectionStrategyTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerSelectionStrategyTest.java @@ -340,6 +340,7 @@ private static DiscoveryPeer createDiscoveryPeer(final Bytes peerId, final int.. new InetSocketAddress(InetAddress.getLoopbackAddress(), peerId.trimLeadingZeros().toInt()), ENR_FORK_ID, SCHEMA_DEFINITIONS.getAttnetsENRFieldSchema().ofBits(attnets), - SCHEMA_DEFINITIONS.getSyncnetsENRFieldSchema().getDefault()); + SCHEMA_DEFINITIONS.getSyncnetsENRFieldSchema().getDefault(), + Optional.empty()); } } 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 652b65587a4..638df14f048 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 @@ -18,6 +18,7 @@ import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.base.Supplier; import java.net.BindException; import java.time.Duration; import java.util.ArrayList; @@ -44,6 +45,7 @@ import tech.pegasys.teku.infrastructure.subscribers.Subscribers; 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.NoOpKZG; import tech.pegasys.teku.network.p2p.jvmlibp2p.PrivateKeyGenerator; import tech.pegasys.teku.networking.eth2.gossip.config.GossipConfigurator; @@ -58,6 +60,8 @@ import tech.pegasys.teku.networking.eth2.gossip.forks.versions.GossipForkSubscriptionsFulu; import tech.pegasys.teku.networking.eth2.gossip.forks.versions.GossipForkSubscriptionsPhase0; import tech.pegasys.teku.networking.eth2.gossip.subnets.AttestationSubnetTopicProvider; +import tech.pegasys.teku.networking.eth2.gossip.subnets.DataColumnSidecarSubnetTopicProvider; +import tech.pegasys.teku.networking.eth2.gossip.subnets.NodeIdToDataColumnSidecarSubnetsCalculator; import tech.pegasys.teku.networking.eth2.gossip.subnets.PeerSubnetSubscriptions; import tech.pegasys.teku.networking.eth2.gossip.subnets.SyncCommitteeSubnetTopicProvider; import tech.pegasys.teku.networking.eth2.gossip.topics.Eth2GossipTopicFilter; @@ -66,6 +70,7 @@ import tech.pegasys.teku.networking.eth2.gossip.topics.VerifiedBlockAttestationsSubscriptionProvider; import tech.pegasys.teku.networking.eth2.peers.Eth2PeerManager; import tech.pegasys.teku.networking.eth2.peers.Eth2PeerSelectionStrategy; +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.encodings.RpcEncoding; import tech.pegasys.teku.networking.p2p.connection.PeerPools; @@ -81,11 +86,14 @@ import tech.pegasys.teku.networking.p2p.reputation.ReputationManager; 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.SpecVersion; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.config.Constants; import tech.pegasys.teku.spec.datastructures.attestation.ProcessedAttestationListener; import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation; 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.operations.Attestation; import tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing; @@ -99,6 +107,9 @@ import tech.pegasys.teku.spec.schemas.SchemaDefinitionsSupplier; import tech.pegasys.teku.statetransition.BeaconChainUtil; import tech.pegasys.teku.statetransition.block.VerifiedBlockOperationsListener; +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.util.DebugDataDumper; import tech.pegasys.teku.storage.api.StorageQueryChannel; import tech.pegasys.teku.storage.api.StubStorageQueryChannel; @@ -144,6 +155,7 @@ public class Eth2P2PNetworkBuilder { protected OperationProcessor signedContributionAndProofProcessor; protected OperationProcessor syncCommitteeMessageProcessor; protected OperationProcessor signedBlsToExecutionChangeProcessor; + protected OperationProcessor dataColumnSidecarOperationProcessor; protected ProcessedAttestationSubscriptionProvider processedAttestationSubscriptionProvider; protected VerifiedBlockAttestationsSubscriptionProvider verifiedBlockAttestationsSubscriptionProvider; @@ -201,21 +213,35 @@ protected Eth2P2PNetwork buildNetwork(final P2PConfig config) { final SubnetSubscriptionService attestationSubnetService = new SubnetSubscriptionService(); final SubnetSubscriptionService syncCommitteeSubnetService = new SubnetSubscriptionService(); + final SubnetSubscriptionService dataColumnSidecarSubnetService = + new SubnetSubscriptionService(); final EarliestAvailableBlockSlot earliestAvailableBlockSlot = new EarliestAvailableBlockSlot( historicalChainData, timeProvider, earliestAvailableBlockSlotFrequency); final CombinedChainDataClient combinedChainDataClient = new CombinedChainDataClient( recentChainData, historicalChainData, spec, earliestAvailableBlockSlot); + final DataColumnSidecarSubnetTopicProvider dataColumnSidecarSubnetTopicProvider = + new DataColumnSidecarSubnetTopicProvider( + combinedChainDataClient.getRecentChainData(), gossipEncoding); if (rpcEncoding == null) { rpcEncoding = RpcEncoding.createSszSnappyEncoding(spec.getNetworkingConfig().getMaxPayloadSize()); } + final Optional dasTotalCustodySubnetCount = + spec.isMilestoneSupported(SpecMilestone.FULU) + ? Optional.of( + UInt64.valueOf( + config.getTotalCustodyGroupCount(spec.forMilestone(SpecMilestone.FULU)))) + : Optional.empty(); final Eth2PeerManager eth2PeerManager = Eth2PeerManager.create( asyncRunner, combinedChainDataClient, + DataColumnSidecarByRootCustody.NOOP, + CustodyGroupCountManager.NOOP, + new MetadataMessagesFactory(), METRICS_SYSTEM, attestationSubnetService, syncCommitteeSubnetService, @@ -231,7 +257,8 @@ protected Eth2P2PNetwork buildNetwork(final P2PConfig config) { P2PConfig.DEFAULT_PEER_REQUEST_LIMIT, spec, NoOpKZG.INSTANCE, - (__) -> Optional.empty()); + (__) -> Optional.empty(), + dasTotalCustodySubnetCount); List> rpcMethods = eth2PeerManager.getBeaconChainMethods().all().stream() @@ -261,8 +288,16 @@ protected Eth2P2PNetwork buildNetwork(final P2PConfig config) { discoConfig.getMinPeers(), discoConfig.getMaxPeers(), discoConfig.getMinRandomlySelectedPeers()); + final Supplier currentSpecVersionSupplier = + () -> combinedChainDataClient.getRecentChainData().getCurrentSpec(); final SchemaDefinitionsSupplier currentSchemaDefinitions = - () -> config.getSpec().getGenesisSchemaDefinitions(); + () -> + combinedChainDataClient + .getRecentChainData() + .getCurrentSpec() + .getSchemaDefinitions(); + final Supplier> currentSlotSupplier = + () -> combinedChainDataClient.getRecentChainData().getCurrentSlot(); final SettableLabelledGauge subnetPeerCountGauge = SettableLabelledGauge.create( metricsSystem, @@ -297,11 +332,15 @@ protected Eth2P2PNetwork buildNetwork(final P2PConfig config) { targetPeerRange, gossipNetwork -> PeerSubnetSubscriptions.create( - currentSchemaDefinitions, + currentSpecVersionSupplier.get(), + NodeIdToDataColumnSidecarSubnetsCalculator.create( + spec, currentSlotSupplier), gossipNetwork, attestationSubnetTopicProvider, syncCommitteeTopicProvider, syncCommitteeSubnetService, + dataColumnSidecarSubnetTopicProvider, + dataColumnSidecarSubnetService, config.getTargetSubnetSubscriberCount(), subnetPeerCountGauge), reputationManager, @@ -334,9 +373,11 @@ protected Eth2P2PNetwork buildNetwork(final P2PConfig config) { recentChainData, attestationSubnetService, syncCommitteeSubnetService, + dataColumnSidecarSubnetService, gossipEncoding, GossipConfigurator.NOOP, processedAttestationSubscriptionProvider, + 0, config.isAllTopicsFilterEnabled()); } } @@ -477,7 +518,9 @@ private GossipForkSubscriptions createSubscriptions( signedContributionAndProofProcessor, syncCommitteeMessageProcessor, signedBlsToExecutionChangeProcessor, - debugDataDumper); + dataColumnSidecarOperationProcessor, + debugDataDumper, + DasGossipLogger.NOOP); }; } @@ -699,6 +742,13 @@ public Eth2P2PNetworkBuilder gossipedSignedBlsToExecutionChangeProcessor( return this; } + public Eth2P2PNetworkBuilder gossipedDataColumnSidecarOperationProcessor( + final OperationProcessor dataColumnSidecarOperationProcessor) { + checkNotNull(dataColumnSidecarOperationProcessor); + this.dataColumnSidecarOperationProcessor = dataColumnSidecarOperationProcessor; + return this; + } + public Eth2P2PNetworkBuilder processedAttestationSubscriptionProvider( final ProcessedAttestationSubscriptionProvider processedAttestationSubscriptionProvider) { checkNotNull(processedAttestationSubscriptionProvider); diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryNetwork.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryNetwork.java index 127586c4a07..419f528ea1d 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryNetwork.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryNetwork.java @@ -45,6 +45,7 @@ public class DiscoveryNetwork

extends DelegatingP2PNetwork

{ public static final String ATTESTATION_SUBNET_ENR_FIELD = "attnets"; public static final String SYNC_COMMITTEE_SUBNET_ENR_FIELD = "syncnets"; + public static final String DAS_CUSTODY_GROUP_COUNT_ENR_FIELD = "cgc"; public static final String ETH2_ENR_FIELD = "eth2"; private final Spec spec; @@ -138,6 +139,15 @@ public void setSyncCommitteeSubnetSubscriptions(final Iterable subnetId .sszSerialize()); } + public void setDASTotalCustodySubnetCount(final int count) { + if (count < 0) { + throw new IllegalArgumentException( + String.format("Custody subnet count should be a positive number, but was %s", count)); + } + discoveryService.updateCustomENRField( + DAS_CUSTODY_GROUP_COUNT_ENR_FIELD, Bytes.ofUnsignedInt(count).trimLeadingZeros()); + } + public void setPreGenesisForkInfo() { final SpecVersion genesisSpec = spec.getGenesisSpec(); final Bytes4 genesisForkVersion = genesisSpec.getConfig().getGenesisForkVersion(); diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryPeer.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryPeer.java index 74412237a60..e1b9b9450f3 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryPeer.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryPeer.java @@ -28,6 +28,7 @@ public class DiscoveryPeer { private final Optional enrForkId; private final SszBitvector persistentAttestationSubnets; private final SszBitvector syncCommitteeSubnets; + private final Optional dasCustodySubnetCount; public DiscoveryPeer( final Bytes publicKey, @@ -35,13 +36,15 @@ public DiscoveryPeer( final InetSocketAddress nodeAddress, final Optional enrForkId, final SszBitvector persistentAttestationSubnets, - final SszBitvector syncCommitteeSubnets) { + final SszBitvector syncCommitteeSubnets, + final Optional dasCustodySubnetCount) { this.publicKey = publicKey; this.nodeId = nodeId; this.nodeAddress = nodeAddress; this.enrForkId = enrForkId; this.persistentAttestationSubnets = persistentAttestationSubnets; this.syncCommitteeSubnets = syncCommitteeSubnets; + this.dasCustodySubnetCount = dasCustodySubnetCount; } public Bytes getPublicKey() { @@ -68,6 +71,10 @@ public SszBitvector getSyncCommitteeSubnets() { return syncCommitteeSubnets; } + public Optional getDasCustodySubnetCount() { + return dasCustodySubnetCount; + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -81,7 +88,8 @@ public boolean equals(final Object o) { && Objects.equal(getNodeAddress(), that.getNodeAddress()) && Objects.equal(getEnrForkId(), that.getEnrForkId()) && Objects.equal(getPersistentAttestationSubnets(), that.getPersistentAttestationSubnets()) - && Objects.equal(getSyncCommitteeSubnets(), that.getSyncCommitteeSubnets()); + && Objects.equal(getSyncCommitteeSubnets(), that.getSyncCommitteeSubnets()) + && Objects.equal(getDasCustodySubnetCount(), that.getDasCustodySubnetCount()); } @Override @@ -91,7 +99,8 @@ public int hashCode() { getNodeAddress(), getEnrForkId(), getPersistentAttestationSubnets(), - getSyncCommitteeSubnets()); + getSyncCommitteeSubnets(), + getDasCustodySubnetCount()); } @Override @@ -102,6 +111,7 @@ public String toString() { .add("enrForkId", enrForkId) .add("persistentSubnets", persistentAttestationSubnets) .add("syncCommitteeSubnets", syncCommitteeSubnets) + .add("dasCustodySubnetCount", dasCustodySubnetCount) .toString(); } } diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/DiscV5Service.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/DiscV5Service.java index ce3bd5513d4..1200daf8f80 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/DiscV5Service.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/DiscV5Service.java @@ -290,9 +290,8 @@ public Optional> getDiscoveryAddresses() { updAddress, Optional.empty(), currentSchemaDefinitionsSupplier.getAttnetsENRFieldSchema().getDefault(), - currentSchemaDefinitionsSupplier - .getSyncnetsENRFieldSchema() - .getDefault()); + currentSchemaDefinitionsSupplier.getSyncnetsENRFieldSchema().getDefault(), + Optional.empty()); return MultiaddrUtil.fromDiscoveryPeerAsUdp(discoveryPeer).toString(); }) .toList(); diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverter.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverter.java index 5bc8703683e..c5973f9da47 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverter.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverter.java @@ -14,6 +14,7 @@ package tech.pegasys.teku.networking.p2p.discovery.discv5; import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.ATTESTATION_SUBNET_ENR_FIELD; +import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.DAS_CUSTODY_GROUP_COUNT_ENR_FIELD; import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.ETH2_ENR_FIELD; import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.SYNC_COMMITTEE_SUBNET_ENR_FIELD; @@ -23,6 +24,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt64; import org.ethereum.beacon.discovery.schema.EnrField; import org.ethereum.beacon.discovery.schema.IdentitySchemaInterpreter; import org.ethereum.beacon.discovery.schema.NodeRecord; @@ -73,6 +75,11 @@ private static DiscoveryPeer socketAddressToDiscoveryPeer( final SszBitvector syncCommitteeSubnets = parseField(nodeRecord, SYNC_COMMITTEE_SUBNET_ENR_FIELD, syncnetsSchema::fromBytes) .orElse(syncnetsSchema.getDefault()); + final Optional dasTotalCustodySubnetCount = + parseField( + nodeRecord, + DAS_CUSTODY_GROUP_COUNT_ENR_FIELD, + bytes -> UInt64.fromBytes(bytes).intValue()); return new DiscoveryPeer( ((Bytes) nodeRecord.get(EnrField.PKEY_SECP256K1)), @@ -80,7 +87,8 @@ private static DiscoveryPeer socketAddressToDiscoveryPeer( address, enrForkId, persistentAttestationSubnets, - syncCommitteeSubnets); + syncCommitteeSubnets, + dasTotalCustodySubnetCount); } private static Optional parseField( @@ -88,7 +96,7 @@ private static Optional parseField( try { return Optional.ofNullable((Bytes) nodeRecord.get(fieldName)).map(parse); } catch (final Exception e) { - LOG.debug("Failed to parse ENR field {}", fieldName, e); + LOG.debug("Failed to parse ENR field {}: {}", fieldName, e); return Optional.empty(); } } diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java index 1ff612e7c1b..32442d812bb 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java @@ -57,7 +57,7 @@ public class LibP2PGossipNetworkBuilder { // Enough to subscribe to three forks simultaneously so testnets can fork in subsequent epochs - @VisibleForTesting public static final int MAX_SUBSCRIBED_TOPICS = 250; + @VisibleForTesting public static final int MAX_SUBSCRIBED_TOPICS = 1000; public static LibP2PGossipNetworkBuilder create() { return new LibP2PGossipNetworkBuilder(); diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/peer/DisconnectReason.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/peer/DisconnectReason.java index 2f0775c62c4..bab2d2ad334 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/peer/DisconnectReason.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/peer/DisconnectReason.java @@ -25,7 +25,11 @@ public enum DisconnectReason { REMOTE_FAULT(GoodbyeMessage.REASON_FAULT_ERROR, false), UNRESPONSIVE(GoodbyeMessage.REASON_FAULT_ERROR, false), SHUTTING_DOWN(GoodbyeMessage.REASON_CLIENT_SHUT_DOWN, false), - RATE_LIMITING(GoodbyeMessage.REASON_RATE_LIMITING, false); + RATE_LIMITING(GoodbyeMessage.REASON_RATE_LIMITING, false), + BAD_SCORE(GoodbyeMessage.REASON_BAD_SCORE, false), + BANNED(GoodbyeMessage.REASON_BANNED, false), + NO_STATUS_RECEIVED(GoodbyeMessage.REASON_FAULT_ERROR, false), + DUPLICATE_CONNECTION(GoodbyeMessage.REASON_FAULT_ERROR, false); private final UInt64 reasonCode; private final boolean isPermanent; diff --git a/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/connection/ConnectionManagerTest.java b/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/connection/ConnectionManagerTest.java index f1972fae82d..fa59b0afd68 100644 --- a/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/connection/ConnectionManagerTest.java +++ b/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/connection/ConnectionManagerTest.java @@ -475,6 +475,7 @@ private static DiscoveryPeer createDiscoveryPeer(final Bytes peerId, final int.. new InetSocketAddress(InetAddress.getLoopbackAddress(), peerId.trimLeadingZeros().toInt()), ENR_FORK_ID, SCHEMA_DEFINITIONS_SUPPLIER.getAttnetsENRFieldSchema().ofBits(subnetIds), - SCHEMA_DEFINITIONS_SUPPLIER.getSyncnetsENRFieldSchema().getDefault()); + SCHEMA_DEFINITIONS_SUPPLIER.getSyncnetsENRFieldSchema().getDefault(), + Optional.empty()); } } diff --git a/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryNetworkTest.java b/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryNetworkTest.java index a7575f39ad9..9f6a6be42cf 100644 --- a/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryNetworkTest.java +++ b/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/discovery/DiscoveryNetworkTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.DAS_CUSTODY_GROUP_COUNT_ENR_FIELD; import java.math.BigInteger; import java.net.InetSocketAddress; @@ -290,6 +291,14 @@ public void nodeIdMustBeWrappedInUint256(final String nodeIdValue) { .isEqualTo(nodeIdValue); } + @ParameterizedTest + @MethodSource("getCscFixtures") + public void cscIsCorrectlyEncoded(final String hexString, final Integer csc) { + discoveryNetwork.setDASTotalCustodySubnetCount(csc); + verify(discoveryService) + .updateCustomENRField(DAS_CUSTODY_GROUP_COUNT_ENR_FIELD, Bytes.fromHexString(hexString)); + } + public DiscoveryPeer createDiscoveryPeer(final Optional maybeForkId) { final SszBitvector syncCommitteeSubnets = schemaDefinitions.getSyncnetsENRFieldSchema().getDefault(); @@ -300,7 +309,8 @@ public DiscoveryPeer createDiscoveryPeer(final Optional maybeForkId) maybeForkId, SszBitvectorSchema.create(spec.getNetworkingConfig().getAttestationSubnetCount()) .getDefault(), - syncCommitteeSubnets); + syncCommitteeSubnets, + Optional.empty()); } public static Stream provideNodeIds() { @@ -311,4 +321,12 @@ public static Stream provideNodeIds() { Arguments.of( "57467522110468688239177851250859789869070302005900722885377252304169193209346")); } + + private static Stream getCscFixtures() { + return Stream.of( + Arguments.of("0x", 0), + Arguments.of("0x80", 128), + Arguments.of("0x8c", 140), + Arguments.of("0x0190", 400)); + } } diff --git a/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverterTest.java b/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverterTest.java index 41a71b6b278..afaa683788e 100644 --- a/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverterTest.java +++ b/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverterTest.java @@ -15,6 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.ATTESTATION_SUBNET_ENR_FIELD; +import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.DAS_CUSTODY_GROUP_COUNT_ENR_FIELD; import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.ETH2_ENR_FIELD; import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork.SYNC_COMMITTEE_SUBNET_ENR_FIELD; @@ -23,6 +24,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Optional; +import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt64; import org.ethereum.beacon.discovery.schema.EnrField; @@ -30,6 +32,9 @@ import org.ethereum.beacon.discovery.schema.NodeRecord; import org.ethereum.beacon.discovery.schema.NodeRecordFactory; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; import tech.pegasys.teku.infrastructure.ssz.schema.collections.SszBitvectorSchema; import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; @@ -75,7 +80,8 @@ public void shouldConvertRealEnrToDiscoveryPeer() throws Exception { new InetSocketAddress(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}), 9000), Optional.empty(), ATTNETS, - SYNCNETS); + SYNCNETS, + Optional.empty()); assertThat(CONVERTER.convertToDiscoveryPeer(nodeRecord, false, SCHEMA_DEFINITIONS)) .contains(expectedPeer); } @@ -117,7 +123,8 @@ public void shouldUseV4PortIfV6PortSpecifiedWithNoV6Ip() { new InetSocketAddress("::1", 30303), ENR_FORK_ID, ATTNETS, - SYNCNETS)); + SYNCNETS, + Optional.empty())); } @Test @@ -151,7 +158,8 @@ public void shouldConvertIpV4Record() { new InetSocketAddress("129.24.31.22", 1234), ENR_FORK_ID, ATTNETS, - SYNCNETS)); + SYNCNETS, + Optional.empty())); } @Test @@ -179,7 +187,8 @@ public void shouldConvertIpV6Record() { new InetSocketAddress("::1", 1234), ENR_FORK_ID, ATTNETS, - SYNCNETS)); + SYNCNETS, + Optional.empty())); } @Test @@ -199,7 +208,8 @@ public void shouldConvertDualStackRecordIfIpV6IsNotSupported() { new InetSocketAddress("127.0.0.1", 1234), ENR_FORK_ID, ATTNETS, - SYNCNETS)); + SYNCNETS, + Optional.empty())); } @Test @@ -220,7 +230,8 @@ public void shouldConvertAttnets() { new InetSocketAddress("::1", 1234), ENR_FORK_ID, persistentSubnets, - SYNCNETS)); + SYNCNETS, + Optional.empty())); } @Test @@ -241,7 +252,8 @@ public void shouldUseEmptyAttnetsWhenFieldValueIsInvalid() { new InetSocketAddress("::1", 1234), ENR_FORK_ID, ATT_SUBNET_SCHEMA.getDefault(), - SYNCNETS)); + SYNCNETS, + Optional.empty())); } @Test @@ -262,7 +274,8 @@ public void shouldConvertSyncnets() { new InetSocketAddress("::1", 1234), ENR_FORK_ID, ATTNETS, - syncnets)); + syncnets, + Optional.empty())); } @Test @@ -285,7 +298,8 @@ public void shouldUseEmptySyncnetsFieldValueIsInvalid() { new InetSocketAddress("::1", 1234), ENR_FORK_ID, ATTNETS, - SYNCNETS)); + SYNCNETS, + Optional.empty())); } @Test @@ -306,7 +320,8 @@ public void shouldConvertEnrForkId() { new InetSocketAddress("::1", 1234), Optional.of(enrForkId), ATTNETS, - SYNCNETS)); + SYNCNETS, + Optional.empty())); } @Test @@ -326,7 +341,28 @@ public void shouldNotHaveEnrForkIdWhenValueIsInvalid() { new InetSocketAddress("::1", 1234), Optional.empty(), ATTNETS, - SYNCNETS)); + SYNCNETS, + Optional.empty())); + } + + @ParameterizedTest + @MethodSource("getCgcFixtures") + public void shouldDecodeCgcCorrectly(final String hexString, final Integer csc) { + assertThat( + convertNodeRecordWithFields( + false, + new EnrField(EnrField.IP_V4, Bytes.wrap(new byte[] {127, 0, 0, 1})), + new EnrField(EnrField.TCP, 1234), + new EnrField(DAS_CUSTODY_GROUP_COUNT_ENR_FIELD, Bytes.fromHexString(hexString)))) + .contains( + new DiscoveryPeer( + PUB_KEY, + NODE_ID, + new InetSocketAddress("127.0.0.1", 1234), + Optional.empty(), + ATTNETS, + SYNCNETS, + Optional.of(csc))); } private Optional convertNodeRecordWithFields( @@ -341,4 +377,13 @@ private NodeRecord createNodeRecord(final EnrField... fields) { fieldList.add(new EnrField(EnrField.PKEY_SECP256K1, PUB_KEY)); return NodeRecordFactory.DEFAULT.createFromValues(UInt64.ZERO, fieldList); } + + private static Stream getCgcFixtures() { + return Stream.of( + Arguments.of("0x00", 0), + Arguments.of("0x", 0), + Arguments.of("0x80", 128), + Arguments.of("0x8c", 140), + Arguments.of("0x0190", 400)); + } } diff --git a/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/libp2p/MultiaddrUtilTest.java b/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/libp2p/MultiaddrUtilTest.java index 2cd9db8404e..b2cc404011b 100644 --- a/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/libp2p/MultiaddrUtilTest.java +++ b/networking/p2p/src/test/java/tech/pegasys/teku/networking/p2p/libp2p/MultiaddrUtilTest.java @@ -84,7 +84,8 @@ public void fromDiscoveryPeer_shouldConvertIpV4Peer() throws Exception { new InetSocketAddress(InetAddress.getByAddress(ipAddress), port), ENR_FORK_ID, PERSISTENT_ATTESTATION_SUBNETS, - SYNC_COMMITTEE_SUBNETS); + SYNC_COMMITTEE_SUBNETS, + Optional.empty()); final Multiaddr result = MultiaddrUtil.fromDiscoveryPeer(peer); assertThat(result).isEqualTo(Multiaddr.fromString("/ip4/123.34.58.22/tcp/5883/p2p/" + PEER_ID)); assertThatComponent(result, Protocol.IP4).isEqualTo(ipAddress); @@ -103,7 +104,8 @@ public void fromDiscoveryPeer_shouldConvertIpV6Peer() throws Exception { new InetSocketAddress(InetAddress.getByAddress(ipAddress), port), ENR_FORK_ID, PERSISTENT_ATTESTATION_SUBNETS, - SYNC_COMMITTEE_SUBNETS); + SYNC_COMMITTEE_SUBNETS, + Optional.empty()); final Multiaddr result = MultiaddrUtil.fromDiscoveryPeer(peer); assertThat(result) .isEqualTo(Multiaddr.fromString("/ip6/3300:4:5000:780:0:12:0:1/tcp/5883/p2p/" + PEER_ID)); @@ -122,7 +124,8 @@ public void fromDiscoveryPeer_shouldConvertRealPeer() throws Exception { new InetSocketAddress(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}), 9000), ENR_FORK_ID, PERSISTENT_ATTESTATION_SUBNETS, - SYNC_COMMITTEE_SUBNETS); + SYNC_COMMITTEE_SUBNETS, + Optional.empty()); final Multiaddr expectedMultiAddr = Multiaddr.fromString( "/ip4/127.0.0.1/tcp/9000/p2p/16Uiu2HAmR4wQRGWgCNy5uzx7HfuV59Q6X1MVzBRmvreuHgEQcCnF"); @@ -139,7 +142,8 @@ public void fromDiscoveryPeerAsUdp_shouldConvertDiscoveryPeer() throws Exception new InetSocketAddress(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}), 9000), ENR_FORK_ID, PERSISTENT_ATTESTATION_SUBNETS, - SYNC_COMMITTEE_SUBNETS); + SYNC_COMMITTEE_SUBNETS, + Optional.empty()); final Multiaddr expectedMultiAddr = Multiaddr.fromString( "/ip4/127.0.0.1/udp/9000/p2p/16Uiu2HAmR4wQRGWgCNy5uzx7HfuV59Q6X1MVzBRmvreuHgEQcCnF"); diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java index cb8db1ac238..2ef304309dc 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java @@ -86,6 +86,7 @@ import tech.pegasys.teku.networking.eth2.gossip.subnets.StableSubnetSubscriber; import tech.pegasys.teku.networking.eth2.gossip.subnets.SyncCommitteeSubscriptionManager; import tech.pegasys.teku.networking.eth2.mock.NoOpEth2P2PNetwork; +import tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods.MetadataMessagesFactory; import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig; import tech.pegasys.teku.networks.Eth2NetworkConfiguration; import tech.pegasys.teku.networks.StateBoostrapConfig; @@ -136,6 +137,8 @@ import tech.pegasys.teku.statetransition.block.BlockManager; import tech.pegasys.teku.statetransition.block.FailedExecutionPool; import tech.pegasys.teku.statetransition.block.ReceivedBlockEventsChannel; +import tech.pegasys.teku.statetransition.datacolumns.CustodyGroupCountManager; +import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; import tech.pegasys.teku.statetransition.forkchoice.ForkChoice; import tech.pegasys.teku.statetransition.forkchoice.ForkChoiceNotifier; import tech.pegasys.teku.statetransition.forkchoice.ForkChoiceNotifierImpl; @@ -284,6 +287,7 @@ public class BeaconChainController extends Service implements BeaconChainControl protected volatile KZG kzg; protected volatile BlobSidecarManager blobSidecarManager; protected volatile BlobSidecarGossipValidator blobSidecarValidator; + // TODO-fulu Add CustodyGroupCountManagerLateInit protected volatile Optional terminalPowBlockMonitor = Optional.empty(); protected volatile ProposersDataManager proposersDataManager; protected volatile KeyValueStore keyValueStore; @@ -1144,6 +1148,9 @@ protected void initP2PNetwork() { .config(beaconConfig.p2pConfig()) .eventChannels(eventChannels) .combinedChainDataClient(combinedChainDataClient) + .dataColumnSidecarCustody(DataColumnSidecarByRootCustody.NOOP) + .custodyGroupCountManager(CustodyGroupCountManager.NOOP) + .metadataMessagesFactory(new MetadataMessagesFactory()) .gossipedBlockProcessor(blockManager::validateAndImportBlock) .gossipedBlobSidecarProcessor(blobSidecarManager::validateAndPrepareForBlockImport) .gossipedAttestationProcessor(attestationManager::addAttestation)