Skip to content

Commit 983a053

Browse files
Sawchordeichhorl
andauthored
refactor(Consensus): Factor CUP functionality out of dkg submodule (#3126)
The functions `make_registry_cup` and `make_registry_cup_from_cup` are used by other crates to create registry CUPs. This PR moves the functions out of the `dkg` submodule into its own module `cup_utils` and directly exports them from the top-level `ic_consensus`. The reason is that these functions also use `idkg` functionality. --------- Co-authored-by: Leo Eichhorn <99166915+eichhorl@users.noreply.github.com>
1 parent 76a634c commit 983a053

File tree

8 files changed

+375
-350
lines changed

8 files changed

+375
-350
lines changed

rs/consensus/src/cup_utils.rs

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
//! This module contains functions for constructing CUPs from registry
2+
3+
use crate::{
4+
dkg::payload_builder::get_dkg_summary_from_cup_contents,
5+
idkg::{
6+
make_bootstrap_summary,
7+
payload_builder::make_bootstrap_summary_with_initial_dealings,
8+
utils::{get_idkg_chain_key_config_if_enabled, inspect_idkg_chain_key_initializations},
9+
},
10+
};
11+
use ic_interfaces_registry::RegistryClient;
12+
use ic_logger::{warn, ReplicaLogger};
13+
use ic_protobuf::registry::subnet::v1::CatchUpPackageContents;
14+
use ic_registry_client_helpers::subnet::SubnetRegistry;
15+
use ic_types::{
16+
batch::ValidationContext,
17+
consensus::{
18+
idkg, Block, BlockPayload, CatchUpContent, CatchUpPackage, HashedBlock, HashedRandomBeacon,
19+
Payload, RandomBeaconContent, Rank, SummaryPayload,
20+
},
21+
crypto::{
22+
crypto_hash, threshold_sig::ni_dkg::NiDkgTag, CombinedThresholdSig, CombinedThresholdSigOf,
23+
CryptoHash, Signed,
24+
},
25+
signature::ThresholdSignature,
26+
Height, RegistryVersion, SubnetId, Time,
27+
};
28+
use phantom_newtype::Id;
29+
30+
/// Constructs a genesis/recovery CUP from the CUP contents associated with the
31+
/// given subnet from the provided CUP contents
32+
pub fn make_registry_cup_from_cup_contents(
33+
registry: &dyn RegistryClient,
34+
subnet_id: SubnetId,
35+
cup_contents: CatchUpPackageContents,
36+
registry_version: RegistryVersion,
37+
logger: &ReplicaLogger,
38+
) -> Option<CatchUpPackage> {
39+
let replica_version = match registry.get_replica_version(subnet_id, registry_version) {
40+
Ok(Some(replica_version)) => replica_version,
41+
err => {
42+
warn!(
43+
logger,
44+
"Failed to retrieve subnet replica version at registry version {:?}: {:?}",
45+
registry_version,
46+
err
47+
);
48+
return None;
49+
}
50+
};
51+
let dkg_summary = get_dkg_summary_from_cup_contents(
52+
cup_contents.clone(),
53+
subnet_id,
54+
registry,
55+
registry_version,
56+
);
57+
let cup_height = Height::new(cup_contents.height);
58+
59+
let idkg_summary = match bootstrap_idkg_summary(
60+
&cup_contents,
61+
subnet_id,
62+
registry_version,
63+
registry,
64+
logger,
65+
) {
66+
Ok(summary) => summary,
67+
Err(err) => {
68+
warn!(
69+
logger,
70+
"Failed constructing IDKG summary block from CUP contents: {}.", err
71+
);
72+
73+
return None;
74+
}
75+
};
76+
77+
let low_dkg_id = dkg_summary
78+
.current_transcript(&NiDkgTag::LowThreshold)
79+
.dkg_id
80+
.clone();
81+
let high_dkg_id = dkg_summary
82+
.current_transcript(&NiDkgTag::HighThreshold)
83+
.dkg_id
84+
.clone();
85+
86+
// In a NNS subnet recovery case the block validation context needs to reference a registry
87+
// version of the NNS to be recovered. Otherwise the validation context points to a registry
88+
// version without the NNS subnet record.
89+
let block_registry_version = cup_contents
90+
.registry_store_uri
91+
.as_ref()
92+
.map(|v| RegistryVersion::from(v.registry_version))
93+
.unwrap_or(registry_version);
94+
let block = Block {
95+
version: replica_version.clone(),
96+
parent: Id::from(CryptoHash(Vec::new())),
97+
payload: Payload::new(
98+
crypto_hash,
99+
BlockPayload::Summary(SummaryPayload {
100+
dkg: dkg_summary,
101+
idkg: idkg_summary,
102+
}),
103+
),
104+
height: cup_height,
105+
rank: Rank(0),
106+
context: ValidationContext {
107+
certified_height: cup_height,
108+
registry_version: block_registry_version,
109+
time: Time::from_nanos_since_unix_epoch(cup_contents.time),
110+
},
111+
};
112+
let random_beacon = Signed {
113+
content: RandomBeaconContent {
114+
version: replica_version,
115+
height: cup_height,
116+
parent: Id::from(CryptoHash(Vec::new())),
117+
},
118+
signature: ThresholdSignature {
119+
signer: low_dkg_id,
120+
signature: CombinedThresholdSigOf::new(CombinedThresholdSig(vec![])),
121+
},
122+
};
123+
124+
Some(CatchUpPackage {
125+
content: CatchUpContent::new(
126+
HashedBlock::new(crypto_hash, block),
127+
HashedRandomBeacon::new(crypto_hash, random_beacon),
128+
Id::from(CryptoHash(cup_contents.state_hash)),
129+
/* oldest_registry_version_in_use_by_replicated_state */ None,
130+
),
131+
signature: ThresholdSignature {
132+
signer: high_dkg_id,
133+
signature: CombinedThresholdSigOf::new(CombinedThresholdSig(vec![])),
134+
},
135+
})
136+
}
137+
138+
/// Constructs a genesis/recovery CUP from the CUP contents associated with the
139+
/// given subnet
140+
pub fn make_registry_cup(
141+
registry: &dyn RegistryClient,
142+
subnet_id: SubnetId,
143+
logger: &ReplicaLogger,
144+
) -> Option<CatchUpPackage> {
145+
let versioned_record = match registry.get_cup_contents(subnet_id, registry.get_latest_version())
146+
{
147+
Ok(versioned_record) => versioned_record,
148+
Err(e) => {
149+
warn!(
150+
logger,
151+
"Failed to retrieve versioned record from the registry {:?}", e,
152+
);
153+
return None;
154+
}
155+
};
156+
157+
let cup_contents = versioned_record.value.expect("Missing CUP contents");
158+
make_registry_cup_from_cup_contents(
159+
registry,
160+
subnet_id,
161+
cup_contents,
162+
versioned_record.version,
163+
logger,
164+
)
165+
}
166+
167+
fn bootstrap_idkg_summary_from_cup_contents(
168+
cup_contents: &CatchUpPackageContents,
169+
subnet_id: SubnetId,
170+
logger: &ReplicaLogger,
171+
) -> Result<idkg::Summary, String> {
172+
let initial_dealings = inspect_idkg_chain_key_initializations(
173+
&cup_contents.ecdsa_initializations,
174+
&cup_contents.chain_key_initializations,
175+
)?;
176+
if initial_dealings.is_empty() {
177+
return Ok(None);
178+
};
179+
180+
make_bootstrap_summary_with_initial_dealings(
181+
subnet_id,
182+
Height::new(cup_contents.height),
183+
initial_dealings,
184+
logger,
185+
)
186+
.map_err(|err| format!("Failed to create IDKG summary block: {:?}", err))
187+
}
188+
189+
fn bootstrap_idkg_summary(
190+
cup_contents: &CatchUpPackageContents,
191+
subnet_id: SubnetId,
192+
registry_version: RegistryVersion,
193+
registry_client: &dyn RegistryClient,
194+
logger: &ReplicaLogger,
195+
) -> Result<idkg::Summary, String> {
196+
if let Some(summary) =
197+
bootstrap_idkg_summary_from_cup_contents(cup_contents, subnet_id, logger)?
198+
{
199+
return Ok(Some(summary));
200+
}
201+
202+
match get_idkg_chain_key_config_if_enabled(subnet_id, registry_version, registry_client)
203+
.map_err(|err| format!("Failed getting the chain key config: {:?}", err))?
204+
{
205+
Some(chain_key_config) => Ok(make_bootstrap_summary(
206+
subnet_id,
207+
chain_key_config
208+
.key_configs
209+
.iter()
210+
.map(|key_config| key_config.key_id.clone())
211+
.filter_map(|key_id| key_id.try_into().ok())
212+
.collect(),
213+
Height::new(cup_contents.height),
214+
)),
215+
None => Ok(None),
216+
}
217+
}
218+
219+
#[cfg(test)]
220+
mod tests {
221+
use crate::cup_utils::make_registry_cup;
222+
use ic_crypto_test_utils_ni_dkg::dummy_initial_dkg_transcript;
223+
use ic_interfaces_registry::{RegistryClient, RegistryVersionedRecord};
224+
use ic_logger::no_op_logger;
225+
use ic_protobuf::registry::subnet::v1::{CatchUpPackageContents, SubnetRecord};
226+
use ic_types::{
227+
consensus::HasVersion,
228+
crypto::{threshold_sig::ni_dkg::NiDkgTag, CryptoHash},
229+
registry::RegistryClientError,
230+
Height, NodeId, PrincipalId, RegistryVersion, ReplicaVersion, Time,
231+
};
232+
use ic_types_test_utils::ids::subnet_test_id;
233+
234+
#[test]
235+
fn test_make_registry_cup() {
236+
let registry_client = MockRegistryClient::new(RegistryVersion::from(12345), |key, _| {
237+
use prost::Message;
238+
if key.starts_with("catch_up_package_contents_") {
239+
// Build a dummy cup
240+
let committee = vec![NodeId::from(PrincipalId::new_node_test_id(0))];
241+
let cup =
242+
CatchUpPackageContents {
243+
initial_ni_dkg_transcript_low_threshold: Some(
244+
dummy_initial_dkg_transcript(committee.clone(), NiDkgTag::LowThreshold),
245+
),
246+
initial_ni_dkg_transcript_high_threshold: Some(
247+
dummy_initial_dkg_transcript(committee, NiDkgTag::HighThreshold),
248+
),
249+
height: 54321,
250+
time: 1,
251+
state_hash: vec![1, 2, 3, 4, 5],
252+
registry_store_uri: None,
253+
ecdsa_initializations: vec![],
254+
chain_key_initializations: vec![],
255+
};
256+
257+
// Encode the cup to protobuf
258+
let mut value = Vec::with_capacity(cup.encoded_len());
259+
cup.encode(&mut value).unwrap();
260+
Some(value)
261+
} else if key.starts_with("subnet_record_") {
262+
// Build a dummy subnet record. The only value used from this are the
263+
// `membership` and `dkg_interval_length` fields.
264+
let subnet_record = SubnetRecord {
265+
membership: vec![PrincipalId::new_subnet_test_id(1).to_vec()],
266+
dkg_interval_length: 99,
267+
replica_version_id: "TestID".to_string(),
268+
..SubnetRecord::default()
269+
};
270+
271+
// Encode the `SubnetRecord` to protobuf
272+
let mut value = Vec::with_capacity(subnet_record.encoded_len());
273+
subnet_record.encode(&mut value).unwrap();
274+
Some(value)
275+
} else {
276+
None
277+
}
278+
});
279+
let result =
280+
make_registry_cup(&registry_client, subnet_test_id(0), &no_op_logger()).unwrap();
281+
282+
assert_eq!(
283+
result.content.state_hash.get_ref(),
284+
&CryptoHash(vec![1, 2, 3, 4, 5])
285+
);
286+
assert_eq!(
287+
result.content.block.get_value().context.registry_version,
288+
RegistryVersion::from(12345)
289+
);
290+
assert_eq!(
291+
result.content.block.get_value().context.certified_height,
292+
Height::from(54321)
293+
);
294+
assert_eq!(
295+
result.content.version(),
296+
&ReplicaVersion::try_from("TestID").unwrap()
297+
);
298+
assert_eq!(result.signature.signer.dealer_subnet, subnet_test_id(0));
299+
}
300+
301+
/// `RegistryClient` implementation that allows to provide a custom function
302+
/// to provide a `get_versioned_value`.
303+
struct MockRegistryClient<F>
304+
where
305+
F: Fn(&str, RegistryVersion) -> Option<Vec<u8>>,
306+
{
307+
latest_registry_version: RegistryVersion,
308+
get_versioned_value_fun: F,
309+
}
310+
311+
impl<F> MockRegistryClient<F>
312+
where
313+
F: Fn(&str, RegistryVersion) -> Option<Vec<u8>>,
314+
{
315+
fn new(latest_registry_version: RegistryVersion, get_versioned_value_fun: F) -> Self {
316+
Self {
317+
latest_registry_version,
318+
get_versioned_value_fun,
319+
}
320+
}
321+
}
322+
323+
impl<F> RegistryClient for MockRegistryClient<F>
324+
where
325+
F: Fn(&str, RegistryVersion) -> Option<Vec<u8>> + Send + Sync,
326+
{
327+
fn get_versioned_value(
328+
&self,
329+
key: &str,
330+
version: RegistryVersion,
331+
) -> ic_interfaces_registry::RegistryClientVersionedResult<Vec<u8>> {
332+
let value = (self.get_versioned_value_fun)(key, version);
333+
Ok(RegistryVersionedRecord {
334+
key: key.to_string(),
335+
version,
336+
value,
337+
})
338+
}
339+
340+
// Not needed for this test
341+
fn get_key_family(
342+
&self,
343+
_: &str,
344+
_: RegistryVersion,
345+
) -> Result<Vec<String>, RegistryClientError> {
346+
Ok(vec![])
347+
}
348+
349+
fn get_latest_version(&self) -> RegistryVersion {
350+
self.latest_registry_version
351+
}
352+
353+
// Not needed for this test
354+
fn get_version_timestamp(&self, _: RegistryVersion) -> Option<Time> {
355+
None
356+
}
357+
}
358+
}

0 commit comments

Comments
 (0)