1pub(crate) mod proto;
4
5use candid::{CandidType, Decode, Deserialize, Encode, Principal};
6use canic_subnet_catalog::{
7 CATALOG_SCHEMA_VERSION, CatalogError, ClassificationSource, GeographicScope, MAINNET_NETWORK,
8 MAINNET_REGISTRY_CANISTER_ID, RoutingRange, SubnetCatalog, SubnetInfo, SubnetKind,
9 SubnetSpecialization,
10};
11use futures::{StreamExt, TryStreamExt, stream};
12use ic_agent::Agent;
13use prost::Message;
14use proto::{
15 CanisterId, DataCenterRecord, LargeValueChunkKeys, NodeOperatorRecord, NodeRecord,
16 RegistryErrorCode, RegistryGetLatestVersionResponse, RegistryGetValueRequest,
17 RegistryGetValueResponse, RoutingTable, SubnetId, SubnetListRecord, SubnetRecord, SubnetType,
18 UInt64Value, registry_get_value_response,
19};
20use serde::Serialize;
21use sha2::{Digest, Sha256};
22use std::collections::{BTreeMap, BTreeSet};
23use thiserror::Error as ThisError;
24
25pub const DEFAULT_MAINNET_ENDPOINT: &str = "https://icp-api.io";
26pub const MAINNET_GOVERNANCE_CANISTER_ID: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai";
27
28const SUBNET_LIST_KEY: &str = "subnet_list";
29const ROUTING_TABLE_KEY: &str = "routing_table";
30const SUBNET_RECORD_KEY_PREFIX: &str = "subnet_record_";
31const NODE_RECORD_KEY_PREFIX: &str = "node_record_";
32const NODE_OPERATOR_RECORD_KEY_PREFIX: &str = "node_operator_record_";
33const DATA_CENTER_RECORD_KEY_PREFIX: &str = "data_center_record_";
34const NODE_PROVIDER_ENRICHMENT_CONCURRENCY: usize = 32;
35const FIDUCIARY_SUBNET: &str = "pzp6e-ekpqk-3c5x7-2h6so-njoeq-mt45d-h3h6c-q3mxf-vpeq5-fk5o7-yae";
36const EUROPEAN_SUBNET: &str = "bkfrj-6k62g-dycql-7h53p-atvkj-zg4to-gaogh-netha-ptybj-ntsgw-rqe";
37
38#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct MainnetRegistryFetchRequest {
43 pub endpoint: String,
44 pub fetched_at: String,
45 pub fetched_by: String,
46}
47
48impl MainnetRegistryFetchRequest {
49 #[must_use]
50 pub fn new(fetched_at: String) -> Self {
51 Self {
52 endpoint: DEFAULT_MAINNET_ENDPOINT.to_string(),
53 fetched_at,
54 fetched_by: "canic-ic-registry".to_string(),
55 }
56 }
57}
58
59#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
63pub struct MainnetRegistryVersion {
64 pub network: String,
65 pub registry_canister_id: String,
66 pub registry_version: u64,
67 pub fetched_at: String,
68 pub fetched_by: String,
69 pub source_endpoint: String,
70}
71
72#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
76pub struct MainnetNodeProviderList {
77 pub network: String,
78 pub governance_canister_id: String,
79 pub registry_canister_id: String,
80 pub registry_version: u64,
81 pub fetched_at: String,
82 pub fetched_by: String,
83 pub source_endpoint: String,
84 pub node_providers: Vec<MainnetNodeProvider>,
85}
86
87#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
91pub struct MainnetNodeProvider {
92 pub principal: String,
93 pub node_count: Option<u32>,
94 pub reward_account_hex: Option<String>,
95}
96
97#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
101pub struct MainnetNodeOperatorList {
102 pub network: String,
103 pub registry_canister_id: String,
104 pub registry_version: u64,
105 pub fetched_at: String,
106 pub fetched_by: String,
107 pub source_endpoint: String,
108 pub node_operators: Vec<MainnetNodeOperator>,
109}
110
111#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
115pub struct MainnetNodeOperator {
116 pub principal: String,
117 pub node_provider_principal: String,
118 pub node_allowance: u64,
119 pub data_center_id: String,
120 pub node_count: Option<u32>,
121}
122
123#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
127pub struct MainnetNodeList {
128 pub network: String,
129 pub registry_canister_id: String,
130 pub registry_version: u64,
131 pub fetched_at: String,
132 pub fetched_by: String,
133 pub source_endpoint: String,
134 pub nodes: Vec<MainnetNode>,
135}
136
137#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
141pub struct MainnetNode {
142 pub principal: String,
143 pub node_operator_principal: String,
144 pub node_provider_principal: String,
145 pub subnet_principal: String,
146 pub subnet_kind: String,
147 pub data_center_id: String,
148}
149
150#[derive(Clone, Debug, PartialEq, Serialize)]
154pub struct MainnetDataCenterList {
155 pub network: String,
156 pub registry_canister_id: String,
157 pub registry_version: u64,
158 pub fetched_at: String,
159 pub fetched_by: String,
160 pub source_endpoint: String,
161 pub data_centers: Vec<MainnetDataCenter>,
162}
163
164#[derive(Clone, Debug, PartialEq, Serialize)]
168pub struct MainnetDataCenter {
169 pub id: String,
170 pub region: String,
171 pub owner: String,
172 pub latitude: Option<f32>,
173 pub longitude: Option<f32>,
174 pub node_operator_count: u32,
175 pub node_provider_count: u32,
176 pub node_count: u32,
177}
178
179#[derive(Debug, ThisError)]
183pub enum RegistryFetchError {
184 #[error("failed to build IC agent for {endpoint}: {reason}")]
185 AgentBuild { endpoint: String, reason: String },
186
187 #[error("registry agent call {method} failed: {reason}")]
188 AgentCall {
189 method: &'static str,
190 reason: String,
191 },
192
193 #[error("failed to encode protobuf {message}: {reason}")]
194 ProtobufEncode {
195 message: &'static str,
196 reason: String,
197 },
198
199 #[error("failed to decode protobuf {message}: {reason}")]
200 ProtobufDecode {
201 message: &'static str,
202 reason: String,
203 },
204
205 #[error("registry get_value for key {key} failed with code {code}: {reason}")]
206 RegistryValue {
207 key: String,
208 code: String,
209 reason: String,
210 },
211
212 #[error("registry get_value for key {key} returned no value content")]
213 MissingValue { key: String },
214
215 #[error("failed to encode candid {message}: {reason}")]
216 CandidEncode {
217 message: &'static str,
218 reason: String,
219 },
220
221 #[error("failed to decode candid {message}: {reason}")]
222 CandidDecode {
223 message: &'static str,
224 reason: String,
225 },
226
227 #[error("registry get_chunk for sha256 {sha256} failed: {reason}")]
228 RegistryChunkRejected { sha256: String, reason: String },
229
230 #[error("registry get_chunk for sha256 {sha256} returned no chunk content")]
231 MissingChunkContent { sha256: String },
232
233 #[error("registry get_chunk for sha256 {sha256} returned content with sha256 {actual_sha256}")]
234 ChunkHashMismatch {
235 sha256: String,
236 actual_sha256: String,
237 },
238
239 #[error("registry protobuf field {field} was missing")]
240 MissingField { field: &'static str },
241
242 #[error("registry principal field {field} is invalid: {reason}")]
243 InvalidPrincipal { field: &'static str, reason: String },
244
245 #[error("data center record id mismatch: key id {key_id}, record id {record_id}")]
246 InvalidDataCenterRecordId { key_id: String, record_id: String },
247
248 #[error("registry subnet list was empty")]
249 EmptySubnetList,
250
251 #[error("registry routing table was empty")]
252 EmptyRoutingTable,
253
254 #[error(transparent)]
255 Catalog(#[from] CatalogError),
256
257 #[error("failed to create Tokio runtime for registry refresh: {0}")]
258 Runtime(String),
259}
260
261#[derive(Debug)]
265enum RegistryValueContent {
266 Value(Vec<u8>),
267 LargeValueChunkKeys(LargeValueChunkKeys),
268}
269
270#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
274struct RegistryGetChunkRequest {
275 content_sha256: Option<Vec<u8>>,
276}
277
278#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
282struct RegistryChunk {
283 content: Option<Vec<u8>>,
284}
285
286#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
290struct ListNodeProvidersResponse {
291 node_providers: Vec<GovernanceNodeProvider>,
292}
293
294#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
298struct GovernanceNodeProvider {
299 id: Option<Principal>,
300 reward_account: Option<GovernanceAccountIdentifier>,
301}
302
303#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
307struct GovernanceAccountIdentifier {
308 hash: Vec<u8>,
309}
310
311pub fn fetch_mainnet_subnet_catalog(
312 request: &MainnetRegistryFetchRequest,
313) -> Result<SubnetCatalog, RegistryFetchError> {
314 let runtime = tokio::runtime::Builder::new_current_thread()
315 .enable_all()
316 .build()
317 .map_err(|err| RegistryFetchError::Runtime(err.to_string()))?;
318 runtime.block_on(fetch_mainnet_subnet_catalog_async(request))
319}
320
321pub fn fetch_mainnet_registry_version(
322 request: &MainnetRegistryFetchRequest,
323) -> Result<MainnetRegistryVersion, RegistryFetchError> {
324 let runtime = tokio::runtime::Builder::new_current_thread()
325 .enable_all()
326 .build()
327 .map_err(|err| RegistryFetchError::Runtime(err.to_string()))?;
328 runtime.block_on(fetch_mainnet_registry_version_async(request))
329}
330
331pub fn fetch_mainnet_node_provider_list(
332 request: &MainnetRegistryFetchRequest,
333) -> Result<MainnetNodeProviderList, RegistryFetchError> {
334 let runtime = tokio::runtime::Builder::new_current_thread()
335 .enable_all()
336 .build()
337 .map_err(|err| RegistryFetchError::Runtime(err.to_string()))?;
338 runtime.block_on(fetch_mainnet_node_provider_list_async(request))
339}
340
341pub fn fetch_mainnet_node_operator_list(
342 request: &MainnetRegistryFetchRequest,
343) -> Result<MainnetNodeOperatorList, RegistryFetchError> {
344 let runtime = tokio::runtime::Builder::new_current_thread()
345 .enable_all()
346 .build()
347 .map_err(|err| RegistryFetchError::Runtime(err.to_string()))?;
348 runtime.block_on(fetch_mainnet_node_operator_list_async(request))
349}
350
351pub fn fetch_mainnet_node_list(
352 request: &MainnetRegistryFetchRequest,
353) -> Result<MainnetNodeList, RegistryFetchError> {
354 let runtime = tokio::runtime::Builder::new_current_thread()
355 .enable_all()
356 .build()
357 .map_err(|err| RegistryFetchError::Runtime(err.to_string()))?;
358 runtime.block_on(fetch_mainnet_node_list_async(request))
359}
360
361pub fn fetch_mainnet_data_center_list(
362 request: &MainnetRegistryFetchRequest,
363) -> Result<MainnetDataCenterList, RegistryFetchError> {
364 let runtime = tokio::runtime::Builder::new_current_thread()
365 .enable_all()
366 .build()
367 .map_err(|err| RegistryFetchError::Runtime(err.to_string()))?;
368 runtime.block_on(fetch_mainnet_data_center_list_async(request))
369}
370
371pub async fn fetch_mainnet_registry_version_async(
372 request: &MainnetRegistryFetchRequest,
373) -> Result<MainnetRegistryVersion, RegistryFetchError> {
374 let agent = Agent::builder()
375 .with_url(&request.endpoint)
376 .build()
377 .map_err(|err| RegistryFetchError::AgentBuild {
378 endpoint: request.endpoint.clone(),
379 reason: err.to_string(),
380 })?;
381 let registry_canister = Principal::from_text(MAINNET_REGISTRY_CANISTER_ID).map_err(|err| {
382 RegistryFetchError::InvalidPrincipal {
383 field: "registry_canister_id",
384 reason: err.to_string(),
385 }
386 })?;
387 let registry_version = get_latest_version(&agent, ®istry_canister).await?;
388 Ok(MainnetRegistryVersion {
389 network: MAINNET_NETWORK.to_string(),
390 registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
391 registry_version,
392 fetched_at: request.fetched_at.clone(),
393 fetched_by: request.fetched_by.clone(),
394 source_endpoint: request.endpoint.clone(),
395 })
396}
397
398pub async fn fetch_mainnet_subnet_catalog_async(
399 request: &MainnetRegistryFetchRequest,
400) -> Result<SubnetCatalog, RegistryFetchError> {
401 let agent = Agent::builder()
402 .with_url(&request.endpoint)
403 .build()
404 .map_err(|err| RegistryFetchError::AgentBuild {
405 endpoint: request.endpoint.clone(),
406 reason: err.to_string(),
407 })?;
408 let registry_canister = Principal::from_text(MAINNET_REGISTRY_CANISTER_ID).map_err(|err| {
409 RegistryFetchError::InvalidPrincipal {
410 field: "registry_canister_id",
411 reason: err.to_string(),
412 }
413 })?;
414 let registry_version = get_latest_version(&agent, ®istry_canister).await?;
415 let subnet_list_bytes = get_registry_value(
416 &agent,
417 ®istry_canister,
418 SUBNET_LIST_KEY,
419 registry_version,
420 )
421 .await?;
422 let routing_table_bytes = get_registry_value(
423 &agent,
424 ®istry_canister,
425 ROUTING_TABLE_KEY,
426 registry_version,
427 )
428 .await?;
429 let subnet_list = decode_message::<SubnetListRecord>("SubnetListRecord", &subnet_list_bytes)?;
430 let routing_table = decode_message::<RoutingTable>("RoutingTable", &routing_table_bytes)?;
431 catalog_from_registry_records(
432 request,
433 registry_version,
434 &agent,
435 ®istry_canister,
436 subnet_list,
437 routing_table,
438 )
439 .await
440}
441
442pub async fn fetch_mainnet_node_provider_list_async(
443 request: &MainnetRegistryFetchRequest,
444) -> Result<MainnetNodeProviderList, RegistryFetchError> {
445 let agent = Agent::builder()
446 .with_url(&request.endpoint)
447 .build()
448 .map_err(|err| RegistryFetchError::AgentBuild {
449 endpoint: request.endpoint.clone(),
450 reason: err.to_string(),
451 })?;
452 let governance_canister =
453 Principal::from_text(MAINNET_GOVERNANCE_CANISTER_ID).map_err(|err| {
454 RegistryFetchError::InvalidPrincipal {
455 field: "governance_canister_id",
456 reason: err.to_string(),
457 }
458 })?;
459 let arg = Encode!().map_err(|err| RegistryFetchError::CandidEncode {
460 message: "list_node_providers",
461 reason: err.to_string(),
462 })?;
463 let bytes = agent
464 .query(&governance_canister, "list_node_providers")
465 .with_arg(arg)
466 .call()
467 .await
468 .map_err(|err| RegistryFetchError::AgentCall {
469 method: "list_node_providers",
470 reason: err.to_string(),
471 })?;
472 let response = Decode!(&bytes, ListNodeProvidersResponse).map_err(|err| {
473 RegistryFetchError::CandidDecode {
474 message: "ListNodeProvidersResponse",
475 reason: err.to_string(),
476 }
477 })?;
478 let registry_canister = Principal::from_text(MAINNET_REGISTRY_CANISTER_ID).map_err(|err| {
479 RegistryFetchError::InvalidPrincipal {
480 field: "registry_canister_id",
481 reason: err.to_string(),
482 }
483 })?;
484 let registry_version = get_latest_version(&agent, ®istry_canister).await?;
485 let node_counts =
486 fetch_node_provider_node_counts(&agent, ®istry_canister, registry_version).await?;
487 node_provider_list_from_response(request, response, node_counts, registry_version)
488}
489
490pub async fn fetch_mainnet_node_operator_list_async(
491 request: &MainnetRegistryFetchRequest,
492) -> Result<MainnetNodeOperatorList, RegistryFetchError> {
493 let agent = Agent::builder()
494 .with_url(&request.endpoint)
495 .build()
496 .map_err(|err| RegistryFetchError::AgentBuild {
497 endpoint: request.endpoint.clone(),
498 reason: err.to_string(),
499 })?;
500 let registry_canister = Principal::from_text(MAINNET_REGISTRY_CANISTER_ID).map_err(|err| {
501 RegistryFetchError::InvalidPrincipal {
502 field: "registry_canister_id",
503 reason: err.to_string(),
504 }
505 })?;
506 let registry_version = get_latest_version(&agent, ®istry_canister).await?;
507 let inventory = fetch_registry_relation_inventory(
508 &agent,
509 ®istry_canister,
510 registry_version,
511 RegistryRelationInventoryScope::BaseRelations,
512 )
513 .await?;
514 node_operator_list_from_inventory(request, inventory, registry_version)
515}
516
517pub async fn fetch_mainnet_node_list_async(
518 request: &MainnetRegistryFetchRequest,
519) -> Result<MainnetNodeList, RegistryFetchError> {
520 let agent = Agent::builder()
521 .with_url(&request.endpoint)
522 .build()
523 .map_err(|err| RegistryFetchError::AgentBuild {
524 endpoint: request.endpoint.clone(),
525 reason: err.to_string(),
526 })?;
527 let registry_canister = Principal::from_text(MAINNET_REGISTRY_CANISTER_ID).map_err(|err| {
528 RegistryFetchError::InvalidPrincipal {
529 field: "registry_canister_id",
530 reason: err.to_string(),
531 }
532 })?;
533 let registry_version = get_latest_version(&agent, ®istry_canister).await?;
534 let inventory = fetch_registry_relation_inventory(
535 &agent,
536 ®istry_canister,
537 registry_version,
538 RegistryRelationInventoryScope::BaseRelations,
539 )
540 .await?;
541 node_list_from_inventory(request, inventory, registry_version)
542}
543
544pub async fn fetch_mainnet_data_center_list_async(
545 request: &MainnetRegistryFetchRequest,
546) -> Result<MainnetDataCenterList, RegistryFetchError> {
547 let agent = Agent::builder()
548 .with_url(&request.endpoint)
549 .build()
550 .map_err(|err| RegistryFetchError::AgentBuild {
551 endpoint: request.endpoint.clone(),
552 reason: err.to_string(),
553 })?;
554 let registry_canister = Principal::from_text(MAINNET_REGISTRY_CANISTER_ID).map_err(|err| {
555 RegistryFetchError::InvalidPrincipal {
556 field: "registry_canister_id",
557 reason: err.to_string(),
558 }
559 })?;
560 let registry_version = get_latest_version(&agent, ®istry_canister).await?;
561 let inventory = fetch_registry_relation_inventory(
562 &agent,
563 ®istry_canister,
564 registry_version,
565 RegistryRelationInventoryScope::WithDataCenters,
566 )
567 .await?;
568 data_center_list_from_inventory(request, inventory, registry_version)
569}
570
571async fn catalog_from_registry_records(
572 request: &MainnetRegistryFetchRequest,
573 registry_version: u64,
574 agent: &Agent,
575 registry_canister: &Principal,
576 subnet_list: SubnetListRecord,
577 routing_table: RoutingTable,
578) -> Result<SubnetCatalog, RegistryFetchError> {
579 if subnet_list.subnets.is_empty() {
580 return Err(RegistryFetchError::EmptySubnetList);
581 }
582 if routing_table.entries.is_empty() {
583 return Err(RegistryFetchError::EmptyRoutingTable);
584 }
585
586 let mut subnets = Vec::with_capacity(subnet_list.subnets.len());
587 for subnet_raw in subnet_list.subnets {
588 let subnet_principal = principal_text_from_raw(&subnet_raw, "subnet_list.subnets")?;
589 let key = subnet_record_key(&subnet_principal);
590 let record_bytes =
591 get_registry_value(agent, registry_canister, &key, registry_version).await?;
592 let record = decode_message::<SubnetRecord>("SubnetRecord", &record_bytes)?;
593 subnets.push(subnet_info_from_record(&subnet_principal, &record));
594 }
595
596 subnets.sort_by(|left, right| left.subnet_principal.cmp(&right.subnet_principal));
597
598 let mut routing_ranges = routing_ranges_from_table(&routing_table)?;
599 routing_ranges.sort_by(|left, right| {
600 left.start_canister_id
601 .cmp(&right.start_canister_id)
602 .then_with(|| left.end_canister_id.cmp(&right.end_canister_id))
603 .then_with(|| left.subnet_principal.cmp(&right.subnet_principal))
604 });
605
606 let mut catalog = SubnetCatalog {
607 catalog_schema_version: CATALOG_SCHEMA_VERSION,
608 network: MAINNET_NETWORK.to_string(),
609 registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
610 registry_version,
611 fetched_at: request.fetched_at.clone(),
612 fetched_by: request.fetched_by.clone(),
613 source_endpoint: request.endpoint.clone(),
614 resolver_backend: "local-nns-subnet-catalog".to_string(),
615 subnets,
616 routing_ranges,
617 };
618 apply_mainnet_annotations(&mut catalog);
619 catalog.validate()?;
620 Ok(catalog)
621}
622
623fn node_provider_list_from_response(
624 request: &MainnetRegistryFetchRequest,
625 response: ListNodeProvidersResponse,
626 node_counts: BTreeMap<String, u32>,
627 registry_version: u64,
628) -> Result<MainnetNodeProviderList, RegistryFetchError> {
629 let mut node_providers = response
630 .node_providers
631 .into_iter()
632 .map(|node_provider| node_provider_from_governance(node_provider, &node_counts))
633 .collect::<Result<Vec<_>, _>>()?;
634 node_providers.sort_by(|left, right| left.principal.cmp(&right.principal));
635 Ok(MainnetNodeProviderList {
636 network: MAINNET_NETWORK.to_string(),
637 governance_canister_id: MAINNET_GOVERNANCE_CANISTER_ID.to_string(),
638 registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
639 registry_version,
640 fetched_at: request.fetched_at.clone(),
641 fetched_by: request.fetched_by.clone(),
642 source_endpoint: request.endpoint.clone(),
643 node_providers,
644 })
645}
646
647fn node_provider_from_governance(
648 node_provider: GovernanceNodeProvider,
649 node_counts: &BTreeMap<String, u32>,
650) -> Result<MainnetNodeProvider, RegistryFetchError> {
651 let principal = node_provider
652 .id
653 .ok_or(RegistryFetchError::MissingField {
654 field: "node_provider.id",
655 })?
656 .to_text();
657 let reward_account_hex = node_provider
658 .reward_account
659 .map(|account| hex_bytes(&account.hash));
660 let node_count = Some(node_counts.get(&principal).copied().unwrap_or(0));
661 Ok(MainnetNodeProvider {
662 principal,
663 node_count,
664 reward_account_hex,
665 })
666}
667
668async fn fetch_node_provider_node_counts(
669 agent: &Agent,
670 registry_canister: &Principal,
671 registry_version: u64,
672) -> Result<BTreeMap<String, u32>, RegistryFetchError> {
673 let inventory = fetch_registry_relation_inventory(
674 agent,
675 registry_canister,
676 registry_version,
677 RegistryRelationInventoryScope::BaseRelations,
678 )
679 .await?;
680 node_provider_counts_from_records(
681 &inventory.node_principals,
682 &inventory.node_records,
683 &inventory.node_operator_records,
684 )
685}
686
687async fn fetch_registry_relation_inventory(
688 agent: &Agent,
689 registry_canister: &Principal,
690 registry_version: u64,
691 scope: RegistryRelationInventoryScope,
692) -> Result<RegistryRelationInventory, RegistryFetchError> {
693 let subnet_list_bytes =
694 get_registry_value(agent, registry_canister, SUBNET_LIST_KEY, registry_version).await?;
695 let subnet_list = decode_message::<SubnetListRecord>("SubnetListRecord", &subnet_list_bytes)?;
696 if subnet_list.subnets.is_empty() {
697 return Err(RegistryFetchError::EmptySubnetList);
698 }
699
700 let subnet_principals = subnet_list
701 .subnets
702 .iter()
703 .map(|subnet_raw| principal_text_from_raw(subnet_raw, "subnet_list.subnets"))
704 .collect::<Result<Vec<_>, _>>()?;
705 let subnet_records = stream::iter(subnet_principals)
706 .map(|subnet_principal| async move {
707 let key = subnet_record_key(&subnet_principal);
708 let record_bytes =
709 get_registry_value(agent, registry_canister, &key, registry_version).await?;
710 let record = decode_message::<SubnetRecord>("SubnetRecord", &record_bytes)?;
711 Ok::<_, RegistryFetchError>((subnet_principal, record))
712 })
713 .buffer_unordered(NODE_PROVIDER_ENRICHMENT_CONCURRENCY)
714 .try_collect::<BTreeMap<_, _>>()
715 .await?;
716
717 let node_principals = assigned_node_principals_from_subnets(&subnet_records)?;
718 let node_records = stream::iter(node_principals.iter().cloned())
719 .map(|node_principal| async move {
720 let key = node_record_key(&node_principal);
721 let record_bytes =
722 get_registry_value(agent, registry_canister, &key, registry_version).await?;
723 let record = decode_message::<NodeRecord>("NodeRecord", &record_bytes)?;
724 Ok::<_, RegistryFetchError>((node_principal, record))
725 })
726 .buffer_unordered(NODE_PROVIDER_ENRICHMENT_CONCURRENCY)
727 .try_collect::<BTreeMap<_, _>>()
728 .await?;
729
730 let mut node_operator_principals = BTreeSet::new();
731 for record in node_records.values() {
732 node_operator_principals.insert(principal_text_from_required_raw(
733 &record.node_operator_id,
734 "node_record.node_operator_id",
735 )?);
736 }
737
738 let node_operator_records = stream::iter(node_operator_principals)
739 .map(|node_operator_principal| async move {
740 let key = node_operator_record_key(&node_operator_principal);
741 let record_bytes =
742 get_registry_value(agent, registry_canister, &key, registry_version).await?;
743 let record = decode_message::<NodeOperatorRecord>("NodeOperatorRecord", &record_bytes)?;
744 Ok::<_, RegistryFetchError>((node_operator_principal, record))
745 })
746 .buffer_unordered(NODE_PROVIDER_ENRICHMENT_CONCURRENCY)
747 .try_collect::<BTreeMap<_, _>>()
748 .await?;
749
750 let data_center_records = match scope {
751 RegistryRelationInventoryScope::BaseRelations => BTreeMap::new(),
752 RegistryRelationInventoryScope::WithDataCenters => {
753 fetch_data_center_records_for_inventory(
754 agent,
755 registry_canister,
756 registry_version,
757 &node_operator_records,
758 )
759 .await?
760 }
761 };
762
763 Ok(RegistryRelationInventory {
764 node_principals,
765 node_records,
766 node_operator_records,
767 subnet_records,
768 data_center_records,
769 })
770}
771
772async fn fetch_data_center_records_for_inventory(
773 agent: &Agent,
774 registry_canister: &Principal,
775 registry_version: u64,
776 node_operator_records: &BTreeMap<String, NodeOperatorRecord>,
777) -> Result<BTreeMap<String, DataCenterRecord>, RegistryFetchError> {
778 let data_center_ids = node_operator_records
779 .values()
780 .filter_map(|record| normalized_data_center_id(&record.dc_id))
781 .collect::<BTreeSet<_>>();
782 stream::iter(data_center_ids)
783 .map(|data_center_id| async move {
784 let key = data_center_record_key(&data_center_id);
785 let record_bytes =
786 get_registry_value(agent, registry_canister, &key, registry_version).await?;
787 let record = decode_message::<DataCenterRecord>("DataCenterRecord", &record_bytes)?;
788 Ok::<_, RegistryFetchError>((data_center_id, record))
789 })
790 .buffer_unordered(NODE_PROVIDER_ENRICHMENT_CONCURRENCY)
791 .try_collect::<BTreeMap<_, _>>()
792 .await
793}
794
795fn node_operator_list_from_inventory(
796 request: &MainnetRegistryFetchRequest,
797 inventory: RegistryRelationInventory,
798 registry_version: u64,
799) -> Result<MainnetNodeOperatorList, RegistryFetchError> {
800 let node_counts = node_operator_counts_from_records(
801 &inventory.node_principals,
802 &inventory.node_records,
803 &inventory.node_operator_records,
804 )?;
805 let mut node_operators = inventory
806 .node_operator_records
807 .into_iter()
808 .map(|(principal, record)| node_operator_from_record(principal, record, &node_counts))
809 .collect::<Result<Vec<_>, _>>()?;
810 node_operators.sort_by(|left, right| left.principal.cmp(&right.principal));
811 Ok(MainnetNodeOperatorList {
812 network: MAINNET_NETWORK.to_string(),
813 registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
814 registry_version,
815 fetched_at: request.fetched_at.clone(),
816 fetched_by: request.fetched_by.clone(),
817 source_endpoint: request.endpoint.clone(),
818 node_operators,
819 })
820}
821
822fn node_operator_from_record(
823 principal: String,
824 record: NodeOperatorRecord,
825 node_counts: &BTreeMap<String, u32>,
826) -> Result<MainnetNodeOperator, RegistryFetchError> {
827 let node_provider_principal = principal_text_from_required_raw(
828 &record.node_provider_principal_id,
829 "node_operator_record.node_provider_principal_id",
830 )?;
831 Ok(MainnetNodeOperator {
832 node_count: Some(node_counts.get(&principal).copied().unwrap_or(0)),
833 principal,
834 node_provider_principal,
835 node_allowance: record.node_allowance,
836 data_center_id: record.dc_id,
837 })
838}
839
840fn node_list_from_inventory(
841 request: &MainnetRegistryFetchRequest,
842 inventory: RegistryRelationInventory,
843 registry_version: u64,
844) -> Result<MainnetNodeList, RegistryFetchError> {
845 let node_subnets = node_subnet_assignments_from_records(&inventory.subnet_records)?;
846 let mut nodes = inventory
847 .node_records
848 .into_iter()
849 .map(|(principal, record)| {
850 node_from_record(
851 principal,
852 record,
853 &inventory.node_operator_records,
854 &inventory.subnet_records,
855 &node_subnets,
856 )
857 })
858 .collect::<Result<Vec<_>, _>>()?;
859 nodes.sort_by(|left, right| left.principal.cmp(&right.principal));
860 Ok(MainnetNodeList {
861 network: MAINNET_NETWORK.to_string(),
862 registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
863 registry_version,
864 fetched_at: request.fetched_at.clone(),
865 fetched_by: request.fetched_by.clone(),
866 source_endpoint: request.endpoint.clone(),
867 nodes,
868 })
869}
870
871fn node_from_record(
872 principal: String,
873 record: NodeRecord,
874 node_operator_records: &BTreeMap<String, NodeOperatorRecord>,
875 subnet_records: &BTreeMap<String, SubnetRecord>,
876 node_subnets: &BTreeMap<String, String>,
877) -> Result<MainnetNode, RegistryFetchError> {
878 let node_operator_principal =
879 principal_text_from_required_raw(&record.node_operator_id, "node_record.node_operator_id")?;
880 let node_operator_record = node_operator_records.get(&node_operator_principal).ok_or(
881 RegistryFetchError::MissingField {
882 field: "node_operator_record",
883 },
884 )?;
885 let node_provider_principal = principal_text_from_required_raw(
886 &node_operator_record.node_provider_principal_id,
887 "node_operator_record.node_provider_principal_id",
888 )?;
889 let subnet_principal =
890 node_subnets
891 .get(&principal)
892 .ok_or(RegistryFetchError::MissingField {
893 field: "node_subnet_assignment",
894 })?;
895 let subnet_record =
896 subnet_records
897 .get(subnet_principal)
898 .ok_or(RegistryFetchError::MissingField {
899 field: "subnet_record",
900 })?;
901 Ok(MainnetNode {
902 principal,
903 node_operator_principal,
904 node_provider_principal,
905 subnet_principal: subnet_principal.clone(),
906 subnet_kind: subnet_kind_text(subnet_record),
907 data_center_id: node_operator_record.dc_id.clone(),
908 })
909}
910
911fn data_center_list_from_inventory(
912 request: &MainnetRegistryFetchRequest,
913 inventory: RegistryRelationInventory,
914 registry_version: u64,
915) -> Result<MainnetDataCenterList, RegistryFetchError> {
916 let node_counts = data_center_node_counts_from_records(
917 &inventory.node_principals,
918 &inventory.node_records,
919 &inventory.node_operator_records,
920 )?;
921 let operator_counts =
922 data_center_operator_counts_from_records(&inventory.node_operator_records);
923 let provider_counts =
924 data_center_provider_counts_from_records(&inventory.node_operator_records)?;
925 let mut data_centers = inventory
926 .data_center_records
927 .into_iter()
928 .map(|(id, record)| {
929 data_center_from_record(id, record, &operator_counts, &provider_counts, &node_counts)
930 })
931 .collect::<Result<Vec<_>, _>>()?;
932 data_centers.sort_by(|left, right| left.id.cmp(&right.id));
933 Ok(MainnetDataCenterList {
934 network: MAINNET_NETWORK.to_string(),
935 registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
936 registry_version,
937 fetched_at: request.fetched_at.clone(),
938 fetched_by: request.fetched_by.clone(),
939 source_endpoint: request.endpoint.clone(),
940 data_centers,
941 })
942}
943
944fn data_center_from_record(
945 id: String,
946 record: DataCenterRecord,
947 operator_counts: &BTreeMap<String, u32>,
948 provider_counts: &BTreeMap<String, u32>,
949 node_counts: &BTreeMap<String, u32>,
950) -> Result<MainnetDataCenter, RegistryFetchError> {
951 if !record.id.is_empty() && normalized_data_center_id(&record.id).as_deref() != Some(&id) {
952 return Err(RegistryFetchError::InvalidDataCenterRecordId {
953 key_id: id,
954 record_id: record.id,
955 });
956 }
957 Ok(MainnetDataCenter {
958 node_operator_count: operator_counts.get(&id).copied().unwrap_or(0),
959 node_provider_count: provider_counts.get(&id).copied().unwrap_or(0),
960 node_count: node_counts.get(&id).copied().unwrap_or(0),
961 id,
962 region: record.region,
963 owner: record.owner,
964 latitude: record.gps.as_ref().map(|gps| gps.latitude),
965 longitude: record.gps.as_ref().map(|gps| gps.longitude),
966 })
967}
968
969async fn get_latest_version(
970 agent: &Agent,
971 registry_canister: &Principal,
972) -> Result<u64, RegistryFetchError> {
973 let bytes = agent
974 .query(registry_canister, "get_latest_version")
975 .with_arg(Vec::<u8>::new())
976 .call()
977 .await
978 .map_err(|err| RegistryFetchError::AgentCall {
979 method: "get_latest_version",
980 reason: err.to_string(),
981 })?;
982 let response = decode_message::<RegistryGetLatestVersionResponse>(
983 "RegistryGetLatestVersionResponse",
984 &bytes,
985 )?;
986 Ok(response.version)
987}
988
989async fn get_registry_value(
990 agent: &Agent,
991 registry_canister: &Principal,
992 key: &str,
993 version: u64,
994) -> Result<Vec<u8>, RegistryFetchError> {
995 let request = RegistryGetValueRequest {
996 version: Some(UInt64Value { value: version }),
997 key: key.as_bytes().to_vec(),
998 };
999 let mut arg = Vec::new();
1000 request
1001 .encode(&mut arg)
1002 .map_err(|err| RegistryFetchError::ProtobufEncode {
1003 message: "RegistryGetValueRequest",
1004 reason: err.to_string(),
1005 })?;
1006 let bytes = agent
1007 .query(registry_canister, "get_value")
1008 .with_arg(arg)
1009 .call()
1010 .await
1011 .map_err(|err| RegistryFetchError::AgentCall {
1012 method: "get_value",
1013 reason: err.to_string(),
1014 })?;
1015 let response = decode_message::<RegistryGetValueResponse>("RegistryGetValueResponse", &bytes)?;
1016 match registry_value_content_from_response(key, response)? {
1017 RegistryValueContent::Value(value) => Ok(value),
1018 RegistryValueContent::LargeValueChunkKeys(keys) => {
1019 get_large_registry_value(agent, registry_canister, &keys).await
1020 }
1021 }
1022}
1023
1024fn registry_value_content_from_response(
1025 key: &str,
1026 response: RegistryGetValueResponse,
1027) -> Result<RegistryValueContent, RegistryFetchError> {
1028 if let Some(error) = response.error {
1029 return Err(RegistryFetchError::RegistryValue {
1030 key: key.to_string(),
1031 code: registry_error_code(error.code).to_string(),
1032 reason: error.reason,
1033 });
1034 }
1035 match response.content {
1036 Some(registry_get_value_response::Content::Value(value)) => {
1037 Ok(RegistryValueContent::Value(value))
1038 }
1039 Some(registry_get_value_response::Content::LargeValueChunkKeys(keys)) => {
1040 Ok(RegistryValueContent::LargeValueChunkKeys(keys))
1041 }
1042 None => Err(RegistryFetchError::MissingValue {
1043 key: key.to_string(),
1044 }),
1045 }
1046}
1047
1048async fn get_large_registry_value(
1049 agent: &Agent,
1050 registry_canister: &Principal,
1051 keys: &LargeValueChunkKeys,
1052) -> Result<Vec<u8>, RegistryFetchError> {
1053 let mut value = Vec::new();
1054 for chunk_sha256 in &keys.chunk_content_sha256s {
1055 let chunk_content = get_registry_chunk(agent, registry_canister, chunk_sha256).await?;
1056 append_validated_chunk(&mut value, chunk_sha256, chunk_content)?;
1057 }
1058 Ok(value)
1059}
1060
1061async fn get_registry_chunk(
1062 agent: &Agent,
1063 registry_canister: &Principal,
1064 content_sha256: &[u8],
1065) -> Result<Vec<u8>, RegistryFetchError> {
1066 let request = RegistryGetChunkRequest {
1067 content_sha256: Some(content_sha256.to_vec()),
1068 };
1069 let arg = Encode!(&request).map_err(|err| RegistryFetchError::CandidEncode {
1070 message: "RegistryGetChunkRequest",
1071 reason: err.to_string(),
1072 })?;
1073 let bytes = agent
1074 .query(registry_canister, "get_chunk")
1075 .with_arg(arg)
1076 .call()
1077 .await
1078 .map_err(|err| RegistryFetchError::AgentCall {
1079 method: "get_chunk",
1080 reason: err.to_string(),
1081 })?;
1082 let result = Decode!(&bytes, Result<RegistryChunk, String>).map_err(|err| {
1083 RegistryFetchError::CandidDecode {
1084 message: "Result<RegistryChunk, String>",
1085 reason: err.to_string(),
1086 }
1087 })?;
1088 match result {
1089 Ok(chunk) => chunk
1090 .content
1091 .ok_or_else(|| RegistryFetchError::MissingChunkContent {
1092 sha256: hex_bytes(content_sha256),
1093 }),
1094 Err(reason) => Err(RegistryFetchError::RegistryChunkRejected {
1095 sha256: hex_bytes(content_sha256),
1096 reason,
1097 }),
1098 }
1099}
1100
1101fn append_validated_chunk(
1102 value: &mut Vec<u8>,
1103 expected_sha256: &[u8],
1104 chunk_content: Vec<u8>,
1105) -> Result<(), RegistryFetchError> {
1106 let actual_sha256 = sha256_digest(&chunk_content);
1107 if actual_sha256.as_slice() != expected_sha256 {
1108 return Err(RegistryFetchError::ChunkHashMismatch {
1109 sha256: hex_bytes(expected_sha256),
1110 actual_sha256: hex_bytes(&actual_sha256),
1111 });
1112 }
1113 value.extend(chunk_content);
1114 Ok(())
1115}
1116
1117fn sha256_digest(bytes: &[u8]) -> [u8; 32] {
1118 Sha256::digest(bytes).into()
1119}
1120
1121fn hex_bytes(bytes: &[u8]) -> String {
1122 const HEX: &[u8; 16] = b"0123456789abcdef";
1123 let mut out = String::with_capacity(bytes.len() * 2);
1124 for byte in bytes {
1125 out.push(HEX[usize::from(byte >> 4)] as char);
1126 out.push(HEX[usize::from(byte & 0x0f)] as char);
1127 }
1128 out
1129}
1130
1131fn decode_message<T>(message: &'static str, bytes: &[u8]) -> Result<T, RegistryFetchError>
1132where
1133 T: Message + Default,
1134{
1135 T::decode(bytes).map_err(|err| RegistryFetchError::ProtobufDecode {
1136 message,
1137 reason: err.to_string(),
1138 })
1139}
1140
1141fn subnet_info_from_record(subnet_principal: &str, record: &SubnetRecord) -> SubnetInfo {
1142 let subnet_kind = match SubnetType::try_from(record.subnet_type).ok() {
1143 Some(SubnetType::Application | SubnetType::VerifiedApplication) => SubnetKind::Application,
1144 Some(SubnetType::CloudEngine) => SubnetKind::CloudEngine,
1145 Some(SubnetType::System) => SubnetKind::System,
1146 Some(SubnetType::Unspecified) | None => SubnetKind::Unknown,
1147 };
1148 let charges_apply_by_default = subnet_kind.charges_apply_by_default();
1149 SubnetInfo {
1150 subnet_principal: subnet_principal.to_string(),
1151 subnet_kind,
1152 subnet_kind_source: ClassificationSource::Registry,
1153 subnet_specialization: SubnetSpecialization::None,
1154 subnet_specialization_source: ClassificationSource::Computed,
1155 geographic_scope: GeographicScope::Global,
1156 geographic_scope_source: ClassificationSource::Computed,
1157 subnet_label: subnet_kind.as_str().to_string(),
1158 subnet_label_source: ClassificationSource::Computed,
1159 node_count: Some(u32::try_from(record.membership.len()).unwrap_or(u32::MAX)),
1160 charges_apply_by_default,
1161 }
1162}
1163
1164fn routing_ranges_from_table(
1165 table: &RoutingTable,
1166) -> Result<Vec<RoutingRange>, RegistryFetchError> {
1167 table
1168 .entries
1169 .iter()
1170 .map(|entry| {
1171 let range = entry
1172 .range
1173 .as_ref()
1174 .ok_or(RegistryFetchError::MissingField {
1175 field: "routing_table.entries.range",
1176 })?;
1177 let subnet_id = entry
1178 .subnet_id
1179 .as_ref()
1180 .ok_or(RegistryFetchError::MissingField {
1181 field: "routing_table.entries.subnet_id",
1182 })?;
1183 Ok(RoutingRange {
1184 start_canister_id: canister_id_text(
1185 range.start_canister_id.as_ref(),
1186 "range.start",
1187 )?,
1188 end_canister_id: canister_id_text(range.end_canister_id.as_ref(), "range.end")?,
1189 subnet_principal: subnet_id_text(subnet_id)?,
1190 })
1191 })
1192 .collect()
1193}
1194
1195fn canister_id_text(
1196 canister_id: Option<&CanisterId>,
1197 field: &'static str,
1198) -> Result<String, RegistryFetchError> {
1199 let principal = canister_id
1200 .and_then(|id| id.principal_id.as_ref())
1201 .ok_or(RegistryFetchError::MissingField { field })?;
1202 principal_text_from_raw(&principal.raw, field)
1203}
1204
1205fn subnet_id_text(subnet_id: &SubnetId) -> Result<String, RegistryFetchError> {
1206 let principal = subnet_id
1207 .principal_id
1208 .as_ref()
1209 .ok_or(RegistryFetchError::MissingField {
1210 field: "routing_table.entries.subnet_id.principal_id",
1211 })?;
1212 principal_text_from_raw(&principal.raw, "routing_table.entries.subnet_id")
1213}
1214
1215fn principal_text_from_raw(raw: &[u8], field: &'static str) -> Result<String, RegistryFetchError> {
1216 Principal::try_from_slice(raw)
1217 .map(|principal| principal.to_text())
1218 .map_err(|err| RegistryFetchError::InvalidPrincipal {
1219 field,
1220 reason: err.to_string(),
1221 })
1222}
1223
1224fn principal_text_from_required_raw(
1225 raw: &[u8],
1226 field: &'static str,
1227) -> Result<String, RegistryFetchError> {
1228 if raw.is_empty() {
1229 return Err(RegistryFetchError::MissingField { field });
1230 }
1231 principal_text_from_raw(raw, field)
1232}
1233
1234fn subnet_record_key(subnet_principal: &str) -> String {
1235 format!("{SUBNET_RECORD_KEY_PREFIX}{subnet_principal}")
1236}
1237
1238fn node_record_key(node_principal: &str) -> String {
1239 format!("{NODE_RECORD_KEY_PREFIX}{node_principal}")
1240}
1241
1242fn node_operator_record_key(node_operator_principal: &str) -> String {
1243 format!("{NODE_OPERATOR_RECORD_KEY_PREFIX}{node_operator_principal}")
1244}
1245
1246fn data_center_record_key(data_center_id: &str) -> String {
1247 format!("{DATA_CENTER_RECORD_KEY_PREFIX}{data_center_id}")
1248}
1249
1250fn normalized_data_center_id(data_center_id: &str) -> Option<String> {
1251 let trimmed = data_center_id.trim();
1252 if trimmed.is_empty() {
1253 None
1254 } else {
1255 Some(trimmed.to_ascii_lowercase())
1256 }
1257}
1258
1259fn assigned_node_principals_from_subnets(
1260 subnet_records: &BTreeMap<String, SubnetRecord>,
1261) -> Result<BTreeSet<String>, RegistryFetchError> {
1262 let mut node_principals = BTreeSet::new();
1263 for record in subnet_records.values() {
1264 for raw in &record.membership {
1265 node_principals.insert(principal_text_from_raw(raw, "subnet_record.membership")?);
1266 }
1267 }
1268 Ok(node_principals)
1269}
1270
1271fn node_subnet_assignments_from_records(
1272 subnet_records: &BTreeMap<String, SubnetRecord>,
1273) -> Result<BTreeMap<String, String>, RegistryFetchError> {
1274 let mut assignments = BTreeMap::new();
1275 for (subnet_principal, record) in subnet_records {
1276 for raw in &record.membership {
1277 let node_principal = principal_text_from_raw(raw, "subnet_record.membership")?;
1278 assignments.insert(node_principal, subnet_principal.clone());
1279 }
1280 }
1281 Ok(assignments)
1282}
1283
1284fn node_provider_counts_from_records(
1285 node_principals: &BTreeSet<String>,
1286 node_records: &BTreeMap<String, NodeRecord>,
1287 node_operator_records: &BTreeMap<String, NodeOperatorRecord>,
1288) -> Result<BTreeMap<String, u32>, RegistryFetchError> {
1289 let mut counts = BTreeMap::<String, u32>::new();
1290 for relation in assigned_node_relations(node_principals, node_records, node_operator_records)? {
1291 let count = counts.entry(relation.node_provider_principal).or_default();
1292 *count = count.saturating_add(1);
1293 }
1294 Ok(counts)
1295}
1296
1297fn node_operator_counts_from_records(
1298 node_principals: &BTreeSet<String>,
1299 node_records: &BTreeMap<String, NodeRecord>,
1300 node_operator_records: &BTreeMap<String, NodeOperatorRecord>,
1301) -> Result<BTreeMap<String, u32>, RegistryFetchError> {
1302 let mut counts = BTreeMap::<String, u32>::new();
1303 for relation in assigned_node_relations(node_principals, node_records, node_operator_records)? {
1304 let count = counts.entry(relation.node_operator_principal).or_default();
1305 *count = count.saturating_add(1);
1306 }
1307 Ok(counts)
1308}
1309
1310fn data_center_node_counts_from_records(
1311 node_principals: &BTreeSet<String>,
1312 node_records: &BTreeMap<String, NodeRecord>,
1313 node_operator_records: &BTreeMap<String, NodeOperatorRecord>,
1314) -> Result<BTreeMap<String, u32>, RegistryFetchError> {
1315 let mut counts = BTreeMap::<String, u32>::new();
1316 for relation in assigned_node_relations(node_principals, node_records, node_operator_records)? {
1317 if let Some(data_center_id) = relation.data_center_id {
1318 let count = counts.entry(data_center_id).or_default();
1319 *count = count.saturating_add(1);
1320 }
1321 }
1322 Ok(counts)
1323}
1324
1325fn assigned_node_relations(
1326 node_principals: &BTreeSet<String>,
1327 node_records: &BTreeMap<String, NodeRecord>,
1328 node_operator_records: &BTreeMap<String, NodeOperatorRecord>,
1329) -> Result<Vec<AssignedNodeRelation>, RegistryFetchError> {
1330 let mut relations = Vec::with_capacity(node_principals.len());
1331 for node_principal in node_principals {
1332 let node_record =
1333 node_records
1334 .get(node_principal)
1335 .ok_or(RegistryFetchError::MissingField {
1336 field: "node_record",
1337 })?;
1338 let node_operator_principal = principal_text_from_required_raw(
1339 &node_record.node_operator_id,
1340 "node_record.node_operator_id",
1341 )?;
1342 let node_operator_record = node_operator_records.get(&node_operator_principal).ok_or(
1343 RegistryFetchError::MissingField {
1344 field: "node_operator_record",
1345 },
1346 )?;
1347 let node_provider_principal = principal_text_from_required_raw(
1348 &node_operator_record.node_provider_principal_id,
1349 "node_operator_record.node_provider_principal_id",
1350 )?;
1351 relations.push(AssignedNodeRelation {
1352 node_operator_principal,
1353 node_provider_principal,
1354 data_center_id: normalized_data_center_id(&node_operator_record.dc_id),
1355 });
1356 }
1357 Ok(relations)
1358}
1359
1360fn data_center_operator_counts_from_records(
1361 node_operator_records: &BTreeMap<String, NodeOperatorRecord>,
1362) -> BTreeMap<String, u32> {
1363 let mut counts = BTreeMap::<String, u32>::new();
1364 for record in node_operator_records.values() {
1365 if let Some(data_center_id) = normalized_data_center_id(&record.dc_id) {
1366 let count = counts.entry(data_center_id).or_default();
1367 *count = count.saturating_add(1);
1368 }
1369 }
1370 counts
1371}
1372
1373fn data_center_provider_counts_from_records(
1374 node_operator_records: &BTreeMap<String, NodeOperatorRecord>,
1375) -> Result<BTreeMap<String, u32>, RegistryFetchError> {
1376 let mut providers_by_data_center = BTreeMap::<String, BTreeSet<String>>::new();
1377 for record in node_operator_records.values() {
1378 let Some(data_center_id) = normalized_data_center_id(&record.dc_id) else {
1379 continue;
1380 };
1381 let node_provider_principal = principal_text_from_required_raw(
1382 &record.node_provider_principal_id,
1383 "node_operator_record.node_provider_principal_id",
1384 )?;
1385 providers_by_data_center
1386 .entry(data_center_id)
1387 .or_default()
1388 .insert(node_provider_principal);
1389 }
1390 Ok(providers_by_data_center
1391 .into_iter()
1392 .map(|(data_center_id, providers)| {
1393 (
1394 data_center_id,
1395 u32::try_from(providers.len()).unwrap_or(u32::MAX),
1396 )
1397 })
1398 .collect())
1399}
1400
1401struct RegistryRelationInventory {
1405 node_principals: BTreeSet<String>,
1406 node_records: BTreeMap<String, NodeRecord>,
1407 node_operator_records: BTreeMap<String, NodeOperatorRecord>,
1408 subnet_records: BTreeMap<String, SubnetRecord>,
1409 data_center_records: BTreeMap<String, DataCenterRecord>,
1410}
1411
1412#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1416enum RegistryRelationInventoryScope {
1417 BaseRelations,
1418 WithDataCenters,
1419}
1420
1421struct AssignedNodeRelation {
1425 node_operator_principal: String,
1426 node_provider_principal: String,
1427 data_center_id: Option<String>,
1428}
1429
1430fn subnet_kind_text(record: &SubnetRecord) -> String {
1431 match SubnetType::try_from(record.subnet_type).ok() {
1432 Some(SubnetType::Application | SubnetType::VerifiedApplication) => "application",
1433 Some(SubnetType::CloudEngine) => "cloud_engine",
1434 Some(SubnetType::System) => "system",
1435 Some(SubnetType::Unspecified) | None => "unknown",
1436 }
1437 .to_string()
1438}
1439
1440fn apply_mainnet_annotations(catalog: &mut SubnetCatalog) {
1441 let annotations = mainnet_annotations();
1442 for subnet in &mut catalog.subnets {
1443 let Some(annotation) = annotations.get(subnet.subnet_principal.as_str()) else {
1444 continue;
1445 };
1446 subnet.subnet_specialization = annotation.specialization;
1447 subnet.subnet_specialization_source = ClassificationSource::Curated;
1448 subnet.geographic_scope = annotation.geographic_scope;
1449 subnet.geographic_scope_source = ClassificationSource::Curated;
1450 subnet.subnet_label.clone_from(&annotation.label);
1451 subnet.subnet_label_source = ClassificationSource::Curated;
1452 }
1453}
1454
1455fn mainnet_annotations() -> BTreeMap<&'static str, MainnetAnnotation> {
1456 BTreeMap::from([
1457 (
1458 FIDUCIARY_SUBNET,
1459 MainnetAnnotation {
1460 specialization: SubnetSpecialization::Fiduciary,
1461 geographic_scope: GeographicScope::Global,
1462 label: "fiduciary".to_string(),
1463 },
1464 ),
1465 (
1466 EUROPEAN_SUBNET,
1467 MainnetAnnotation {
1468 specialization: SubnetSpecialization::European,
1469 geographic_scope: GeographicScope::Europe,
1470 label: "european".to_string(),
1471 },
1472 ),
1473 ])
1474}
1475
1476#[derive(Clone, Debug, Eq, PartialEq)]
1480struct MainnetAnnotation {
1481 specialization: SubnetSpecialization,
1482 geographic_scope: GeographicScope,
1483 label: String,
1484}
1485
1486fn registry_error_code(code: i32) -> &'static str {
1487 match RegistryErrorCode::try_from(code).ok() {
1488 Some(RegistryErrorCode::MalformedMessage) => "malformed_message",
1489 Some(RegistryErrorCode::KeyNotPresent) => "key_not_present",
1490 Some(RegistryErrorCode::KeyAlreadyPresent) => "key_already_present",
1491 Some(RegistryErrorCode::VersionNotLatest) => "version_not_latest",
1492 Some(RegistryErrorCode::VersionBeyondLatest) => "version_beyond_latest",
1493 Some(RegistryErrorCode::Authorization) => "authorization",
1494 Some(RegistryErrorCode::InternalError) => "internal_error",
1495 None => "unknown",
1496 }
1497}
1498
1499#[cfg(test)]
1500mod tests {
1501 use super::*;
1502 use proto::{CanisterIdRange, PrincipalId, RoutingTableEntry};
1503
1504 const SUBNET_A: &str = "pzp6e-ekpqk-3c5x7-2h6so-njoeq-mt45d-h3h6c-q3mxf-vpeq5-fk5o7-yae";
1505 const SUBNET_B: &str = "aaaaa-aa";
1506 const CANISTER_A: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai";
1507
1508 #[test]
1509 fn registry_records_convert_to_catalog_domain_structs() {
1510 let request = MainnetRegistryFetchRequest {
1511 endpoint: "https://icp-api.io".to_string(),
1512 fetched_at: "2026-06-04T00:00:00Z".to_string(),
1513 fetched_by: "test".to_string(),
1514 };
1515 let subnet_records = BTreeMap::from([
1516 (
1517 SUBNET_A.to_string(),
1518 subnet_record(SubnetType::Application, 34),
1519 ),
1520 (SUBNET_B.to_string(), subnet_record(SubnetType::System, 13)),
1521 ]);
1522 let catalog = catalog_from_parts_for_test(
1523 &request,
1524 42,
1525 subnet_list_record([SUBNET_A, SUBNET_B]),
1526 routing_table_record([(CANISTER_A, CANISTER_A, SUBNET_A)]),
1527 subnet_records,
1528 )
1529 .expect("catalog");
1530
1531 assert_eq!(catalog.registry_version, 42);
1532 assert_eq!(catalog.subnets.len(), 2);
1533 assert_eq!(catalog.routing_ranges.len(), 1);
1534 let fiduciary = catalog.subnet_by_principal(SUBNET_A).expect("fiduciary");
1535 assert_eq!(
1536 fiduciary.subnet_specialization,
1537 SubnetSpecialization::Fiduciary
1538 );
1539 assert_eq!(fiduciary.node_count, Some(34));
1540 assert!(fiduciary.charges_apply_by_default);
1541 let system = catalog.subnet_by_principal(SUBNET_B).expect("system");
1542 assert_eq!(system.subnet_kind, SubnetKind::System);
1543 assert!(!system.charges_apply_by_default);
1544 }
1545
1546 #[test]
1547 fn registry_records_preserve_cloud_engine_subnet_type() {
1548 let request = MainnetRegistryFetchRequest {
1549 endpoint: "https://icp-api.io".to_string(),
1550 fetched_at: "2026-06-04T00:00:00Z".to_string(),
1551 fetched_by: "test".to_string(),
1552 };
1553 let subnet_records = BTreeMap::from([(
1554 SUBNET_A.to_string(),
1555 subnet_record(SubnetType::CloudEngine, 13),
1556 )]);
1557 let catalog = catalog_from_parts_for_test(
1558 &request,
1559 42,
1560 subnet_list_record([SUBNET_A]),
1561 routing_table_record([(CANISTER_A, CANISTER_A, SUBNET_A)]),
1562 subnet_records,
1563 )
1564 .expect("catalog");
1565
1566 let subnet = catalog.subnet_by_principal(SUBNET_A).expect("subnet");
1567 assert_eq!(subnet.subnet_kind, SubnetKind::CloudEngine);
1568 assert!(subnet.charges_apply_by_default);
1569 }
1570
1571 #[test]
1572 fn get_value_response_reports_large_value_chunk_keys() {
1573 let response = RegistryGetValueResponse {
1574 error: None,
1575 version: 1,
1576 content: Some(registry_get_value_response::Content::LargeValueChunkKeys(
1577 proto::LargeValueChunkKeys {
1578 chunk_content_sha256s: vec![vec![1], vec![2]],
1579 },
1580 )),
1581 timestamp_nanoseconds: 0,
1582 };
1583
1584 let content = registry_value_content_from_response("routing_table", response)
1585 .expect("large value chunk keys");
1586
1587 match content {
1588 RegistryValueContent::LargeValueChunkKeys(keys) => {
1589 assert_eq!(keys.chunk_content_sha256s, vec![vec![1], vec![2]]);
1590 }
1591 RegistryValueContent::Value(value) => {
1592 panic!("expected chunk keys, got inline value {value:?}");
1593 }
1594 }
1595 }
1596
1597 #[test]
1598 fn registry_get_chunk_request_candid_round_trips() {
1599 let request = RegistryGetChunkRequest {
1600 content_sha256: Some(vec![1, 2, 3]),
1601 };
1602
1603 let bytes = Encode!(&request).expect("encode");
1604 let decoded = Decode!(&bytes, RegistryGetChunkRequest).expect("decode");
1605
1606 assert_eq!(decoded, request);
1607 }
1608
1609 #[test]
1610 fn governance_node_provider_response_converts_to_domain_structs() {
1611 let request = MainnetRegistryFetchRequest {
1612 endpoint: "https://icp-api.io".to_string(),
1613 fetched_at: "2026-06-04T00:00:00Z".to_string(),
1614 fetched_by: "test".to_string(),
1615 };
1616 let node_counts = BTreeMap::from([("aaaaa-aa".to_string(), 2)]);
1617 let response = ListNodeProvidersResponse {
1618 node_providers: vec![
1619 governance_node_provider("ryjl3-tyaaa-aaaaa-aaaba-cai", None),
1620 governance_node_provider("aaaaa-aa", Some(vec![0xab, 0xcd])),
1621 ],
1622 };
1623
1624 let list = node_provider_list_from_response(&request, response, node_counts, 42)
1625 .expect("node providers");
1626
1627 assert_eq!(list.network, MAINNET_NETWORK);
1628 assert_eq!(list.governance_canister_id, MAINNET_GOVERNANCE_CANISTER_ID);
1629 assert_eq!(list.registry_canister_id, MAINNET_REGISTRY_CANISTER_ID);
1630 assert_eq!(list.registry_version, 42);
1631 assert_eq!(list.node_providers.len(), 2);
1632 assert_eq!(list.node_providers[0].principal, "aaaaa-aa");
1633 assert_eq!(list.node_providers[0].node_count, Some(2));
1634 assert_eq!(
1635 list.node_providers[0].reward_account_hex.as_deref(),
1636 Some("abcd")
1637 );
1638 assert_eq!(
1639 list.node_providers[1].principal,
1640 "ryjl3-tyaaa-aaaaa-aaaba-cai"
1641 );
1642 assert_eq!(list.node_providers[1].node_count, Some(0));
1643 assert_eq!(list.node_providers[1].reward_account_hex, None);
1644 }
1645
1646 #[test]
1647 fn node_provider_counts_follow_subnet_nodes_to_providers() {
1648 let provider_a = Principal::self_authenticating(b"provider-a").to_text();
1649 let provider_b = Principal::self_authenticating(b"provider-b").to_text();
1650 let operator_a = Principal::self_authenticating(b"operator-a").to_text();
1651 let operator_b = Principal::self_authenticating(b"operator-b").to_text();
1652 let node_a = Principal::self_authenticating(b"node-a").to_text();
1653 let node_b = Principal::self_authenticating(b"node-b").to_text();
1654 let node_c = Principal::self_authenticating(b"node-c").to_text();
1655 let subnet = Principal::self_authenticating(b"subnet").to_text();
1656 let subnet_records = BTreeMap::from([(
1657 subnet,
1658 SubnetRecord {
1659 membership: vec![
1660 principal_raw(&node_a),
1661 principal_raw(&node_b),
1662 principal_raw(&node_c),
1663 ],
1664 subnet_type: SubnetType::Application as i32,
1665 canister_cycles_cost_schedule: 0,
1666 },
1667 )]);
1668 let node_principals =
1669 assigned_node_principals_from_subnets(&subnet_records).expect("node principals");
1670 let node_records = BTreeMap::from([
1671 (node_a, node_record(&operator_a)),
1672 (node_b, node_record(&operator_a)),
1673 (node_c, node_record(&operator_b)),
1674 ]);
1675 let node_operator_records = BTreeMap::from([
1676 (operator_a, node_operator_record(&provider_a)),
1677 (operator_b, node_operator_record(&provider_b)),
1678 ]);
1679
1680 let counts = node_provider_counts_from_records(
1681 &node_principals,
1682 &node_records,
1683 &node_operator_records,
1684 )
1685 .expect("provider counts");
1686
1687 assert_eq!(counts.get(&provider_a), Some(&2));
1688 assert_eq!(counts.get(&provider_b), Some(&1));
1689 }
1690
1691 #[test]
1692 fn node_operator_list_follows_assigned_nodes_to_operator_records() {
1693 let request = MainnetRegistryFetchRequest {
1694 endpoint: "https://icp-api.io".to_string(),
1695 fetched_at: "2026-06-04T00:00:00Z".to_string(),
1696 fetched_by: "test".to_string(),
1697 };
1698 let provider = Principal::self_authenticating(b"provider").to_text();
1699 let primary_operator = Principal::self_authenticating(b"operator-a").to_text();
1700 let secondary_operator = Principal::self_authenticating(b"operator-b").to_text();
1701 let node_a = Principal::self_authenticating(b"node-a").to_text();
1702 let node_b = Principal::self_authenticating(b"node-b").to_text();
1703 let node_c = Principal::self_authenticating(b"node-c").to_text();
1704 let subnet = Principal::self_authenticating(b"subnet").to_text();
1705 let inventory = RegistryRelationInventory {
1706 node_principals: BTreeSet::from([node_a.clone(), node_b.clone(), node_c.clone()]),
1707 node_records: BTreeMap::from([
1708 (node_a.clone(), node_record(&primary_operator)),
1709 (node_b.clone(), node_record(&primary_operator)),
1710 (node_c.clone(), node_record(&secondary_operator)),
1711 ]),
1712 node_operator_records: BTreeMap::from([
1713 (
1714 primary_operator.clone(),
1715 NodeOperatorRecord {
1716 node_operator_principal_id: principal_raw(&primary_operator),
1717 node_allowance: 4,
1718 node_provider_principal_id: principal_raw(&provider),
1719 dc_id: "dc-a".to_string(),
1720 },
1721 ),
1722 (
1723 secondary_operator.clone(),
1724 NodeOperatorRecord {
1725 node_operator_principal_id: principal_raw(&secondary_operator),
1726 node_allowance: 7,
1727 node_provider_principal_id: principal_raw(&provider),
1728 dc_id: "dc-b".to_string(),
1729 },
1730 ),
1731 ]),
1732 subnet_records: BTreeMap::from([(
1733 subnet,
1734 SubnetRecord {
1735 membership: vec![
1736 principal_raw(&node_a),
1737 principal_raw(&node_b),
1738 principal_raw(&node_c),
1739 ],
1740 subnet_type: SubnetType::Application as i32,
1741 canister_cycles_cost_schedule: 0,
1742 },
1743 )]),
1744 data_center_records: BTreeMap::new(),
1745 };
1746
1747 let list =
1748 node_operator_list_from_inventory(&request, inventory, 42).expect("node operators");
1749
1750 assert_eq!(list.network, MAINNET_NETWORK);
1751 assert_eq!(list.registry_canister_id, MAINNET_REGISTRY_CANISTER_ID);
1752 assert_eq!(list.registry_version, 42);
1753 assert_eq!(list.node_operators.len(), 2);
1754 let primary_result = list
1755 .node_operators
1756 .iter()
1757 .find(|operator| operator.principal == primary_operator)
1758 .expect("primary operator");
1759 assert_eq!(primary_result.node_provider_principal, provider);
1760 assert_eq!(primary_result.node_allowance, 4);
1761 assert_eq!(primary_result.data_center_id, "dc-a");
1762 assert_eq!(primary_result.node_count, Some(2));
1763 let secondary_result = list
1764 .node_operators
1765 .iter()
1766 .find(|operator| operator.principal == secondary_operator)
1767 .expect("secondary operator");
1768 assert_eq!(secondary_result.node_count, Some(1));
1769 }
1770
1771 #[test]
1772 fn node_list_follows_nodes_to_subnets_operators_and_providers() {
1773 let request = MainnetRegistryFetchRequest {
1774 endpoint: "https://icp-api.io".to_string(),
1775 fetched_at: "2026-06-04T00:00:00Z".to_string(),
1776 fetched_by: "test".to_string(),
1777 };
1778 let provider = Principal::self_authenticating(b"provider").to_text();
1779 let operator = Principal::self_authenticating(b"operator").to_text();
1780 let node = Principal::self_authenticating(b"node").to_text();
1781 let subnet = Principal::self_authenticating(b"subnet").to_text();
1782 let inventory = RegistryRelationInventory {
1783 node_principals: BTreeSet::from([node.clone()]),
1784 node_records: BTreeMap::from([(node.clone(), node_record(&operator))]),
1785 node_operator_records: BTreeMap::from([(
1786 operator.clone(),
1787 NodeOperatorRecord {
1788 node_operator_principal_id: principal_raw(&operator),
1789 node_allowance: 4,
1790 node_provider_principal_id: principal_raw(&provider),
1791 dc_id: "dc-a".to_string(),
1792 },
1793 )]),
1794 subnet_records: BTreeMap::from([(
1795 subnet.clone(),
1796 SubnetRecord {
1797 membership: vec![principal_raw(&node)],
1798 subnet_type: SubnetType::Application as i32,
1799 canister_cycles_cost_schedule: 0,
1800 },
1801 )]),
1802 data_center_records: BTreeMap::new(),
1803 };
1804
1805 let list = node_list_from_inventory(&request, inventory, 42).expect("nodes");
1806
1807 assert_eq!(list.registry_version, 42);
1808 assert_eq!(list.nodes.len(), 1);
1809 assert_eq!(list.nodes[0].principal, node);
1810 assert_eq!(list.nodes[0].node_operator_principal, operator);
1811 assert_eq!(list.nodes[0].node_provider_principal, provider);
1812 assert_eq!(list.nodes[0].subnet_principal, subnet);
1813 assert_eq!(list.nodes[0].subnet_kind, "application");
1814 assert_eq!(list.nodes[0].data_center_id, "dc-a");
1815 }
1816
1817 #[test]
1818 fn data_center_list_aggregates_registry_relations() {
1819 let request = MainnetRegistryFetchRequest {
1820 endpoint: "https://icp-api.io".to_string(),
1821 fetched_at: "2026-06-04T00:00:00Z".to_string(),
1822 fetched_by: "test".to_string(),
1823 };
1824 let provider_a = Principal::self_authenticating(b"provider-a").to_text();
1825 let provider_b = Principal::self_authenticating(b"provider-b").to_text();
1826 let operator_a = Principal::self_authenticating(b"operator-a").to_text();
1827 let operator_b = Principal::self_authenticating(b"operator-b").to_text();
1828 let node_a = Principal::self_authenticating(b"node-a").to_text();
1829 let node_b = Principal::self_authenticating(b"node-b").to_text();
1830 let node_c = Principal::self_authenticating(b"node-c").to_text();
1831 let subnet = Principal::self_authenticating(b"subnet").to_text();
1832 let inventory = RegistryRelationInventory {
1833 node_principals: BTreeSet::from([node_a.clone(), node_b.clone(), node_c.clone()]),
1834 node_records: BTreeMap::from([
1835 (node_a.clone(), node_record(&operator_a)),
1836 (node_b.clone(), node_record(&operator_a)),
1837 (node_c.clone(), node_record(&operator_b)),
1838 ]),
1839 node_operator_records: BTreeMap::from([
1840 (
1841 operator_a.clone(),
1842 NodeOperatorRecord {
1843 node_operator_principal_id: principal_raw(&operator_a),
1844 node_allowance: 4,
1845 node_provider_principal_id: principal_raw(&provider_a),
1846 dc_id: "dc-a".to_string(),
1847 },
1848 ),
1849 (
1850 operator_b.clone(),
1851 NodeOperatorRecord {
1852 node_operator_principal_id: principal_raw(&operator_b),
1853 node_allowance: 7,
1854 node_provider_principal_id: principal_raw(&provider_b),
1855 dc_id: "DC-A".to_string(),
1856 },
1857 ),
1858 ]),
1859 subnet_records: BTreeMap::from([(
1860 subnet,
1861 SubnetRecord {
1862 membership: vec![
1863 principal_raw(&node_a),
1864 principal_raw(&node_b),
1865 principal_raw(&node_c),
1866 ],
1867 subnet_type: SubnetType::Application as i32,
1868 canister_cycles_cost_schedule: 0,
1869 },
1870 )]),
1871 data_center_records: BTreeMap::from([(
1872 "dc-a".to_string(),
1873 DataCenterRecord {
1874 id: "dc-a".to_string(),
1875 region: "eu-west".to_string(),
1876 owner: "example owner".to_string(),
1877 gps: Some(proto::Gps {
1878 latitude: 48.8566,
1879 longitude: 2.3522,
1880 }),
1881 },
1882 )]),
1883 };
1884
1885 let list = data_center_list_from_inventory(&request, inventory, 42).expect("data centers");
1886
1887 assert_eq!(list.registry_version, 42);
1888 assert_eq!(list.data_centers.len(), 1);
1889 assert_eq!(list.data_centers[0].id, "dc-a");
1890 assert_eq!(list.data_centers[0].region, "eu-west");
1891 assert_eq!(list.data_centers[0].owner, "example owner");
1892 assert_eq!(list.data_centers[0].latitude, Some(48.8566));
1893 assert_eq!(list.data_centers[0].longitude, Some(2.3522));
1894 assert_eq!(list.data_centers[0].node_operator_count, 2);
1895 assert_eq!(list.data_centers[0].node_provider_count, 2);
1896 assert_eq!(list.data_centers[0].node_count, 3);
1897 }
1898
1899 #[test]
1900 fn governance_node_provider_requires_principal() {
1901 let err = node_provider_from_governance(
1902 GovernanceNodeProvider {
1903 id: None,
1904 reward_account: None,
1905 },
1906 &BTreeMap::new(),
1907 )
1908 .expect_err("missing principal");
1909
1910 assert!(matches!(
1911 err,
1912 RegistryFetchError::MissingField { field } if field == "node_provider.id"
1913 ));
1914 }
1915
1916 #[test]
1917 fn validated_chunk_append_concatenates_matching_chunks() {
1918 let first = b"hello ".to_vec();
1919 let second = b"world".to_vec();
1920 let first_hash = sha256_digest(&first);
1921 let second_hash = sha256_digest(&second);
1922 let mut value = Vec::new();
1923
1924 append_validated_chunk(&mut value, &first_hash, first).expect("first chunk");
1925 append_validated_chunk(&mut value, &second_hash, second).expect("second chunk");
1926
1927 assert_eq!(value, b"hello world");
1928 }
1929
1930 #[test]
1931 fn validated_chunk_append_rejects_hash_mismatch() {
1932 let expected = sha256_digest(b"expected");
1933
1934 let err = append_validated_chunk(&mut Vec::new(), &expected, b"actual".to_vec())
1935 .expect_err("hash mismatch");
1936
1937 assert!(matches!(
1938 err,
1939 RegistryFetchError::ChunkHashMismatch {
1940 sha256,
1941 actual_sha256
1942 } if sha256 == hex_bytes(&expected)
1943 && actual_sha256 == hex_bytes(&sha256_digest(b"actual"))
1944 ));
1945 }
1946
1947 #[test]
1948 fn get_value_response_reports_registry_errors() {
1949 let response = RegistryGetValueResponse {
1950 error: Some(proto::RegistryError {
1951 code: RegistryErrorCode::KeyNotPresent as i32,
1952 reason: "missing".to_string(),
1953 key: b"routing_table".to_vec(),
1954 }),
1955 version: 1,
1956 content: None,
1957 timestamp_nanoseconds: 0,
1958 };
1959
1960 let err =
1961 registry_value_content_from_response("routing_table", response).expect_err("registry");
1962
1963 assert!(matches!(
1964 err,
1965 RegistryFetchError::RegistryValue {
1966 key,
1967 code,
1968 reason
1969 } if key == "routing_table" && code == "key_not_present" && reason == "missing"
1970 ));
1971 }
1972
1973 fn catalog_from_parts_for_test(
1974 request: &MainnetRegistryFetchRequest,
1975 registry_version: u64,
1976 subnet_list: SubnetListRecord,
1977 routing_table: RoutingTable,
1978 subnet_records: BTreeMap<String, SubnetRecord>,
1979 ) -> Result<SubnetCatalog, RegistryFetchError> {
1980 if subnet_list.subnets.is_empty() {
1981 return Err(RegistryFetchError::EmptySubnetList);
1982 }
1983 if routing_table.entries.is_empty() {
1984 return Err(RegistryFetchError::EmptyRoutingTable);
1985 }
1986 let mut subnets = subnet_list
1987 .subnets
1988 .iter()
1989 .map(|subnet_raw| {
1990 let subnet_principal = principal_text_from_raw(subnet_raw, "subnet_list.subnets")?;
1991 let record = subnet_records.get(&subnet_principal).ok_or(
1992 RegistryFetchError::MissingField {
1993 field: "subnet_record",
1994 },
1995 )?;
1996 Ok(subnet_info_from_record(&subnet_principal, record))
1997 })
1998 .collect::<Result<Vec<_>, RegistryFetchError>>()?;
1999 subnets.sort_by(|left, right| left.subnet_principal.cmp(&right.subnet_principal));
2000 let mut catalog = SubnetCatalog {
2001 catalog_schema_version: CATALOG_SCHEMA_VERSION,
2002 network: MAINNET_NETWORK.to_string(),
2003 registry_canister_id: MAINNET_REGISTRY_CANISTER_ID.to_string(),
2004 registry_version,
2005 fetched_at: request.fetched_at.clone(),
2006 fetched_by: request.fetched_by.clone(),
2007 source_endpoint: request.endpoint.clone(),
2008 resolver_backend: "local-nns-subnet-catalog".to_string(),
2009 subnets,
2010 routing_ranges: routing_ranges_from_table(&routing_table)?,
2011 };
2012 apply_mainnet_annotations(&mut catalog);
2013 catalog.validate()?;
2014 Ok(catalog)
2015 }
2016
2017 fn subnet_list_record<const N: usize>(subnets: [&str; N]) -> SubnetListRecord {
2018 SubnetListRecord {
2019 subnets: subnets.iter().map(|subnet| principal_raw(subnet)).collect(),
2020 }
2021 }
2022
2023 fn subnet_record(subnet_type: SubnetType, members: usize) -> SubnetRecord {
2024 SubnetRecord {
2025 membership: (0..members)
2026 .map(|index| {
2027 let index = u8::try_from(index).expect("fixture member index fits in u8");
2028 principal_raw(&Principal::self_authenticating([index]).to_text())
2029 })
2030 .collect(),
2031 subnet_type: subnet_type as i32,
2032 canister_cycles_cost_schedule: 0,
2033 }
2034 }
2035
2036 fn routing_table_record<const N: usize>(ranges: [(&str, &str, &str); N]) -> RoutingTable {
2037 RoutingTable {
2038 entries: ranges
2039 .iter()
2040 .map(|(start, end, subnet)| RoutingTableEntry {
2041 range: Some(CanisterIdRange {
2042 start_canister_id: Some(canister_id(start)),
2043 end_canister_id: Some(canister_id(end)),
2044 }),
2045 subnet_id: Some(subnet_id(subnet)),
2046 })
2047 .collect(),
2048 }
2049 }
2050
2051 fn canister_id(principal: &str) -> CanisterId {
2052 CanisterId {
2053 principal_id: Some(PrincipalId {
2054 raw: principal_raw(principal),
2055 }),
2056 }
2057 }
2058
2059 fn subnet_id(principal: &str) -> SubnetId {
2060 SubnetId {
2061 principal_id: Some(PrincipalId {
2062 raw: principal_raw(principal),
2063 }),
2064 }
2065 }
2066
2067 fn principal_raw(principal: &str) -> Vec<u8> {
2068 Principal::from_text(principal)
2069 .expect("principal")
2070 .as_slice()
2071 .to_vec()
2072 }
2073
2074 fn governance_node_provider(
2075 principal: &str,
2076 reward_account_hash: Option<Vec<u8>>,
2077 ) -> GovernanceNodeProvider {
2078 GovernanceNodeProvider {
2079 id: Some(Principal::from_text(principal).expect("principal")),
2080 reward_account: reward_account_hash.map(|hash| GovernanceAccountIdentifier { hash }),
2081 }
2082 }
2083
2084 fn node_record(node_operator: &str) -> NodeRecord {
2085 NodeRecord {
2086 node_operator_id: principal_raw(node_operator),
2087 }
2088 }
2089
2090 fn node_operator_record(node_provider: &str) -> NodeOperatorRecord {
2091 NodeOperatorRecord {
2092 node_operator_principal_id: Vec::new(),
2093 node_allowance: 0,
2094 node_provider_principal_id: principal_raw(node_provider),
2095 dc_id: "dc1".to_string(),
2096 }
2097 }
2098}