feat(subnet-splitting): implement PostSplit CUPs making & validation - #10980
feat(subnet-splitting): implement PostSplit CUPs making & validation#10980pierugo-dfinity wants to merge 34 commits into
PostSplit CUPs making & validation#10980Conversation
5097309 to
eef64e2
Compare
eef64e2 to
e8bb30d
Compare
8d7e105 to
7bee851
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds support for “PostSplit” catch-up packages (CUPs) to enable subnet splitting: it records subnet-splitting status in DKG summaries, creates post-split CUP shares/CUPs with on-the-fly summary + dummy random beacon, and extends validation/aggregation/priority logic so replicas can accept the correct CUP artifacts during a split.
Changes:
- Extend DKG summary construction to carry
SubnetSplittingStatus(incl.PostSplit) and add utilities to compute post-split subnet assignment from the registry. - Introduce
CatchUpPackageTypeand update CUP share creation, share aggregation, and artifact validation to handle post-split CUPs (including dummy random beacons and height/state-hash handling). - Update consensus “bouncer”/pool-reader helpers and add/adjust tests across consensus components to cover subnet-splitting CUP behavior.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| rs/types/types/src/consensus/dkg.rs | Thread SubnetSplittingStatus through DkgSummary construction; add creation error variant for subnet-splitting status retrieval. |
| rs/types/types/src/consensus/catchup.rs | Introduce CatchUpPackageType (Normal vs PostSplit { new_subnet_id }) for CUP maker behavior. |
| rs/test_utilities/consensus/src/fake.rs | Update Fake DkgSummary construction to pass default SubnetSplittingStatus. |
| rs/replica/setup_ic_network/src/lib.rs | Pass registry_client and replica_config to consensus components that now require them. |
| rs/replay/src/validator.rs | Pass registry and replica_cfg through to validator construction paths. |
| rs/registry/helpers/src/node.rs | Add NodeRegistry::get_subnet_id_from_node_id helper plus unit tests. |
| rs/consensus/utils/src/subnet_splitting.rs | Add helpers to detect scheduled splits and compute post-split subnet assignment; expand tests. |
| rs/consensus/utils/src/pool_reader.rs | Add get_next_summary_height() and tests to distinguish summary height progression from CUP height. |
| rs/consensus/utils/src/lib.rs | Add aggregate_with_threshold() to aggregate shares when committee threshold can’t be derived from pool membership (post-split CUPs). |
| rs/consensus/tests/payload.rs | Update test consensus wiring to pass registry_client and replica_config where required. |
| rs/consensus/tests/framework/runner.rs | Update consensus runner wiring to pass registry_client and replica_config where required. |
| rs/consensus/src/consensus/validator.rs | Extend CUP/CUP-share validation for subnet splitting: correct subnet filtering post-split, post-split block/beacon handling, and associated tests. |
| rs/consensus/src/consensus/share_aggregator.rs | Aggregate post-split CUP shares by deriving transcript/threshold from the post-split summary block; log aggregation events; update tests. |
| rs/consensus/src/consensus/priority.rs | Adjust bouncer logic to fetch CUP shares up to the next summary height and exempt CUP shares from the “validator-CUP gap”; add tests. |
| rs/consensus/src/consensus/catchup_package_maker.rs | Implement post-split CUP share creation (post-split summary + dummy beacon), factoring out CUP-type detection and helper constructors; expand tests. |
| rs/consensus/src/consensus/block_maker.rs | Minor test flow adjustment around when summary blocks are produced in subnet-splitting-related tests. |
| rs/consensus/src/consensus.rs | Wire new dependencies (registry_client, replica_config) into consensus subcomponents (CUP maker, share aggregator, etc.). |
| rs/consensus/idkg/src/payload_builder.rs | Update test DKG summary construction call site to pass default SubnetSplittingStatus. |
| rs/consensus/dkg/src/remote.rs | Update test DKG summary construction call site to pass default SubnetSplittingStatus. |
| rs/consensus/dkg/src/payload_validator.rs | Update test/component construction paths to pass registry and replica_config where required. |
| rs/consensus/dkg/src/payload_builder.rs | Record subnet-splitting status in created summaries; add post-split summary construction from registry CUP contents; add tests. |
| rs/consensus/dkg/src/lib.rs | Update DKG tests to pass registry/replica_config into DkgKeyManager construction helper(s). |
| rs/consensus/dkg/src/dkg_key_manager.rs | Load post-split transcripts (from post-split summary) instead of “Scheduled” summary transcripts during a split; add tests. |
| rs/consensus/dkg/Cargo.toml | Add rstest dependency for DKG test expansions. |
| rs/consensus/dkg/BUILD.bazel | Add rstest to Bazel test deps. |
| Cargo.lock | Add rstest to the resolved dependency set for affected crates. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 26 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
rs/consensus/src/consensus/priority.rs:115
- This now requests every advertised CUP share between the current CUP and the next summary, although only shares at already-finalized heights or exactly the synthetic post-split height can be useful. Shares for intermediate future/data heights fail with
FinalizedBlockNotFoundand remain unvalidated, allowing peers to consume pool/network resources across the whole DKG interval. Preserve the old finalized-height condition and add onlyheight == next_summary_heightfor the post-split case.
} else if height <= next_summary_height {
The post-split transcript loading in the DKG key manager is being split out into `pierugo/subnet-splitting/dkg-key-manager`, which is stacked on top of this branch.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
rs/consensus/utils/src/subnet_splitting.rs:122
- The contained subnet ID is omitted from this error's display text, so failures only report that a membership change occurred without identifying the unexpected assignment. Include
{0}to make production diagnostics actionable.
#[error("The node changed subnets during subnet splitting")]
DisallowedMembershipChange(SubnetId),
|
✅ No security or compliance issues detected. Reviewed everything up to ae301cb. Security Overview
Detected Code ChangesThe diff is too large to display a summary of code changes. |
|
✅ No security or compliance issues detected. Reviewed everything up to ae301cb. Security Overview
Detected Code ChangesThe diff is too large to display a summary of code changes. |
| Arc::clone(®istry_client), | ||
| registry_client.clone(), |
There was a problem hiding this comment.
I think the convention is actually Arc::clone, but we don't seem to follow it so no action required.
| debug!( | ||
| self.log, | ||
| "Proposing a CatchUpPackageShare (type: {cup_type:?}) at height {height}" | ||
| ); |
There was a problem hiding this comment.
I don't think debug logs show up anywhere
| // Skip if this node has already made a share | ||
| if pool | ||
| .get_catch_up_package_shares(height) | ||
| .any(|share| share.signature.signer == my_node_id) | ||
| { | ||
| return None; | ||
| } |
There was a problem hiding this comment.
Maybe we can still do this check more towards the top of the function
| self.replica_config.node_id, | ||
| &start_block, | ||
| ) | ||
| .inspect_err(|err| warn!(self.log, "Failed to get the catch up package type: {err}")) |
There was a problem hiding this comment.
Adding every_n_seconds would avoid flooding the logs, if this ever were to happen. Same for the logs below
| state_manager, | ||
| .. | ||
| } = DependenciesBuilder::multiple_subnets(pool_config, records) | ||
| .with_replica_config(ReplicaConfig { | ||
| node_id: NODE_5, | ||
| subnet_id: SOURCE_SUBNET_ID, | ||
| }) | ||
| .build(); | ||
|
|
||
| let fake_state_hash = CryptoHashOfState::from(CryptoHash(vec![1, 2, 3])); | ||
| state_manager | ||
| .get_mut() | ||
| .expect_get_state_hash_at() | ||
| .return_const(Ok(fake_state_hash.clone())); | ||
|
|
||
| let message_routing = FakeMessageRouting::new(); | ||
| *message_routing.next_batch_height.write().unwrap() = Height::from(2); | ||
| let message_routing = Arc::new(message_routing); | ||
|
|
||
| let cup_maker = CatchUpPackageMaker::new( | ||
| ReplicaConfig { | ||
| node_id: NODE_5, | ||
| subnet_id: SOURCE_SUBNET_ID, | ||
| }, |
There was a problem hiding this comment.
| state_manager, | |
| .. | |
| } = DependenciesBuilder::multiple_subnets(pool_config, records) | |
| .with_replica_config(ReplicaConfig { | |
| node_id: NODE_5, | |
| subnet_id: SOURCE_SUBNET_ID, | |
| }) | |
| .build(); | |
| let fake_state_hash = CryptoHashOfState::from(CryptoHash(vec![1, 2, 3])); | |
| state_manager | |
| .get_mut() | |
| .expect_get_state_hash_at() | |
| .return_const(Ok(fake_state_hash.clone())); | |
| let message_routing = FakeMessageRouting::new(); | |
| *message_routing.next_batch_height.write().unwrap() = Height::from(2); | |
| let message_routing = Arc::new(message_routing); | |
| let cup_maker = CatchUpPackageMaker::new( | |
| ReplicaConfig { | |
| node_id: NODE_5, | |
| subnet_id: SOURCE_SUBNET_ID, | |
| }, | |
| state_manager, | |
| replica_config, | |
| .. | |
| } = DependenciesBuilder::multiple_subnets(pool_config, records) | |
| .with_replica_config(ReplicaConfig { | |
| node_id: NODE_5, | |
| subnet_id: SOURCE_SUBNET_ID, | |
| }) | |
| .build(); | |
| let fake_state_hash = CryptoHashOfState::from(CryptoHash(vec![1, 2, 3])); | |
| state_manager | |
| .get_mut() | |
| .expect_get_state_hash_at() | |
| .return_const(Ok(fake_state_hash.clone())); | |
| let message_routing = FakeMessageRouting::new(); | |
| *message_routing.next_batch_height.write().unwrap() = Height::from(2); | |
| let message_routing = Arc::new(message_routing); | |
| let cup_maker = CatchUpPackageMaker::new( | |
| replica_config, |
| debug_assert!(matches!( | ||
| last_summary.subnet_splitting_status(), | ||
| SubnetSplittingStatus::Scheduled(..) | ||
| )); |
There was a problem hiding this comment.
Should we return an error in this case?
| FailedToGetSubnetIdFromTheRegistry(RegistryVersion, RegistryClientError), | ||
| #[error("The node is unassigned at registry version {0}")] | ||
| Unassigned(RegistryVersion), | ||
| #[error("The node changed subnets during subnet splitting")] |
There was a problem hiding this comment.
| #[error("The node changed subnets during subnet splitting")] | |
| #[error("The node changed to subnet {0} during the split, which is neither the source nor destination subnet")] |
| warn!( | ||
| self.log, |
There was a problem hiding this comment.
| warn!( | |
| self.log, | |
| warn!( | |
| every_n_seconds => ..., | |
| self.log, |
| let shares = pool | ||
| .get_catch_up_package_shares(block.height()) | ||
| .map(|share| Signed { | ||
| content: CatchUpContent::from_share_content(share.content, block.clone()), |
There was a problem hiding this comment.
Should we keep the check that the hash in the share actually matches the block (that check was previously hidden in get_block)
| let subnet_splitting_status = get_subnet_splitting_status( | ||
| subnet_id, | ||
| registry_client, | ||
| registry_version, | ||
| validation_context.registry_version, | ||
| )?; | ||
|
|
There was a problem hiding this comment.
If the DKG interval is smaller than ACCEPTABLE_NOTARIZATION_CERTIFICATION_GAP, could it happen that we create valid summary at the "post-split CUP height" the usual way?
This PR implements the core logic behind subnet splitting in Consensus. It affects the following components, in suggested order of review:
DKG summary payload builder
When a subnet split is detected in the registry, the validation context's registry version is frozen until reaching the next summary, at which point it is bumped to precisely the version it was scheduled. The payload builder thus fills the
subnet_splitting_statusfield withScheduledif there is a scheduled split at the validation context's registry version. Otherwise, it fills it withNotScheduled. It should never fill it withPostSplit, this will be the role of the CUP-shares maker.CUP-shares maker
Instead of unconditionally creating a CUP for finalized summary blocks, the CUP-shares maker now looks at the block's
subnet_splitting_statusfield. If it isScheduled, then it will not create a CUP for it. It will instead create a CUP for a "post-split" block forH+500which is not notarized/finalized like usual, but is instead computed locally (it is deterministic) and its hash is gossiped as part of the CUP share.CUP-shares makers will create such shares based on the subnet they will take part in after the split. This means that there will be two distinct sets of CUP shares gossiped around, one for each resulting subnet. Each set of CUP shares contains the relevant catchup information for the new subnet: the DKG transcripts taken from the registry (not the ones from the original summary), the state hash and oldest registry version in use from the split state at the height of the original summary, and a dummy random beacon.
CUP-shares validator
As soon as the
Scheduledsummary is reached, half the CUP-shares maker will create shares for a CUP A and the other half for a CUP B. The role of the CUP-shares validator will be to invalidate the CUP shares intended for the other subnet and validate the ones intended for its own subnet. To do so, it recreates the post-split block, state hash, etc and only validates shares which match these. The other subnet's CUP shares will be invalidated as the block, beacon and state hash will be different.CUP-shares aggregator
This component is not changed by much but its logic still had to be slightly rewritten to consider these new post-split CUPs.
CUP validator
Similar to the CUP-shares validator, here as soon as enough shares for the two CUPs are gossiped, they will be aggregated and the two CUPs will be gossiped around. The role of the CUP validator will be to invalidate the CUP intended for the other subnet and validate the one intended for its own subnet. To do so, we look at the CUP's block's
subnet_splitting_statusas it contains the subnet the CUP is intended for (PostSplit { new_subnet_id }).Bouncer
Finally, the bouncer needs to tell P2P to start downloading CUP shares also for the next summary height since this is the height at which the post-split CUP will be created.
Remaining changes
Follow-ups will adapt
Scheduledsummary is reached and actually be able to sign the post-split CUP sharesReplicaConfig.subnet_id