1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::net::SocketAddr;
3use std::time::Duration;
4
5use anyhow::{Context, bail, ensure, format_err};
6use bitcoin::hashes::sha256;
7use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
8pub use fedimint_core::config::{
9 ClientConfig, FederationId, GlobalClientConfig, JsonWithKind, ModuleInitRegistry, P2PMessage,
10 PeerUrl, ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
11 serde_binary_human_readable,
12};
13use fedimint_core::core::{ModuleInstanceId, ModuleKind};
14use fedimint_core::envs::is_running_in_test_env;
15use fedimint_core::invite_code::InviteCode;
16use fedimint_core::module::{
17 ApiAuth, ApiVersion, CORE_CONSENSUS_VERSION, CoreConsensusVersion, MultiApiVersion,
18 SupportedApiVersionsSummary, SupportedCoreApiVersions,
19};
20use fedimint_core::net::peers::{DynP2PConnections, Recipient};
21use fedimint_core::setup_code::{PeerEndpoints, PeerSetupCode};
22use fedimint_core::task::sleep;
23use fedimint_core::util::SafeUrl;
24use fedimint_core::{NumPeersExt, PeerId, secp256k1, timing};
25use fedimint_logging::LOG_NET_PEER_DKG;
26use fedimint_server_core::config::PeerHandleOpsExt as _;
27use fedimint_server_core::{DynServerModuleInit, ServerModuleInitRegistry};
28use hex::{FromHex, ToHex};
29use peer_handle::PeerHandle;
30use rand::rngs::OsRng;
31use secp256k1::{PublicKey, Secp256k1, SecretKey};
32use serde::{Deserialize, Serialize};
33use tokio_rustls::rustls;
34use tracing::info;
35
36use crate::fedimint_core::encoding::Encodable;
37use crate::net::p2p::P2PStatusReceivers;
38use crate::net::p2p_connector::TlsConfig;
39
40pub mod dkg;
41pub mod io;
42pub mod peer_handle;
43pub mod setup;
44
45pub const DEFAULT_MAX_CLIENT_CONNECTIONS: u32 = 1000;
47
48const DEFAULT_BROADCAST_ROUND_DELAY_MS: u16 = 50;
50const DEFAULT_BROADCAST_ROUNDS_PER_SESSION: u16 = 3600;
51
52fn default_broadcast_rounds_per_session() -> u16 {
53 DEFAULT_BROADCAST_ROUNDS_PER_SESSION
54}
55
56const DEFAULT_TEST_BROADCAST_ROUND_DELAY_MS: u16 = 50;
58const DEFAULT_TEST_BROADCAST_ROUNDS_PER_SESSION: u16 = 200;
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ServerConfig {
63 pub consensus: ServerConfigConsensus,
65 pub local: ServerConfigLocal,
67 pub private: ServerConfigPrivate,
70}
71
72impl ServerConfig {
73 pub fn iter_module_instances(
74 &self,
75 ) -> impl Iterator<Item = (ModuleInstanceId, &ModuleKind)> + '_ {
76 self.consensus.iter_module_instances()
77 }
78
79 pub(crate) fn supported_api_versions_summary(
80 modules: &BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
81 module_inits: &ServerModuleInitRegistry,
82 ) -> SupportedApiVersionsSummary {
83 SupportedApiVersionsSummary {
84 core: Self::supported_api_versions(),
85 modules: modules
86 .iter()
87 .map(|(&id, config)| {
88 (
89 id,
90 module_inits
91 .get(&config.kind)
92 .expect("missing module kind gen")
93 .supported_api_versions(),
94 )
95 })
96 .collect(),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ServerConfigPrivate {
103 pub api_auth: ApiAuth,
105 pub tls_key: Option<String>,
107 #[serde(default)]
109 pub iroh_api_sk: Option<iroh::SecretKey>,
110 #[serde(default)]
112 pub iroh_p2p_sk: Option<iroh::SecretKey>,
113 pub broadcast_secret_key: SecretKey,
115 pub modules: BTreeMap<ModuleInstanceId, JsonWithKind>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, Encodable)]
120pub struct ServerConfigConsensus {
121 pub code_version: String,
123 pub version: CoreConsensusVersion,
125 pub broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
127 #[serde(default = "default_broadcast_rounds_per_session")]
129 pub broadcast_rounds_per_session: u16,
130 pub api_endpoints: BTreeMap<PeerId, PeerUrl>,
132 #[serde(default)]
134 pub iroh_endpoints: BTreeMap<PeerId, PeerIrohEndpoints>,
135 pub tls_certs: BTreeMap<PeerId, String>,
137 pub modules: BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
139 pub meta: BTreeMap<String, String>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, Encodable)]
144pub struct PeerIrohEndpoints {
145 pub name: String,
147 pub api_pk: iroh::PublicKey,
149 pub p2p_pk: iroh::PublicKey,
151}
152
153pub fn legacy_consensus_config_hash(cfg: &ServerConfigConsensus) -> sha256::Hash {
154 #[derive(Encodable)]
155 struct LegacyServerConfigConsensusHashMap {
156 code_version: String,
157 version: CoreConsensusVersion,
158 broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
159 broadcast_rounds_per_session: u16,
160 api_endpoints: BTreeMap<PeerId, PeerUrl>,
161 tls_certs: BTreeMap<PeerId, String>,
162 modules: BTreeMap<ModuleInstanceId, ServerModuleConsensusConfig>,
163 meta: BTreeMap<String, String>,
164 }
165
166 LegacyServerConfigConsensusHashMap {
167 code_version: cfg.code_version.clone(),
168 version: cfg.version,
169 broadcast_public_keys: cfg.broadcast_public_keys.clone(),
170 broadcast_rounds_per_session: cfg.broadcast_rounds_per_session,
171 api_endpoints: cfg.api_endpoints.clone(),
172 tls_certs: cfg.tls_certs.clone(),
173 modules: cfg.modules.clone(),
174 meta: cfg.meta.clone(),
175 }
176 .consensus_hash_sha256()
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ServerConfigLocal {
183 pub p2p_endpoints: BTreeMap<PeerId, PeerUrl>,
185 pub identity: PeerId,
187 pub max_connections: u32,
189 pub broadcast_round_delay_ms: u16,
194}
195
196#[derive(Debug, Clone)]
198pub struct ConfigGenSettings {
199 pub p2p_bind: SocketAddr,
201 pub api_bind: SocketAddr,
203 pub ui_bind: SocketAddr,
205 pub p2p_url: Option<SafeUrl>,
207 pub api_url: Option<SafeUrl>,
209 pub enable_iroh: bool,
211 pub iroh_dns: Option<SafeUrl>,
213 pub iroh_relays: Vec<SafeUrl>,
215 pub modules: ServerModuleConfigGenParamsRegistry,
217 pub registry: ServerModuleInitRegistry,
219}
220
221#[derive(Debug, Clone)]
222pub struct ConfigGenParams {
227 pub identity: PeerId,
229 pub tls_key: Option<rustls::PrivateKey>,
231 pub iroh_api_sk: Option<iroh::SecretKey>,
233 pub iroh_p2p_sk: Option<iroh::SecretKey>,
235 pub api_auth: ApiAuth,
237 pub peers: BTreeMap<PeerId, PeerSetupCode>,
239 pub meta: BTreeMap<String, String>,
241}
242
243impl ServerConfigConsensus {
244 pub fn api_endpoints(&self) -> BTreeMap<PeerId, PeerUrl> {
245 if self.iroh_endpoints.is_empty() {
246 self.api_endpoints.clone()
247 } else {
248 self.iroh_endpoints
249 .iter()
250 .map(|(peer, endpoints)| {
251 let url = PeerUrl {
252 name: endpoints.name.clone(),
253 url: SafeUrl::parse(&format!("iroh://{}", endpoints.api_pk))
254 .expect("Failed to parse iroh url"),
255 };
256
257 (*peer, url)
258 })
259 .collect()
260 }
261 }
262
263 pub fn iter_module_instances(
264 &self,
265 ) -> impl Iterator<Item = (ModuleInstanceId, &ModuleKind)> + '_ {
266 self.modules.iter().map(|(k, v)| (*k, &v.kind))
267 }
268
269 pub fn to_client_config(
270 &self,
271 module_config_gens: &ModuleInitRegistry<DynServerModuleInit>,
272 ) -> Result<ClientConfig, anyhow::Error> {
273 let client = ClientConfig {
274 global: GlobalClientConfig {
275 api_endpoints: self.api_endpoints(),
276 broadcast_public_keys: Some(self.broadcast_public_keys.clone()),
277 consensus_version: self.version,
278 meta: self.meta.clone(),
279 },
280 modules: self
281 .modules
282 .iter()
283 .map(|(k, v)| {
284 let r#gen = module_config_gens
285 .get(&v.kind)
286 .ok_or_else(|| format_err!("Module gen kind={} not found", v.kind))?;
287 Ok((*k, r#gen.get_client_config(*k, v)?))
288 })
289 .collect::<anyhow::Result<BTreeMap<_, _>>>()?,
290 };
291 Ok(client)
292 }
293}
294
295impl ServerConfig {
296 pub fn supported_api_versions() -> SupportedCoreApiVersions {
298 SupportedCoreApiVersions {
299 core_consensus: CORE_CONSENSUS_VERSION,
300 api: MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 5 }])
301 .expect("not version conflicts"),
302 }
303 }
304 pub fn from(
307 params: ConfigGenParams,
308 identity: PeerId,
309 broadcast_public_keys: BTreeMap<PeerId, PublicKey>,
310 broadcast_secret_key: SecretKey,
311 modules: BTreeMap<ModuleInstanceId, ServerModuleConfig>,
312 code_version: String,
313 ) -> Self {
314 let consensus = ServerConfigConsensus {
315 code_version,
316 version: CORE_CONSENSUS_VERSION,
317 broadcast_public_keys,
318 broadcast_rounds_per_session: if is_running_in_test_env() {
319 DEFAULT_TEST_BROADCAST_ROUNDS_PER_SESSION
320 } else {
321 DEFAULT_BROADCAST_ROUNDS_PER_SESSION
322 },
323 api_endpoints: params.api_urls(),
324 iroh_endpoints: params.iroh_endpoints(),
325 tls_certs: params.tls_certs(),
326 modules: modules
327 .iter()
328 .map(|(peer, cfg)| (*peer, cfg.consensus.clone()))
329 .collect(),
330 meta: params.meta.clone(),
331 };
332
333 let local = ServerConfigLocal {
334 p2p_endpoints: params.p2p_urls(),
335 identity,
336 max_connections: DEFAULT_MAX_CLIENT_CONNECTIONS,
337 broadcast_round_delay_ms: if is_running_in_test_env() {
338 DEFAULT_TEST_BROADCAST_ROUND_DELAY_MS
339 } else {
340 DEFAULT_BROADCAST_ROUND_DELAY_MS
341 },
342 };
343
344 let private = ServerConfigPrivate {
345 api_auth: params.api_auth.clone(),
346 tls_key: params.tls_key.map(|key| key.0.encode_hex()),
347 iroh_api_sk: params.iroh_api_sk,
348 iroh_p2p_sk: params.iroh_p2p_sk,
349 broadcast_secret_key,
350 modules: modules
351 .iter()
352 .map(|(peer, cfg)| (*peer, cfg.private.clone()))
353 .collect(),
354 };
355
356 Self {
357 consensus,
358 local,
359 private,
360 }
361 }
362
363 pub fn get_invite_code(&self, api_secret: Option<String>) -> InviteCode {
364 InviteCode::new(
365 self.consensus.api_endpoints()[&self.local.identity]
366 .url
367 .clone(),
368 self.local.identity,
369 self.calculate_federation_id(),
370 api_secret,
371 )
372 }
373
374 pub fn calculate_federation_id(&self) -> FederationId {
375 FederationId(self.consensus.api_endpoints().consensus_hash())
376 }
377
378 pub fn get_module_config_typed<T: TypedServerModuleConfig>(
380 &self,
381 id: ModuleInstanceId,
382 ) -> anyhow::Result<T> {
383 let private = Self::get_module_cfg_by_instance_id(&self.private.modules, id)?;
384 let consensus = self
385 .consensus
386 .modules
387 .get(&id)
388 .ok_or_else(|| format_err!("Module {id} not found"))?
389 .clone();
390 let module = ServerModuleConfig::from(private, consensus);
391
392 module.to_typed()
393 }
394 pub fn get_module_id_by_kind(
395 &self,
396 kind: impl Into<ModuleKind>,
397 ) -> anyhow::Result<ModuleInstanceId> {
398 let kind = kind.into();
399 Ok(*self
400 .consensus
401 .modules
402 .iter()
403 .find(|(_, v)| v.kind == kind)
404 .ok_or_else(|| format_err!("Module {kind} not found"))?
405 .0)
406 }
407
408 pub fn get_module_config(&self, id: ModuleInstanceId) -> anyhow::Result<ServerModuleConfig> {
410 let private = Self::get_module_cfg_by_instance_id(&self.private.modules, id)?;
411 let consensus = self
412 .consensus
413 .modules
414 .get(&id)
415 .ok_or_else(|| format_err!("Module {id} not found"))?
416 .clone();
417 Ok(ServerModuleConfig::from(private, consensus))
418 }
419
420 fn get_module_cfg_by_instance_id(
421 json: &BTreeMap<ModuleInstanceId, JsonWithKind>,
422 id: ModuleInstanceId,
423 ) -> anyhow::Result<JsonWithKind> {
424 Ok(json
425 .get(&id)
426 .ok_or_else(|| format_err!("Module {id} not found"))
427 .cloned()?
428 .with_fixed_empty_value())
429 }
430
431 pub fn validate_config(
432 &self,
433 identity: &PeerId,
434 module_config_gens: &ServerModuleInitRegistry,
435 ) -> anyhow::Result<()> {
436 let endpoints = self.consensus.api_endpoints().clone();
437 let consensus = self.consensus.clone();
438 let private = self.private.clone();
439
440 let my_public_key = private.broadcast_secret_key.public_key(&Secp256k1::new());
441
442 if Some(&my_public_key) != consensus.broadcast_public_keys.get(identity) {
443 bail!("Broadcast secret key doesn't match corresponding public key");
444 }
445 if endpoints.keys().max().copied().map(PeerId::to_usize) != Some(endpoints.len() - 1) {
446 bail!("Peer ids are not indexed from 0");
447 }
448 if endpoints.keys().min().copied() != Some(PeerId::from(0)) {
449 bail!("Peer ids are not indexed from 0");
450 }
451
452 for (module_id, module_kind) in &self
453 .consensus
454 .modules
455 .iter()
456 .map(|(id, config)| Ok((*id, config.kind.clone())))
457 .collect::<anyhow::Result<BTreeSet<_>>>()?
458 {
459 module_config_gens
460 .get(module_kind)
461 .ok_or_else(|| format_err!("module config gen not found {module_kind}"))?
462 .validate_config(identity, self.get_module_config(*module_id)?)?;
463 }
464
465 Ok(())
466 }
467
468 pub fn trusted_dealer_gen(
469 modules: ServerModuleConfigGenParamsRegistry,
470 params: &HashMap<PeerId, ConfigGenParams>,
471 registry: &ServerModuleInitRegistry,
472 code_version_str: &str,
473 ) -> BTreeMap<PeerId, Self> {
474 let peer0 = ¶ms[&PeerId::from(0)];
475
476 let mut broadcast_pks = BTreeMap::new();
477 let mut broadcast_sks = BTreeMap::new();
478 for peer_id in peer0.peer_ids() {
479 let (broadcast_sk, broadcast_pk) = secp256k1::generate_keypair(&mut OsRng);
480 broadcast_pks.insert(peer_id, broadcast_pk);
481 broadcast_sks.insert(peer_id, broadcast_sk);
482 }
483
484 let module_configs: BTreeMap<_, _> = modules
485 .iter_modules()
486 .map(|(module_id, kind, module_params)| {
487 (
488 module_id,
489 registry
490 .get(kind)
491 .expect("Module not registered")
492 .trusted_dealer_gen(&peer0.peer_ids(), module_params),
493 )
494 })
495 .collect();
496
497 let server_config: BTreeMap<_, _> = peer0
498 .peer_ids()
499 .iter()
500 .map(|&id| {
501 let config = ServerConfig::from(
502 params[&id].clone(),
503 id,
504 broadcast_pks.clone(),
505 *broadcast_sks.get(&id).expect("We created this entry"),
506 module_configs
507 .iter()
508 .map(|(module_id, cfgs)| (*module_id, cfgs[&id].clone()))
509 .collect(),
510 code_version_str.to_string(),
511 );
512 (id, config)
513 })
514 .collect();
515
516 server_config
517 }
518
519 pub async fn distributed_gen(
521 modules: ServerModuleConfigGenParamsRegistry,
522 params: &ConfigGenParams,
523 registry: ServerModuleInitRegistry,
524 code_version_str: String,
525 connections: DynP2PConnections<P2PMessage>,
526 p2p_status_receivers: P2PStatusReceivers,
527 ) -> anyhow::Result<Self> {
528 let _timing = timing::TimeReporter::new("distributed-gen").info();
529
530 if params.peer_ids().len() == 1 {
532 let server = Self::trusted_dealer_gen(
533 modules,
534 &HashMap::from([(params.identity, params.clone())]),
535 ®istry,
536 &code_version_str,
537 );
538 return Ok(server[¶ms.identity].clone());
539 }
540
541 info!(
542 target: LOG_NET_PEER_DKG,
543 "Waiting for all p2p connections to open..."
544 );
545
546 while p2p_status_receivers.values().any(|r| r.borrow().is_none()) {
547 let peers = p2p_status_receivers
548 .iter()
549 .filter_map(|entry| entry.1.borrow().map(|_| *entry.0))
550 .collect::<Vec<PeerId>>();
551
552 info!(
553 target: LOG_NET_PEER_DKG,
554 "Connected to peers: {peers:?}..."
555 );
556
557 sleep(Duration::from_secs(1)).await;
558 }
559
560 let checksum = params.peers.consensus_hash_sha256();
561
562 info!(
563 target: LOG_NET_PEER_DKG,
564 "Comparing connection codes checksum {checksum}..."
565 );
566
567 connections
568 .send(Recipient::Everyone, P2PMessage::Checksum(checksum))
569 .await;
570
571 for peer in params
572 .peer_ids()
573 .into_iter()
574 .filter(|p| *p != params.identity)
575 {
576 ensure!(
577 connections
578 .receive_from_peer(peer)
579 .await
580 .context("Unexpected shutdown of p2p connections")?
581 == P2PMessage::Checksum(checksum),
582 "Peer {peer} has not send the correct checksum message"
583 );
584 }
585
586 info!(
587 target: LOG_NET_PEER_DKG,
588 "Running config generation..."
589 );
590
591 let handle = PeerHandle::new(
592 params.peer_ids().to_num_peers(),
593 params.identity,
594 &connections,
595 );
596
597 let (broadcast_sk, broadcast_pk) = secp256k1::generate_keypair(&mut OsRng);
598
599 let broadcast_public_keys = handle.exchange_encodable(broadcast_pk).await?;
600
601 let mut module_cfgs = BTreeMap::new();
602
603 for (module_id, kind, module_params) in modules.iter_modules() {
604 info!(
605 target: LOG_NET_PEER_DKG,
606 "Running config generation for module of kind {kind}..."
607 );
608
609 let cfg = registry
610 .get(kind)
611 .with_context(|| format!("Module of kind {kind} not found"))?
612 .distributed_gen(&handle, module_params)
613 .await?;
614
615 module_cfgs.insert(module_id, cfg);
616 }
617
618 let cfg = ServerConfig::from(
619 params.clone(),
620 params.identity,
621 broadcast_public_keys,
622 broadcast_sk,
623 module_cfgs,
624 code_version_str,
625 );
626
627 let checksum = cfg.consensus.consensus_hash_sha256();
628
629 info!(
630 target: LOG_NET_PEER_DKG,
631 "Comparing consensus config checksum {checksum}..."
632 );
633
634 connections
635 .send(Recipient::Everyone, P2PMessage::Checksum(checksum))
636 .await;
637
638 for peer in params
639 .peer_ids()
640 .into_iter()
641 .filter(|p| *p != params.identity)
642 {
643 ensure!(
644 connections
645 .receive_from_peer(peer)
646 .await
647 .context("Unexpected shutdown of p2p connections")?
648 == P2PMessage::Checksum(checksum),
649 "Peer {peer} has not send the correct checksum message"
650 );
651 }
652
653 info!(
654 target: LOG_NET_PEER_DKG,
655 "Config generation has completed successfully!"
656 );
657
658 Ok(cfg)
659 }
660}
661
662impl ServerConfig {
663 pub fn tls_config(&self) -> TlsConfig {
664 TlsConfig {
665 private_key: rustls::PrivateKey(
666 Vec::from_hex(self.private.tls_key.clone().unwrap()).unwrap(),
667 ),
668 certificates: self
669 .consensus
670 .tls_certs
671 .iter()
672 .map(|(peer, cert)| (*peer, rustls::Certificate(Vec::from_hex(cert).unwrap())))
673 .collect(),
674 peer_names: self
675 .local
676 .p2p_endpoints
677 .iter()
678 .map(|(id, endpoint)| (*id, endpoint.name.to_string()))
679 .collect(),
680 }
681 }
682}
683
684impl ConfigGenParams {
685 pub fn peer_ids(&self) -> Vec<PeerId> {
686 self.peers.keys().copied().collect()
687 }
688
689 pub fn tls_config(&self) -> TlsConfig {
690 TlsConfig {
691 private_key: self.tls_key.clone().unwrap(),
692 certificates: self
693 .tls_certs()
694 .iter()
695 .map(|(peer, cert)| (*peer, rustls::Certificate(Vec::from_hex(cert).unwrap())))
696 .collect(),
697 peer_names: self
698 .p2p_urls()
699 .into_iter()
700 .map(|(id, peer)| (id, peer.name))
701 .collect(),
702 }
703 }
704
705 pub fn tls_certs(&self) -> BTreeMap<PeerId, String> {
706 self.peers
707 .iter()
708 .filter_map(|(id, peer)| {
709 match peer.endpoints.clone() {
710 PeerEndpoints::Tcp { cert, .. } => Some(cert.encode_hex()),
711 PeerEndpoints::Iroh { .. } => None,
712 }
713 .map(|peer| (*id, peer))
714 })
715 .collect()
716 }
717
718 pub fn p2p_urls(&self) -> BTreeMap<PeerId, PeerUrl> {
719 self.peers
720 .iter()
721 .filter_map(|(id, peer)| {
722 match peer.endpoints.clone() {
723 PeerEndpoints::Tcp { p2p_url, .. } => Some(PeerUrl {
724 name: peer.name.clone(),
725 url: p2p_url.clone(),
726 }),
727 PeerEndpoints::Iroh { .. } => None,
728 }
729 .map(|peer| (*id, peer))
730 })
731 .collect()
732 }
733
734 pub fn api_urls(&self) -> BTreeMap<PeerId, PeerUrl> {
735 self.peers
736 .iter()
737 .filter_map(|(id, peer)| {
738 match peer.endpoints.clone() {
739 PeerEndpoints::Tcp { api_url, .. } => Some(PeerUrl {
740 name: peer.name.clone(),
741 url: api_url.clone(),
742 }),
743 PeerEndpoints::Iroh { .. } => None,
744 }
745 .map(|peer| (*id, peer))
746 })
747 .collect()
748 }
749
750 pub fn iroh_endpoints(&self) -> BTreeMap<PeerId, PeerIrohEndpoints> {
751 self.peers
752 .iter()
753 .filter_map(|(id, peer)| {
754 match peer.endpoints.clone() {
755 PeerEndpoints::Tcp { .. } => None,
756 PeerEndpoints::Iroh { api_pk, p2p_pk } => Some(PeerIrohEndpoints {
757 name: peer.name.clone(),
758 api_pk,
759 p2p_pk,
760 }),
761 }
762 .map(|peer| (*id, peer))
763 })
764 .collect()
765 }
766}