Skip to content

Commit 06d6a7b

Browse files
feat(subnet-splitting): freeze registry version until summary is reached (#10938)
Because Consensus passes the block’s validation context's registry version to other components (including DSM), it could happen that a subnet split has not yet happened from Consensus' point of view but it would have from the other components' point of view. While it does not necessarily have to lead to misbehavior, this PR proposes to "freeze" the registry version in the validation context from the moment a split is detected in the registry until the next summary block (which will have a `Scheduled` status). More precisely, we will not bump the registry version in the validation context of any data block in a DKG interval to any version of the registry which has a flag indicating that the subnet splitting should happen. We only bump it when reaching the summary block (to exactly the registry version the split was scheduled at, not a more recent one).
1 parent 5aeb9e5 commit 06d6a7b

9 files changed

Lines changed: 959 additions & 47 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/consensus/src/consensus/block_maker.rs

Lines changed: 328 additions & 11 deletions
Large diffs are not rendered by default.

rs/consensus/src/consensus/malicious_consensus.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ impl ConsensusImpl {
148148
// Get the subnet records that are relevant to making a block
149149
let stable_registry_version = self
150150
.block_maker
151-
.get_stable_registry_version(parent.as_ref())?;
151+
.get_stable_registry_version(parent.as_ref(), &last_summary_block)?;
152152
let subnet_records = block_maker::subnet_records_for_registry_version(
153153
&self.block_maker,
154154
registry_version,

rs/consensus/src/consensus/validator.rs

Lines changed: 305 additions & 0 deletions
Large diffs are not rendered by default.

rs/consensus/utils/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ rust_library(
2929
"@crate_index//:rand",
3030
"@crate_index//:rayon",
3131
"@crate_index//:slog",
32+
"@crate_index//:thiserror",
3233
],
3334
)
3435

@@ -50,6 +51,7 @@ rust_test(
5051
"//rs/monitoring/metrics",
5152
"//rs/protobuf",
5253
"//rs/registry/helpers",
54+
"//rs/registry/keys",
5355
"//rs/replicated_state",
5456
"//rs/test_utilities",
5557
"//rs/test_utilities/consensus",
@@ -64,6 +66,7 @@ rust_test(
6466
"@crate_index//:prometheus",
6567
"@crate_index//:rand",
6668
"@crate_index//:rayon",
69+
"@crate_index//:rstest",
6770
"@crate_index//:slog",
6871
],
6972
)

rs/consensus/utils/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ ic-types = { path = "../../types/types" }
1919
prometheus = { workspace = true }
2020
rand = { workspace = true }
2121
slog = { workspace = true }
22+
thiserror = { workspace = true }
2223

2324
[dev-dependencies]
2425
assert_matches = { workspace = true }
2526
ic-consensus-mocks = { path = "../mocks" }
2627
ic-management-canister-types-private = { path = "../../types/management_canister_types" }
28+
ic-registry-keys = { path = "../../registry/keys" }
2729
ic-test-utilities = { path = "../../test_utilities" }
2830
ic-test-utilities-consensus = { path = "../../test_utilities/consensus" }
2931
ic-test-utilities-registry = { path = "../../test_utilities/registry" }
3032
ic-test-utilities-state = { path = "../../test_utilities/state" }
3133
ic-test-utilities-time = { path = "../../test_utilities/time" }
3234
ic-test-utilities-types = { path = "../../test_utilities/types" }
3335
ic-types-cycles = { path = "../../types/cycles" }
36+
rstest = { workspace = true }

rs/consensus/utils/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod chain_key;
2525
pub mod crypto;
2626
pub mod membership;
2727
pub mod pool_reader;
28+
pub mod subnet_splitting;
2829

2930
/// When purging consensus or certification artifacts, we always keep a
3031
/// minimum chain length below the catch-up height.
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
use ic_interfaces_registry::RegistryClient;
2+
use ic_protobuf::{
3+
proxy::ProxyDecodeError, registry::subnet::v1::catch_up_package_contents::CupType,
4+
};
5+
use ic_registry_client_helpers::subnet::SubnetRegistry;
6+
use ic_types::{
7+
RegistryVersion, SubnetId, consensus::SubnetSplittingArgs, registry::RegistryClientError,
8+
};
9+
use thiserror::Error;
10+
11+
#[derive(Debug, Clone, PartialEq, Eq)]
12+
pub enum Status {
13+
NotScheduled,
14+
Scheduled {
15+
destination_subnet_id: SubnetId,
16+
/// The registry version at which the subnet was scheduled to be split
17+
scheduled_at: RegistryVersion,
18+
},
19+
}
20+
21+
#[derive(Debug, Error)]
22+
pub enum StatusError {
23+
#[error("Error while getting CatchUpContents at registry version {0}: {1:?}")]
24+
FailedToGetCatchUpContents(RegistryVersion, RegistryClientError),
25+
#[error("CatchUpContents not found at registry version: {0}")]
26+
CatchUpContentsMissingInRegistry(RegistryVersion),
27+
#[error("Failed to deserialize CatchUpContents: {0}")]
28+
CatchUpContentsDeserializationError(ProxyDecodeError),
29+
}
30+
31+
/// Returns whether a split of `subnet_id` is still pending, as seen from
32+
/// `looked_up_registry_version`.
33+
///
34+
/// A [`CupType::SubnetSplitting`] record is never deleted — a later split, a recovery or the
35+
/// genesis record just overwrite it — so its presence alone doesn't mean the split is still ahead
36+
/// of us. Three versions decide that:
37+
///
38+
/// * `looked_up_registry_version` is only an upper bound: the lookup returns the latest CUP
39+
/// contents record written at or below it.
40+
/// * That record's own version is the version the subnet must adopt for the split to happen, and
41+
/// is reported as `scheduled_at`.
42+
/// * `last_summary_block_registry_version` is the watermark: the block maker bumps the registry
43+
/// version to `scheduled_at` exactly at the summary block starting the split (see
44+
/// `BlockMaker::get_stable_registry_version`), so a record at or below the last summary's version
45+
/// describes a split already picked up — [`Status::NotScheduled`].
46+
///
47+
/// Two consequences: a lookup at or below `last_summary_block_registry_version` never reports
48+
/// [`Status::Scheduled`], and a fixed `looked_up_registry_version` flips to
49+
/// [`Status::NotScheduled`] once a summary block adopts `scheduled_at`.
50+
/// Said differently, the following invariant holds for a returned [`Status::Scheduled`] value:
51+
/// `last_summary_block_registry_version < scheduled_at <= looked_up_registry_version`.
52+
pub fn get_status(
53+
registry_client: &dyn RegistryClient,
54+
subnet_id: SubnetId,
55+
last_summary_block_registry_version: RegistryVersion,
56+
looked_up_registry_version: RegistryVersion,
57+
) -> Result<Status, StatusError> {
58+
let versioned_record = registry_client
59+
.get_cup_contents(subnet_id, looked_up_registry_version)
60+
.map_err(|err| StatusError::FailedToGetCatchUpContents(looked_up_registry_version, err))?;
61+
62+
let Some(contents) = versioned_record.value else {
63+
return Err(StatusError::CatchUpContentsMissingInRegistry(
64+
looked_up_registry_version,
65+
));
66+
};
67+
68+
let Some(CupType::SubnetSplitting(subnet_splitting_args_proto)) = contents.cup_type else {
69+
return Ok(Status::NotScheduled);
70+
};
71+
72+
if versioned_record.version <= last_summary_block_registry_version {
73+
// The last summary block already references this version, so this record corresponds to a
74+
// past subnet split rather than a pending one.
75+
return Ok(Status::NotScheduled);
76+
}
77+
78+
let subnet_splitting_args = SubnetSplittingArgs::try_from(subnet_splitting_args_proto)
79+
.map_err(StatusError::CatchUpContentsDeserializationError)?;
80+
81+
Ok(Status::Scheduled {
82+
destination_subnet_id: subnet_splitting_args.destination_subnet_id,
83+
scheduled_at: versioned_record.version,
84+
})
85+
}
86+
87+
#[cfg(test)]
88+
mod tests {
89+
use ic_protobuf::registry::subnet::v1::CatchUpPackageContents;
90+
use ic_protobuf::registry::subnet::v1::{GenesisArgs, RecoveryArgs};
91+
use ic_registry_keys::make_catch_up_package_contents_key;
92+
use ic_test_utilities_registry::{SubnetRecordBuilder, setup_registry_non_final};
93+
use ic_test_utilities_types::ids::{NODE_1, SUBNET_1, SUBNET_2};
94+
use ic_types::subnet_id_into_protobuf;
95+
use rstest::rstest;
96+
use std::sync::Arc;
97+
98+
const SOURCE_SUBNET_ID: SubnetId = SUBNET_1;
99+
const DESTINATION_SUBNET_ID: SubnetId = SUBNET_2;
100+
const REGISTRY_CUP_REGISTRY_VERSION: RegistryVersion = RegistryVersion::new(2);
101+
102+
use super::*;
103+
104+
fn set_up_registry(cup_type: Option<CupType>) -> Arc<dyn RegistryClient> {
105+
let (registry_data_provider, registry) = setup_registry_non_final(
106+
SOURCE_SUBNET_ID,
107+
(1..=REGISTRY_CUP_REGISTRY_VERSION.increment().get())
108+
.map(|version| (version, SubnetRecordBuilder::from(&[NODE_1]).build()))
109+
.collect(),
110+
);
111+
registry_data_provider
112+
.add(
113+
&make_catch_up_package_contents_key(SOURCE_SUBNET_ID),
114+
REGISTRY_CUP_REGISTRY_VERSION,
115+
Some(CatchUpPackageContents {
116+
cup_type,
117+
..Default::default()
118+
}),
119+
)
120+
.unwrap();
121+
registry.update_to_latest_version();
122+
123+
registry
124+
}
125+
126+
#[rstest]
127+
fn get_status_should_return_not_scheduled_when_latest_cup_is_not_subnet_splitting_test(
128+
#[values(
129+
None,
130+
Some(CupType::Genesis(GenesisArgs { height: 0 })),
131+
Some(CupType::Recovery(RecoveryArgs {
132+
height: 1_000,
133+
time: 1,
134+
state_hash: vec![],
135+
})),
136+
)]
137+
cup_type: Option<CupType>,
138+
#[values(
139+
REGISTRY_CUP_REGISTRY_VERSION.decrement(),
140+
REGISTRY_CUP_REGISTRY_VERSION,
141+
REGISTRY_CUP_REGISTRY_VERSION.increment(),
142+
)]
143+
last_summary_block_registry_version: RegistryVersion,
144+
#[values(
145+
REGISTRY_CUP_REGISTRY_VERSION.decrement(),
146+
REGISTRY_CUP_REGISTRY_VERSION,
147+
REGISTRY_CUP_REGISTRY_VERSION.increment(),
148+
)]
149+
looked_up_registry_version: RegistryVersion,
150+
) {
151+
let registry = set_up_registry(cup_type);
152+
153+
let status = get_status(
154+
registry.as_ref(),
155+
SUBNET_1,
156+
last_summary_block_registry_version,
157+
looked_up_registry_version,
158+
)
159+
.expect("Should succeed given correct inputs");
160+
161+
assert_eq!(status, Status::NotScheduled);
162+
}
163+
164+
#[rstest]
165+
#[case(
166+
REGISTRY_CUP_REGISTRY_VERSION.decrement(),
167+
REGISTRY_CUP_REGISTRY_VERSION,
168+
)]
169+
#[case(
170+
REGISTRY_CUP_REGISTRY_VERSION.decrement(),
171+
REGISTRY_CUP_REGISTRY_VERSION.increment(),
172+
)]
173+
fn get_status_should_return_scheduled_test(
174+
#[case] last_summary_block_registry_version: RegistryVersion,
175+
#[case] looked_up_registry_version: RegistryVersion,
176+
) {
177+
let registry = set_up_registry(Some(CupType::SubnetSplitting(
178+
ic_protobuf::registry::subnet::v1::SubnetSplittingArgs {
179+
destination_subnet_id: Some(subnet_id_into_protobuf(DESTINATION_SUBNET_ID)),
180+
},
181+
)));
182+
183+
let status = get_status(
184+
registry.as_ref(),
185+
SOURCE_SUBNET_ID,
186+
last_summary_block_registry_version,
187+
looked_up_registry_version,
188+
)
189+
.expect("Should succeed given correct inputs");
190+
191+
assert_eq!(
192+
status,
193+
Status::Scheduled {
194+
destination_subnet_id: DESTINATION_SUBNET_ID,
195+
scheduled_at: REGISTRY_CUP_REGISTRY_VERSION,
196+
}
197+
);
198+
// Asserting the invariant described in the function documentation:
199+
// `last_summary_block_registry_version < scheduled_at <= looked_up_registry_version`.
200+
assert!(
201+
last_summary_block_registry_version < REGISTRY_CUP_REGISTRY_VERSION
202+
&& REGISTRY_CUP_REGISTRY_VERSION <= looked_up_registry_version
203+
);
204+
}
205+
206+
#[rstest]
207+
#[case(
208+
REGISTRY_CUP_REGISTRY_VERSION.decrement(),
209+
REGISTRY_CUP_REGISTRY_VERSION.decrement(),
210+
)]
211+
#[case(
212+
REGISTRY_CUP_REGISTRY_VERSION,
213+
REGISTRY_CUP_REGISTRY_VERSION.decrement(),
214+
)]
215+
#[case(REGISTRY_CUP_REGISTRY_VERSION, REGISTRY_CUP_REGISTRY_VERSION)]
216+
#[case(
217+
REGISTRY_CUP_REGISTRY_VERSION,
218+
REGISTRY_CUP_REGISTRY_VERSION.increment(),
219+
)]
220+
#[case(
221+
REGISTRY_CUP_REGISTRY_VERSION.increment(),
222+
REGISTRY_CUP_REGISTRY_VERSION.decrement(),
223+
)]
224+
#[case(
225+
REGISTRY_CUP_REGISTRY_VERSION.increment(),
226+
REGISTRY_CUP_REGISTRY_VERSION,
227+
)]
228+
#[case(
229+
REGISTRY_CUP_REGISTRY_VERSION.increment(),
230+
REGISTRY_CUP_REGISTRY_VERSION.increment(),
231+
)]
232+
fn get_status_should_return_not_scheduled_when_subnet_splitting_was_already_done_test(
233+
#[case] last_summary_block_registry_version: RegistryVersion,
234+
#[case] looked_up_registry_version: RegistryVersion,
235+
) {
236+
let registry = set_up_registry(Some(CupType::SubnetSplitting(
237+
ic_protobuf::registry::subnet::v1::SubnetSplittingArgs {
238+
destination_subnet_id: Some(subnet_id_into_protobuf(DESTINATION_SUBNET_ID)),
239+
},
240+
)));
241+
242+
let status = get_status(
243+
registry.as_ref(),
244+
SOURCE_SUBNET_ID,
245+
last_summary_block_registry_version,
246+
looked_up_registry_version,
247+
)
248+
.expect("Should succeed given correct inputs");
249+
250+
assert_eq!(status, Status::NotScheduled);
251+
}
252+
}

0 commit comments

Comments
 (0)