Skip to content

feat(subnet-splitting): implement PostSplit CUPs making & validation - #10980

Open
pierugo-dfinity wants to merge 34 commits into
masterfrom
pierugo/subnet-splitting/cup-maker
Open

feat(subnet-splitting): implement PostSplit CUPs making & validation#10980
pierugo-dfinity wants to merge 34 commits into
masterfrom
pierugo/subnet-splitting/cup-maker

Conversation

@pierugo-dfinity

@pierugo-dfinity pierugo-dfinity commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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
  • CUP-shares maker
  • CUP-shares validator
  • CUP-shares aggregator
  • CUP validator
  • Bouncer

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_status field with Scheduled if there is a scheduled split at the validation context's registry version. Otherwise, it fills it with NotScheduled. It should never fill it with PostSplit, 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_status field. If it is Scheduled, then it will not create a CUP for it. It will instead create a CUP for a "post-split" block for H+500 which 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 Scheduled summary 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_status as 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

  • DKG key manager to preemptively load the post-split transcripts as soon as the Scheduled summary is reached and actually be able to sign the post-split CUP shares
  • Batch delivery to tell DSM to actually split the state
  • The orchestrator to restart destination replicas and update their ReplicaConfig.subnet_id

@github-actions github-actions Bot added the feat label Jul 31, 2026
@pierugo-dfinity
pierugo-dfinity changed the base branch from master to pierugo/subnet-splitting/freeze-registry-version July 31, 2026 07:42
@pierugo-dfinity
pierugo-dfinity force-pushed the pierugo/subnet-splitting/cup-maker branch from 5097309 to eef64e2 Compare July 31, 2026 09:24
@pierugo-dfinity
pierugo-dfinity force-pushed the pierugo/subnet-splitting/cup-maker branch from eef64e2 to e8bb30d Compare August 4, 2026 11:54
Base automatically changed from pierugo/subnet-splitting/freeze-registry-version to master August 6, 2026 15:14
@pierugo-dfinity
pierugo-dfinity force-pushed the pierugo/subnet-splitting/cup-maker branch from 8d7e105 to 7bee851 Compare August 14, 2026 16:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CatchUpPackageType and 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.

Comment thread rs/types/types/src/consensus/catchup.rs Outdated
Comment thread rs/consensus/src/consensus/catchup_package_maker.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FinalizedBlockNotFound and remain unvalidated, allowing peers to consume pool/network resources across the whole DKG interval. Preserve the old finalized-height condition and add only height == next_summary_height for the post-split case.
            } else if height <= next_summary_height {

Comment thread rs/consensus/dkg/src/dkg_key_manager.rs Outdated
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

Comment thread rs/consensus/src/consensus/validator.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Comment thread rs/consensus/src/consensus/priority.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

@pierugo-dfinity
pierugo-dfinity marked this pull request as ready for review August 19, 2026 07:47
@pierugo-dfinity
pierugo-dfinity requested review from a team as code owners August 19, 2026 07:47
@zeropath-ai

zeropath-ai Bot commented Aug 19, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to ae301cb.

Security Overview
Detected Code Changes

The diff is too large to display a summary of code changes.

@zeropath-ai

zeropath-ai Bot commented Aug 19, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to ae301cb.

Security Overview
Detected Code Changes

The diff is too large to display a summary of code changes.

Comment on lines -275 to +276
Arc::clone(&registry_client),
registry_client.clone(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the convention is actually Arc::clone, but we don't seem to follow it so no action required.

Comment on lines +320 to +323
debug!(
self.log,
"Proposing a CatchUpPackageShare (type: {cup_type:?}) at height {height}"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think debug logs show up anywhere

Comment on lines +293 to +299
// 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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding every_n_seconds would avoid flooding the logs, if this ever were to happen. Same for the logs below

Comment on lines +1198 to +1221
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,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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,

Comment on lines +943 to +946
debug_assert!(matches!(
last_summary.subnet_splitting_status(),
SubnetSplittingStatus::Scheduled(..)
));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[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")]

Comment on lines +158 to +159
warn!(
self.log,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the check that the hash in the share actually matches the block (that check was previously hidden in get_block)

Comment on lines +545 to +551
let subnet_splitting_status = get_subnet_splitting_status(
subnet_id,
registry_client,
registry_version,
validation_context.registry_version,
)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants