1mod chain_wiring;
30
31use std::{future::Future, pin::Pin, sync::Arc, time::Duration};
32
33use futures::{FutureExt, StreamExt};
34pub use hopr_api::types::crypto::{
35 keypairs::Keypair,
36 prelude::{ChainKeypair, OffchainKeypair},
37};
38use hopr_api::{
39 chain::{AnnouncementError, HoprChainApi, SafeRegistrationError, StateSyncOptions},
40 ct::{CoverTrafficGeneration, ProbingTrafficGeneration},
41 graph::HoprGraphApi,
42 network::{BoxedProcessFn, NetworkStreamControl, NetworkView},
43 node::{AtomicHoprState, HoprState, NodeOnchainIdentity, PixEvent, TicketEvent},
44 tickets::{TicketFactory, TicketManagement},
45 types::{chain::chain_events::ChainEvent, internal::prelude::ChannelDirection, primitive::prelude::Address},
46};
47use hopr_transport::{HoprTransport, IncomingSession};
48use hopr_utils::{
49 network_types::{
50 addr::is_public_address,
51 crossfire_sink::{CrossfireSink, bounded_sink_channel},
52 },
53 runtime::{AbortableList, prelude::spawn},
54};
55use validator::Validate;
56
57use crate::{
58 Hopr, HoprLibError, HoprLibProcess, MIN_NATIVE_BALANCE, NODE_READY_TIMEOUT, SUGGESTED_NATIVE_BALANCE,
59 config::HoprLibConfig, constants, helpers::BroadcastSenderSink,
60};
61
62#[cfg(all(feature = "telemetry", not(test)))]
63lazy_static::lazy_static! {
64 static ref METRIC_PROCESS_START_TIME: hopr_api::types::telemetry::SimpleGauge = hopr_api::types::telemetry::SimpleGauge::new(
65 "hopr_start_time",
66 "The unix timestamp in seconds at which the process was started"
67 ).unwrap();
68 static ref METRIC_HOPR_LIB_VERSION: hopr_api::types::telemetry::MultiGauge = hopr_api::types::telemetry::MultiGauge::new(
69 "hopr_lib_version",
70 "Executed version of hopr-lib",
71 &["version"]
72 ).unwrap();
73 static ref METRIC_HOPR_NODE_INFO: hopr_api::types::telemetry::MultiGauge = hopr_api::types::telemetry::MultiGauge::new(
74 "hopr_node_addresses",
75 "Node on-chain and off-chain addresses",
76 &["peerid", "address", "safe_address", "module_address"]
77 ).unwrap();
78}
79
80const PEER_DISCOVERY_CHANNEL_CAPACITY: usize = 2048;
81
82type PeerDiscoveryRx =
83 Arc<parking_lot::Mutex<Option<futures::stream::BoxStream<'static, (hopr_api::PeerId, Vec<hopr_api::Multiaddr>)>>>>;
84
85type BuildParts<Chain, Graph, Net, Ct> = (
89 HoprBuilderConfigured<Chain, Graph, Net, Ct>,
90 futures::channel::mpsc::Sender<IncomingSession>,
91 Option<hopr_transport::SessionAdmissionSink>,
92 AbortableList<HoprLibProcess>,
93);
94
95type Factory<T> = Box<dyn FnOnce(&BuildCtx) -> T + Send>;
97type AsyncFactory<T> = Box<dyn FnOnce(BuildCtx) -> Pin<Box<dyn Future<Output = T> + Send>> + Send>;
98
99#[derive(Clone)]
101pub struct BuildCtx {
102 pub chain_key: ChainKeypair,
104 pub packet_key: OffchainKeypair,
106 pub cfg: HoprLibConfig,
108 peer_discovery_rx: PeerDiscoveryRx,
109}
110
111impl BuildCtx {
112 pub fn take_peer_discovery_rx(
114 &self,
115 ) -> Option<futures::stream::BoxStream<'static, (hopr_api::PeerId, Vec<hopr_api::Multiaddr>)>> {
116 self.peer_discovery_rx.lock().take()
117 }
118}
119
120#[derive(Default)]
126pub struct HoprBuilder;
127
128impl HoprBuilder {
129 pub fn with_identity(self, chain_key: &ChainKeypair, offchain_key: &OffchainKeypair) -> HoprBuilderWithIdentity {
131 HoprBuilderWithIdentity {
132 chain_key: chain_key.clone(),
133 packet_key: offchain_key.clone(),
134 }
135 }
136}
137
138pub struct HoprBuilderWithIdentity {
140 chain_key: ChainKeypair,
141 packet_key: OffchainKeypair,
142}
143
144impl HoprBuilderWithIdentity {
145 pub fn with_config(self, cfg: HoprLibConfig) -> HoprBuilderConfigured {
147 let (peer_discovery_tx, peer_discovery_rx) =
148 bounded_sink_channel::<(hopr_api::PeerId, Vec<hopr_api::Multiaddr>)>(PEER_DISCOVERY_CHANNEL_CAPACITY);
149 HoprBuilderConfigured {
150 ctx: BuildCtx {
151 chain_key: self.chain_key,
152 packet_key: self.packet_key,
153 cfg,
154 peer_discovery_rx: Arc::new(parking_lot::Mutex::new(Some(peer_discovery_rx))),
155 },
156 safe_and_module: None,
157 chain_factory: None,
158 graph_factory: None,
159 network_factory: None,
160 ct_factory: None,
161 peer_discovery_tx,
162 }
163 }
164}
165
166pub struct HoprBuilderConfigured<Chain = (), Graph = (), Net = (), Ct = ()> {
178 ctx: BuildCtx,
179 safe_and_module: Option<(Address, Address)>,
180 chain_factory: Option<Factory<Chain>>,
181 graph_factory: Option<Factory<Graph>>,
182 network_factory: Option<AsyncFactory<Result<(Net, BoxedProcessFn), HoprLibError>>>,
183 ct_factory: Option<Factory<Ct>>,
184 peer_discovery_tx: CrossfireSink<(hopr_api::PeerId, Vec<hopr_api::Multiaddr>)>,
185}
186
187impl<Chain, Graph, Net, Ct> HoprBuilderConfigured<Chain, Graph, Net, Ct> {
188 pub fn with_safe_module(mut self, safe: &Address, module: &Address) -> Self {
190 self.safe_and_module = Some((*safe, *module));
191 self
192 }
193
194 pub fn with_chain_api<NewChain>(
196 self,
197 f: impl FnOnce(&BuildCtx) -> NewChain + Send + 'static,
198 ) -> HoprBuilderConfigured<NewChain, Graph, Net, Ct> {
199 HoprBuilderConfigured {
200 ctx: self.ctx,
201 safe_and_module: self.safe_and_module,
202 chain_factory: Some(Box::new(f)),
203 graph_factory: self.graph_factory,
204 network_factory: self.network_factory,
205 ct_factory: self.ct_factory,
206 peer_discovery_tx: self.peer_discovery_tx,
207 }
208 }
209
210 pub fn with_graph<NewGraph>(
212 self,
213 f: impl FnOnce(&BuildCtx) -> NewGraph + Send + 'static,
214 ) -> HoprBuilderConfigured<Chain, NewGraph, Net, Ct> {
215 HoprBuilderConfigured {
216 ctx: self.ctx,
217 safe_and_module: self.safe_and_module,
218 chain_factory: self.chain_factory,
219 graph_factory: Some(Box::new(f)),
220 network_factory: self.network_factory,
221 ct_factory: self.ct_factory,
222 peer_discovery_tx: self.peer_discovery_tx,
223 }
224 }
225
226 pub fn with_network<NewNet>(
232 self,
233 f: impl FnOnce(BuildCtx) -> Pin<Box<dyn Future<Output = Result<(NewNet, BoxedProcessFn), HoprLibError>> + Send>>
234 + Send
235 + 'static,
236 ) -> HoprBuilderConfigured<Chain, Graph, NewNet, Ct> {
237 HoprBuilderConfigured {
238 ctx: self.ctx,
239 safe_and_module: self.safe_and_module,
240 chain_factory: self.chain_factory,
241 graph_factory: self.graph_factory,
242 network_factory: Some(Box::new(f)),
243 ct_factory: self.ct_factory,
244 peer_discovery_tx: self.peer_discovery_tx,
245 }
246 }
247
248 pub fn with_cover_traffic<NewCt>(
250 self,
251 f: impl FnOnce(&BuildCtx) -> NewCt + Send + 'static,
252 ) -> HoprBuilderConfigured<Chain, Graph, Net, NewCt> {
253 HoprBuilderConfigured {
254 ctx: self.ctx,
255 safe_and_module: self.safe_and_module,
256 chain_factory: self.chain_factory,
257 graph_factory: self.graph_factory,
258 network_factory: self.network_factory,
259 ct_factory: Some(Box::new(f)),
260 peer_discovery_tx: self.peer_discovery_tx,
261 }
262 }
263
264 #[cfg(feature = "session-server")]
274 pub fn with_session_server(
275 self,
276 server: impl hopr_api::node::HoprSessionServer<Session = IncomingSession, Error: std::fmt::Display>
280 + Clone
281 + Send
282 + Sync
283 + 'static,
284 ) -> HoprBuilderWithSession<Chain, Graph, Net, Ct> {
285 let incoming_session_capacity = self.ctx.cfg.incoming_session_capacity.max(1);
286
287 let (session_tx, session_rx) = futures::channel::mpsc::channel::<IncomingSession>(incoming_session_capacity);
288
289 let (admission_tx, admission_rx) = futures::channel::mpsc::channel(incoming_session_capacity);
293
294 let admission_server = server.clone();
295 let admission_diag = hopr_utils::runtime::diagnostics::ConcurrentDiagnostics::new(
296 "session_admission_for_each_concurrent",
297 module_path!(),
298 file!(),
299 line!(),
300 );
301 let admission_handle = hopr_utils::spawn_as_abortable_named!(
302 "hopr_lib_session_admission",
303 admission_rx
304 .for_each_concurrent(
305 None,
306 move |(request, reply): (_, hopr_transport::SessionAdmissionReply)| {
307 let server = admission_server.clone();
308 let admission_diag = admission_diag.clone();
309 admission_diag.wrap(|| async move {
310 let decision = server.admit(request).await.map_err(|error| error.to_string());
313 if reply.send(decision).is_err() {
314 tracing::debug!("session admission decided after the transport stopped waiting");
317 }
318 })
319 }
320 )
321 .inspect(|_| tracing::warn!(
322 task = %HoprLibProcess::SessionAdmission,
323 "long-running background task finished"
324 ))
325 );
326
327 tracing::debug!(capacity = incoming_session_capacity, "spawning session server");
328 let session_diag = hopr_utils::runtime::diagnostics::ConcurrentDiagnostics::new(
329 "session_server_for_each_concurrent",
330 module_path!(),
331 file!(),
332 line!(),
333 );
334 let handle = hopr_utils::spawn_as_abortable_named!(
335 "hopr_lib_session_server",
336 session_rx
337 .for_each_concurrent(None, move |session| {
338 let server = server.clone();
339 let session_diag = session_diag.clone();
340 session_diag.wrap(|| async move {
341 let session_id = *session.session.id();
342 match server.process(session).await {
343 Ok(()) => tracing::debug!(?session_id, "session processed successfully"),
344 Err(error) => {
345 tracing::error!(?session_id, %error, "session processing failed")
346 }
347 }
348 })
349 })
350 .inspect(|_| tracing::warn!(
351 task = %HoprLibProcess::SessionServer,
352 "long-running background task finished"
353 ))
354 );
355
356 HoprBuilderWithSession {
357 inner: self,
358 session_tx,
359 admission_tx,
360 session_handle: handle,
361 admission_handle,
362 }
363 }
364}
365
366#[cfg(feature = "session-server")]
374pub struct HoprBuilderWithSession<Chain = (), Graph = (), Net = (), Ct = ()> {
375 inner: HoprBuilderConfigured<Chain, Graph, Net, Ct>,
376 session_tx: futures::channel::mpsc::Sender<IncomingSession>,
377 admission_tx: hopr_transport::SessionAdmissionSink,
378 session_handle: hopr_utils::runtime::AbortHandle,
379 admission_handle: hopr_utils::runtime::AbortHandle,
380}
381
382struct PreHopr<Chain, Graph, Net, Ct> {
387 chain_id: ChainKeypair,
388 transport_id: OffchainKeypair,
389 cfg: HoprLibConfig,
390 state: Arc<AtomicHoprState>,
391 transport_api: HoprTransport<Chain, Graph, Net>,
392 chain_api: Chain,
393
394 ticket_event_subscribers: (
395 async_broadcast::Sender<TicketEvent>,
396 async_broadcast::InactiveReceiver<TicketEvent>,
397 ),
398 pix_event_subscribers: (
399 async_broadcast::Sender<PixEvent>,
400 async_broadcast::InactiveReceiver<PixEvent>,
401 ),
402 processes: AbortableList<HoprLibProcess>,
403 session_tx: futures::channel::mpsc::Sender<IncomingSession>,
404 admission_tx: Option<hopr_transport::SessionAdmissionSink>,
405 cover_traffic: Ct,
406 network: Net,
407 network_process: BoxedProcessFn,
408}
409
410async fn drain_incoming_data<S: futures::Stream + Unpin>(mut reader: S) {
417 let mut received: u64 = 0;
418 let mut last_report = std::time::Instant::now();
419 while reader.next().await.is_some() {
420 received += 1;
421 if last_report.elapsed().as_secs() >= 60 {
422 tracing::info!(
423 received,
424 "incoming-data drain: unrelated packets discarded in last ~1 min"
425 );
426 received = 0;
427 last_report = std::time::Instant::now();
428 }
429 }
430}
431
432async fn pre_build_inner<Chain, Graph, Net, Ct>(
433 configured: HoprBuilderConfigured<Chain, Graph, Net, Ct>,
434 session_tx: futures::channel::mpsc::Sender<IncomingSession>,
435 admission_tx: Option<hopr_transport::SessionAdmissionSink>,
436 mut processes: AbortableList<HoprLibProcess>,
437) -> Result<PreHopr<Chain, Graph, Net, Ct>, HoprLibError>
438where
439 Chain: HoprChainApi + Clone + Send + Sync + 'static,
440 Graph: HoprGraphApi<HoprNodeId = hopr_api::OffchainPublicKey> + Clone + Send + Sync + 'static,
441 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
442 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
443 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
444 Net: NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
445 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
446{
447 let peer_discovery_tx = Some(configured.peer_discovery_tx);
448 let ctx = configured.ctx;
449 ctx.cfg.validate()?;
450
451 let chain_api = (configured
452 .chain_factory
453 .ok_or(HoprLibError::BuilderError("missing chain factory"))?)(&ctx);
454 let graph = (configured
455 .graph_factory
456 .ok_or(HoprLibError::BuilderError("missing graph factory"))?)(&ctx);
457 let (network, network_process) =
458 (configured
459 .network_factory
460 .ok_or(HoprLibError::BuilderError("missing network factory"))?)(ctx.clone())
461 .await?;
462 let cover_traffic = (configured
463 .ct_factory
464 .ok_or(HoprLibError::BuilderError("missing cover traffic factory"))?)(&ctx);
465
466 let (chain_id, transport_id) = (ctx.chain_key.clone(), ctx.packet_key.clone());
467
468 let transport_api = HoprTransport::new(
469 (&chain_id, &transport_id),
470 chain_api.clone(),
471 graph.clone(),
472 vec![(&ctx.cfg.host).try_into().map_err(HoprLibError::TransportError)?],
473 ctx.cfg.protocol.clone(),
474 )
475 .map_err(HoprLibError::TransportError)?;
476
477 #[cfg(all(feature = "telemetry", not(test)))]
478 {
479 use hopr_api::types::primitive::traits::AsUnixTimestamp;
480 METRIC_PROCESS_START_TIME.set(std::time::SystemTime::now().as_unix_timestamp().as_secs_f64());
481 METRIC_HOPR_LIB_VERSION.set(
482 &[const_format::formatcp!("{}", constants::APP_VERSION)],
483 const_format::formatcp!(
484 "{}.{}",
485 env!("CARGO_PKG_VERSION_MAJOR"),
486 env!("CARGO_PKG_VERSION_MINOR")
487 )
488 .parse()
489 .unwrap_or(0.0),
490 );
491 }
492
493 let (mut new_tickets_tx, new_tickets_rx) = async_broadcast::broadcast(2048);
494 new_tickets_tx.set_await_active(false);
495 new_tickets_tx.set_overflow(true);
496
497 let (mut ssa_tx, ssa_rx) = async_broadcast::broadcast(2048);
498 ssa_tx.set_await_active(false);
499 ssa_tx.set_overflow(true);
500
501 let me_onchain = chain_id.public().to_address();
502
503 #[cfg(feature = "testing")]
504 tracing::warn!("!! FOR TESTING ONLY !! Node is running with some safety checks disabled!");
505
506 tracing::info!(
507 address = %me_onchain,
508 minimum_balance = %*SUGGESTED_NATIVE_BALANCE,
509 "node is not started, please fund this node",
510 );
511
512 tracing::info!(
513 suggested_minimum_balance = %*SUGGESTED_NATIVE_BALANCE,
514 "node about to start, checking for funds",
515 );
516 let funding_timeout = Duration::from_secs(200);
517 crate::helpers::wait_for_balance(*MIN_NATIVE_BALANCE, funding_timeout, me_onchain, &chain_api)
518 .await
519 .map_err(|_| {
520 HoprLibError::InsufficientFunds(format!(
521 "failed to fund the node within {} seconds",
522 funding_timeout.as_secs()
523 ))
524 })?;
525
526 tracing::info!("starting HOPR node...");
527 let balance: hopr_api::types::primitive::prelude::XDaiBalance =
528 chain_api.balance(me_onchain).await.map_err(HoprLibError::chain)?;
529 let minimum_balance = *constants::MIN_NATIVE_BALANCE;
530
531 tracing::info!(address = %me_onchain, %balance, %minimum_balance, "node information");
532
533 if balance.le(&minimum_balance) {
534 return Err(HoprLibError::InsufficientFunds(
535 "cannot start the node without a sufficiently funded wallet".into(),
536 ));
537 }
538
539 #[cfg(debug_assertions)]
540 let skip_protocol_checks = ctx.cfg.disable_protocol_checks;
541 #[cfg(not(debug_assertions))]
542 let skip_protocol_checks = false;
543
544 let network_min_ticket_price = chain_api.minimum_ticket_price().await.map_err(HoprLibError::chain)?;
545 let configured_ticket_price = ctx.cfg.protocol.packet.codec.outgoing_ticket_price;
546 if !skip_protocol_checks && configured_ticket_price.is_some_and(|c| c < network_min_ticket_price) {
547 return Err(HoprLibError::GeneralError(format!(
548 "configured outgoing ticket price < network minimum: {configured_ticket_price:?} < \
549 {network_min_ticket_price}"
550 )));
551 }
552
553 let network_min_win_prob = chain_api
554 .minimum_incoming_ticket_win_prob()
555 .await
556 .map_err(HoprLibError::chain)?;
557 let configured_win_prob = ctx.cfg.protocol.packet.codec.outgoing_win_prob;
558
559 if !skip_protocol_checks && configured_win_prob.is_some_and(|c| c.approx_cmp(&network_min_win_prob).is_lt()) {
560 return Err(HoprLibError::GeneralError(format!(
561 "configured outgoing win probability < network minimum: {configured_win_prob:?} < {network_min_win_prob}"
562 )));
563 }
564
565 tracing::info!(
566 peer_id = %transport_id.public().to_peerid_str(),
567 address = %me_onchain,
568 hopr_lib_version = constants::APP_VERSION,
569 "Node information"
570 );
571
572 let safe_addr = ctx.cfg.safe_module.safe_address;
573 if me_onchain == safe_addr {
574 return Err(HoprLibError::GeneralError(
575 "cannot use self as staking safe address".into(),
576 ));
577 }
578
579 tracing::info!(%safe_addr, "registering safe with this node");
580 match chain_api.register_safe(&safe_addr).await {
581 Ok(awaiter) => {
582 awaiter.await.map_err(|error| {
583 tracing::error!(%safe_addr, %error, "safe registration failed");
584 HoprLibError::chain(error)
585 })?;
586 tracing::info!(%safe_addr, "safe successfully registered with this node");
587 }
588 Err(SafeRegistrationError::AlreadyRegistered(registered_safe)) => {
589 if registered_safe == safe_addr {
590 tracing::info!(%safe_addr, "this safe is already registered with this node");
591 } else {
592 tracing::error!(%safe_addr, %registered_safe, "node registered with different safe");
593 return Err(HoprLibError::GeneralError("node registered with different safe".into()));
594 }
595 }
596 Err(error) => {
597 tracing::error!(%safe_addr, %error, "safe registration failed");
598 return Err(HoprLibError::chain(error));
599 }
600 }
601
602 let multiaddresses_to_announce = if ctx.cfg.publish {
603 transport_api.announceable_multiaddresses()
604 } else {
605 Vec::new()
606 };
607
608 multiaddresses_to_announce
609 .iter()
610 .filter(|a| !is_public_address(a))
611 .for_each(|multi_addr| tracing::warn!(?multi_addr, "announcing private multiaddress"));
612
613 if ctx.cfg.publish {
615 let key_binding_fee = chain_api.key_binding_fee().await.map_err(HoprLibError::chain)?;
616 if !key_binding_fee.is_zero() {
617 let safe_hopr_balance: hopr_api::types::primitive::prelude::HoprBalance =
618 chain_api.balance(safe_addr).await.map_err(HoprLibError::chain)?;
619 if safe_hopr_balance < key_binding_fee
624 && chain_api
625 .await_key_binding(transport_id.public(), Duration::from_secs(1))
626 .await
627 .is_err()
628 {
629 tracing::warn!(
630 %safe_addr,
631 safe_balance = %safe_hopr_balance,
632 required = %key_binding_fee,
633 "the safe does not hold enough wxHOPR to pay the node announcement (key-binding) fee, waiting for \
634 it to be funded before announcing the node",
635 );
636 let announce_timeout = Duration::from_secs(200);
637 crate::helpers::wait_for_balance(key_binding_fee, announce_timeout, safe_addr, &chain_api)
638 .await
639 .map_err(|_| {
640 HoprLibError::InsufficientFunds(format!(
641 "the safe {safe_addr} does not hold enough wxHOPR (needs at least {key_binding_fee}) to \
642 announce the node on chain; fund the safe with wxHOPR and restart the node"
643 ))
644 })?;
645 }
646 }
647 }
648
649 let chain_api_clone = chain_api.clone();
650 let me_offchain = *transport_id.public();
651 let node_ready = spawn(async move {
652 chain_api_clone
653 .await_key_binding(&me_offchain, NODE_READY_TIMEOUT)
654 .await
655 });
656
657 tracing::info!(?multiaddresses_to_announce, "announcing node on chain");
658 match chain_api.announce(&multiaddresses_to_announce, &transport_id).await {
659 Ok(awaiter) => {
660 awaiter.await.map_err(|error| {
661 tracing::error!(
662 ?multiaddresses_to_announce,
663 %error,
664 "node announcement failed; this is commonly caused by the safe not holding enough wxHOPR to pay \
665 the key-binding fee",
666 );
667 HoprLibError::chain(error)
668 })?;
669 tracing::info!(?multiaddresses_to_announce, "node announced successfully");
670 }
671 Err(AnnouncementError::AlreadyAnnounced) => {
672 tracing::info!("node already announced on chain");
673 }
674 Err(error) => {
675 tracing::error!(%error, "failed to transmit node announcement");
676 return Err(HoprLibError::chain(error));
677 }
678 }
679
680 let this_node_account = node_ready
681 .await
682 .map_err(HoprLibError::other)?
683 .map_err(HoprLibError::chain)?;
684 if this_node_account.chain_addr != me_onchain || this_node_account.safe_address.is_none_or(|a| a != safe_addr) {
685 tracing::error!(%this_node_account, "account key-binding mismatch");
686 return Err(HoprLibError::GeneralError("account key-binding mismatch".into()));
687 }
688
689 tracing::info!(%this_node_account, "node account is ready");
690
691 {
693 let network_events = network.subscribe_network_events();
694 let graph_updater = graph.clone();
695 spawn(async move {
696 network_events
697 .for_each(|event| {
698 let graph_updater = graph_updater.clone();
699 async move {
700 let (peer_id, connected) = match event {
701 hopr_api::network::NetworkEvent::PeerConnected(p) => (p, true),
702 hopr_api::network::NetworkEvent::PeerDisconnected(p) => (p, false),
703 };
704 if let Ok(opk) = hopr_api::OffchainPublicKey::from_peerid(&peer_id) {
705 graph_updater.record_edge(hopr_api::graph::MeasurableEdge::<
706 hopr_transport::NeighborTelemetry,
707 hopr_transport::PathTelemetry,
708 >::ConnectionStatus {
709 peer: opk,
710 connected,
711 });
712 } else {
713 tracing::error!(%peer_id, "failed to convert peer ID to public key for graph update");
714 }
715 }
716 })
717 .await;
718 });
719 }
720
721 {
723 let chain_events = chain_api
724 .subscribe_with_state_sync([StateSyncOptions::PublicAccounts, StateSyncOptions::OpenedChannels])
725 .map_err(HoprLibError::chain)?;
726
727 let graph_updater = graph.clone();
728 let chain_reader = chain_api.clone();
729
730 let own_chain_addr = me_onchain;
731 let own_packet_key = *transport_id.public();
732
733 let ticket_price = Arc::new(parking_lot::RwLock::new(
734 chain_reader.minimum_ticket_price().await.unwrap_or_default(),
735 ));
736 let win_probability = Arc::new(parking_lot::RwLock::new(
737 chain_reader
738 .minimum_incoming_ticket_win_prob()
739 .await
740 .unwrap_or_default(),
741 ));
742
743 let proc = chain_wiring::process_chain_events(
744 chain_reader,
745 graph_updater,
746 transport_api.surb_store().clone(),
747 chain_events,
748 own_chain_addr,
749 own_packet_key,
750 ticket_price,
751 win_probability,
752 peer_discovery_tx,
753 )
754 .inspect(|_| {
755 tracing::warn!(
756 task = "chain-to-graph event wiring",
757 "long-running background task finished"
758 )
759 });
760 processes.insert(HoprLibProcess::ChannelEvents, hopr_utils::spawn_as_abortable!(proc));
761 }
762
763 Ok(PreHopr {
764 chain_id,
765 transport_id,
766 cfg: ctx.cfg,
767 state: Arc::new(AtomicHoprState::new(HoprState::Uninitialized)),
768 transport_api,
769 chain_api,
770 ticket_event_subscribers: (new_tickets_tx, new_tickets_rx.deactivate()),
771 pix_event_subscribers: (ssa_tx, ssa_rx.deactivate()),
772 processes,
773 session_tx,
774 admission_tx,
775 cover_traffic,
776 network,
777 network_process,
778 })
779}
780
781macro_rules! impl_build_methods {
786 () => {
787 pub async fn build_edge<TFact>(
789 self,
790 ticket_factory: TFact,
791 ) -> Result<Hopr<Chain, Graph, Net, ()>, HoprLibError>
792 where
793 TFact: TicketFactory + Clone + Send + Sync + 'static,
794 {
795 let (configured, session_tx, admission_tx, processes) = self.into_parts();
796 let pre = pre_build_inner(configured, session_tx, admission_tx, processes).await?;
797
798 tracing::info!("starting transport for edge (entry) node");
799 let (socket, transport_processes) = pre
800 .transport_api
801 .run_entry(
802 pre.cover_traffic,
803 pre.network,
804 pre.network_process,
805 ticket_factory,
806 Some(BroadcastSenderSink(pre.pix_event_subscribers.0.clone())),
807 )
808 .await?;
809
810 spawn(drain_incoming_data(socket.reader()));
812
813 let mut processes = pre.processes;
814 processes.flat_map_extend_from(transport_processes, HoprLibProcess::Transport);
815
816 let hopr = Hopr {
817 chain_id: NodeOnchainIdentity {
818 node_address: pre.chain_id.public().to_address(),
819 safe_address: pre.cfg.safe_module.safe_address,
820 module_address: pre.cfg.safe_module.module_address,
821 },
822 cfg: pre.cfg,
823 state: pre.state.clone(),
824 ticket_event_subscribers: pre.ticket_event_subscribers,
825 pix_event_subscribers: pre.pix_event_subscribers,
826 transport_id: pre.transport_id,
827 transport_api: pre.transport_api,
828 chain_api: pre.chain_api,
829 processes,
830 ticket_manager: (),
831 };
832
833 hopr.state.store(HoprState::Running, std::sync::atomic::Ordering::Relaxed);
834 tracing::info!(
835 id = %hopr.transport_id.public().to_peerid_str(),
836 hopr_lib_version = constants::APP_VERSION,
837 "EDGE NODE STARTED AND RUNNING"
838 );
839
840 Ok(hopr)
841 }
842
843 pub async fn build_entry<TFact>(
848 self,
849 ticket_factory: TFact,
850 ) -> Result<Hopr<Chain, Graph, Net, ()>, HoprLibError>
851 where
852 TFact: TicketFactory + Clone + Send + Sync + 'static,
853 {
854 let (configured, session_tx, admission_tx, processes) = self.into_parts();
855 let pre = pre_build_inner(configured, session_tx, admission_tx, processes).await?;
856
857 tracing::info!("starting transport for entry node");
858 let (socket, transport_processes) = pre
859 .transport_api
860 .run_entry(
861 pre.cover_traffic,
862 pre.network,
863 pre.network_process,
864 ticket_factory,
865 Some(BroadcastSenderSink(pre.pix_event_subscribers.0.clone())),
866 )
867 .await?;
868
869 spawn(drain_incoming_data(socket.reader()));
871
872 let mut processes = pre.processes;
873 processes.flat_map_extend_from(transport_processes, HoprLibProcess::Transport);
874
875 let hopr = Hopr {
876 chain_id: NodeOnchainIdentity {
877 node_address: pre.chain_id.public().to_address(),
878 safe_address: pre.cfg.safe_module.safe_address,
879 module_address: pre.cfg.safe_module.module_address,
880 },
881 cfg: pre.cfg,
882 state: pre.state.clone(),
883 ticket_event_subscribers: pre.ticket_event_subscribers,
884 pix_event_subscribers: pre.pix_event_subscribers,
885 transport_id: pre.transport_id,
886 transport_api: pre.transport_api,
887 chain_api: pre.chain_api,
888 processes,
889 ticket_manager: (),
890 };
891
892 hopr.state.store(HoprState::Running, std::sync::atomic::Ordering::Relaxed);
893 tracing::info!(
894 id = %hopr.transport_id.public().to_peerid_str(),
895 hopr_lib_version = constants::APP_VERSION,
896 "ENTRY NODE STARTED AND RUNNING"
897 );
898
899 Ok(hopr)
900 }
901
902 pub async fn build_exit<TFact>(
907 self,
908 ticket_factory: TFact,
909 ) -> Result<Hopr<Chain, Graph, Net, ()>, HoprLibError>
910 where
911 TFact: TicketFactory + Clone + Send + Sync + 'static,
912 {
913 let (configured, session_tx, admission_tx, processes) = self.into_parts();
914 let pre = pre_build_inner(configured, session_tx, admission_tx, processes).await?;
915
916 tracing::info!("starting transport for exit node");
917 let (socket, transport_processes) = pre
918 .transport_api
919 .run_exit::<TFact, Ct, _>(
920 pre.cover_traffic,
921 pre.network,
922 pre.network_process,
923 ticket_factory,
924 Some(BroadcastSenderSink(pre.pix_event_subscribers.0.clone())),
925 pre.session_tx,
926 pre.admission_tx,
927 )
928 .await?;
929
930 spawn(drain_incoming_data(socket.reader()));
932
933 let mut processes = pre.processes;
934 processes.flat_map_extend_from(transport_processes, HoprLibProcess::Transport);
935
936 let hopr = Hopr {
937 chain_id: NodeOnchainIdentity {
938 node_address: pre.chain_id.public().to_address(),
939 safe_address: pre.cfg.safe_module.safe_address,
940 module_address: pre.cfg.safe_module.module_address,
941 },
942 cfg: pre.cfg,
943 state: pre.state.clone(),
944 ticket_event_subscribers: pre.ticket_event_subscribers,
945 pix_event_subscribers: pre.pix_event_subscribers,
946 transport_id: pre.transport_id,
947 transport_api: pre.transport_api,
948 chain_api: pre.chain_api,
949 processes,
950 ticket_manager: (),
951 };
952
953 hopr.state.store(HoprState::Running, std::sync::atomic::Ordering::Relaxed);
954 tracing::info!(
955 id = %hopr.transport_id.public().to_peerid_str(),
956 hopr_lib_version = constants::APP_VERSION,
957 "EXIT NODE STARTED AND RUNNING"
958 );
959
960 Ok(hopr)
961 }
962
963 pub async fn build_full<TMgr, TFact>(
965 self,
966 ticket_manager: TMgr,
967 ticket_factory: TFact,
968 ) -> Result<Hopr<Chain, Graph, Net, TMgr>, HoprLibError>
969 where
970 TMgr: TicketManagement + Clone + Send + Sync + 'static,
971 TFact: TicketFactory + Clone + Send + Sync + 'static,
972 {
973 let (configured, session_tx, admission_tx, processes) = self.into_parts();
974 let pre = pre_build_inner(configured, session_tx, admission_tx, processes).await?;
975 let mut processes = pre.processes;
976
977 tracing::info!("starting ticket events processor");
978 let (tickets_tx, tickets_rx) = bounded_sink_channel::<TicketEvent>(8192);
979
980 let (tickets_rx_stream, tickets_handle) = hopr_utils::runtime::DropAbortable::new(tickets_rx);
982
983 processes.insert(HoprLibProcess::TicketEvents, tickets_handle);
984 let new_ticket_tx = pre.ticket_event_subscribers.0.clone();
985 let tmgr_clone = ticket_manager.clone();
986 spawn(
987 hopr_utils::runtime::diagnostics::instrument(
988 tickets_rx_stream
989 .for_each(move |event| {
990 if let TicketEvent::WinningTicket(ticket) = &event
991 && let Err(error) = tmgr_clone.insert_incoming_ticket(**ticket)
992 {
993 tracing::error!(%error, "failed to insert incoming ticket");
994 }
995 if let Err(error) = new_ticket_tx.try_broadcast(event) {
996 tracing::error!(%error, "failed to broadcast ticket event");
997 }
998 futures::future::ready(())
999 })
1000 .inspect(|_| {
1001 tracing::warn!(task = %HoprLibProcess::TicketEvents, "long-running background task finished")
1002 }),
1003 "hopr_lib_ticket_events",
1004 module_path!(),
1005 file!(),
1006 line!(),
1007 ),
1008 );
1009
1010 {
1011 let chain_for_neglect = pre.chain_api.clone();
1012 let tmgr_for_neglect = ticket_manager.clone();
1013 let events = pre.chain_api.subscribe().map_err(HoprLibError::chain)?;
1014 let (neglect_handle, neglect_reg) = hopr_utils::runtime::AbortHandle::new_pair();
1015 let neglect_task = futures::stream::Abortable::new(
1016 events.filter_map(move |event| {
1017 futures::future::ready(match event {
1018 ChainEvent::ChannelClosed(ch) => Some(ch),
1019 _ => None,
1020 })
1021 }),
1022 neglect_reg,
1023 )
1024 .for_each(move |closed_channel| {
1025 let chain = chain_for_neglect.clone();
1026 let tmgr = tmgr_for_neglect.clone();
1027 async move {
1028 match closed_channel.direction(chain.me()) {
1029 Some(ChannelDirection::Incoming) => {
1030 match tmgr.neglect_tickets(closed_channel.get_id(), None) {
1031 Ok(neglected) if !neglected.is_empty() => {
1032 tracing::warn!(
1033 num_neglected = neglected.len(),
1034 %closed_channel,
1035 "tickets on incoming closed channel were neglected"
1036 );
1037 }
1038 Ok(_) => {}
1039 Err(error) => {
1040 tracing::error!(
1041 %error, %closed_channel,
1042 "failed to neglect tickets on closed channel"
1043 );
1044 }
1045 }
1046 }
1047 Some(ChannelDirection::Outgoing) => {}
1048 _ => {}
1049 }
1050 }
1051 })
1052 .inspect(|_| {
1053 tracing::warn!(
1054 task = %HoprLibProcess::ChannelClosureNeglect,
1055 "channel closure ticket neglect task finished"
1056 )
1057 });
1058 spawn(hopr_utils::runtime::diagnostics::instrument(
1059 neglect_task,
1060 "hopr_lib_channel_closure_neglect",
1061 module_path!(),
1062 file!(),
1063 line!(),
1064 ));
1065 processes.insert(HoprLibProcess::ChannelClosureNeglect, neglect_handle);
1066 }
1067
1068 tracing::info!("starting transport for full (relay) node");
1069 let (socket, transport_processes) = pre
1070 .transport_api
1071 .run_relay(
1072 pre.cover_traffic,
1073 pre.network,
1074 pre.network_process,
1075 tickets_tx,
1076 ticket_factory,
1077 Some(BroadcastSenderSink(pre.pix_event_subscribers.0.clone())),
1078 pre.session_tx,
1079 pre.admission_tx,
1080 )
1081 .await?;
1082 spawn(drain_incoming_data(socket.reader()));
1084 processes.flat_map_extend_from(transport_processes, HoprLibProcess::Transport);
1085
1086 let hopr = Hopr {
1087 chain_id: NodeOnchainIdentity {
1088 node_address: pre.chain_id.public().to_address(),
1089 safe_address: pre.cfg.safe_module.safe_address,
1090 module_address: pre.cfg.safe_module.module_address,
1091 },
1092 cfg: pre.cfg,
1093 state: pre.state.clone(),
1094 ticket_event_subscribers: pre.ticket_event_subscribers,
1095 pix_event_subscribers: pre.pix_event_subscribers,
1096 transport_id: pre.transport_id,
1097 transport_api: pre.transport_api,
1098 chain_api: pre.chain_api,
1099 processes,
1100 ticket_manager,
1101 };
1102
1103 hopr.state.store(HoprState::Running, std::sync::atomic::Ordering::Relaxed);
1104
1105 tracing::info!(
1106 id = %hopr.transport_id.public().to_peerid_str(),
1107 hopr_lib_version = constants::APP_VERSION,
1108 "FULL NODE STARTED AND RUNNING"
1109 );
1110
1111 #[cfg(all(feature = "telemetry", not(test)))]
1112 METRIC_HOPR_NODE_INFO.set(
1113 &[
1114 &hopr.transport_id.public().to_peerid_str(),
1115 &hopr.chain_id.node_address.to_string(),
1116 &hopr.chain_id.safe_address.to_string(),
1117 &hopr.chain_id.module_address.to_string(),
1118 ],
1119 1.0,
1120 );
1121
1122 Ok(hopr)
1123 }
1124 };
1125}
1126
1127#[cfg(feature = "session-server")]
1129impl<Chain, Graph, Net, Ct> HoprBuilderWithSession<Chain, Graph, Net, Ct>
1130where
1131 Chain: HoprChainApi + Clone + Send + Sync + 'static,
1132 Graph: HoprGraphApi<HoprNodeId = hopr_api::OffchainPublicKey> + Clone + Send + Sync + 'static,
1133 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
1134 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
1135 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
1136 Net: NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
1137 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
1138{
1139 impl_build_methods!();
1140
1141 fn into_parts(self) -> BuildParts<Chain, Graph, Net, Ct> {
1142 let mut processes = AbortableList::<HoprLibProcess>::default();
1143 processes.insert(HoprLibProcess::SessionServer, self.session_handle);
1144 processes.insert(HoprLibProcess::SessionAdmission, self.admission_handle);
1145 (self.inner, self.session_tx, Some(self.admission_tx), processes)
1146 }
1147}
1148
1149#[cfg(not(feature = "session-server"))]
1151impl<Chain, Graph, Net, Ct> HoprBuilderConfigured<Chain, Graph, Net, Ct>
1152where
1153 Chain: HoprChainApi + Clone + Send + Sync + 'static,
1154 Graph: HoprGraphApi<HoprNodeId = hopr_api::OffchainPublicKey> + Clone + Send + Sync + 'static,
1155 <Graph as hopr_api::graph::NetworkGraphTraverse>::Observed:
1156 hopr_api::graph::traits::EdgeObservableRead + Send + 'static,
1157 <Graph as hopr_api::graph::NetworkGraphWrite>::Observed: hopr_api::graph::traits::EdgeObservableWrite + Send,
1158 Net: NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
1159 Ct: ProbingTrafficGeneration + CoverTrafficGeneration + Send + Sync + 'static,
1160{
1161 impl_build_methods!();
1162
1163 fn into_parts(self) -> BuildParts<Chain, Graph, Net, Ct> {
1164 let (tx, _rx) = futures::channel::mpsc::channel::<IncomingSession>(1);
1165 let processes = AbortableList::<HoprLibProcess>::default();
1166 (self, tx, None, processes)
1167 }
1168}