From 4c23204e8f81f6bd1e350d0acce343d167bf21ca Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 19 Aug 2024 18:30:58 +0300 Subject: [PATCH 01/37] create bare RPC structure Signed-off-by: onur-ozkan --- Cargo.lock | 1 + .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 2 ++ mm2src/mm2_net/Cargo.toml | 2 ++ mm2src/mm2_net/src/is_peer_connected_rpc.rs | 26 +++++++++++++++++++ mm2src/mm2_net/src/lib.rs | 1 + 5 files changed, 32 insertions(+) create mode 100644 mm2src/mm2_net/src/is_peer_connected_rpc.rs diff --git a/Cargo.lock b/Cargo.lock index 681184de65..cc871ee6ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4707,6 +4707,7 @@ dependencies = [ "prost", "rand 0.7.3", "rustls 0.21.10", + "ser_error_derive", "serde", "serde_json", "thiserror", diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 5403392240..225764311b 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -57,6 +57,7 @@ use http::Response; use mm2_core::data_asker::send_asked_data_rpc; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_net::is_peer_connected_rpc::is_peer_connected; use mm2_rpc::mm_protocol::{MmRpcBuilder, MmRpcRequest, MmRpcVersion}; use nft::{clear_nft_db, get_nft_list, get_nft_metadata, get_nft_transfers, refresh_nft_metadata, update_nft, withdraw_nft}; @@ -213,6 +214,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, withdraw).await, "ibc_chains" => handle_mmrpc(ctx, request, ibc_chains).await, "ibc_transfer_channels" => handle_mmrpc(ctx, request, ibc_transfer_channels).await, + "is_peer_connected" => handle_mmrpc(ctx, request, is_peer_connected).await, "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, "start_eth_fee_estimator" => handle_mmrpc(ctx, request, start_eth_fee_estimator).await, "stop_eth_fee_estimator" => handle_mmrpc(ctx, request, stop_eth_fee_estimator).await, diff --git a/mm2src/mm2_net/Cargo.toml b/mm2src/mm2_net/Cargo.toml index d92e0c08b6..3ed5018818 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -29,6 +29,8 @@ mm2-libp2p = { path = "../mm2_p2p", package = "mm2_p2p", optional = true } parking_lot = { version = "0.12.0", features = ["nightly"], optional = true } prost = "0.11" rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } +ser_error = { path = "../derives/ser_error" } +ser_error_derive = { path = "../derives/ser_error_derive" } serde = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } thiserror = "1.0.30" diff --git a/mm2src/mm2_net/src/is_peer_connected_rpc.rs b/mm2src/mm2_net/src/is_peer_connected_rpc.rs new file mode 100644 index 0000000000..8c69702584 --- /dev/null +++ b/mm2src/mm2_net/src/is_peer_connected_rpc.rs @@ -0,0 +1,26 @@ +use common::HttpStatusCode; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; +use mm2_libp2p::PeerId; +use ser_error_derive::SerializeErrorType; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Clone, Deserialize)] +pub struct RequestPayload { + peer_id: String, +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum QueryError {} + +impl HttpStatusCode for QueryError { + fn status_code(&self) -> common::StatusCode { todo!() } +} + +pub async fn is_peer_connected(_ctx: MmArc, req: RequestPayload) -> Result> { + let _peer_id = PeerId::from_str(&req.peer_id).unwrap(); + todo!() +} diff --git a/mm2src/mm2_net/src/lib.rs b/mm2src/mm2_net/src/lib.rs index 954e25c5a0..48fdeaa89c 100644 --- a/mm2src/mm2_net/src/lib.rs +++ b/mm2src/mm2_net/src/lib.rs @@ -1,4 +1,5 @@ pub mod grpc_web; +pub mod is_peer_connected_rpc; #[cfg(feature = "event-stream")] pub mod network_event; #[cfg(feature = "p2p")] pub mod p2p; pub mod transport; From ad1cdb94f5651c906927d4cb1615d948e9099a5d Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 20 Aug 2024 13:41:10 +0300 Subject: [PATCH 02/37] implement `PeerConnectionHealth` p2p command Signed-off-by: onur-ozkan --- Cargo.lock | 1 + .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 4 +-- mm2src/mm2_net/src/lib.rs | 2 +- ..._rpc.rs => peer_connection_healthcheck.rs} | 10 ++++-- mm2src/mm2_p2p/src/behaviours/atomicdex.rs | 31 +++++++++++++++++++ mm2src/mm2_p2p/src/lib.rs | 7 +++-- 6 files changed, 46 insertions(+), 9 deletions(-) rename mm2src/mm2_net/src/{is_peer_connected_rpc.rs => peer_connection_healthcheck.rs} (60%) diff --git a/Cargo.lock b/Cargo.lock index cc871ee6ef..e49900b7c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4707,6 +4707,7 @@ dependencies = [ "prost", "rand 0.7.3", "rustls 0.21.10", + "ser_error", "ser_error_derive", "serde", "serde_json", diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 225764311b..ba265b1f23 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -57,7 +57,7 @@ use http::Response; use mm2_core::data_asker::send_asked_data_rpc; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_net::is_peer_connected_rpc::is_peer_connected; +use mm2_net::peer_connection_healthcheck::peer_connection_healthcheck_rpc; use mm2_rpc::mm_protocol::{MmRpcBuilder, MmRpcRequest, MmRpcVersion}; use nft::{clear_nft_db, get_nft_list, get_nft_metadata, get_nft_transfers, refresh_nft_metadata, update_nft, withdraw_nft}; @@ -214,7 +214,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, withdraw).await, "ibc_chains" => handle_mmrpc(ctx, request, ibc_chains).await, "ibc_transfer_channels" => handle_mmrpc(ctx, request, ibc_transfer_channels).await, - "is_peer_connected" => handle_mmrpc(ctx, request, is_peer_connected).await, + "peer_connection_healthcheck" => handle_mmrpc(ctx, request, peer_connection_healthcheck_rpc).await, "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, "start_eth_fee_estimator" => handle_mmrpc(ctx, request, start_eth_fee_estimator).await, "stop_eth_fee_estimator" => handle_mmrpc(ctx, request, stop_eth_fee_estimator).await, diff --git a/mm2src/mm2_net/src/lib.rs b/mm2src/mm2_net/src/lib.rs index 48fdeaa89c..5202991876 100644 --- a/mm2src/mm2_net/src/lib.rs +++ b/mm2src/mm2_net/src/lib.rs @@ -1,7 +1,7 @@ pub mod grpc_web; -pub mod is_peer_connected_rpc; #[cfg(feature = "event-stream")] pub mod network_event; #[cfg(feature = "p2p")] pub mod p2p; +pub mod peer_connection_healthcheck; pub mod transport; #[cfg(not(target_arch = "wasm32"))] pub mod ip_addr; diff --git a/mm2src/mm2_net/src/is_peer_connected_rpc.rs b/mm2src/mm2_net/src/peer_connection_healthcheck.rs similarity index 60% rename from mm2src/mm2_net/src/is_peer_connected_rpc.rs rename to mm2src/mm2_net/src/peer_connection_healthcheck.rs index 8c69702584..68c341eb95 100644 --- a/mm2src/mm2_net/src/is_peer_connected_rpc.rs +++ b/mm2src/mm2_net/src/peer_connection_healthcheck.rs @@ -7,6 +7,8 @@ use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; use std::str::FromStr; +use crate::p2p::P2PContext; + #[derive(Clone, Deserialize)] pub struct RequestPayload { peer_id: String, @@ -20,7 +22,9 @@ impl HttpStatusCode for QueryError { fn status_code(&self) -> common::StatusCode { todo!() } } -pub async fn is_peer_connected(_ctx: MmArc, req: RequestPayload) -> Result> { - let _peer_id = PeerId::from_str(&req.peer_id).unwrap(); - todo!() +pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> Result> { + let peer_id = PeerId::from_str(&req.peer_id).unwrap(); + let ctx = P2PContext::fetch_from_mm_arc(&ctx); + let cmd_tx = ctx.cmd_tx.lock().clone(); + Ok(mm2_libp2p::peer_connection_healthcheck(cmd_tx, peer_id).await) } diff --git a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs index db907711ba..2b79e2c003 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -113,6 +113,11 @@ pub enum AdexBehaviourCmd { msg: Vec, from: PeerId, }, + /// TODO + PeerConnectionHealth { + peer: PeerId, + response_tx: oneshot::Sender<()>, + }, /// Request relays sequential until a response is received. RequestAnyRelay { req: Vec, @@ -162,6 +167,13 @@ pub enum AdexBehaviourCmd { }, } +pub async fn peer_connection_healthcheck(mut cmd_tx: AdexCmdTx, peer: PeerId) -> bool { + let (response_tx, rx) = oneshot::channel(); + let cmd = AdexBehaviourCmd::PeerConnectionHealth { peer, response_tx }; + cmd_tx.send(cmd).await.expect("Rx should be present"); + rx.await.is_ok() +} + /// Returns info about connected peers pub async fn get_peers_info(mut cmd_tx: AdexCmdTx) -> HashMap> { let (result_tx, rx) = oneshot::channel(); @@ -199,6 +211,21 @@ pub async fn get_relay_mesh(mut cmd_tx: AdexCmdTx) -> Vec { rx.await.expect("Tx should be present") } +async fn peer_connection_healthcheck_inner( + peer: PeerId, + request_response_tx: RequestResponseSender, + response_tx: oneshot::Sender<()>, +) { + match request_one_peer(peer, vec![], request_response_tx).await { + PeerResponse::Ok { .. } if response_tx.send(()).is_ok() => { + info!("Connection is healthy for '{peer}' peer."); + }, + response => { + error!("Connection isn't healthy for '{peer}' peer, response {response:?}"); + }, + } +} + async fn request_one_peer(peer: PeerId, req: Vec, mut request_response_tx: RequestResponseSender) -> PeerResponse { // Use the internal receiver to receive a response to this request. let (internal_response_tx, internal_response_rx) = oneshot::channel(); @@ -343,6 +370,10 @@ impl AtomicDexBehaviour { .gossipsub .publish_from(TopicHash::from_raw(topic), msg, from)?; }, + AdexBehaviourCmd::PeerConnectionHealth { peer, response_tx } => { + let future = peer_connection_healthcheck_inner(peer, self.core.request_response.sender(), response_tx); + self.spawn(future); + }, AdexBehaviourCmd::RequestAnyRelay { req, response_tx } => { let relays = self.core.gossipsub.get_relay_mesh(); // spawn the `request_any_peer` future diff --git a/mm2src/mm2_p2p/src/lib.rs b/mm2src/mm2_p2p/src/lib.rs index e9a8f78ad4..c10f1bb701 100644 --- a/mm2src/mm2_p2p/src/lib.rs +++ b/mm2src/mm2_p2p/src/lib.rs @@ -17,9 +17,10 @@ pub use crate::swarm_runtime::SwarmRuntime; // atomicdex related re-exports pub use behaviours::atomicdex::{get_gossip_mesh, get_gossip_peer_topics, get_gossip_topic_peers, get_peers_info, - get_relay_mesh, spawn_gossipsub, AdexBehaviourCmd, AdexBehaviourError, - AdexBehaviourEvent, AdexCmdTx, AdexEventRx, AdexResponse, AdexResponseChannel, - GossipsubEvent, GossipsubMessage, MessageId, NodeType, TopicHash, WssCerts}; + get_relay_mesh, peer_connection_healthcheck, spawn_gossipsub, AdexBehaviourCmd, + AdexBehaviourError, AdexBehaviourEvent, AdexCmdTx, AdexEventRx, AdexResponse, + AdexResponseChannel, GossipsubEvent, GossipsubMessage, MessageId, NodeType, TopicHash, + WssCerts}; // peers-exchange re-exports pub use behaviours::peers_exchange::PeerAddresses; From 838b5a57c57853cf5283271acef8841a11063f41 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 26 Aug 2024 18:19:03 +0300 Subject: [PATCH 03/37] add new p2p message topic "hcheck" Signed-off-by: onur-ozkan --- .../src/lp_healthcheck.rs} | 22 ++++++++++--- mm2src/mm2_main/src/lp_native_dex.rs | 9 ++++-- mm2src/mm2_main/src/lp_network.rs | 7 +++-- mm2src/mm2_main/src/mm2.rs | 1 + .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 2 +- mm2src/mm2_net/src/lib.rs | 1 - mm2src/mm2_p2p/src/behaviours/atomicdex.rs | 31 ------------------- mm2src/mm2_p2p/src/lib.rs | 7 ++--- 8 files changed, 34 insertions(+), 46 deletions(-) rename mm2src/{mm2_net/src/peer_connection_healthcheck.rs => mm2_main/src/lp_healthcheck.rs} (54%) diff --git a/mm2src/mm2_net/src/peer_connection_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs similarity index 54% rename from mm2src/mm2_net/src/peer_connection_healthcheck.rs rename to mm2src/mm2_main/src/lp_healthcheck.rs index 68c341eb95..bdc124a1cf 100644 --- a/mm2src/mm2_net/src/peer_connection_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -2,12 +2,20 @@ use common::HttpStatusCode; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; -use mm2_libp2p::PeerId; +use mm2_libp2p::{pub_sub_topic, PeerId, TopicPrefix}; +use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use crate::p2p::P2PContext; +use crate::lp_network::broadcast_p2p_msg; + +pub const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; + +#[inline] +pub fn peer_healthcheck_topic(peer_id: &PeerId) -> String { + pub_sub_topic(PEER_HEALTHCHECK_PREFIX, &peer_id.to_string()) +} #[derive(Clone, Deserialize)] pub struct RequestPayload { @@ -24,7 +32,11 @@ impl HttpStatusCode for QueryError { pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> Result> { let peer_id = PeerId::from_str(&req.peer_id).unwrap(); - let ctx = P2PContext::fetch_from_mm_arc(&ctx); - let cmd_tx = ctx.cmd_tx.lock().clone(); - Ok(mm2_libp2p::peer_connection_healthcheck(cmd_tx, peer_id).await) + let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); + let cmd_tx = p2p_ctx.cmd_tx.lock().clone(); + + broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&peer_id), vec![], Some(p2p_ctx.peer_id())); + // TODO: wait (timeout is 5 seconds) for peer's answer. + + todo!() } diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 3b73947ffb..eff961dec8 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -47,8 +47,9 @@ use std::{fs, usize}; #[cfg(not(target_arch = "wasm32"))] use crate::database::init_and_migrate_sql_db; use crate::heartbeat_event::HeartbeatEvent; +use crate::lp_healthcheck::peer_healthcheck_topic; use crate::lp_message_service::{init_message_service, InitMessageServiceError}; -use crate::lp_network::{lp_network_ports, p2p_event_process_loop, NetIdError}; +use crate::lp_network::{lp_network_ports, p2p_event_process_loop, subscribe_to_topic, NetIdError}; use crate::lp_ordermatch::{broadcast_maker_orders_keep_alive_loop, clean_memory_loop, init_ordermatch_context, lp_ordermatch_loop, orders_kick_start, BalanceUpdateOrdermatchHandler, OrdermatchInitError}; use crate::lp_swap::{running_swaps_num, swap_kick_starts}; @@ -633,7 +634,11 @@ pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { ); }) .await; - let (cmd_tx, event_rx, _peer_id) = spawn_result?; + + let (cmd_tx, event_rx, peer_id) = spawn_result?; + + // Listen for health check messages. + subscribe_to_topic(&ctx, peer_healthcheck_topic(&peer_id)); let p2p_context = P2PContext::new(cmd_tx, generate_ed25519_keypair(p2p_key)); p2p_context.store_to_mm_arc(&ctx); diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index eb84f390d1..ed5ee8b215 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -38,8 +38,7 @@ use mm2_net::p2p::P2PContext; use serde::de; use std::net::ToSocketAddrs; -use crate::lp_ordermatch; -use crate::{lp_stats, lp_swap}; +use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; pub type P2PRequestResult = Result>; pub type P2PProcessResult = Result>; @@ -216,6 +215,10 @@ async fn process_p2p_message( } } }, + Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { + // .. + todo!() + }, None | Some(_) => (), } diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 7dcc5572cb..d4d1375a35 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -74,6 +74,7 @@ use mm2_err_handle::prelude::*; pub mod heartbeat_event; pub mod lp_dispatcher; +pub mod lp_healthcheck; pub mod lp_message_service; pub mod lp_network; pub mod lp_ordermatch; diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index ba265b1f23..773c3719e5 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,4 +1,5 @@ use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; +use crate::lp_healthcheck::peer_connection_healthcheck_rpc; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; #[cfg(target_arch = "wasm32")] use crate::lp_native_dex::init_metamask::{cancel_connect_metamask, connect_metamask, connect_metamask_status}; @@ -57,7 +58,6 @@ use http::Response; use mm2_core::data_asker::send_asked_data_rpc; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_net::peer_connection_healthcheck::peer_connection_healthcheck_rpc; use mm2_rpc::mm_protocol::{MmRpcBuilder, MmRpcRequest, MmRpcVersion}; use nft::{clear_nft_db, get_nft_list, get_nft_metadata, get_nft_transfers, refresh_nft_metadata, update_nft, withdraw_nft}; diff --git a/mm2src/mm2_net/src/lib.rs b/mm2src/mm2_net/src/lib.rs index 5202991876..954e25c5a0 100644 --- a/mm2src/mm2_net/src/lib.rs +++ b/mm2src/mm2_net/src/lib.rs @@ -1,7 +1,6 @@ pub mod grpc_web; #[cfg(feature = "event-stream")] pub mod network_event; #[cfg(feature = "p2p")] pub mod p2p; -pub mod peer_connection_healthcheck; pub mod transport; #[cfg(not(target_arch = "wasm32"))] pub mod ip_addr; diff --git a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs index 2b79e2c003..db907711ba 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -113,11 +113,6 @@ pub enum AdexBehaviourCmd { msg: Vec, from: PeerId, }, - /// TODO - PeerConnectionHealth { - peer: PeerId, - response_tx: oneshot::Sender<()>, - }, /// Request relays sequential until a response is received. RequestAnyRelay { req: Vec, @@ -167,13 +162,6 @@ pub enum AdexBehaviourCmd { }, } -pub async fn peer_connection_healthcheck(mut cmd_tx: AdexCmdTx, peer: PeerId) -> bool { - let (response_tx, rx) = oneshot::channel(); - let cmd = AdexBehaviourCmd::PeerConnectionHealth { peer, response_tx }; - cmd_tx.send(cmd).await.expect("Rx should be present"); - rx.await.is_ok() -} - /// Returns info about connected peers pub async fn get_peers_info(mut cmd_tx: AdexCmdTx) -> HashMap> { let (result_tx, rx) = oneshot::channel(); @@ -211,21 +199,6 @@ pub async fn get_relay_mesh(mut cmd_tx: AdexCmdTx) -> Vec { rx.await.expect("Tx should be present") } -async fn peer_connection_healthcheck_inner( - peer: PeerId, - request_response_tx: RequestResponseSender, - response_tx: oneshot::Sender<()>, -) { - match request_one_peer(peer, vec![], request_response_tx).await { - PeerResponse::Ok { .. } if response_tx.send(()).is_ok() => { - info!("Connection is healthy for '{peer}' peer."); - }, - response => { - error!("Connection isn't healthy for '{peer}' peer, response {response:?}"); - }, - } -} - async fn request_one_peer(peer: PeerId, req: Vec, mut request_response_tx: RequestResponseSender) -> PeerResponse { // Use the internal receiver to receive a response to this request. let (internal_response_tx, internal_response_rx) = oneshot::channel(); @@ -370,10 +343,6 @@ impl AtomicDexBehaviour { .gossipsub .publish_from(TopicHash::from_raw(topic), msg, from)?; }, - AdexBehaviourCmd::PeerConnectionHealth { peer, response_tx } => { - let future = peer_connection_healthcheck_inner(peer, self.core.request_response.sender(), response_tx); - self.spawn(future); - }, AdexBehaviourCmd::RequestAnyRelay { req, response_tx } => { let relays = self.core.gossipsub.get_relay_mesh(); // spawn the `request_any_peer` future diff --git a/mm2src/mm2_p2p/src/lib.rs b/mm2src/mm2_p2p/src/lib.rs index c10f1bb701..e9a8f78ad4 100644 --- a/mm2src/mm2_p2p/src/lib.rs +++ b/mm2src/mm2_p2p/src/lib.rs @@ -17,10 +17,9 @@ pub use crate::swarm_runtime::SwarmRuntime; // atomicdex related re-exports pub use behaviours::atomicdex::{get_gossip_mesh, get_gossip_peer_topics, get_gossip_topic_peers, get_peers_info, - get_relay_mesh, peer_connection_healthcheck, spawn_gossipsub, AdexBehaviourCmd, - AdexBehaviourError, AdexBehaviourEvent, AdexCmdTx, AdexEventRx, AdexResponse, - AdexResponseChannel, GossipsubEvent, GossipsubMessage, MessageId, NodeType, TopicHash, - WssCerts}; + get_relay_mesh, spawn_gossipsub, AdexBehaviourCmd, AdexBehaviourError, + AdexBehaviourEvent, AdexCmdTx, AdexEventRx, AdexResponse, AdexResponseChannel, + GossipsubEvent, GossipsubMessage, MessageId, NodeType, TopicHash, WssCerts}; // peers-exchange re-exports pub use behaviours::peers_exchange::PeerAddresses; From 886d13e58e15c38f667ec21fad1f054cb403f678 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 26 Aug 2024 19:01:35 +0300 Subject: [PATCH 04/37] fix p2p race condition Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_native_dex.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index eff961dec8..19ad5901bc 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -637,15 +637,15 @@ pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { let (cmd_tx, event_rx, peer_id) = spawn_result?; - // Listen for health check messages. - subscribe_to_topic(&ctx, peer_healthcheck_topic(&peer_id)); - let p2p_context = P2PContext::new(cmd_tx, generate_ed25519_keypair(p2p_key)); p2p_context.store_to_mm_arc(&ctx); let fut = p2p_event_process_loop(ctx.weak(), event_rx, i_am_seed); ctx.spawner().spawn(fut); + // Listen for health check messages. + subscribe_to_topic(&ctx, peer_healthcheck_topic(&peer_id)); + Ok(()) } From 9ee8485126bef9a438b462445bab6e1f64207786 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 26 Aug 2024 19:49:43 +0300 Subject: [PATCH 05/37] WIP: `HealtCheckMsg` Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 20 ++++++++++++++++---- mm2src/mm2_main/src/lp_network.rs | 4 +++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index bdc124a1cf..d42decd342 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,8 +1,9 @@ use common::HttpStatusCode; +use crypto::CryptoCtx; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; -use mm2_libp2p::{pub_sub_topic, PeerId, TopicPrefix}; +use mm2_libp2p::{encode_and_sign, pub_sub_topic, PeerId, TopicPrefix}; use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; @@ -12,6 +13,12 @@ use crate::lp_network::broadcast_p2p_msg; pub const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; +struct HealtCheckMsg { + peer_id: String, + public_key_encoded: Vec, + signature_bytes: Vec, +} + #[inline] pub fn peer_healthcheck_topic(peer_id: &PeerId) -> String { pub_sub_topic(PEER_HEALTHCHECK_PREFIX, &peer_id.to_string()) @@ -31,11 +38,16 @@ impl HttpStatusCode for QueryError { } pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> Result> { - let peer_id = PeerId::from_str(&req.peer_id).unwrap(); + let target_peer_id = PeerId::from_str(&req.peer_id).unwrap(); let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - let cmd_tx = p2p_ctx.cmd_tx.lock().clone(); + let crypto_ctx = CryptoCtx::from_ctx(&ctx).expect("CryptoCtx must be initialized already"); + let my_peer_id = p2p_ctx.peer_id(); + + let secret = crypto_ctx.mm2_internal_privkey_secret().take(); + let msg = encode_and_sign(&my_peer_id.to_bytes(), &secret).expect("TODO"); - broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&peer_id), vec![], Some(p2p_ctx.peer_id())); + println!("0000000000 SENDING FROM {:?}", my_peer_id.to_string()); + broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), msg, None); // TODO: wait (timeout is 5 seconds) for peer's answer. todo!() diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index ed5ee8b215..5cdbc0b39f 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -216,7 +216,9 @@ async fn process_p2p_message( } }, Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { - // .. + let sender_peer_id = PeerId::from_bytes(&message.data).expect("TODO"); + println!("0000000000 COMING FROM {:?}", sender_peer_id.to_string()); + todo!() }, None | Some(_) => (), From db49d0d5baa9a2cda464c74c74bebfcb9d9f4d21 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 27 Aug 2024 10:01:29 +0300 Subject: [PATCH 06/37] implement signing and verification logic Signed-off-by: onur-ozkan --- mm2src/mm2_main/Cargo.toml | 1 + mm2src/mm2_main/src/lp_healthcheck.rs | 69 +++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index f3232ac91e..3ed7d02fae 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -32,6 +32,7 @@ bitcrypto = { path = "../mm2_bitcoin/crypto" } blake2 = "0.10.6" bytes = "0.4" chain = { path = "../mm2_bitcoin/chain" } +chrono = "0.4" cfg-if = "1.0" coins = { path = "../coins" } coins_activation = { path = "../coins_activation" } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index d42decd342..48b9b254b5 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,9 +1,10 @@ +use chrono::Utc; use common::HttpStatusCode; use crypto::CryptoCtx; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; -use mm2_libp2p::{encode_and_sign, pub_sub_topic, PeerId, TopicPrefix}; +use mm2_libp2p::{encode_and_sign, pub_sub_topic, Libp2pPublic, PeerId, SigningError, TopicPrefix}; use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; @@ -13,10 +14,68 @@ use crate::lp_network::broadcast_p2p_msg; pub const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; -struct HealtCheckMsg { - peer_id: String, - public_key_encoded: Vec, - signature_bytes: Vec, +struct HealthCheckMessage { + signature: Vec, + data: HealthCheckData, +} + +impl HealthCheckMessage { + pub fn generate_message(ctx: &MmArc, target_peer: PeerId, expires_in_seconds: i64) -> Result { + let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); + let sender_peer = p2p_ctx.peer_id().to_string(); + let keypair = p2p_ctx.keypair(); + let sender_public_key = keypair.public().encode_protobuf(); + let target_peer = target_peer.to_string(); + + let data = HealthCheckData { + sender_peer, + sender_public_key, + target_peer, + expires_at: Utc::now().timestamp() + expires_in_seconds, + }; + + let signature = keypair.sign(&data.encode())?; + + Ok(Self { signature, data }) + } + + fn is_received_message_valid(&self, my_peer_id: PeerId) -> bool { + if Utc::now().timestamp() > self.data.expires_at { + return false; + } + + if self.data.target_peer != my_peer_id.to_string() { + return false; + } + + let Ok(public_key) = Libp2pPublic::try_decode_protobuf(&self.data.sender_public_key) else { return false }; + + if self.data.sender_peer != public_key.to_peer_id().to_string() { + return false; + } + + public_key.verify(&self.data.encode(), &self.signature) + } +} + +struct HealthCheckData { + sender_peer: String, + sender_public_key: Vec, + target_peer: String, + expires_at: i64, +} + +impl HealthCheckData { + fn encode(&self) -> Vec { + let mut bytes = vec![]; + + bytes.extend(self.sender_peer.as_bytes()); + bytes.extend(&self.sender_public_key); + bytes.extend(self.target_peer.as_bytes()); + bytes.extend(self.expires_at.to_ne_bytes()); + + bytes + } } #[inline] From a5d7f815d6275ae54147990885bfd6780b66be53 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 27 Aug 2024 15:10:17 +0300 Subject: [PATCH 07/37] simulate request-response behaviour Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 9 ++- mm2src/mm2_main/src/lp_healthcheck.rs | 79 +++++++++++++++++++-------- mm2src/mm2_main/src/lp_network.rs | 25 +++++++-- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index e891f0b649..6a62569e56 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -1,9 +1,11 @@ #[cfg(feature = "track-ctx-pointer")] use common::executor::Timer; -use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner}, - graceful_shutdown, AbortSettings, AbortableSystem, SpawnAbortable, SpawnFuture}; use common::log::{self, LogLevel, LogOnError, LogState}; use common::{cfg_native, cfg_wasm32, small_rng}; +use common::{executor::{abortable_queue::{AbortableQueue, WeakSpawner}, + graceful_shutdown, AbortSettings, AbortableSystem, SpawnAbortable, SpawnFuture}, + expirable_map::ExpirableMap}; +use futures::lock::Mutex as AsyncMutex; use gstuff::{try_s, Constructible, ERR, ERRL}; use lazy_static::lazy_static; use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; @@ -30,7 +32,6 @@ cfg_wasm32! { cfg_native! { use db_common::async_sql_conn::AsyncConnection; use db_common::sqlite::rusqlite::Connection; - use futures::lock::Mutex as AsyncMutex; use rustls::ServerName; use mm2_metrics::prometheus; use mm2_metrics::MmMetricsError; @@ -142,6 +143,7 @@ pub struct MmCtx { /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] pub async_sqlite_connection: Constructible>>, + pub healthcheck_book: AsyncMutex>, } impl MmCtx { @@ -191,6 +193,7 @@ impl MmCtx { nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: Constructible::default(), + healthcheck_book: AsyncMutex::new(ExpirableMap::default()), } } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 48b9b254b5..37959d9d6f 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,10 +1,10 @@ use chrono::Utc; -use common::HttpStatusCode; +use common::{executor::Timer, HttpStatusCode}; use crypto::CryptoCtx; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; -use mm2_libp2p::{encode_and_sign, pub_sub_topic, Libp2pPublic, PeerId, SigningError, TopicPrefix}; +use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerId, SigningError, TopicPrefix}; use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; @@ -14,13 +14,19 @@ use crate::lp_network::broadcast_p2p_msg; pub const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; -struct HealthCheckMessage { +#[derive(Deserialize, Serialize)] +pub(crate) struct HealthCheckMessage { signature: Vec, data: HealthCheckData, } impl HealthCheckMessage { - pub fn generate_message(ctx: &MmArc, target_peer: PeerId, expires_in_seconds: i64) -> Result { + pub fn generate_message( + ctx: &MmArc, + target_peer: PeerId, + is_a_reply: bool, + expires_in_seconds: i64, + ) -> Result { let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); let sender_peer = p2p_ctx.peer_id().to_string(); let keypair = p2p_ctx.keypair(); @@ -32,14 +38,15 @@ impl HealthCheckMessage { sender_public_key, target_peer, expires_at: Utc::now().timestamp() + expires_in_seconds, + is_a_reply, }; - let signature = keypair.sign(&data.encode())?; + let signature = keypair.sign(&data.encode().unwrap())?; Ok(Self { signature, data }) } - fn is_received_message_valid(&self, my_peer_id: PeerId) -> bool { + pub(crate) fn is_received_message_valid(&self, my_peer_id: PeerId) -> bool { if Utc::now().timestamp() > self.data.expires_at { return false; } @@ -54,28 +61,31 @@ impl HealthCheckMessage { return false; } - public_key.verify(&self.data.encode(), &self.signature) + public_key.verify(&self.data.encode().unwrap(), &self.signature) } + + pub(crate) fn encode(&self) -> Result, rmp_serde::encode::Error> { encode_message(self) } + + pub(crate) fn decode(bytes: &[u8]) -> Result { decode_message(bytes) } + + pub(crate) fn should_reply(&self) -> bool { !self.data.is_a_reply } + + pub(crate) fn sender_peer(&self) -> &str { &self.data.sender_peer } + + pub(crate) fn target_peer(&self) -> &str { &self.data.target_peer } } +#[derive(Deserialize, Serialize)] struct HealthCheckData { sender_peer: String, sender_public_key: Vec, target_peer: String, expires_at: i64, + is_a_reply: bool, } impl HealthCheckData { - fn encode(&self) -> Vec { - let mut bytes = vec![]; - - bytes.extend(self.sender_peer.as_bytes()); - bytes.extend(&self.sender_public_key); - bytes.extend(self.target_peer.as_bytes()); - bytes.extend(self.expires_at.to_ne_bytes()); - - bytes - } + fn encode(&self) -> Result, rmp_serde::encode::Error> { encode_message(self) } } #[inline] @@ -99,15 +109,36 @@ impl HttpStatusCode for QueryError { pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> Result> { let target_peer_id = PeerId::from_str(&req.peer_id).unwrap(); let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - let crypto_ctx = CryptoCtx::from_ctx(&ctx).expect("CryptoCtx must be initialized already"); let my_peer_id = p2p_ctx.peer_id(); - let secret = crypto_ctx.mm2_internal_privkey_secret().take(); - let msg = encode_and_sign(&my_peer_id.to_bytes(), &secret).expect("TODO"); + let msg = HealthCheckMessage::generate_message(&ctx, target_peer_id, false, 10).unwrap(); + + broadcast_p2p_msg( + &ctx, + peer_healthcheck_topic(&target_peer_id), + msg.encode().unwrap(), + None, + ); + + const MAX_ATTEMPTS: usize = 5; + let mut attempts = 0; + loop { + if attempts > MAX_ATTEMPTS { + // timeout + return Ok(false); + } + + { + let mut book = ctx.healthcheck_book.lock().await; + book.clear_expired_entries(); + if book.remove(&target_peer_id.to_string()).is_some() { + break; + } + } - println!("0000000000 SENDING FROM {:?}", my_peer_id.to_string()); - broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), msg, None); - // TODO: wait (timeout is 5 seconds) for peer's answer. + attempts += 1; + Timer::sleep(1.0).await; + } - todo!() + Ok(true) } diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 5cdbc0b39f..1010ab00d5 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -25,7 +25,7 @@ use common::executor::SpawnFuture; use common::{log, Future01CompatExt}; use derive_more::Display; use futures::{channel::oneshot, StreamExt}; -use instant::Instant; +use instant::{Duration, Instant}; use keys::KeyPair; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; @@ -37,7 +37,9 @@ use mm2_metrics::{mm_label, mm_timing}; use mm2_net::p2p::P2PContext; use serde::de; use std::net::ToSocketAddrs; +use std::str::FromStr; +use crate::lp_healthcheck::{peer_healthcheck_topic, HealthCheckMessage}; use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; pub type P2PRequestResult = Result>; @@ -216,10 +218,23 @@ async fn process_p2p_message( } }, Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { - let sender_peer_id = PeerId::from_bytes(&message.data).expect("TODO"); - println!("0000000000 COMING FROM {:?}", sender_peer_id.to_string()); - - todo!() + let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); + + let data = HealthCheckMessage::decode(&message.data).expect("!!TODO"); + assert!(data.is_received_message_valid(p2p_ctx.peer_id()), "must ve valid, TODO"); + + if data.should_reply() { + // reply + let target_peer_id = PeerId::from_str(data.sender_peer()).expect("!!!! TODO"); + let topic = peer_healthcheck_topic(&target_peer_id); + let msg = HealthCheckMessage::generate_message(&ctx, target_peer_id, true, 10).unwrap(); + broadcast_p2p_msg(&ctx, topic, msg.encode().unwrap(), None); + } else { + // save it to healthcheck book + let mut book = ctx.healthcheck_book.lock().await; + book.clear_expired_entries(); + book.insert(data.sender_peer().to_owned(), (), Duration::from_secs(5)); + } }, None | Some(_) => (), } From 90e5599eafc7bc6445f8e9113df424ac8a637def Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 28 Aug 2024 11:31:58 +0300 Subject: [PATCH 08/37] improve the performance and logging Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 3 +- mm2src/mm2_main/src/lp_healthcheck.rs | 44 +++++++++++---------------- mm2src/mm2_main/src/lp_network.rs | 15 ++++++--- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 6a62569e56..6135d1fb32 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -5,6 +5,7 @@ use common::{cfg_native, cfg_wasm32, small_rng}; use common::{executor::{abortable_queue::{AbortableQueue, WeakSpawner}, graceful_shutdown, AbortSettings, AbortableSystem, SpawnAbortable, SpawnFuture}, expirable_map::ExpirableMap}; +use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; use gstuff::{try_s, Constructible, ERR, ERRL}; use lazy_static::lazy_static; @@ -143,7 +144,7 @@ pub struct MmCtx { /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] pub async_sqlite_connection: Constructible>>, - pub healthcheck_book: AsyncMutex>, + pub healthcheck_book: AsyncMutex>>, } impl MmCtx { diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 37959d9d6f..a7987ad991 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,7 +1,9 @@ +use async_std::prelude::FutureExt; use chrono::Utc; -use common::{executor::Timer, HttpStatusCode}; -use crypto::CryptoCtx; +use common::HttpStatusCode; use derive_more::Display; +use futures::channel::oneshot::{self, Receiver, Sender}; +use instant::Duration; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerId, SigningError, TopicPrefix}; @@ -107,12 +109,22 @@ impl HttpStatusCode for QueryError { } pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> Result> { - let target_peer_id = PeerId::from_str(&req.peer_id).unwrap(); - let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - let my_peer_id = p2p_ctx.peer_id(); + /// When things go awry, we want records to clear themselves to keep the memory clean of unused data. + /// This is unrelated to the timeout logic. + const ADDRESS_RECORD_EXPIRATION: Duration = Duration::from_secs(60); + + const RESULT_CHANNEL_TIMEOUT: Duration = Duration::from_secs(10); + let target_peer_id = PeerId::from_str(&req.peer_id).unwrap(); let msg = HealthCheckMessage::generate_message(&ctx, target_peer_id, false, 10).unwrap(); + let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); + { + let mut book = ctx.healthcheck_book.lock().await; + book.clear_expired_entries(); + book.insert(target_peer_id.to_string(), tx, ADDRESS_RECORD_EXPIRATION); + } + broadcast_p2p_msg( &ctx, peer_healthcheck_topic(&target_peer_id), @@ -120,25 +132,5 @@ pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> None, ); - const MAX_ATTEMPTS: usize = 5; - let mut attempts = 0; - loop { - if attempts > MAX_ATTEMPTS { - // timeout - return Ok(false); - } - - { - let mut book = ctx.healthcheck_book.lock().await; - book.clear_expired_entries(); - if book.remove(&target_peer_id.to_string()).is_some() { - break; - } - } - - attempts += 1; - Timer::sleep(1.0).await; - } - - Ok(true) + Ok(rx.timeout(RESULT_CHANNEL_TIMEOUT).await == Ok(Ok(()))) } diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 1010ab00d5..5afdd9403b 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -25,7 +25,7 @@ use common::executor::SpawnFuture; use common::{log, Future01CompatExt}; use derive_more::Display; use futures::{channel::oneshot, StreamExt}; -use instant::{Duration, Instant}; +use instant::Instant; use keys::KeyPair; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; @@ -224,16 +224,21 @@ async fn process_p2p_message( assert!(data.is_received_message_valid(p2p_ctx.peer_id()), "must ve valid, TODO"); if data.should_reply() { - // reply + // Reply the message so they know we are healthy. let target_peer_id = PeerId::from_str(data.sender_peer()).expect("!!!! TODO"); let topic = peer_healthcheck_topic(&target_peer_id); let msg = HealthCheckMessage::generate_message(&ctx, target_peer_id, true, 10).unwrap(); broadcast_p2p_msg(&ctx, topic, msg.encode().unwrap(), None); } else { - // save it to healthcheck book + // The requested peer is healthy; signal the result channel. let mut book = ctx.healthcheck_book.lock().await; - book.clear_expired_entries(); - book.insert(data.sender_peer().to_owned(), (), Duration::from_secs(5)); + if let Some(tx) = book.remove(&data.sender_peer().to_owned()) { + if tx.send(()).is_err() { + log::error!("Result channel isn't present for peer '{}'.", data.sender_peer()); + }; + } else { + log::info!("Peer '{}' isn't recorded in the healthcheck book.", data.sender_peer()); + }; } }, None | Some(_) => (), From ecf9ceac2b7bac8fc0c4ce182328e7d54ea6d5e0 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 28 Aug 2024 11:34:29 +0300 Subject: [PATCH 09/37] remove redundant crates Signed-off-by: onur-ozkan --- Cargo.lock | 2 -- mm2src/mm2_net/Cargo.toml | 2 -- 2 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e49900b7c3..681184de65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4707,8 +4707,6 @@ dependencies = [ "prost", "rand 0.7.3", "rustls 0.21.10", - "ser_error", - "ser_error_derive", "serde", "serde_json", "thiserror", diff --git a/mm2src/mm2_net/Cargo.toml b/mm2src/mm2_net/Cargo.toml index 3ed5018818..d92e0c08b6 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -29,8 +29,6 @@ mm2-libp2p = { path = "../mm2_p2p", package = "mm2_p2p", optional = true } parking_lot = { version = "0.12.0", features = ["nightly"], optional = true } prost = "0.11" rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -ser_error = { path = "../derives/ser_error" } -ser_error_derive = { path = "../derives/ser_error_derive" } serde = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } thiserror = "1.0.30" From 1d9d87489632fa4121530534ac8ea30b55e345f0 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 28 Aug 2024 11:44:20 +0300 Subject: [PATCH 10/37] add debug logs for invalid messages Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 16 +++++++--------- mm2src/mm2_main/src/lp_network.rs | 13 +++++++++---- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index a7987ad991..ba49e1a181 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -14,16 +14,16 @@ use std::str::FromStr; use crate::lp_network::broadcast_p2p_msg; -pub const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; +pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; -#[derive(Deserialize, Serialize)] -pub(crate) struct HealthCheckMessage { +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct HealthcheckMessage { signature: Vec, data: HealthCheckData, } -impl HealthCheckMessage { - pub fn generate_message( +impl HealthcheckMessage { + pub(crate) fn generate_message( ctx: &MmArc, target_peer: PeerId, is_a_reply: bool, @@ -73,11 +73,9 @@ impl HealthCheckMessage { pub(crate) fn should_reply(&self) -> bool { !self.data.is_a_reply } pub(crate) fn sender_peer(&self) -> &str { &self.data.sender_peer } - - pub(crate) fn target_peer(&self) -> &str { &self.data.target_peer } } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] struct HealthCheckData { sender_peer: String, sender_public_key: Vec, @@ -116,7 +114,7 @@ pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> const RESULT_CHANNEL_TIMEOUT: Duration = Duration::from_secs(10); let target_peer_id = PeerId::from_str(&req.peer_id).unwrap(); - let msg = HealthCheckMessage::generate_message(&ctx, target_peer_id, false, 10).unwrap(); + let msg = HealthcheckMessage::generate_message(&ctx, target_peer_id, false, 10).unwrap(); let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); { diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 5afdd9403b..d6813de075 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -39,7 +39,7 @@ use serde::de; use std::net::ToSocketAddrs; use std::str::FromStr; -use crate::lp_healthcheck::{peer_healthcheck_topic, HealthCheckMessage}; +use crate::lp_healthcheck::{peer_healthcheck_topic, HealthcheckMessage}; use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; pub type P2PRequestResult = Result>; @@ -220,14 +220,19 @@ async fn process_p2p_message( Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - let data = HealthCheckMessage::decode(&message.data).expect("!!TODO"); - assert!(data.is_received_message_valid(p2p_ctx.peer_id()), "must ve valid, TODO"); + let data = HealthcheckMessage::decode(&message.data).expect("!!TODO"); + + if !data.is_received_message_valid(p2p_ctx.peer_id()) { + log::error!("Received an invalid healthcheck message."); + log::debug!("Message context: {:?}", data); + return; + }; if data.should_reply() { // Reply the message so they know we are healthy. let target_peer_id = PeerId::from_str(data.sender_peer()).expect("!!!! TODO"); let topic = peer_healthcheck_topic(&target_peer_id); - let msg = HealthCheckMessage::generate_message(&ctx, target_peer_id, true, 10).unwrap(); + let msg = HealthcheckMessage::generate_message(&ctx, target_peer_id, true, 10).unwrap(); broadcast_p2p_msg(&ctx, topic, msg.encode().unwrap(), None); } else { // The requested peer is healthy; signal the result channel. From 493585aff41645249ec957656dc70a33debc54c3 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 28 Aug 2024 13:09:48 +0300 Subject: [PATCH 11/37] improve response status, error logs and types Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 91 ++++++++++++++++++++------- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index ba49e1a181..68f7ad17c4 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,12 +1,12 @@ use async_std::prelude::FutureExt; use chrono::Utc; -use common::HttpStatusCode; +use common::{log, HttpStatusCode, StatusCode}; use derive_more::Display; use futures::channel::oneshot::{self, Receiver, Sender}; use instant::Duration; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; -use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerId, SigningError, TopicPrefix}; +use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerId, TopicPrefix}; use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; @@ -19,7 +19,7 @@ pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; #[derive(Debug, Deserialize, Serialize)] pub(crate) struct HealthcheckMessage { signature: Vec, - data: HealthCheckData, + data: HealthcheckData, } impl HealthcheckMessage { @@ -28,14 +28,14 @@ impl HealthcheckMessage { target_peer: PeerId, is_a_reply: bool, expires_in_seconds: i64, - ) -> Result { + ) -> Result { let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); let sender_peer = p2p_ctx.peer_id().to_string(); let keypair = p2p_ctx.keypair(); let sender_public_key = keypair.public().encode_protobuf(); let target_peer = target_peer.to_string(); - let data = HealthCheckData { + let data = HealthcheckData { sender_peer, sender_public_key, target_peer, @@ -43,27 +43,54 @@ impl HealthcheckMessage { is_a_reply, }; - let signature = keypair.sign(&data.encode().unwrap())?; + let signature = try_s!(keypair.sign(&try_s!(data.encode()))); Ok(Self { signature, data }) } pub(crate) fn is_received_message_valid(&self, my_peer_id: PeerId) -> bool { - if Utc::now().timestamp() > self.data.expires_at { + let now = Utc::now().timestamp(); + if now > self.data.expires_at { + log::debug!( + "Healthcheck message is expired. Current time in UTC: {now}, healthcheck `expires_at` in UTC: {}", + self.data.expires_at + ); return false; } if self.data.target_peer != my_peer_id.to_string() { + log::debug!( + "`target_peer` doesn't match with our peer address. Our address: '{}', healthcheck `target_peer`: '{}'.", + my_peer_id, + self.data.target_peer + ); return false; } - let Ok(public_key) = Libp2pPublic::try_decode_protobuf(&self.data.sender_public_key) else { return false }; + let Ok(public_key) = Libp2pPublic::try_decode_protobuf(&self.data.sender_public_key) else { + log::debug!("Couldn't decode public key from the healthcheck message."); + + return false + }; if self.data.sender_peer != public_key.to_peer_id().to_string() { + log::debug!("`sender_peer` and `sender_public_key` doesn't belong each other."); + return false; } - public_key.verify(&self.data.encode().unwrap(), &self.signature) + let Ok(encoded_message) = self.data.encode() else { + log::debug!("Couldn't encode healthcheck data."); + return false + }; + + let res = public_key.verify(&encoded_message, &self.signature); + + if !res { + log::debug!("Healthcheck isn't signed correctly."); + } + + res } pub(crate) fn encode(&self) -> Result, rmp_serde::encode::Error> { encode_message(self) } @@ -76,7 +103,7 @@ impl HealthcheckMessage { } #[derive(Debug, Deserialize, Serialize)] -struct HealthCheckData { +struct HealthcheckData { sender_peer: String, sender_public_key: Vec, target_peer: String, @@ -84,7 +111,7 @@ struct HealthCheckData { is_a_reply: bool, } -impl HealthCheckData { +impl HealthcheckData { fn encode(&self) -> Result, rmp_serde::encode::Error> { encode_message(self) } } @@ -100,21 +127,44 @@ pub struct RequestPayload { #[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] -pub enum QueryError {} +pub enum HealthcheckRpcError { + InvalidPeerAddress { reason: String }, + MessageGenerationFailed { reason: String }, + MessageEncodingFailed { reason: String }, +} -impl HttpStatusCode for QueryError { - fn status_code(&self) -> common::StatusCode { todo!() } +impl HttpStatusCode for HealthcheckRpcError { + fn status_code(&self) -> common::StatusCode { + match self { + HealthcheckRpcError::InvalidPeerAddress { .. } => StatusCode::BAD_REQUEST, + HealthcheckRpcError::MessageGenerationFailed { .. } | HealthcheckRpcError::MessageEncodingFailed { .. } => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } } -pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> Result> { +pub async fn peer_connection_healthcheck_rpc( + ctx: MmArc, + req: RequestPayload, +) -> Result> { /// When things go awry, we want records to clear themselves to keep the memory clean of unused data. /// This is unrelated to the timeout logic. const ADDRESS_RECORD_EXPIRATION: Duration = Duration::from_secs(60); const RESULT_CHANNEL_TIMEOUT: Duration = Duration::from_secs(10); - let target_peer_id = PeerId::from_str(&req.peer_id).unwrap(); - let msg = HealthcheckMessage::generate_message(&ctx, target_peer_id, false, 10).unwrap(); + const HEALTHCHECK_MESSAGE_EXPIRATION: i64 = 10; + + let target_peer_id = PeerId::from_str(&req.peer_id) + .map_err(|e| HealthcheckRpcError::InvalidPeerAddress { reason: e.to_string() })?; + + let message = HealthcheckMessage::generate_message(&ctx, target_peer_id, false, HEALTHCHECK_MESSAGE_EXPIRATION) + .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; + + let encoded_message = message + .encode() + .map_err(|e| HealthcheckRpcError::MessageEncodingFailed { reason: e.to_string() })?; let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); { @@ -123,12 +173,7 @@ pub async fn peer_connection_healthcheck_rpc(ctx: MmArc, req: RequestPayload) -> book.insert(target_peer_id.to_string(), tx, ADDRESS_RECORD_EXPIRATION); } - broadcast_p2p_msg( - &ctx, - peer_healthcheck_topic(&target_peer_id), - msg.encode().unwrap(), - None, - ); + broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); Ok(rx.timeout(RESULT_CHANNEL_TIMEOUT).await == Ok(Ok(()))) } From f5c98dabc227b18f7f3cd48f587d698d93e110db Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 28 Aug 2024 13:13:31 +0300 Subject: [PATCH 12/37] inline various functions Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 68f7ad17c4..cb555ee616 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -93,12 +93,16 @@ impl HealthcheckMessage { res } + #[inline] pub(crate) fn encode(&self) -> Result, rmp_serde::encode::Error> { encode_message(self) } + #[inline] pub(crate) fn decode(bytes: &[u8]) -> Result { decode_message(bytes) } + #[inline] pub(crate) fn should_reply(&self) -> bool { !self.data.is_a_reply } + #[inline] pub(crate) fn sender_peer(&self) -> &str { &self.data.sender_peer } } @@ -112,6 +116,7 @@ struct HealthcheckData { } impl HealthcheckData { + #[inline] fn encode(&self) -> Result, rmp_serde::encode::Error> { encode_message(self) } } From 0aaa0119a6d3dd7d58493e70b81e7877aa5473fa Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 28 Aug 2024 14:27:24 +0300 Subject: [PATCH 13/37] prevent brute-force attacks Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 8 ++++++-- mm2src/mm2_main/src/lp_healthcheck.rs | 14 +++++++++----- mm2src/mm2_main/src/lp_network.rs | 28 +++++++++++++++++++-------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 6135d1fb32..ef868ebb54 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -144,7 +144,10 @@ pub struct MmCtx { /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] pub async_sqlite_connection: Constructible>>, - pub healthcheck_book: AsyncMutex>>, + /// Links the RPC context to the P2P context to handle health check responses. + pub healthcheck_response_handler: AsyncMutex>>, + /// This is used to record healthcheck sender peers in an expirable manner to prevent brute-force attacks. + pub healthcheck_bruteforce_shield: AsyncMutex>, } impl MmCtx { @@ -194,7 +197,8 @@ impl MmCtx { nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: Constructible::default(), - healthcheck_book: AsyncMutex::new(ExpirableMap::default()), + healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), + healthcheck_bruteforce_shield: AsyncMutex::new(ExpirableMap::default()), } } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index cb555ee616..82d26c19ec 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -16,6 +16,10 @@ use crate::lp_network::broadcast_p2p_msg; pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; +/// The default duration required to wait before sending another healthcheck +/// request to the same peer. +pub(crate) const HEALTHCHECK_BLOCKING_DURATION: Duration = Duration::from_millis(750); + #[derive(Debug, Deserialize, Serialize)] pub(crate) struct HealthcheckMessage { signature: Vec, @@ -172,11 +176,11 @@ pub async fn peer_connection_healthcheck_rpc( .map_err(|e| HealthcheckRpcError::MessageEncodingFailed { reason: e.to_string() })?; let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); - { - let mut book = ctx.healthcheck_book.lock().await; - book.clear_expired_entries(); - book.insert(target_peer_id.to_string(), tx, ADDRESS_RECORD_EXPIRATION); - } + + let mut book = ctx.healthcheck_response_handler.lock().await; + book.clear_expired_entries(); + book.insert(target_peer_id.to_string(), tx, ADDRESS_RECORD_EXPIRATION); + drop(book); broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index d6813de075..de3ad72fe7 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -39,7 +39,7 @@ use serde::de; use std::net::ToSocketAddrs; use std::str::FromStr; -use crate::lp_healthcheck::{peer_healthcheck_topic, HealthcheckMessage}; +use crate::lp_healthcheck::{peer_healthcheck_topic, HealthcheckMessage, HEALTHCHECK_BLOCKING_DURATION}; use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; pub type P2PRequestResult = Result>; @@ -219,9 +219,21 @@ async fn process_p2p_message( }, Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - let data = HealthcheckMessage::decode(&message.data).expect("!!TODO"); + let sender_peer = data.sender_peer().to_owned(); + + let mut bruteforce_shield = ctx.healthcheck_bruteforce_shield.lock().await; + bruteforce_shield.clear_expired_entries(); + if bruteforce_shield + .insert(sender_peer.clone(), (), HEALTHCHECK_BLOCKING_DURATION) + .is_some() + { + log::warn!("Peer '{sender_peer}' exceeded the healthcheck blocking time, skipping their message."); + return; + } + drop(bruteforce_shield); + if !data.is_received_message_valid(p2p_ctx.peer_id()) { log::error!("Received an invalid healthcheck message."); log::debug!("Message context: {:?}", data); @@ -230,19 +242,19 @@ async fn process_p2p_message( if data.should_reply() { // Reply the message so they know we are healthy. - let target_peer_id = PeerId::from_str(data.sender_peer()).expect("!!!! TODO"); + let target_peer_id = PeerId::from_str(&sender_peer).expect("!!!! TODO"); let topic = peer_healthcheck_topic(&target_peer_id); let msg = HealthcheckMessage::generate_message(&ctx, target_peer_id, true, 10).unwrap(); broadcast_p2p_msg(&ctx, topic, msg.encode().unwrap(), None); } else { - // The requested peer is healthy; signal the result channel. - let mut book = ctx.healthcheck_book.lock().await; - if let Some(tx) = book.remove(&data.sender_peer().to_owned()) { + // The requested peer is healthy; signal the response channel. + let mut book = ctx.healthcheck_response_handler.lock().await; + if let Some(tx) = book.remove(&sender_peer) { if tx.send(()).is_err() { - log::error!("Result channel isn't present for peer '{}'.", data.sender_peer()); + log::error!("Result channel isn't present for peer '{sender_peer}'."); }; } else { - log::info!("Peer '{}' isn't recorded in the healthcheck book.", data.sender_peer()); + log::info!("Peer '{sender_peer}' isn't recorded in the healthcheck book."); }; } }, From b73a0f23821e5df517d8b4fed7f9a6346f5b76d6 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 2 Sep 2024 12:04:15 +0300 Subject: [PATCH 14/37] handle panics Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_network.rs | 42 +++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index de3ad72fe7..aa7287eddc 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -218,8 +218,23 @@ async fn process_p2p_message( } }, Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { + macro_rules! try_or_return { + ($exp:expr, $msg: expr) => { + match $exp { + Ok(t) => t, + Err(e) => { + log::error!("{}, e: {e:?}", $msg); + return; + }, + } + }; + } + let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - let data = HealthcheckMessage::decode(&message.data).expect("!!TODO"); + let data = try_or_return!( + HealthcheckMessage::decode(&message.data), + "Couldn't decode healthcheck message" + ); let sender_peer = data.sender_peer().to_owned(); @@ -242,19 +257,32 @@ async fn process_p2p_message( if data.should_reply() { // Reply the message so they know we are healthy. - let target_peer_id = PeerId::from_str(&sender_peer).expect("!!!! TODO"); + let target_peer_id = try_or_return!( + PeerId::from_str(&sender_peer), + format!("'{sender_peer}' is not a valid address") + ); let topic = peer_healthcheck_topic(&target_peer_id); - let msg = HealthcheckMessage::generate_message(&ctx, target_peer_id, true, 10).unwrap(); - broadcast_p2p_msg(&ctx, topic, msg.encode().unwrap(), None); + + let msg = try_or_return!( + HealthcheckMessage::generate_message(&ctx, target_peer_id, true, 10), + "Couldn't generate the healthcheck message, this is very unusual!" + ); + + let encoded_msg = try_or_return!( + msg.encode(), + "Couldn't encode healthcheck message, this is very unusual!" + ); + + broadcast_p2p_msg(&ctx, topic, encoded_msg, None); } else { // The requested peer is healthy; signal the response channel. - let mut book = ctx.healthcheck_response_handler.lock().await; - if let Some(tx) = book.remove(&sender_peer) { + let mut response_handler = ctx.healthcheck_response_handler.lock().await; + if let Some(tx) = response_handler.remove(&sender_peer) { if tx.send(()).is_err() { log::error!("Result channel isn't present for peer '{sender_peer}'."); }; } else { - log::info!("Peer '{sender_peer}' isn't recorded in the healthcheck book."); + log::info!("Peer '{sender_peer}' isn't recorded in the healthcheck response handler."); }; } }, From e6f29c7d66b090836fe5966caff210fc0c012bc1 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 2 Sep 2024 13:07:21 +0300 Subject: [PATCH 15/37] add unit test coverage for healthcheck implementation Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 81 ++++++++++++++++++++++++- mm2src/mm2_main/src/ordermatch_tests.rs | 2 +- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 82d26c19ec..59790fe0f8 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -21,6 +21,7 @@ pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; pub(crate) const HEALTHCHECK_BLOCKING_DURATION: Duration = Duration::from_millis(750); #[derive(Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] pub(crate) struct HealthcheckMessage { signature: Vec, data: HealthcheckData, @@ -111,6 +112,7 @@ impl HealthcheckMessage { } #[derive(Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(PartialEq))] struct HealthcheckData { sender_peer: String, sender_public_key: Vec, @@ -129,12 +131,12 @@ pub fn peer_healthcheck_topic(peer_id: &PeerId) -> String { pub_sub_topic(PEER_HEALTHCHECK_PREFIX, &peer_id.to_string()) } -#[derive(Clone, Deserialize)] +#[derive(Deserialize)] pub struct RequestPayload { peer_id: String, } -#[derive(Clone, Debug, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum HealthcheckRpcError { InvalidPeerAddress { reason: String }, @@ -186,3 +188,78 @@ pub async fn peer_connection_healthcheck_rpc( Ok(rx.timeout(RESULT_CHANNEL_TIMEOUT).await == Ok(Ok(()))) } + +#[cfg(test)] +mod tests { + use mm2_test_helpers::for_tests::mm_ctx_with_iguana; + + use crate::lp_ordermatch::ordermatch_tests::init_p2p_context; + + use super::*; + + fn create_test_peer_id() -> PeerId { + let keypair = mm2_libp2p::Keypair::generate_ed25519(); + PeerId::from(keypair.public()) + } + + fn ctx() -> MmArc { + let ctx = mm_ctx_with_iguana(Some("dummy-value")); + init_p2p_context(&ctx); + ctx + } + + #[test] + fn test_valid_message() { + let ctx = ctx(); + let target_peer = create_test_peer_id(); + let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); + assert!(message.is_received_message_valid(target_peer)); + } + + #[test] + fn test_corrupted_messages() { + let ctx = ctx(); + let target_peer = create_test_peer_id(); + + let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); + message.data.expires_at += 1; + assert!(!message.is_received_message_valid(target_peer)); + + let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); + message.data.is_a_reply = !message.data.is_a_reply; + assert!(!message.is_received_message_valid(target_peer)); + + let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); + message.data.sender_peer += "0"; + assert!(!message.is_received_message_valid(target_peer)); + + let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); + message.data.target_peer += "0"; + assert!(!message.is_received_message_valid(target_peer)); + + let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); + assert!(!message.is_received_message_valid(PeerId::from_str(&message.data.sender_peer).unwrap())); + } + + #[test] + fn test_expired_message() { + let ctx = ctx(); + let target_peer = create_test_peer_id(); + let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, -1).unwrap(); + assert!(!message.is_received_message_valid(target_peer)); + } + + #[test] + fn test_encode_decode() { + let ctx = ctx(); + let target_peer = create_test_peer_id(); + + let original = HealthcheckMessage::generate_message(&ctx, target_peer, false, 10).unwrap(); + + let encoded = original.encode().unwrap(); + assert!(!encoded.is_empty()); + + let decoded = HealthcheckMessage::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } +} diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 38d17af4ed..0f5d64aad7 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -1677,7 +1677,7 @@ fn pubkey_and_secret_for_test(passphrase: &str) -> (String, [u8; 32]) { (pubkey, secret) } -fn init_p2p_context(ctx: &MmArc) -> (mpsc::Sender, mpsc::Receiver) { +pub(crate) fn init_p2p_context(ctx: &MmArc) -> (mpsc::Sender, mpsc::Receiver) { let (cmd_tx, cmd_rx) = mpsc::channel(10); let p2p_key = { From 8a20b8c9b5514356c92d45a15979faead82d9e32 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 2 Sep 2024 13:19:37 +0300 Subject: [PATCH 16/37] extend healthcheck coverage to WASM Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 49 ++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 59790fe0f8..3819e0c888 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -21,7 +21,7 @@ pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; pub(crate) const HEALTHCHECK_BLOCKING_DURATION: Duration = Duration::from_millis(750); #[derive(Debug, Deserialize, Serialize)] -#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] pub(crate) struct HealthcheckMessage { signature: Vec, data: HealthcheckData, @@ -112,7 +112,7 @@ impl HealthcheckMessage { } #[derive(Debug, Deserialize, Serialize)] -#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] struct HealthcheckData { sender_peer: String, sender_public_key: Vec, @@ -189,13 +189,18 @@ pub async fn peer_connection_healthcheck_rpc( Ok(rx.timeout(RESULT_CHANNEL_TIMEOUT).await == Ok(Ok(()))) } -#[cfg(test)] +#[cfg(any(test, target_arch = "wasm32"))] mod tests { + use super::*; + use common::cross_test; + use crypto::CryptoCtx; + use mm2_libp2p::behaviours::atomicdex::generate_ed25519_keypair; use mm2_test_helpers::for_tests::mm_ctx_with_iguana; - use crate::lp_ordermatch::ordermatch_tests::init_p2p_context; - - use super::*; + common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } fn create_test_peer_id() -> PeerId { let keypair = mm2_libp2p::Keypair::generate_ed25519(); @@ -204,20 +209,28 @@ mod tests { fn ctx() -> MmArc { let ctx = mm_ctx_with_iguana(Some("dummy-value")); - init_p2p_context(&ctx); + let p2p_key = { + let crypto_ctx = CryptoCtx::from_ctx(&ctx).unwrap(); + let key = bitcrypto::sha256(crypto_ctx.mm2_internal_privkey_slice()); + key.take() + }; + + let (cmd_tx, _) = futures::channel::mpsc::channel(0); + + let p2p_context = P2PContext::new(cmd_tx, generate_ed25519_keypair(p2p_key)); + p2p_context.store_to_mm_arc(&ctx); + ctx } - #[test] - fn test_valid_message() { + cross_test!(test_valid_message, { let ctx = ctx(); let target_peer = create_test_peer_id(); let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); assert!(message.is_received_message_valid(target_peer)); - } + }); - #[test] - fn test_corrupted_messages() { + cross_test!(test_corrupted_messages, { let ctx = ctx(); let target_peer = create_test_peer_id(); @@ -239,18 +252,16 @@ mod tests { let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); assert!(!message.is_received_message_valid(PeerId::from_str(&message.data.sender_peer).unwrap())); - } + }); - #[test] - fn test_expired_message() { + cross_test!(test_expired_message, { let ctx = ctx(); let target_peer = create_test_peer_id(); let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, -1).unwrap(); assert!(!message.is_received_message_valid(target_peer)); - } + }); - #[test] - fn test_encode_decode() { + cross_test!(test_encode_decode, { let ctx = ctx(); let target_peer = create_test_peer_id(); @@ -261,5 +272,5 @@ mod tests { let decoded = HealthcheckMessage::decode(&encoded).unwrap(); assert_eq!(original, decoded); - } + }); } From f550550d9ce4a095c2e211f9f7d1c619cd748638 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 2 Sep 2024 14:12:18 +0300 Subject: [PATCH 17/37] add integration test coverage for `peer_connection_healthcheck` RPC Signed-off-by: onur-ozkan --- .../tests/mm2_tests/mm2_tests_inner.rs | 32 ++++++++++++++++++- mm2src/mm2_test_helpers/src/for_tests.rs | 24 ++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 137eb6ef09..68c6bebfd9 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -35,7 +35,7 @@ use uuid::Uuid; cfg_native! { use common::block_on; - use mm2_test_helpers::for_tests::{get_passphrase, new_mm2_temp_folder_path}; + use mm2_test_helpers::for_tests::{get_passphrase, new_mm2_temp_folder_path, peer_connection_healthcheck}; use mm2_io::fs::slurp; use hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN; } @@ -5919,6 +5919,36 @@ fn test_sign_raw_transaction_p2wpkh() { assert!(response["error"].as_str().unwrap().contains("Signing error")); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_connection_healthcheck_rpc() { + const BOB_ADDRESS: &str = "12D3KooWEtuv7kmgGCC7oAQ31hB7AR5KkhT3eEWB2bP2roo3M7rY"; + const BOB_SEED: &str = "dummy-value-bob"; + + const ALICE_ADDRESS: &str = "12D3KooWHnoKd2Lr7BoxHCCeBhcnfAZsdiCdojbEMLE7DDSbMo1g"; + const ALICE_SEED: &str = "dummy-value-alice"; + + let bob_conf = Mm2TestConf::seednode(BOB_SEED, &json!([])); + let bob_mm = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + + thread::sleep(Duration::from_secs(2)); + + let mut alice_conf = Mm2TestConf::seednode(ALICE_SEED, &json!([])); + alice_conf.conf["seednodes"] = json!([bob_mm.my_seed_addr()]); + alice_conf.conf["skip_startup_checks"] = json!(true); + let alice_mm = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + + thread::sleep(Duration::from_secs(2)); + + let response = block_on(peer_connection_healthcheck(&bob_mm, ALICE_ADDRESS)); + assert_eq!(response["result"], json!(true)); + + thread::sleep(Duration::from_secs(1)); + + let response = block_on(peer_connection_healthcheck(&alice_mm, BOB_ADDRESS)); + assert_eq!(response["result"], json!(true)); +} + #[cfg(all(feature = "run-device-tests", not(target_arch = "wasm32")))] mod trezor_tests { use coins::eth::{eth_coin_from_conf_and_request, gas_limit, EthCoin}; diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index e650ef4293..8b98c5e399 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -1846,6 +1846,30 @@ pub async fn enable_qrc20( json::from_str(&electrum.1).unwrap() } +pub async fn peer_connection_healthcheck(mm: &MarketMakerIt, peer_id: &str) -> Json { + let response = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "peer_connection_healthcheck", + "mmrpc": "2.0", + "params": { + "peer_id": peer_id + } + })) + .await + .unwrap(); + + assert_eq!( + response.0, + StatusCode::OK, + "RPC «peer_connection_healthcheck» failed with {} {}", + response.0, + response.1 + ); + + json::from_str(&response.1).unwrap() +} + /// Reads passphrase and userpass from .env file pub fn from_env_file(env: Vec) -> (Option, Option) { use regex::bytes::Regex; From 9774f15f5734d53fbe4170c50998cac102f0ed85 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 2 Sep 2024 14:44:57 +0300 Subject: [PATCH 18/37] add global configuration interface for healthchecks Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 44 +++++++++++++++++++++++++++ mm2src/mm2_main/src/lp_healthcheck.rs | 16 +++------- mm2src/mm2_main/src/lp_network.rs | 10 ++++-- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index ef868ebb54..9652727e34 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -13,6 +13,7 @@ use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; use mm2_metrics::{MetricsArc, MetricsOps}; use primitives::hash::H160; use rand::Rng; +use serde::Deserialize; use serde_json::{self as json, Value as Json}; use shared_ref_counter::{SharedRc, WeakRc}; use std::any::Any; @@ -44,6 +45,39 @@ cfg_native! { /// Default interval to export and record metrics to log. const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; +mod healthcheck_defaults { + /// The default duration required to wait before sending another healthcheck + /// request to the same peer. + pub(crate) const fn default_healthcheck_blocking_ms() -> u64 { 750 } + + pub(crate) const fn default_healthcheck_message_expiration() -> i64 { 10 } + + pub(crate) const fn default_result_channel_timeout() -> u64 { 10 } +} + +#[derive(Debug, Deserialize)] +pub struct HealthcheckConfig { + /// TODO + #[serde(default = "healthcheck_defaults::default_healthcheck_blocking_ms")] + pub blocking_ms_for_per_address: u64, + /// TODO + #[serde(default = "healthcheck_defaults::default_healthcheck_message_expiration")] + pub message_expiration: i64, + /// TODO + #[serde(default = "healthcheck_defaults::default_result_channel_timeout")] + pub timeout: u64, +} + +impl Default for HealthcheckConfig { + fn default() -> Self { + Self { + blocking_ms_for_per_address: healthcheck_defaults::default_healthcheck_blocking_ms(), + message_expiration: healthcheck_defaults::default_healthcheck_message_expiration(), + timeout: healthcheck_defaults::default_result_channel_timeout(), + } + } +} + /// MarketMaker state, shared between the various MarketMaker threads. /// /// Every MarketMaker has one and only one instance of `MmCtx`. @@ -148,6 +182,8 @@ pub struct MmCtx { pub healthcheck_response_handler: AsyncMutex>>, /// This is used to record healthcheck sender peers in an expirable manner to prevent brute-force attacks. pub healthcheck_bruteforce_shield: AsyncMutex>, + /// TODO + pub healthcheck_config: HealthcheckConfig, } impl MmCtx { @@ -199,6 +235,7 @@ impl MmCtx { async_sqlite_connection: Constructible::default(), healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), healthcheck_bruteforce_shield: AsyncMutex::new(ExpirableMap::default()), + healthcheck_config: HealthcheckConfig::default(), } } @@ -768,6 +805,13 @@ impl MmCtxBuilder { .expect("Invalid json value in 'event_stream_configuration'."); ctx.event_stream_configuration = Some(event_stream_configuration); } + + let healthcheck_config = &ctx.conf["healthcheck_config"]; + if !healthcheck_config.is_null() { + let healthcheck_config: HealthcheckConfig = + json::from_value(healthcheck_config.clone()).expect("Invalid json value in 'healthcheck_config'."); + ctx.healthcheck_config = healthcheck_config; + } } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 3819e0c888..3c220b4cdd 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -16,10 +16,6 @@ use crate::lp_network::broadcast_p2p_msg; pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; -/// The default duration required to wait before sending another healthcheck -/// request to the same peer. -pub(crate) const HEALTHCHECK_BLOCKING_DURATION: Duration = Duration::from_millis(750); - #[derive(Debug, Deserialize, Serialize)] #[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] pub(crate) struct HealthcheckMessage { @@ -163,15 +159,12 @@ pub async fn peer_connection_healthcheck_rpc( /// This is unrelated to the timeout logic. const ADDRESS_RECORD_EXPIRATION: Duration = Duration::from_secs(60); - const RESULT_CHANNEL_TIMEOUT: Duration = Duration::from_secs(10); - - const HEALTHCHECK_MESSAGE_EXPIRATION: i64 = 10; - let target_peer_id = PeerId::from_str(&req.peer_id) .map_err(|e| HealthcheckRpcError::InvalidPeerAddress { reason: e.to_string() })?; - let message = HealthcheckMessage::generate_message(&ctx, target_peer_id, false, HEALTHCHECK_MESSAGE_EXPIRATION) - .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; + let message = + HealthcheckMessage::generate_message(&ctx, target_peer_id, false, ctx.healthcheck_config.message_expiration) + .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; let encoded_message = message .encode() @@ -186,7 +179,8 @@ pub async fn peer_connection_healthcheck_rpc( broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); - Ok(rx.timeout(RESULT_CHANNEL_TIMEOUT).await == Ok(Ok(()))) + let timeout_duration = Duration::from_millis(ctx.healthcheck_config.timeout); + Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } #[cfg(any(test, target_arch = "wasm32"))] diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index aa7287eddc..ce83a68027 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -25,7 +25,7 @@ use common::executor::SpawnFuture; use common::{log, Future01CompatExt}; use derive_more::Display; use futures::{channel::oneshot, StreamExt}; -use instant::Instant; +use instant::{Duration, Instant}; use keys::KeyPair; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; @@ -39,7 +39,7 @@ use serde::de; use std::net::ToSocketAddrs; use std::str::FromStr; -use crate::lp_healthcheck::{peer_healthcheck_topic, HealthcheckMessage, HEALTHCHECK_BLOCKING_DURATION}; +use crate::lp_healthcheck::{peer_healthcheck_topic, HealthcheckMessage}; use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; pub type P2PRequestResult = Result>; @@ -241,7 +241,11 @@ async fn process_p2p_message( let mut bruteforce_shield = ctx.healthcheck_bruteforce_shield.lock().await; bruteforce_shield.clear_expired_entries(); if bruteforce_shield - .insert(sender_peer.clone(), (), HEALTHCHECK_BLOCKING_DURATION) + .insert( + sender_peer.clone(), + (), + Duration::from_millis(ctx.healthcheck_config.blocking_ms_for_per_address), + ) .is_some() { log::warn!("Peer '{sender_peer}' exceeded the healthcheck blocking time, skipping their message."); From 6286299ef73bd0b7b9a9b514df35824935385c7d Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 3 Sep 2024 07:38:41 +0300 Subject: [PATCH 19/37] add doc-comments Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 19 +++++++++---------- mm2src/mm2_main/src/lp_healthcheck.rs | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 9652727e34..e40e9f85e4 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -46,26 +46,25 @@ cfg_native! { const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; mod healthcheck_defaults { - /// The default duration required to wait before sending another healthcheck - /// request to the same peer. pub(crate) const fn default_healthcheck_blocking_ms() -> u64 { 750 } pub(crate) const fn default_healthcheck_message_expiration() -> i64 { 10 } - pub(crate) const fn default_result_channel_timeout() -> u64 { 10 } + pub(crate) const fn default_timeout_ms() -> u64 { 10 } } #[derive(Debug, Deserialize)] pub struct HealthcheckConfig { - /// TODO + /// Required time (millisecond) to wait before processing another healthcheck request from the same peer. #[serde(default = "healthcheck_defaults::default_healthcheck_blocking_ms")] pub blocking_ms_for_per_address: u64, - /// TODO + /// Lifetime of the message. + /// Do not change this unless you know what you are doing. #[serde(default = "healthcheck_defaults::default_healthcheck_message_expiration")] pub message_expiration: i64, - /// TODO - #[serde(default = "healthcheck_defaults::default_result_channel_timeout")] - pub timeout: u64, + /// Maximum time (milliseconds) to wait for healthcheck response. + #[serde(default = "healthcheck_defaults::default_timeout_ms")] + pub timeout_ms: u64, } impl Default for HealthcheckConfig { @@ -73,7 +72,7 @@ impl Default for HealthcheckConfig { Self { blocking_ms_for_per_address: healthcheck_defaults::default_healthcheck_blocking_ms(), message_expiration: healthcheck_defaults::default_healthcheck_message_expiration(), - timeout: healthcheck_defaults::default_result_channel_timeout(), + timeout_ms: healthcheck_defaults::default_timeout_ms(), } } } @@ -182,7 +181,7 @@ pub struct MmCtx { pub healthcheck_response_handler: AsyncMutex>>, /// This is used to record healthcheck sender peers in an expirable manner to prevent brute-force attacks. pub healthcheck_bruteforce_shield: AsyncMutex>, - /// TODO + /// Global configuration of healthchecks. pub healthcheck_config: HealthcheckConfig, } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 3c220b4cdd..d58b4d3ee1 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -179,7 +179,7 @@ pub async fn peer_connection_healthcheck_rpc( broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); - let timeout_duration = Duration::from_millis(ctx.healthcheck_config.timeout); + let timeout_duration = Duration::from_millis(ctx.healthcheck_config.timeout_ms); Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } From 14eeaeeb3da0745f7f4af1bf241e9c2d8755bc50 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 4 Sep 2024 11:58:41 +0300 Subject: [PATCH 20/37] handle our address Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 7 +++++++ mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index d58b4d3ee1..988a5e22fc 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -162,6 +162,13 @@ pub async fn peer_connection_healthcheck_rpc( let target_peer_id = PeerId::from_str(&req.peer_id) .map_err(|e| HealthcheckRpcError::InvalidPeerAddress { reason: e.to_string() })?; + let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); + + if target_peer_id == p2p_ctx.peer_id() { + // That's us, so return true. + return Ok(true); + } + let message = HealthcheckMessage::generate_message(&ctx, target_peer_id, false, ctx.healthcheck_config.message_expiration) .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 68c6bebfd9..d6016fb175 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -5940,11 +5940,21 @@ fn test_connection_healthcheck_rpc() { thread::sleep(Duration::from_secs(2)); + // Self-address check for Bob + let response = block_on(peer_connection_healthcheck(&bob_mm, BOB_ADDRESS)); + assert_eq!(response["result"], json!(true)); + + // Check address of Alice let response = block_on(peer_connection_healthcheck(&bob_mm, ALICE_ADDRESS)); assert_eq!(response["result"], json!(true)); thread::sleep(Duration::from_secs(1)); + // Self-address check for Alice + let response = block_on(peer_connection_healthcheck(&alice_mm, ALICE_ADDRESS)); + assert_eq!(response["result"], json!(true)); + + // Check address of Bob let response = block_on(peer_connection_healthcheck(&alice_mm, BOB_ADDRESS)); assert_eq!(response["result"], json!(true)); } From b5275d15e0439af1984d69ca9d036999fd12c268 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 18 Sep 2024 12:07:22 +0300 Subject: [PATCH 21/37] update time related logics Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 15 ++++++++------- mm2src/mm2_main/src/lp_healthcheck.rs | 10 +++++++--- mm2src/mm2_main/src/lp_network.rs | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index e40e9f85e4..e05f8663c8 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -48,31 +48,32 @@ const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; mod healthcheck_defaults { pub(crate) const fn default_healthcheck_blocking_ms() -> u64 { 750 } - pub(crate) const fn default_healthcheck_message_expiration() -> i64 { 10 } + pub(crate) const fn default_healthcheck_message_expiration_secs() -> i64 { 10 } - pub(crate) const fn default_timeout_ms() -> u64 { 10 } + pub(crate) const fn default_timeout_secs() -> u64 { 10 } } #[derive(Debug, Deserialize)] +#[serde(default)] pub struct HealthcheckConfig { /// Required time (millisecond) to wait before processing another healthcheck request from the same peer. #[serde(default = "healthcheck_defaults::default_healthcheck_blocking_ms")] pub blocking_ms_for_per_address: u64, /// Lifetime of the message. /// Do not change this unless you know what you are doing. - #[serde(default = "healthcheck_defaults::default_healthcheck_message_expiration")] + #[serde(default = "healthcheck_defaults::default_healthcheck_message_expiration_secs")] pub message_expiration: i64, /// Maximum time (milliseconds) to wait for healthcheck response. - #[serde(default = "healthcheck_defaults::default_timeout_ms")] - pub timeout_ms: u64, + #[serde(default = "healthcheck_defaults::default_timeout_secs")] + pub timeout_secs: u64, } impl Default for HealthcheckConfig { fn default() -> Self { Self { blocking_ms_for_per_address: healthcheck_defaults::default_healthcheck_blocking_ms(), - message_expiration: healthcheck_defaults::default_healthcheck_message_expiration(), - timeout_ms: healthcheck_defaults::default_timeout_ms(), + message_expiration: healthcheck_defaults::default_healthcheck_message_expiration_secs(), + timeout_secs: healthcheck_defaults::default_timeout_secs(), } } } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 988a5e22fc..73672108dc 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -11,6 +11,7 @@ use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; use std::str::FromStr; +use std::sync::OnceLock; use crate::lp_network::broadcast_p2p_msg; @@ -157,7 +158,10 @@ pub async fn peer_connection_healthcheck_rpc( ) -> Result> { /// When things go awry, we want records to clear themselves to keep the memory clean of unused data. /// This is unrelated to the timeout logic. - const ADDRESS_RECORD_EXPIRATION: Duration = Duration::from_secs(60); + static ADDRESS_RECORD_EXPIRATION: OnceLock = OnceLock::new(); + + let address_record_exp = + ADDRESS_RECORD_EXPIRATION.get_or_init(|| Duration::from_secs(ctx.healthcheck_config.timeout_secs)); let target_peer_id = PeerId::from_str(&req.peer_id) .map_err(|e| HealthcheckRpcError::InvalidPeerAddress { reason: e.to_string() })?; @@ -181,12 +185,12 @@ pub async fn peer_connection_healthcheck_rpc( let mut book = ctx.healthcheck_response_handler.lock().await; book.clear_expired_entries(); - book.insert(target_peer_id.to_string(), tx, ADDRESS_RECORD_EXPIRATION); + book.insert(target_peer_id.to_string(), tx, *address_record_exp); drop(book); broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); - let timeout_duration = Duration::from_millis(ctx.healthcheck_config.timeout_ms); + let timeout_duration = Duration::from_secs(ctx.healthcheck_config.timeout_secs); Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index ce83a68027..836b760217 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -223,7 +223,7 @@ async fn process_p2p_message( match $exp { Ok(t) => t, Err(e) => { - log::error!("{}, e: {e:?}", $msg); + log::error!("{}, error: {e:?}", $msg); return; }, } From 5b39e75bbe0acf6967227e4f1eefa3624b741a7a Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 18 Sep 2024 12:32:55 +0300 Subject: [PATCH 22/37] remove manual default value handling Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index e05f8663c8..6d805c2f44 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -57,14 +57,11 @@ mod healthcheck_defaults { #[serde(default)] pub struct HealthcheckConfig { /// Required time (millisecond) to wait before processing another healthcheck request from the same peer. - #[serde(default = "healthcheck_defaults::default_healthcheck_blocking_ms")] pub blocking_ms_for_per_address: u64, /// Lifetime of the message. /// Do not change this unless you know what you are doing. - #[serde(default = "healthcheck_defaults::default_healthcheck_message_expiration_secs")] pub message_expiration: i64, /// Maximum time (milliseconds) to wait for healthcheck response. - #[serde(default = "healthcheck_defaults::default_timeout_secs")] pub timeout_secs: u64, } From 7fccb1553c32a60c6a9d3b5a81b3ac6dc3bc2af1 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 18 Sep 2024 12:56:10 +0300 Subject: [PATCH 23/37] pack healthcheck related `Ctx` fields Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 25 +++++++++++++++---------- mm2src/mm2_main/src/lp_healthcheck.rs | 8 ++++---- mm2src/mm2_main/src/lp_network.rs | 6 +++--- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 6d805c2f44..f2bbd5db83 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -53,6 +53,14 @@ mod healthcheck_defaults { pub(crate) const fn default_timeout_secs() -> u64 { 10 } } +pub struct Healthcheck { + /// Links the RPC context to the P2P context to handle health check responses. + pub response_handler: AsyncMutex>>, + /// This is used to record healthcheck sender peers in an expirable manner to prevent brute-force attacks. + pub bruteforce_shield: AsyncMutex>, + pub config: HealthcheckConfig, +} + #[derive(Debug, Deserialize)] #[serde(default)] pub struct HealthcheckConfig { @@ -175,12 +183,7 @@ pub struct MmCtx { /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] pub async_sqlite_connection: Constructible>>, - /// Links the RPC context to the P2P context to handle health check responses. - pub healthcheck_response_handler: AsyncMutex>>, - /// This is used to record healthcheck sender peers in an expirable manner to prevent brute-force attacks. - pub healthcheck_bruteforce_shield: AsyncMutex>, - /// Global configuration of healthchecks. - pub healthcheck_config: HealthcheckConfig, + pub healthcheck: Healthcheck, } impl MmCtx { @@ -230,9 +233,11 @@ impl MmCtx { nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: Constructible::default(), - healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), - healthcheck_bruteforce_shield: AsyncMutex::new(ExpirableMap::default()), - healthcheck_config: HealthcheckConfig::default(), + healthcheck: Healthcheck { + response_handler: AsyncMutex::new(ExpirableMap::default()), + bruteforce_shield: AsyncMutex::new(ExpirableMap::default()), + config: HealthcheckConfig::default(), + }, } } @@ -807,7 +812,7 @@ impl MmCtxBuilder { if !healthcheck_config.is_null() { let healthcheck_config: HealthcheckConfig = json::from_value(healthcheck_config.clone()).expect("Invalid json value in 'healthcheck_config'."); - ctx.healthcheck_config = healthcheck_config; + ctx.healthcheck.config = healthcheck_config; } } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 73672108dc..6a8cf2824a 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -161,7 +161,7 @@ pub async fn peer_connection_healthcheck_rpc( static ADDRESS_RECORD_EXPIRATION: OnceLock = OnceLock::new(); let address_record_exp = - ADDRESS_RECORD_EXPIRATION.get_or_init(|| Duration::from_secs(ctx.healthcheck_config.timeout_secs)); + ADDRESS_RECORD_EXPIRATION.get_or_init(|| Duration::from_secs(ctx.healthcheck.config.timeout_secs)); let target_peer_id = PeerId::from_str(&req.peer_id) .map_err(|e| HealthcheckRpcError::InvalidPeerAddress { reason: e.to_string() })?; @@ -174,7 +174,7 @@ pub async fn peer_connection_healthcheck_rpc( } let message = - HealthcheckMessage::generate_message(&ctx, target_peer_id, false, ctx.healthcheck_config.message_expiration) + HealthcheckMessage::generate_message(&ctx, target_peer_id, false, ctx.healthcheck.config.message_expiration) .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; let encoded_message = message @@ -183,14 +183,14 @@ pub async fn peer_connection_healthcheck_rpc( let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); - let mut book = ctx.healthcheck_response_handler.lock().await; + let mut book = ctx.healthcheck.response_handler.lock().await; book.clear_expired_entries(); book.insert(target_peer_id.to_string(), tx, *address_record_exp); drop(book); broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); - let timeout_duration = Duration::from_secs(ctx.healthcheck_config.timeout_secs); + let timeout_duration = Duration::from_secs(ctx.healthcheck.config.timeout_secs); Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 836b760217..d7e53f8c74 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -238,13 +238,13 @@ async fn process_p2p_message( let sender_peer = data.sender_peer().to_owned(); - let mut bruteforce_shield = ctx.healthcheck_bruteforce_shield.lock().await; + let mut bruteforce_shield = ctx.healthcheck.bruteforce_shield.lock().await; bruteforce_shield.clear_expired_entries(); if bruteforce_shield .insert( sender_peer.clone(), (), - Duration::from_millis(ctx.healthcheck_config.blocking_ms_for_per_address), + Duration::from_millis(ctx.healthcheck.config.blocking_ms_for_per_address), ) .is_some() { @@ -280,7 +280,7 @@ async fn process_p2p_message( broadcast_p2p_msg(&ctx, topic, encoded_msg, None); } else { // The requested peer is healthy; signal the response channel. - let mut response_handler = ctx.healthcheck_response_handler.lock().await; + let mut response_handler = ctx.healthcheck.response_handler.lock().await; if let Some(tx) = response_handler.remove(&sender_peer) { if tx.send(()).is_err() { log::error!("Result channel isn't present for peer '{sender_peer}'."); From 1777559ae2a1ddfd426f4dc4fad67c9bdc2ed489 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 18 Sep 2024 14:30:56 +0300 Subject: [PATCH 24/37] make safer ser and deser functions for bytes and peer addresses Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 117 ++++++++++++++++++++++---- mm2src/mm2_main/src/lp_network.rs | 14 ++- 2 files changed, 106 insertions(+), 25 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 6a8cf2824a..30135cc9e7 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -20,6 +20,7 @@ pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; #[derive(Debug, Deserialize, Serialize)] #[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] pub(crate) struct HealthcheckMessage { + #[serde(deserialize_with = "deserialize_bytes")] signature: Vec, data: HealthcheckData, } @@ -32,10 +33,9 @@ impl HealthcheckMessage { expires_in_seconds: i64, ) -> Result { let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); - let sender_peer = p2p_ctx.peer_id().to_string(); + let sender_peer = p2p_ctx.peer_id(); let keypair = p2p_ctx.keypair(); let sender_public_key = keypair.public().encode_protobuf(); - let target_peer = target_peer.to_string(); let data = HealthcheckData { sender_peer, @@ -60,7 +60,7 @@ impl HealthcheckMessage { return false; } - if self.data.target_peer != my_peer_id.to_string() { + if self.data.target_peer != my_peer_id { log::debug!( "`target_peer` doesn't match with our peer address. Our address: '{}', healthcheck `target_peer`: '{}'.", my_peer_id, @@ -75,7 +75,7 @@ impl HealthcheckMessage { return false }; - if self.data.sender_peer != public_key.to_peer_id().to_string() { + if self.data.sender_peer != public_key.to_peer_id() { log::debug!("`sender_peer` and `sender_public_key` doesn't belong each other."); return false; @@ -105,15 +105,18 @@ impl HealthcheckMessage { pub(crate) fn should_reply(&self) -> bool { !self.data.is_a_reply } #[inline] - pub(crate) fn sender_peer(&self) -> &str { &self.data.sender_peer } + pub(crate) fn sender_peer(&self) -> PeerId { self.data.sender_peer } } #[derive(Debug, Deserialize, Serialize)] #[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] struct HealthcheckData { - sender_peer: String, + #[serde(deserialize_with = "deserialize_peer_id", serialize_with = "serialize_peer_id")] + sender_peer: PeerId, + #[serde(deserialize_with = "deserialize_bytes")] sender_public_key: Vec, - target_peer: String, + #[serde(deserialize_with = "deserialize_peer_id", serialize_with = "serialize_peer_id")] + target_peer: PeerId, expires_at: i64, is_a_reply: bool, } @@ -130,13 +133,98 @@ pub fn peer_healthcheck_topic(peer_id: &PeerId) -> String { #[derive(Deserialize)] pub struct RequestPayload { - peer_id: String, + #[serde(deserialize_with = "deserialize_peer_id")] + peer_id: PeerId, +} + +fn deserialize_peer_id<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct PeerIdVisitor; + + impl<'de> serde::de::Visitor<'de> for PeerIdVisitor { + type Value = PeerId; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representation of peer id.") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if value.len() > 100 { + return Err(serde::de::Error::invalid_length( + value.len(), + &"peer id cannot exceed 100 characters.", + )); + } + + PeerId::from_str(value).map_err(serde::de::Error::custom) + } + + fn visit_string(self, value: String) -> Result + where + E: serde::de::Error, + { + self.visit_str(&value) + } + } + + deserializer.deserialize_str(PeerIdVisitor) +} + +fn serialize_peer_id(peer_id: &PeerId, s: S) -> Result +where + S: serde::Serializer, +{ + s.serialize_str(&peer_id.to_string()) +} + +fn deserialize_bytes<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + struct ByteVisitor; + + impl<'de> serde::de::Visitor<'de> for ByteVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a non-empty byte array up to 512 bytes") + } + + fn visit_seq(self, mut seq: A) -> Result, A::Error> + where + A: serde::de::SeqAccess<'de>, + { + let mut buffer = vec![]; + while let Some(byte) = seq.next_element()? { + if buffer.len() >= 512 { + return Err(serde::de::Error::invalid_length( + buffer.len(), + &"longest possible length allowed for this field is 512 bytes (with RSA algorithm).", + )); + } + + buffer.push(byte); + } + + if buffer.is_empty() { + return Err(serde::de::Error::custom("Can't be empty.")); + } + + Ok(buffer) + } + } + + deserializer.deserialize_seq(ByteVisitor) } #[derive(Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum HealthcheckRpcError { - InvalidPeerAddress { reason: String }, MessageGenerationFailed { reason: String }, MessageEncodingFailed { reason: String }, } @@ -144,7 +232,6 @@ pub enum HealthcheckRpcError { impl HttpStatusCode for HealthcheckRpcError { fn status_code(&self) -> common::StatusCode { match self { - HealthcheckRpcError::InvalidPeerAddress { .. } => StatusCode::BAD_REQUEST, HealthcheckRpcError::MessageGenerationFailed { .. } | HealthcheckRpcError::MessageEncodingFailed { .. } => { StatusCode::INTERNAL_SERVER_ERROR }, @@ -163,11 +250,9 @@ pub async fn peer_connection_healthcheck_rpc( let address_record_exp = ADDRESS_RECORD_EXPIRATION.get_or_init(|| Duration::from_secs(ctx.healthcheck.config.timeout_secs)); - let target_peer_id = PeerId::from_str(&req.peer_id) - .map_err(|e| HealthcheckRpcError::InvalidPeerAddress { reason: e.to_string() })?; + let target_peer_id = req.peer_id; let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - if target_peer_id == p2p_ctx.peer_id() { // That's us, so return true. return Ok(true); @@ -248,15 +333,15 @@ mod tests { assert!(!message.is_received_message_valid(target_peer)); let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - message.data.sender_peer += "0"; + message.data.sender_peer = message.data.target_peer; assert!(!message.is_received_message_valid(target_peer)); let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - message.data.target_peer += "0"; + message.data.target_peer = message.data.sender_peer; assert!(!message.is_received_message_valid(target_peer)); let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - assert!(!message.is_received_message_valid(PeerId::from_str(&message.data.sender_peer).unwrap())); + assert!(!message.is_received_message_valid(message.data.sender_peer)); }); cross_test!(test_expired_message, { diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index d7e53f8c74..1e73bc6b6e 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -37,7 +37,6 @@ use mm2_metrics::{mm_label, mm_timing}; use mm2_net::p2p::P2PContext; use serde::de; use std::net::ToSocketAddrs; -use std::str::FromStr; use crate::lp_healthcheck::{peer_healthcheck_topic, HealthcheckMessage}; use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; @@ -242,7 +241,7 @@ async fn process_p2p_message( bruteforce_shield.clear_expired_entries(); if bruteforce_shield .insert( - sender_peer.clone(), + sender_peer.to_string(), (), Duration::from_millis(ctx.healthcheck.config.blocking_ms_for_per_address), ) @@ -261,14 +260,11 @@ async fn process_p2p_message( if data.should_reply() { // Reply the message so they know we are healthy. - let target_peer_id = try_or_return!( - PeerId::from_str(&sender_peer), - format!("'{sender_peer}' is not a valid address") - ); - let topic = peer_healthcheck_topic(&target_peer_id); + + let topic = peer_healthcheck_topic(&sender_peer); let msg = try_or_return!( - HealthcheckMessage::generate_message(&ctx, target_peer_id, true, 10), + HealthcheckMessage::generate_message(&ctx, sender_peer, true, 10), "Couldn't generate the healthcheck message, this is very unusual!" ); @@ -281,7 +277,7 @@ async fn process_p2p_message( } else { // The requested peer is healthy; signal the response channel. let mut response_handler = ctx.healthcheck.response_handler.lock().await; - if let Some(tx) = response_handler.remove(&sender_peer) { + if let Some(tx) = response_handler.remove(&sender_peer.to_string()) { if tx.send(()).is_err() { log::error!("Result channel isn't present for peer '{sender_peer}'."); }; From 13fe925e3c62c95b980de8ec279119a8593e0f70 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 19 Sep 2024 13:07:39 +0300 Subject: [PATCH 25/37] separate the healthcheck processing logic and improve the performance Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 78 +++++++++++++++++++++++++++ mm2src/mm2_main/src/lp_network.rs | 72 +------------------------ 2 files changed, 80 insertions(+), 70 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 30135cc9e7..ffce972137 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,5 +1,6 @@ use async_std::prelude::FutureExt; use chrono::Utc; +use common::executor::SpawnFuture; use common::{log, HttpStatusCode, StatusCode}; use derive_more::Display; use futures::channel::oneshot::{self, Receiver, Sender}; @@ -279,6 +280,83 @@ pub async fn peer_connection_healthcheck_rpc( Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } +pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_libp2p::GossipsubMessage) { + macro_rules! try_or_return { + ($exp:expr, $msg: expr) => { + match $exp { + Ok(t) => t, + Err(e) => { + log::error!("{}, error: {e:?}", $msg); + return; + }, + } + }; + } + + let data = try_or_return!( + HealthcheckMessage::decode(&message.data), + "Couldn't decode healthcheck message" + ); + + let sender_peer = data.sender_peer().to_owned(); + + let mut bruteforce_shield = ctx.healthcheck.bruteforce_shield.lock().await; + bruteforce_shield.clear_expired_entries(); + if bruteforce_shield + .insert( + sender_peer.to_string(), + (), + Duration::from_millis(ctx.healthcheck.config.blocking_ms_for_per_address), + ) + .is_some() + { + log::warn!("Peer '{sender_peer}' exceeded the healthcheck blocking time, skipping their message."); + return; + } + drop(bruteforce_shield); + + let ctx_c = ctx.clone(); + + // Pass the remaining work to another thread to free up this one as soon as possible, + // so KDF can handle a high amount of healthcheck messages more efficiently. + ctx.spawner().spawn(async move { + let my_peer_id = P2PContext::fetch_from_mm_arc(&ctx_c).peer_id(); + if !data.is_received_message_valid(my_peer_id) { + log::error!("Received an invalid healthcheck message."); + log::debug!("Message context: {:?}", data); + return; + }; + + if data.should_reply() { + // Reply the message so they know we are healthy. + + let topic = peer_healthcheck_topic(&sender_peer); + + let msg = try_or_return!( + HealthcheckMessage::generate_message(&ctx_c, sender_peer, true, 10), + "Couldn't generate the healthcheck message, this is very unusual!" + ); + + let encoded_msg = try_or_return!( + msg.encode(), + "Couldn't encode healthcheck message, this is very unusual!" + ); + + broadcast_p2p_msg(&ctx_c, topic, encoded_msg, None); + } else { + // The requested peer is healthy; signal the response channel. + let mut response_handler = ctx_c.healthcheck.response_handler.lock().await; + if let Some(tx) = response_handler.remove(&sender_peer.to_string()) { + if tx.send(()).is_err() { + log::error!("Result channel isn't present for peer '{sender_peer}'."); + }; + } else { + log::info!("Peer '{sender_peer}' isn't recorded in the healthcheck response handler."); + }; + } + }); +} + #[cfg(any(test, target_arch = "wasm32"))] mod tests { use super::*; diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 1e73bc6b6e..8e5195e93a 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -25,7 +25,7 @@ use common::executor::SpawnFuture; use common::{log, Future01CompatExt}; use derive_more::Display; use futures::{channel::oneshot, StreamExt}; -use instant::{Duration, Instant}; +use instant::Instant; use keys::KeyPair; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; @@ -38,7 +38,6 @@ use mm2_net::p2p::P2PContext; use serde::de; use std::net::ToSocketAddrs; -use crate::lp_healthcheck::{peer_healthcheck_topic, HealthcheckMessage}; use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; pub type P2PRequestResult = Result>; @@ -217,74 +216,7 @@ async fn process_p2p_message( } }, Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { - macro_rules! try_or_return { - ($exp:expr, $msg: expr) => { - match $exp { - Ok(t) => t, - Err(e) => { - log::error!("{}, error: {e:?}", $msg); - return; - }, - } - }; - } - - let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - let data = try_or_return!( - HealthcheckMessage::decode(&message.data), - "Couldn't decode healthcheck message" - ); - - let sender_peer = data.sender_peer().to_owned(); - - let mut bruteforce_shield = ctx.healthcheck.bruteforce_shield.lock().await; - bruteforce_shield.clear_expired_entries(); - if bruteforce_shield - .insert( - sender_peer.to_string(), - (), - Duration::from_millis(ctx.healthcheck.config.blocking_ms_for_per_address), - ) - .is_some() - { - log::warn!("Peer '{sender_peer}' exceeded the healthcheck blocking time, skipping their message."); - return; - } - drop(bruteforce_shield); - - if !data.is_received_message_valid(p2p_ctx.peer_id()) { - log::error!("Received an invalid healthcheck message."); - log::debug!("Message context: {:?}", data); - return; - }; - - if data.should_reply() { - // Reply the message so they know we are healthy. - - let topic = peer_healthcheck_topic(&sender_peer); - - let msg = try_or_return!( - HealthcheckMessage::generate_message(&ctx, sender_peer, true, 10), - "Couldn't generate the healthcheck message, this is very unusual!" - ); - - let encoded_msg = try_or_return!( - msg.encode(), - "Couldn't encode healthcheck message, this is very unusual!" - ); - - broadcast_p2p_msg(&ctx, topic, encoded_msg, None); - } else { - // The requested peer is healthy; signal the response channel. - let mut response_handler = ctx.healthcheck.response_handler.lock().await; - if let Some(tx) = response_handler.remove(&sender_peer.to_string()) { - if tx.send(()).is_err() { - log::error!("Result channel isn't present for peer '{sender_peer}'."); - }; - } else { - log::info!("Peer '{sender_peer}' isn't recorded in the healthcheck response handler."); - }; - } + lp_healthcheck::process_p2p_healthcheck_message(&ctx, message).await }, None | Some(_) => (), } From 1a3bb84835da2459321a554c39de1084fcfb6c09 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 19 Sep 2024 15:33:20 +0300 Subject: [PATCH 26/37] rename `bruteforce_shield` into `ddos_shield` Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 6 +++--- mm2src/mm2_main/src/lp_healthcheck.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index f2bbd5db83..55dbecf1f4 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -56,8 +56,8 @@ mod healthcheck_defaults { pub struct Healthcheck { /// Links the RPC context to the P2P context to handle health check responses. pub response_handler: AsyncMutex>>, - /// This is used to record healthcheck sender peers in an expirable manner to prevent brute-force attacks. - pub bruteforce_shield: AsyncMutex>, + /// This is used to record healthcheck sender peers in an expirable manner to prevent DDoS attacks. + pub ddos_shield: AsyncMutex>, pub config: HealthcheckConfig, } @@ -235,7 +235,7 @@ impl MmCtx { async_sqlite_connection: Constructible::default(), healthcheck: Healthcheck { response_handler: AsyncMutex::new(ExpirableMap::default()), - bruteforce_shield: AsyncMutex::new(ExpirableMap::default()), + ddos_shield: AsyncMutex::new(ExpirableMap::default()), config: HealthcheckConfig::default(), }, } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index ffce972137..b7eddb3d70 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -300,9 +300,9 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li let sender_peer = data.sender_peer().to_owned(); - let mut bruteforce_shield = ctx.healthcheck.bruteforce_shield.lock().await; - bruteforce_shield.clear_expired_entries(); - if bruteforce_shield + let mut ddos_shield = ctx.healthcheck.ddos_shield.lock().await; + ddos_shield.clear_expired_entries(); + if ddos_shield .insert( sender_peer.to_string(), (), @@ -313,7 +313,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li log::warn!("Peer '{sender_peer}' exceeded the healthcheck blocking time, skipping their message."); return; } - drop(bruteforce_shield); + drop(ddos_shield); let ctx_c = ctx.clone(); From 5605a008a2acd0cfe6208f9c634b3781f7d8a4e4 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 24 Sep 2024 10:43:30 +0300 Subject: [PATCH 27/37] keep `init_p2p_context` private Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/ordermatch_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 0f5d64aad7..38d17af4ed 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -1677,7 +1677,7 @@ fn pubkey_and_secret_for_test(passphrase: &str) -> (String, [u8; 32]) { (pubkey, secret) } -pub(crate) fn init_p2p_context(ctx: &MmArc) -> (mpsc::Sender, mpsc::Receiver) { +fn init_p2p_context(ctx: &MmArc) -> (mpsc::Sender, mpsc::Receiver) { let (cmd_tx, cmd_rx) = mpsc::channel(10); let p2p_key = { From 74c8bea8ddbf057d62c6def473ac5dd763ced926 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 25 Sep 2024 07:51:24 +0300 Subject: [PATCH 28/37] nit fixes Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 24 +++++--------- mm2src/mm2_main/src/lp_healthcheck.rs | 45 ++++++++++++++++----------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 98864df40f..8d5969332f 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -45,15 +45,7 @@ cfg_native! { /// Default interval to export and record metrics to log. const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; -mod healthcheck_defaults { - pub(crate) const fn default_healthcheck_blocking_ms() -> u64 { 750 } - - pub(crate) const fn default_healthcheck_message_expiration_secs() -> i64 { 10 } - - pub(crate) const fn default_timeout_secs() -> u64 { 10 } -} - -pub struct Healthcheck { +pub struct HealthChecker { /// Links the RPC context to the P2P context to handle health check responses. pub response_handler: AsyncMutex>>, /// This is used to record healthcheck sender peers in an expirable manner to prevent DDoS attacks. @@ -68,7 +60,7 @@ pub struct HealthcheckConfig { pub blocking_ms_for_per_address: u64, /// Lifetime of the message. /// Do not change this unless you know what you are doing. - pub message_expiration: i64, + pub message_expiration: u64, /// Maximum time (milliseconds) to wait for healthcheck response. pub timeout_secs: u64, } @@ -76,9 +68,9 @@ pub struct HealthcheckConfig { impl Default for HealthcheckConfig { fn default() -> Self { Self { - blocking_ms_for_per_address: healthcheck_defaults::default_healthcheck_blocking_ms(), - message_expiration: healthcheck_defaults::default_healthcheck_message_expiration_secs(), - timeout_secs: healthcheck_defaults::default_timeout_secs(), + blocking_ms_for_per_address: 750, + message_expiration: 10, + timeout_secs: 10, } } } @@ -183,7 +175,7 @@ pub struct MmCtx { /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] pub async_sqlite_connection: Constructible>>, - pub healthcheck: Healthcheck, + pub health_checker: HealthChecker, } impl MmCtx { @@ -233,7 +225,7 @@ impl MmCtx { nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: Constructible::default(), - healthcheck: Healthcheck { + health_checker: HealthChecker { response_handler: AsyncMutex::new(ExpirableMap::default()), ddos_shield: AsyncMutex::new(ExpirableMap::default()), config: HealthcheckConfig::default(), @@ -828,7 +820,7 @@ impl MmCtxBuilder { if !healthcheck_config.is_null() { let healthcheck_config: HealthcheckConfig = json::from_value(healthcheck_config.clone()).expect("Invalid json value in 'healthcheck_config'."); - ctx.healthcheck.config = healthcheck_config; + ctx.health_checker.config = healthcheck_config; } } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index b7eddb3d70..833e43938e 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -11,8 +11,9 @@ use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, Pe use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::num::TryFromIntError; use std::str::FromStr; -use std::sync::OnceLock; use crate::lp_network::broadcast_p2p_msg; @@ -228,14 +229,15 @@ where pub enum HealthcheckRpcError { MessageGenerationFailed { reason: String }, MessageEncodingFailed { reason: String }, + Internal { reason: String }, } impl HttpStatusCode for HealthcheckRpcError { fn status_code(&self) -> common::StatusCode { match self { - HealthcheckRpcError::MessageGenerationFailed { .. } | HealthcheckRpcError::MessageEncodingFailed { .. } => { - StatusCode::INTERNAL_SERVER_ERROR - }, + HealthcheckRpcError::MessageGenerationFailed { .. } + | HealthcheckRpcError::Internal { .. } + | HealthcheckRpcError::MessageEncodingFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -244,12 +246,9 @@ pub async fn peer_connection_healthcheck_rpc( ctx: MmArc, req: RequestPayload, ) -> Result> { - /// When things go awry, we want records to clear themselves to keep the memory clean of unused data. - /// This is unrelated to the timeout logic. - static ADDRESS_RECORD_EXPIRATION: OnceLock = OnceLock::new(); - - let address_record_exp = - ADDRESS_RECORD_EXPIRATION.get_or_init(|| Duration::from_secs(ctx.healthcheck.config.timeout_secs)); + // When things go awry, we want records to clear themselves to keep the memory clean of unused data. + // This is unrelated to the timeout logic. + let address_record_exp = Duration::from_secs(ctx.health_checker.config.timeout_secs); let target_peer_id = req.peer_id; @@ -259,9 +258,17 @@ pub async fn peer_connection_healthcheck_rpc( return Ok(true); } - let message = - HealthcheckMessage::generate_message(&ctx, target_peer_id, false, ctx.healthcheck.config.message_expiration) - .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; + let message = HealthcheckMessage::generate_message( + &ctx, + target_peer_id, + false, + ctx.health_checker + .config + .message_expiration + .try_into() + .map_err(|e: TryFromIntError| HealthcheckRpcError::Internal { reason: e.to_string() })?, + ) + .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; let encoded_message = message .encode() @@ -269,14 +276,14 @@ pub async fn peer_connection_healthcheck_rpc( let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); - let mut book = ctx.healthcheck.response_handler.lock().await; + let mut book = ctx.health_checker.response_handler.lock().await; book.clear_expired_entries(); - book.insert(target_peer_id.to_string(), tx, *address_record_exp); + book.insert(target_peer_id.to_string(), tx, address_record_exp); drop(book); broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); - let timeout_duration = Duration::from_secs(ctx.healthcheck.config.timeout_secs); + let timeout_duration = Duration::from_secs(ctx.health_checker.config.timeout_secs); Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } @@ -300,13 +307,13 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li let sender_peer = data.sender_peer().to_owned(); - let mut ddos_shield = ctx.healthcheck.ddos_shield.lock().await; + let mut ddos_shield = ctx.health_checker.ddos_shield.lock().await; ddos_shield.clear_expired_entries(); if ddos_shield .insert( sender_peer.to_string(), (), - Duration::from_millis(ctx.healthcheck.config.blocking_ms_for_per_address), + Duration::from_millis(ctx.health_checker.config.blocking_ms_for_per_address), ) .is_some() { @@ -345,7 +352,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li broadcast_p2p_msg(&ctx_c, topic, encoded_msg, None); } else { // The requested peer is healthy; signal the response channel. - let mut response_handler = ctx_c.healthcheck.response_handler.lock().await; + let mut response_handler = ctx_c.health_checker.response_handler.lock().await; if let Some(tx) = response_handler.remove(&sender_peer.to_string()) { if tx.send(()).is_err() { log::error!("Result channel isn't present for peer '{sender_peer}'."); From de61cc2e5dfb2285cae4c788635e7ec873805448 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 25 Sep 2024 07:55:08 +0300 Subject: [PATCH 29/37] switch to sync `Mutex` Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 8 ++++---- mm2src/mm2_main/src/lp_healthcheck.rs | 13 +++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 8d5969332f..245b21af16 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -47,9 +47,9 @@ const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; pub struct HealthChecker { /// Links the RPC context to the P2P context to handle health check responses. - pub response_handler: AsyncMutex>>, + pub response_handler: Mutex>>, /// This is used to record healthcheck sender peers in an expirable manner to prevent DDoS attacks. - pub ddos_shield: AsyncMutex>, + pub ddos_shield: Mutex>, pub config: HealthcheckConfig, } @@ -226,8 +226,8 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: Constructible::default(), health_checker: HealthChecker { - response_handler: AsyncMutex::new(ExpirableMap::default()), - ddos_shield: AsyncMutex::new(ExpirableMap::default()), + response_handler: Mutex::new(ExpirableMap::default()), + ddos_shield: Mutex::new(ExpirableMap::default()), config: HealthcheckConfig::default(), }, } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 833e43938e..9c9b543c1a 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -276,10 +276,11 @@ pub async fn peer_connection_healthcheck_rpc( let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); - let mut book = ctx.health_checker.response_handler.lock().await; - book.clear_expired_entries(); - book.insert(target_peer_id.to_string(), tx, address_record_exp); - drop(book); + { + let mut book = ctx.health_checker.response_handler.lock().unwrap(); + book.clear_expired_entries(); + book.insert(target_peer_id.to_string(), tx, address_record_exp); + } broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); @@ -307,7 +308,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li let sender_peer = data.sender_peer().to_owned(); - let mut ddos_shield = ctx.health_checker.ddos_shield.lock().await; + let mut ddos_shield = ctx.health_checker.ddos_shield.lock().unwrap(); ddos_shield.clear_expired_entries(); if ddos_shield .insert( @@ -352,7 +353,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li broadcast_p2p_msg(&ctx_c, topic, encoded_msg, None); } else { // The requested peer is healthy; signal the response channel. - let mut response_handler = ctx_c.health_checker.response_handler.lock().await; + let mut response_handler = ctx_c.health_checker.response_handler.lock().unwrap(); if let Some(tx) = response_handler.remove(&sender_peer.to_string()) { if tx.send(()).is_err() { log::error!("Result channel isn't present for peer '{sender_peer}'."); From 93efaa2a8c32f20e6937e39ee3c1a9017845b745 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 25 Sep 2024 10:58:41 +0300 Subject: [PATCH 30/37] set max limit for expiration time logic Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 4 ++-- mm2src/mm2_main/src/lp_healthcheck.rs | 34 +++++++++++++++++---------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 245b21af16..273882a656 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -60,7 +60,7 @@ pub struct HealthcheckConfig { pub blocking_ms_for_per_address: u64, /// Lifetime of the message. /// Do not change this unless you know what you are doing. - pub message_expiration: u64, + pub message_expiration_secs: u64, /// Maximum time (milliseconds) to wait for healthcheck response. pub timeout_secs: u64, } @@ -69,7 +69,7 @@ impl Default for HealthcheckConfig { fn default() -> Self { Self { blocking_ms_for_per_address: 750, - message_expiration: 10, + message_expiration_secs: 10, timeout_secs: 10, } } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 9c9b543c1a..2971ac17bc 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -5,12 +5,13 @@ use common::{log, HttpStatusCode, StatusCode}; use derive_more::Display; use futures::channel::oneshot::{self, Receiver, Sender}; use instant::Duration; -use mm2_core::mm_ctx::MmArc; +use mm2_core::mm_ctx::{HealthcheckConfig, MmArc}; use mm2_err_handle::prelude::MmError; use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerId, TopicPrefix}; use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; use std::convert::TryInto; use std::num::TryFromIntError; use std::str::FromStr; @@ -52,14 +53,23 @@ impl HealthcheckMessage { Ok(Self { signature, data }) } - pub(crate) fn is_received_message_valid(&self, my_peer_id: PeerId) -> bool { + pub(crate) fn is_received_message_valid(&self, my_peer_id: PeerId, healthcheck_config: &HealthcheckConfig) -> bool { let now = Utc::now().timestamp(); - if now > self.data.expires_at { + let remaining_expiration_seconds = u64::try_from(self.data.expires_at - now).unwrap_or(0); + + if remaining_expiration_seconds == 0 { log::debug!( "Healthcheck message is expired. Current time in UTC: {now}, healthcheck `expires_at` in UTC: {}", self.data.expires_at ); return false; + } else if remaining_expiration_seconds > healthcheck_config.message_expiration_secs { + log::debug!( + "Healthcheck message have too high expiration time.\nMax allowed expiration seconds: {}\nReceived message expiration seconds: {}", + self.data.expires_at, + remaining_expiration_seconds, + ); + return false; } if self.data.target_peer != my_peer_id { @@ -264,7 +274,7 @@ pub async fn peer_connection_healthcheck_rpc( false, ctx.health_checker .config - .message_expiration + .message_expiration_secs .try_into() .map_err(|e: TryFromIntError| HealthcheckRpcError::Internal { reason: e.to_string() })?, ) @@ -329,7 +339,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li // so KDF can handle a high amount of healthcheck messages more efficiently. ctx.spawner().spawn(async move { let my_peer_id = P2PContext::fetch_from_mm_arc(&ctx_c).peer_id(); - if !data.is_received_message_valid(my_peer_id) { + if !data.is_received_message_valid(my_peer_id, &ctx_c.health_checker.config) { log::error!("Received an invalid healthcheck message."); log::debug!("Message context: {:?}", data); return; @@ -403,7 +413,7 @@ mod tests { let ctx = ctx(); let target_peer = create_test_peer_id(); let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - assert!(message.is_received_message_valid(target_peer)); + assert!(message.is_received_message_valid(target_peer, &ctx.health_checker.config)); }); cross_test!(test_corrupted_messages, { @@ -412,29 +422,29 @@ mod tests { let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); message.data.expires_at += 1; - assert!(!message.is_received_message_valid(target_peer)); + assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); message.data.is_a_reply = !message.data.is_a_reply; - assert!(!message.is_received_message_valid(target_peer)); + assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); message.data.sender_peer = message.data.target_peer; - assert!(!message.is_received_message_valid(target_peer)); + assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); message.data.target_peer = message.data.sender_peer; - assert!(!message.is_received_message_valid(target_peer)); + assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - assert!(!message.is_received_message_valid(message.data.sender_peer)); + assert!(!message.is_received_message_valid(message.data.sender_peer, &ctx.health_checker.config)); }); cross_test!(test_expired_message, { let ctx = ctx(); let target_peer = create_test_peer_id(); let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, -1).unwrap(); - assert!(!message.is_received_message_valid(target_peer)); + assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); }); cross_test!(test_encode_decode, { From 9b086958ae8a5419221d5b4f42c302caca3e71d7 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 25 Sep 2024 11:06:32 +0300 Subject: [PATCH 31/37] fix WASM lint Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 273882a656..b9bc8ae3a7 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -6,7 +6,6 @@ use common::{executor::{abortable_queue::{AbortableQueue, WeakSpawner}, graceful_shutdown, AbortSettings, AbortableSystem, SpawnAbortable, SpawnFuture}, expirable_map::ExpirableMap}; use futures::channel::oneshot; -use futures::lock::Mutex as AsyncMutex; use gstuff::{try_s, Constructible, ERR, ERRL}; use lazy_static::lazy_static; use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; @@ -40,6 +39,7 @@ cfg_native! { use std::net::{IpAddr, SocketAddr, AddrParseError}; use std::path::{Path, PathBuf}; use std::sync::MutexGuard; + use futures::lock::Mutex as AsyncMutex; } /// Default interval to export and record metrics to log. From ad61f31610aed08b53c5c55b1cf58c0869c971b6 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 25 Sep 2024 12:07:26 +0300 Subject: [PATCH 32/37] run ddos protection logic after message verification Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 43 ++++++++++++++------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 2971ac17bc..fd31f5a82a 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -318,40 +318,41 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li let sender_peer = data.sender_peer().to_owned(); - let mut ddos_shield = ctx.health_checker.ddos_shield.lock().unwrap(); - ddos_shield.clear_expired_entries(); - if ddos_shield - .insert( - sender_peer.to_string(), - (), - Duration::from_millis(ctx.health_checker.config.blocking_ms_for_per_address), - ) - .is_some() - { - log::warn!("Peer '{sender_peer}' exceeded the healthcheck blocking time, skipping their message."); - return; - } - drop(ddos_shield); - - let ctx_c = ctx.clone(); + let ctx = ctx.clone(); // Pass the remaining work to another thread to free up this one as soon as possible, // so KDF can handle a high amount of healthcheck messages more efficiently. ctx.spawner().spawn(async move { - let my_peer_id = P2PContext::fetch_from_mm_arc(&ctx_c).peer_id(); - if !data.is_received_message_valid(my_peer_id, &ctx_c.health_checker.config) { + let my_peer_id = P2PContext::fetch_from_mm_arc(&ctx).peer_id(); + + if !data.is_received_message_valid(my_peer_id, &ctx.health_checker.config) { log::error!("Received an invalid healthcheck message."); log::debug!("Message context: {:?}", data); return; }; + let mut ddos_shield = ctx.health_checker.ddos_shield.lock().unwrap(); + ddos_shield.clear_expired_entries(); + if ddos_shield + .insert( + sender_peer.to_string(), + (), + Duration::from_millis(ctx.health_checker.config.blocking_ms_for_per_address), + ) + .is_some() + { + log::warn!("Peer '{sender_peer}' exceeded the healthcheck blocking time, skipping their message."); + return; + } + drop(ddos_shield); + if data.should_reply() { // Reply the message so they know we are healthy. let topic = peer_healthcheck_topic(&sender_peer); let msg = try_or_return!( - HealthcheckMessage::generate_message(&ctx_c, sender_peer, true, 10), + HealthcheckMessage::generate_message(&ctx, sender_peer, true, 10), "Couldn't generate the healthcheck message, this is very unusual!" ); @@ -360,10 +361,10 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li "Couldn't encode healthcheck message, this is very unusual!" ); - broadcast_p2p_msg(&ctx_c, topic, encoded_msg, None); + broadcast_p2p_msg(&ctx, topic, encoded_msg, None); } else { // The requested peer is healthy; signal the response channel. - let mut response_handler = ctx_c.health_checker.response_handler.lock().unwrap(); + let mut response_handler = ctx.health_checker.response_handler.lock().unwrap(); if let Some(tx) = response_handler.remove(&sender_peer.to_string()) { if tx.send(()).is_err() { log::error!("Result channel isn't present for peer '{sender_peer}'."); From b4d63f6cb2edb05a8aacea081b6e65002ade6db9 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 26 Sep 2024 11:30:25 +0300 Subject: [PATCH 33/37] create `PeerAddress` as a wrapper of `libp2p::PeerId` Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 192 ++++++++++++++--------- mm2src/mm2_main/src/lp_native_dex.rs | 4 +- mm2src/mm2_test_helpers/src/for_tests.rs | 18 ++- 3 files changed, 130 insertions(+), 84 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index fd31f5a82a..663b57a76d 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -7,7 +7,7 @@ use futures::channel::oneshot::{self, Receiver, Sender}; use instant::Duration; use mm2_core::mm_ctx::{HealthcheckConfig, MmArc}; use mm2_err_handle::prelude::MmError; -use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerId, TopicPrefix}; +use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, TopicPrefix}; use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; @@ -28,15 +28,80 @@ pub(crate) struct HealthcheckMessage { data: HealthcheckData, } +/// Wrapper of `libp2p::PeerId` with trait additional implementations. +/// +/// TODO: This should be used as a replacement of `libp2p::PeerId` in the entire project. +#[derive(Clone, Copy, Debug, Display, PartialEq)] +pub struct PeerAddress(mm2_libp2p::PeerId); + +impl From for PeerAddress { + fn from(value: mm2_libp2p::PeerId) -> Self { Self(value) } +} + +impl From for mm2_libp2p::PeerId { + fn from(value: PeerAddress) -> Self { value.0 } +} + +impl Serialize for PeerAddress { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for PeerAddress { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct PeerAddressVisitor; + + impl<'de> serde::de::Visitor<'de> for PeerAddressVisitor { + type Value = PeerAddress; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representation of peer id.") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if value.len() > 100 { + return Err(serde::de::Error::invalid_length( + value.len(), + &"peer id cannot exceed 100 characters.", + )); + } + + Ok(mm2_libp2p::PeerId::from_str(value) + .map_err(serde::de::Error::custom)? + .into()) + } + + fn visit_string(self, value: String) -> Result + where + E: serde::de::Error, + { + self.visit_str(&value) + } + } + + deserializer.deserialize_str(PeerAddressVisitor) + } +} + impl HealthcheckMessage { pub(crate) fn generate_message( ctx: &MmArc, - target_peer: PeerId, + target_peer: PeerAddress, is_a_reply: bool, expires_in_seconds: i64, ) -> Result { let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); - let sender_peer = p2p_ctx.peer_id(); + let sender_peer = p2p_ctx.peer_id().into(); let keypair = p2p_ctx.keypair(); let sender_public_key = keypair.public().encode_protobuf(); @@ -53,7 +118,11 @@ impl HealthcheckMessage { Ok(Self { signature, data }) } - pub(crate) fn is_received_message_valid(&self, my_peer_id: PeerId, healthcheck_config: &HealthcheckConfig) -> bool { + pub(crate) fn is_received_message_valid( + &self, + my_peer_address: PeerAddress, + healthcheck_config: &HealthcheckConfig, + ) -> bool { let now = Utc::now().timestamp(); let remaining_expiration_seconds = u64::try_from(self.data.expires_at - now).unwrap_or(0); @@ -72,10 +141,10 @@ impl HealthcheckMessage { return false; } - if self.data.target_peer != my_peer_id { + if self.data.target_peer != my_peer_address { log::debug!( "`target_peer` doesn't match with our peer address. Our address: '{}', healthcheck `target_peer`: '{}'.", - my_peer_id, + my_peer_address, self.data.target_peer ); return false; @@ -87,7 +156,7 @@ impl HealthcheckMessage { return false }; - if self.data.sender_peer != public_key.to_peer_id() { + if self.data.sender_peer != public_key.to_peer_id().into() { log::debug!("`sender_peer` and `sender_public_key` doesn't belong each other."); return false; @@ -117,18 +186,16 @@ impl HealthcheckMessage { pub(crate) fn should_reply(&self) -> bool { !self.data.is_a_reply } #[inline] - pub(crate) fn sender_peer(&self) -> PeerId { self.data.sender_peer } + pub(crate) fn sender_peer(&self) -> PeerAddress { self.data.sender_peer } } #[derive(Debug, Deserialize, Serialize)] #[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] struct HealthcheckData { - #[serde(deserialize_with = "deserialize_peer_id", serialize_with = "serialize_peer_id")] - sender_peer: PeerId, + sender_peer: PeerAddress, #[serde(deserialize_with = "deserialize_bytes")] sender_public_key: Vec, - #[serde(deserialize_with = "deserialize_peer_id", serialize_with = "serialize_peer_id")] - target_peer: PeerId, + target_peer: PeerAddress, expires_at: i64, is_a_reply: bool, } @@ -139,59 +206,13 @@ impl HealthcheckData { } #[inline] -pub fn peer_healthcheck_topic(peer_id: &PeerId) -> String { - pub_sub_topic(PEER_HEALTHCHECK_PREFIX, &peer_id.to_string()) +pub fn peer_healthcheck_topic(peer_address: &PeerAddress) -> String { + pub_sub_topic(PEER_HEALTHCHECK_PREFIX, &peer_address.to_string()) } #[derive(Deserialize)] pub struct RequestPayload { - #[serde(deserialize_with = "deserialize_peer_id")] - peer_id: PeerId, -} - -fn deserialize_peer_id<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - struct PeerIdVisitor; - - impl<'de> serde::de::Visitor<'de> for PeerIdVisitor { - type Value = PeerId; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string representation of peer id.") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - if value.len() > 100 { - return Err(serde::de::Error::invalid_length( - value.len(), - &"peer id cannot exceed 100 characters.", - )); - } - - PeerId::from_str(value).map_err(serde::de::Error::custom) - } - - fn visit_string(self, value: String) -> Result - where - E: serde::de::Error, - { - self.visit_str(&value) - } - } - - deserializer.deserialize_str(PeerIdVisitor) -} - -fn serialize_peer_id(peer_id: &PeerId, s: S) -> Result -where - S: serde::Serializer, -{ - s.serialize_str(&peer_id.to_string()) + peer_address: PeerAddress, } fn deserialize_bytes<'de, D>(deserializer: D) -> Result, D::Error> @@ -260,17 +281,17 @@ pub async fn peer_connection_healthcheck_rpc( // This is unrelated to the timeout logic. let address_record_exp = Duration::from_secs(ctx.health_checker.config.timeout_secs); - let target_peer_id = req.peer_id; + let target_peer_address = req.peer_address; let p2p_ctx = P2PContext::fetch_from_mm_arc(&ctx); - if target_peer_id == p2p_ctx.peer_id() { + if target_peer_address == p2p_ctx.peer_id().into() { // That's us, so return true. return Ok(true); } let message = HealthcheckMessage::generate_message( &ctx, - target_peer_id, + target_peer_address, false, ctx.health_checker .config @@ -289,10 +310,15 @@ pub async fn peer_connection_healthcheck_rpc( { let mut book = ctx.health_checker.response_handler.lock().unwrap(); book.clear_expired_entries(); - book.insert(target_peer_id.to_string(), tx, address_record_exp); + book.insert(target_peer_address.to_string(), tx, address_record_exp); } - broadcast_p2p_msg(&ctx, peer_healthcheck_topic(&target_peer_id), encoded_message, None); + broadcast_p2p_msg( + &ctx, + peer_healthcheck_topic(&target_peer_address), + encoded_message, + None, + ); let timeout_duration = Duration::from_secs(ctx.health_checker.config.timeout_secs); Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) @@ -316,16 +342,16 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li "Couldn't decode healthcheck message" ); - let sender_peer = data.sender_peer().to_owned(); + let sender_peer = data.sender_peer(); let ctx = ctx.clone(); // Pass the remaining work to another thread to free up this one as soon as possible, // so KDF can handle a high amount of healthcheck messages more efficiently. ctx.spawner().spawn(async move { - let my_peer_id = P2PContext::fetch_from_mm_arc(&ctx).peer_id(); + let my_peer_address = P2PContext::fetch_from_mm_arc(&ctx).peer_id().into(); - if !data.is_received_message_valid(my_peer_id, &ctx.health_checker.config) { + if !data.is_received_message_valid(my_peer_address, &ctx.health_checker.config) { log::error!("Received an invalid healthcheck message."); log::debug!("Message context: {:?}", data); return; @@ -389,9 +415,9 @@ mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); } - fn create_test_peer_id() -> PeerId { + fn create_test_peer_address() -> PeerAddress { let keypair = mm2_libp2p::Keypair::generate_ed25519(); - PeerId::from(keypair.public()) + mm2_libp2p::PeerId::from(keypair.public()).into() } fn ctx() -> MmArc { @@ -410,16 +436,32 @@ mod tests { ctx } + cross_test!(test_peer_address, { + #[derive(Deserialize, Serialize)] + struct PeerAddressTest { + peer_address: PeerAddress, + } + + let address_str = "12D3KooWEtuv7kmgGCC7oAQ31hB7AR5KkhT3eEWB2bP2roo3M7rY"; + let json_content = format!("{{\"peer_address\": \"{address_str}\"}}"); + let address_struct: PeerAddressTest = serde_json::from_str(&json_content).unwrap(); + + let actual_peer_id = mm2_libp2p::PeerId::from_str(address_str).unwrap(); + let deserialized_peer_id: mm2_libp2p::PeerId = address_struct.peer_address.into(); + + assert_eq!(deserialized_peer_id, actual_peer_id); + }); + cross_test!(test_valid_message, { let ctx = ctx(); - let target_peer = create_test_peer_id(); + let target_peer = create_test_peer_address(); let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); assert!(message.is_received_message_valid(target_peer, &ctx.health_checker.config)); }); cross_test!(test_corrupted_messages, { let ctx = ctx(); - let target_peer = create_test_peer_id(); + let target_peer = create_test_peer_address(); let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); message.data.expires_at += 1; @@ -443,14 +485,14 @@ mod tests { cross_test!(test_expired_message, { let ctx = ctx(); - let target_peer = create_test_peer_id(); + let target_peer = create_test_peer_address(); let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, -1).unwrap(); assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); }); cross_test!(test_encode_decode, { let ctx = ctx(); - let target_peer = create_test_peer_id(); + let target_peer = create_test_peer_address(); let original = HealthcheckMessage::generate_message(&ctx, target_peer, false, 10).unwrap(); diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index f4d8b4e0e6..cd055132a5 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -36,7 +36,7 @@ use mm2_metrics::mm_gauge; use mm2_net::network_event::NetworkEvent; use mm2_net::p2p::P2PContext; use rpc_task::RpcTaskError; -use serde_json::{self as json}; +use serde_json as json; use std::convert::TryInto; use std::io; use std::path::PathBuf; @@ -646,7 +646,7 @@ pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { ctx.spawner().spawn(fut); // Listen for health check messages. - subscribe_to_topic(&ctx, peer_healthcheck_topic(&peer_id)); + subscribe_to_topic(&ctx, peer_healthcheck_topic(&peer_id.into())); Ok(()) } diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 48606435b3..a248d34730 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -853,9 +853,7 @@ pub fn nft_dev_conf() -> Json { }) } -fn set_chain_id(conf: &mut Json, chain_id: u64) { - conf["chain_id"] = json!(chain_id); -} +fn set_chain_id(conf: &mut Json, chain_id: u64) { conf["chain_id"] = json!(chain_id); } pub fn eth_sepolia_conf() -> Json { json!({ @@ -1840,14 +1838,14 @@ pub async fn enable_qrc20( json::from_str(&electrum.1).unwrap() } -pub async fn peer_connection_healthcheck(mm: &MarketMakerIt, peer_id: &str) -> Json { +pub async fn peer_connection_healthcheck(mm: &MarketMakerIt, peer_address: &str) -> Json { let response = mm .rpc(&json!({ "userpass": mm.userpass, "method": "peer_connection_healthcheck", "mmrpc": "2.0", "params": { - "peer_id": peer_id + "peer_address": peer_address } })) .await @@ -2927,7 +2925,10 @@ pub async fn enable_tendermint( tx_history: bool, ) -> Json { let ibc_requests: Vec<_> = ibc_assets.iter().map(|ticker| json!({ "ticker": ticker })).collect(); - let nodes: Vec = rpc_urls.iter().map(|u| json!({"url": u, "komodo_proxy": false })).collect(); + let nodes: Vec = rpc_urls + .iter() + .map(|u| json!({"url": u, "komodo_proxy": false })) + .collect(); let request = json!({ "userpass": mm.userpass, @@ -2964,7 +2965,10 @@ pub async fn enable_tendermint_without_balance( tx_history: bool, ) -> Json { let ibc_requests: Vec<_> = ibc_assets.iter().map(|ticker| json!({ "ticker": ticker })).collect(); - let nodes: Vec = rpc_urls.iter().map(|u| json!({"url": u, "komodo_proxy": false })).collect(); + let nodes: Vec = rpc_urls + .iter() + .map(|u| json!({"url": u, "komodo_proxy": false })) + .collect(); let request = json!({ "userpass": mm.userpass, From 8727e524e4803bcd1f911b271b39dcc5b8972106 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 26 Sep 2024 12:36:12 +0300 Subject: [PATCH 34/37] switch back to async mutexa again Signed-off-by: onur-ozkan --- mm2src/mm2_core/src/mm_ctx.rs | 10 +++++----- mm2src/mm2_main/src/lp_healthcheck.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index b9bc8ae3a7..54b1b0fb6b 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -6,6 +6,7 @@ use common::{executor::{abortable_queue::{AbortableQueue, WeakSpawner}, graceful_shutdown, AbortSettings, AbortableSystem, SpawnAbortable, SpawnFuture}, expirable_map::ExpirableMap}; use futures::channel::oneshot; +use futures::lock::Mutex as AsyncMutex; use gstuff::{try_s, Constructible, ERR, ERRL}; use lazy_static::lazy_static; use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; @@ -39,7 +40,6 @@ cfg_native! { use std::net::{IpAddr, SocketAddr, AddrParseError}; use std::path::{Path, PathBuf}; use std::sync::MutexGuard; - use futures::lock::Mutex as AsyncMutex; } /// Default interval to export and record metrics to log. @@ -47,9 +47,9 @@ const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; pub struct HealthChecker { /// Links the RPC context to the P2P context to handle health check responses. - pub response_handler: Mutex>>, + pub response_handler: AsyncMutex>>, /// This is used to record healthcheck sender peers in an expirable manner to prevent DDoS attacks. - pub ddos_shield: Mutex>, + pub ddos_shield: AsyncMutex>, pub config: HealthcheckConfig, } @@ -226,8 +226,8 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: Constructible::default(), health_checker: HealthChecker { - response_handler: Mutex::new(ExpirableMap::default()), - ddos_shield: Mutex::new(ExpirableMap::default()), + response_handler: AsyncMutex::new(ExpirableMap::default()), + ddos_shield: AsyncMutex::new(ExpirableMap::default()), config: HealthcheckConfig::default(), }, } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 663b57a76d..be1f80daa7 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -308,7 +308,7 @@ pub async fn peer_connection_healthcheck_rpc( let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); { - let mut book = ctx.health_checker.response_handler.lock().unwrap(); + let mut book = ctx.health_checker.response_handler.lock().await; book.clear_expired_entries(); book.insert(target_peer_address.to_string(), tx, address_record_exp); } @@ -357,7 +357,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li return; }; - let mut ddos_shield = ctx.health_checker.ddos_shield.lock().unwrap(); + let mut ddos_shield = ctx.health_checker.ddos_shield.lock().await; ddos_shield.clear_expired_entries(); if ddos_shield .insert( @@ -390,7 +390,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li broadcast_p2p_msg(&ctx, topic, encoded_msg, None); } else { // The requested peer is healthy; signal the response channel. - let mut response_handler = ctx.health_checker.response_handler.lock().unwrap(); + let mut response_handler = ctx.health_checker.response_handler.lock().await; if let Some(tx) = response_handler.remove(&sender_peer.to_string()) { if tx.send(()).is_err() { log::error!("Result channel isn't present for peer '{sender_peer}'."); From d2d9fff4685ce4f5edb41fbe96d61a0a667844f9 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 30 Sep 2024 18:19:06 +0300 Subject: [PATCH 35/37] implement reusable messages Signed-off-by: onur-ozkan --- mm2src/common/expirable_map.rs | 11 +++++ mm2src/mm2_core/src/mm_ctx.rs | 6 --- mm2src/mm2_main/src/lp_healthcheck.rs | 68 ++++++++++++++++++--------- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/mm2src/common/expirable_map.rs b/mm2src/common/expirable_map.rs index 9b85dea84c..98f7e53dca 100644 --- a/mm2src/common/expirable_map.rs +++ b/mm2src/common/expirable_map.rs @@ -42,6 +42,17 @@ impl ExpirableMap { #[inline] pub fn get(&mut self, k: &K) -> Option<&V> { self.0.get(k).map(|v| &v.value) } + /// Returns the associated value if present and has longer ttl than the given one. + pub fn get_if_has_longer_life_than(&mut self, k: &K, min_ttl: Duration) -> Option<&V> { + self.0.get(k).and_then(|entry| { + if entry.expires_at > Instant::now() + min_ttl { + Some(&entry.value) + } else { + None + } + }) + } + /// Inserts a key-value pair with an expiration duration. /// /// If a value already exists for the given key, it will be updated and then diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 54b1b0fb6b..6a0473186a 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -48,16 +48,12 @@ const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; pub struct HealthChecker { /// Links the RPC context to the P2P context to handle health check responses. pub response_handler: AsyncMutex>>, - /// This is used to record healthcheck sender peers in an expirable manner to prevent DDoS attacks. - pub ddos_shield: AsyncMutex>, pub config: HealthcheckConfig, } #[derive(Debug, Deserialize)] #[serde(default)] pub struct HealthcheckConfig { - /// Required time (millisecond) to wait before processing another healthcheck request from the same peer. - pub blocking_ms_for_per_address: u64, /// Lifetime of the message. /// Do not change this unless you know what you are doing. pub message_expiration_secs: u64, @@ -68,7 +64,6 @@ pub struct HealthcheckConfig { impl Default for HealthcheckConfig { fn default() -> Self { Self { - blocking_ms_for_per_address: 750, message_expiration_secs: 10, timeout_secs: 10, } @@ -227,7 +222,6 @@ impl MmCtx { async_sqlite_connection: Constructible::default(), health_checker: HealthChecker { response_handler: AsyncMutex::new(ExpirableMap::default()), - ddos_shield: AsyncMutex::new(ExpirableMap::default()), config: HealthcheckConfig::default(), }, } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index be1f80daa7..26c469a085 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,10 +1,13 @@ use async_std::prelude::FutureExt; use chrono::Utc; use common::executor::SpawnFuture; +use common::expirable_map::ExpirableMap; use common::{log, HttpStatusCode, StatusCode}; use derive_more::Display; use futures::channel::oneshot::{self, Receiver, Sender}; +use futures::lock::Mutex as AsyncMutex; use instant::Duration; +use lazy_static::lazy_static; use mm2_core::mm_ctx::{HealthcheckConfig, MmArc}; use mm2_err_handle::prelude::MmError; use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, TopicPrefix}; @@ -20,7 +23,7 @@ use crate::lp_network::broadcast_p2p_msg; pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] pub(crate) struct HealthcheckMessage { #[serde(deserialize_with = "deserialize_bytes")] @@ -189,7 +192,7 @@ impl HealthcheckMessage { pub(crate) fn sender_peer(&self) -> PeerAddress { self.data.sender_peer } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] struct HealthcheckData { sender_peer: PeerAddress, @@ -325,6 +328,13 @@ pub async fn peer_connection_healthcheck_rpc( } pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_libp2p::GossipsubMessage) { + lazy_static! { + static ref RECENTLY_GENERATED_MESSAGES: AsyncMutex> = + AsyncMutex::new(ExpirableMap::new()); + } + + const MIN_DURATION_FOR_REUSABLE_MSG: Duration = Duration::from_secs(6); + macro_rules! try_or_return { ($exp:expr, $msg: expr) => { match $exp { @@ -357,36 +367,50 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li return; }; - let mut ddos_shield = ctx.health_checker.ddos_shield.lock().await; - ddos_shield.clear_expired_entries(); - if ddos_shield - .insert( - sender_peer.to_string(), - (), - Duration::from_millis(ctx.health_checker.config.blocking_ms_for_per_address), - ) - .is_some() - { - log::warn!("Peer '{sender_peer}' exceeded the healthcheck blocking time, skipping their message."); - return; - } - drop(ddos_shield); - if data.should_reply() { // Reply the message so they know we are healthy. - let topic = peer_healthcheck_topic(&sender_peer); + // If message has longer life than `MIN_DURATION_FOR_REUSABLE_MSG`, we are reusing them to + // reduce the message generation overhead under high pressure. + let mut messages = RECENTLY_GENERATED_MESSAGES.lock().await; + messages.clear_expired_entries(); - let msg = try_or_return!( - HealthcheckMessage::generate_message(&ctx, sender_peer, true, 10), - "Couldn't generate the healthcheck message, this is very unusual!" - ); + let message_map_key = sender_peer.to_string(); + + let expiration_secs = ctx + .health_checker + .config + .message_expiration_secs + .try_into() + .unwrap_or(HealthcheckConfig::default().message_expiration_secs as i64); + + let msg = match messages + .get_if_has_longer_life_than(&message_map_key, MIN_DURATION_FOR_REUSABLE_MSG) + .cloned() + { + Some(t) => t, + None => { + let msg = try_or_return!( + HealthcheckMessage::generate_message(&ctx, sender_peer, true, expiration_secs), + "Couldn't generate the healthcheck message, this is very unusual!" + ); + + messages.insert( + message_map_key, + msg.clone(), + Duration::from_secs(expiration_secs as u64), + ); + + msg + }, + }; let encoded_msg = try_or_return!( msg.encode(), "Couldn't encode healthcheck message, this is very unusual!" ); + let topic = peer_healthcheck_topic(&sender_peer); broadcast_p2p_msg(&ctx, topic, encoded_msg, None); } else { // The requested peer is healthy; signal the response channel. From b98832c2eb46360588117555a4330b90ae69dfcb Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 30 Sep 2024 21:09:19 +0300 Subject: [PATCH 36/37] reduce the healthcheck overhead Signed-off-by: onur-ozkan --- mm2src/common/expirable_map.rs | 33 +++-- mm2src/mm2_core/src/mm_ctx.rs | 41 +----- mm2src/mm2_main/src/lp_healthcheck.rs | 199 +++++++++++--------------- 3 files changed, 102 insertions(+), 171 deletions(-) diff --git a/mm2src/common/expirable_map.rs b/mm2src/common/expirable_map.rs index 98f7e53dca..e9fe7f0b4f 100644 --- a/mm2src/common/expirable_map.rs +++ b/mm2src/common/expirable_map.rs @@ -14,9 +14,26 @@ pub struct ExpirableEntry { } impl ExpirableEntry { + #[inline(always)] + pub fn new(v: V, exp: Duration) -> Self { + Self { + expires_at: Instant::now() + exp, + value: v, + } + } + + #[inline(always)] pub fn get_element(&self) -> &V { &self.value } + #[inline(always)] + pub fn update_value(&mut self, v: V) { self.value = v } + + #[inline(always)] pub fn update_expiration(&mut self, expires_at: Instant) { self.expires_at = expires_at } + + /// Checks whether entry has longer ttl than the given one. + #[inline(always)] + pub fn has_longer_life_than(&self, min_ttl: Duration) -> bool { self.expires_at > Instant::now() + min_ttl } } impl Default for ExpirableMap { @@ -42,26 +59,12 @@ impl ExpirableMap { #[inline] pub fn get(&mut self, k: &K) -> Option<&V> { self.0.get(k).map(|v| &v.value) } - /// Returns the associated value if present and has longer ttl than the given one. - pub fn get_if_has_longer_life_than(&mut self, k: &K, min_ttl: Duration) -> Option<&V> { - self.0.get(k).and_then(|entry| { - if entry.expires_at > Instant::now() + min_ttl { - Some(&entry.value) - } else { - None - } - }) - } - /// Inserts a key-value pair with an expiration duration. /// /// If a value already exists for the given key, it will be updated and then /// the old one will be returned. pub fn insert(&mut self, k: K, v: V, exp: Duration) -> Option { - let entry = ExpirableEntry { - expires_at: Instant::now() + exp, - value: v, - }; + let entry = ExpirableEntry::new(v, exp); self.0.insert(k, entry).map(|v| v.value) } diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 6a0473186a..0bea4abd1f 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -13,7 +13,6 @@ use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; use mm2_metrics::{MetricsArc, MetricsOps}; use primitives::hash::H160; use rand::Rng; -use serde::Deserialize; use serde_json::{self as json, Value as Json}; use shared_ref_counter::{SharedRc, WeakRc}; use std::any::Any; @@ -45,31 +44,6 @@ cfg_native! { /// Default interval to export and record metrics to log. const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; -pub struct HealthChecker { - /// Links the RPC context to the P2P context to handle health check responses. - pub response_handler: AsyncMutex>>, - pub config: HealthcheckConfig, -} - -#[derive(Debug, Deserialize)] -#[serde(default)] -pub struct HealthcheckConfig { - /// Lifetime of the message. - /// Do not change this unless you know what you are doing. - pub message_expiration_secs: u64, - /// Maximum time (milliseconds) to wait for healthcheck response. - pub timeout_secs: u64, -} - -impl Default for HealthcheckConfig { - fn default() -> Self { - Self { - message_expiration_secs: 10, - timeout_secs: 10, - } - } -} - /// MarketMaker state, shared between the various MarketMaker threads. /// /// Every MarketMaker has one and only one instance of `MmCtx`. @@ -170,7 +144,8 @@ pub struct MmCtx { /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] pub async_sqlite_connection: Constructible>>, - pub health_checker: HealthChecker, + /// Links the RPC context to the P2P context to handle health check responses. + pub healthcheck_response_handler: AsyncMutex>>, } impl MmCtx { @@ -220,10 +195,7 @@ impl MmCtx { nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: Constructible::default(), - health_checker: HealthChecker { - response_handler: AsyncMutex::new(ExpirableMap::default()), - config: HealthcheckConfig::default(), - }, + healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), } } @@ -809,13 +781,6 @@ impl MmCtxBuilder { .expect("Invalid json value in 'event_stream_configuration'."); ctx.event_stream_configuration = Some(event_stream_configuration); } - - let healthcheck_config = &ctx.conf["healthcheck_config"]; - if !healthcheck_config.is_null() { - let healthcheck_config: HealthcheckConfig = - json::from_value(healthcheck_config.clone()).expect("Invalid json value in 'healthcheck_config'."); - ctx.health_checker.config = healthcheck_config; - } } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 26c469a085..565752d341 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,28 +1,34 @@ use async_std::prelude::FutureExt; use chrono::Utc; use common::executor::SpawnFuture; -use common::expirable_map::ExpirableMap; +use common::expirable_map::ExpirableEntry; use common::{log, HttpStatusCode, StatusCode}; use derive_more::Display; use futures::channel::oneshot::{self, Receiver, Sender}; -use futures::lock::Mutex as AsyncMutex; -use instant::Duration; +use instant::{Duration, Instant}; use lazy_static::lazy_static; -use mm2_core::mm_ctx::{HealthcheckConfig, MmArc}; +use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, TopicPrefix}; use mm2_net::p2p::P2PContext; use ser_error_derive::SerializeErrorType; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; -use std::convert::TryInto; -use std::num::TryFromIntError; use std::str::FromStr; +use std::sync::Mutex; use crate::lp_network::broadcast_p2p_msg; pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; +const fn healthcheck_message_exp_secs() -> u64 { + #[cfg(test)] + return 3; + + #[cfg(not(test))] + 10 +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] pub(crate) struct HealthcheckMessage { @@ -97,12 +103,7 @@ impl<'de> Deserialize<'de> for PeerAddress { } impl HealthcheckMessage { - pub(crate) fn generate_message( - ctx: &MmArc, - target_peer: PeerAddress, - is_a_reply: bool, - expires_in_seconds: i64, - ) -> Result { + pub(crate) fn generate_message(ctx: &MmArc, is_a_reply: bool) -> Result { let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); let sender_peer = p2p_ctx.peer_id().into(); let keypair = p2p_ctx.keypair(); @@ -111,8 +112,7 @@ impl HealthcheckMessage { let data = HealthcheckData { sender_peer, sender_public_key, - target_peer, - expires_at: Utc::now().timestamp() + expires_in_seconds, + expires_at: Utc::now().timestamp() + healthcheck_message_exp_secs() as i64, is_a_reply, }; @@ -121,11 +121,43 @@ impl HealthcheckMessage { Ok(Self { signature, data }) } - pub(crate) fn is_received_message_valid( - &self, - my_peer_address: PeerAddress, - healthcheck_config: &HealthcheckConfig, - ) -> bool { + fn generate_or_use_cached_message(ctx: &MmArc) -> Result { + const MIN_DURATION_FOR_REUSABLE_MSG: Duration = Duration::from_secs(5); + + lazy_static! { + static ref RECENTLY_GENERATED_MESSAGE: Mutex> = + Mutex::new(ExpirableEntry::new( + // Using dummy values in order to initialize `HealthcheckMessage` context. + HealthcheckMessage { + signature: vec![], + data: HealthcheckData { + sender_peer: create_test_peer_address(), + sender_public_key: vec![], + expires_at: 0, + is_a_reply: false, + }, + }, + Duration::from_secs(0) + )); + } + + // If recently generated message has longer life than `MIN_DURATION_FOR_REUSABLE_MSG`, we can reuse it to + // reduce the message generation overhead under high pressure. + let mut mutexed_msg = RECENTLY_GENERATED_MESSAGE.lock().unwrap(); + + if mutexed_msg.has_longer_life_than(MIN_DURATION_FOR_REUSABLE_MSG) { + Ok(mutexed_msg.get_element().clone()) + } else { + let new_msg = HealthcheckMessage::generate_message(ctx, true)?; + + mutexed_msg.update_value(new_msg.clone()); + mutexed_msg.update_expiration(Instant::now() + Duration::from_secs(healthcheck_message_exp_secs())); + + Ok(new_msg) + } + } + + pub(crate) fn is_received_message_valid(&self) -> bool { let now = Utc::now().timestamp(); let remaining_expiration_seconds = u64::try_from(self.data.expires_at - now).unwrap_or(0); @@ -135,7 +167,7 @@ impl HealthcheckMessage { self.data.expires_at ); return false; - } else if remaining_expiration_seconds > healthcheck_config.message_expiration_secs { + } else if remaining_expiration_seconds > healthcheck_message_exp_secs() { log::debug!( "Healthcheck message have too high expiration time.\nMax allowed expiration seconds: {}\nReceived message expiration seconds: {}", self.data.expires_at, @@ -144,15 +176,6 @@ impl HealthcheckMessage { return false; } - if self.data.target_peer != my_peer_address { - log::debug!( - "`target_peer` doesn't match with our peer address. Our address: '{}', healthcheck `target_peer`: '{}'.", - my_peer_address, - self.data.target_peer - ); - return false; - } - let Ok(public_key) = Libp2pPublic::try_decode_protobuf(&self.data.sender_public_key) else { log::debug!("Couldn't decode public key from the healthcheck message."); @@ -198,7 +221,6 @@ struct HealthcheckData { sender_peer: PeerAddress, #[serde(deserialize_with = "deserialize_bytes")] sender_public_key: Vec, - target_peer: PeerAddress, expires_at: i64, is_a_reply: bool, } @@ -282,7 +304,7 @@ pub async fn peer_connection_healthcheck_rpc( ) -> Result> { // When things go awry, we want records to clear themselves to keep the memory clean of unused data. // This is unrelated to the timeout logic. - let address_record_exp = Duration::from_secs(ctx.health_checker.config.timeout_secs); + let address_record_exp = Duration::from_secs(healthcheck_message_exp_secs()); let target_peer_address = req.peer_address; @@ -292,17 +314,8 @@ pub async fn peer_connection_healthcheck_rpc( return Ok(true); } - let message = HealthcheckMessage::generate_message( - &ctx, - target_peer_address, - false, - ctx.health_checker - .config - .message_expiration_secs - .try_into() - .map_err(|e: TryFromIntError| HealthcheckRpcError::Internal { reason: e.to_string() })?, - ) - .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; + let message = HealthcheckMessage::generate_message(&ctx, false) + .map_err(|reason| HealthcheckRpcError::MessageGenerationFailed { reason })?; let encoded_message = message .encode() @@ -311,7 +324,7 @@ pub async fn peer_connection_healthcheck_rpc( let (tx, rx): (Sender<()>, Receiver<()>) = oneshot::channel(); { - let mut book = ctx.health_checker.response_handler.lock().await; + let mut book = ctx.healthcheck_response_handler.lock().await; book.clear_expired_entries(); book.insert(target_peer_address.to_string(), tx, address_record_exp); } @@ -323,18 +336,11 @@ pub async fn peer_connection_healthcheck_rpc( None, ); - let timeout_duration = Duration::from_secs(ctx.health_checker.config.timeout_secs); + let timeout_duration = Duration::from_secs(healthcheck_message_exp_secs()); Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_libp2p::GossipsubMessage) { - lazy_static! { - static ref RECENTLY_GENERATED_MESSAGES: AsyncMutex> = - AsyncMutex::new(ExpirableMap::new()); - } - - const MIN_DURATION_FOR_REUSABLE_MSG: Duration = Duration::from_secs(6); - macro_rules! try_or_return { ($exp:expr, $msg: expr) => { match $exp { @@ -359,9 +365,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li // Pass the remaining work to another thread to free up this one as soon as possible, // so KDF can handle a high amount of healthcheck messages more efficiently. ctx.spawner().spawn(async move { - let my_peer_address = P2PContext::fetch_from_mm_arc(&ctx).peer_id().into(); - - if !data.is_received_message_valid(my_peer_address, &ctx.health_checker.config) { + if !data.is_received_message_valid() { log::error!("Received an invalid healthcheck message."); log::debug!("Message context: {:?}", data); return; @@ -370,40 +374,10 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li if data.should_reply() { // Reply the message so they know we are healthy. - // If message has longer life than `MIN_DURATION_FOR_REUSABLE_MSG`, we are reusing them to - // reduce the message generation overhead under high pressure. - let mut messages = RECENTLY_GENERATED_MESSAGES.lock().await; - messages.clear_expired_entries(); - - let message_map_key = sender_peer.to_string(); - - let expiration_secs = ctx - .health_checker - .config - .message_expiration_secs - .try_into() - .unwrap_or(HealthcheckConfig::default().message_expiration_secs as i64); - - let msg = match messages - .get_if_has_longer_life_than(&message_map_key, MIN_DURATION_FOR_REUSABLE_MSG) - .cloned() - { - Some(t) => t, - None => { - let msg = try_or_return!( - HealthcheckMessage::generate_message(&ctx, sender_peer, true, expiration_secs), - "Couldn't generate the healthcheck message, this is very unusual!" - ); - - messages.insert( - message_map_key, - msg.clone(), - Duration::from_secs(expiration_secs as u64), - ); - - msg - }, - }; + let msg = try_or_return!( + HealthcheckMessage::generate_or_use_cached_message(&ctx), + "Couldn't generate the healthcheck message, this is very unusual!" + ); let encoded_msg = try_or_return!( msg.encode(), @@ -414,7 +388,7 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li broadcast_p2p_msg(&ctx, topic, encoded_msg, None); } else { // The requested peer is healthy; signal the response channel. - let mut response_handler = ctx.health_checker.response_handler.lock().await; + let mut response_handler = ctx.healthcheck_response_handler.lock().await; if let Some(tx) = response_handler.remove(&sender_peer.to_string()) { if tx.send(()).is_err() { log::error!("Result channel isn't present for peer '{sender_peer}'."); @@ -426,6 +400,11 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li }); } +fn create_test_peer_address() -> PeerAddress { + let keypair = mm2_libp2p::Keypair::generate_ed25519(); + mm2_libp2p::PeerId::from(keypair.public()).into() +} + #[cfg(any(test, target_arch = "wasm32"))] mod tests { use super::*; @@ -439,11 +418,6 @@ mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); } - fn create_test_peer_address() -> PeerAddress { - let keypair = mm2_libp2p::Keypair::generate_ed25519(); - mm2_libp2p::PeerId::from(keypair.public()).into() - } - fn ctx() -> MmArc { let ctx = mm_ctx_with_iguana(Some("dummy-value")); let p2p_key = { @@ -478,47 +452,36 @@ mod tests { cross_test!(test_valid_message, { let ctx = ctx(); - let target_peer = create_test_peer_address(); - let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - assert!(message.is_received_message_valid(target_peer, &ctx.health_checker.config)); + let message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); + assert!(message.is_received_message_valid()); }); cross_test!(test_corrupted_messages, { let ctx = ctx(); - let target_peer = create_test_peer_address(); - let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); + let mut message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); message.data.expires_at += 1; - assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); + assert!(!message.is_received_message_valid()); - let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); + let mut message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); message.data.is_a_reply = !message.data.is_a_reply; - assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); - - let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - message.data.sender_peer = message.data.target_peer; - assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); + assert!(!message.is_received_message_valid()); - let mut message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - message.data.target_peer = message.data.sender_peer; - assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); - - let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, 5).unwrap(); - assert!(!message.is_received_message_valid(message.data.sender_peer, &ctx.health_checker.config)); + let mut message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); + message.data.sender_peer = create_test_peer_address(); + assert!(!message.is_received_message_valid()); }); cross_test!(test_expired_message, { let ctx = ctx(); - let target_peer = create_test_peer_address(); - let message = HealthcheckMessage::generate_message(&ctx, target_peer, false, -1).unwrap(); - assert!(!message.is_received_message_valid(target_peer, &ctx.health_checker.config)); + let message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); + common::executor::Timer::sleep(3.).await; + assert!(!message.is_received_message_valid()); }); cross_test!(test_encode_decode, { let ctx = ctx(); - let target_peer = create_test_peer_address(); - - let original = HealthcheckMessage::generate_message(&ctx, target_peer, false, 10).unwrap(); + let original = HealthcheckMessage::generate_message(&ctx, false).unwrap(); let encoded = original.encode().unwrap(); assert!(!encoded.is_empty()); From 3169081455a91e8b78d34cbc38d9044315a416ba Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 2 Oct 2024 14:06:58 +0300 Subject: [PATCH 37/37] use more precise error type and remove sender_peer from hc message Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_healthcheck.rs | 145 +++++++++++++++----------- 1 file changed, 83 insertions(+), 62 deletions(-) diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 565752d341..722bc21402 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -102,17 +102,37 @@ impl<'de> Deserialize<'de> for PeerAddress { } } +#[derive(Debug, Display)] +enum SignValidationError { + #[display( + fmt = "Healthcheck message is expired. Current time in UTC: {now_secs}, healthcheck `expires_at` in UTC: {expires_at_secs}" + )] + Expired { now_secs: u64, expires_at_secs: u64 }, + #[display( + fmt = "Healthcheck message have too high expiration time. Max allowed expiration seconds: {max_allowed_expiration_secs}, received message expiration seconds: {remaining_expiration_secs}" + )] + LifetimeOverflow { + max_allowed_expiration_secs: u64, + remaining_expiration_secs: u64, + }, + #[display(fmt = "Public key is not valid.")] + InvalidPublicKey, + #[display(fmt = "Signature integrity doesn't match with the public key.")] + FakeSignature, + #[display(fmt = "Process failed unexpectedly due to this reason: {reason}")] + Internal { reason: String }, +} + impl HealthcheckMessage { pub(crate) fn generate_message(ctx: &MmArc, is_a_reply: bool) -> Result { let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); - let sender_peer = p2p_ctx.peer_id().into(); let keypair = p2p_ctx.keypair(); let sender_public_key = keypair.public().encode_protobuf(); let data = HealthcheckData { - sender_peer, sender_public_key, - expires_at: Utc::now().timestamp() + healthcheck_message_exp_secs() as i64, + expires_at_secs: u64::try_from(Utc::now().timestamp()).map_err(|e| e.to_string())? + + healthcheck_message_exp_secs(), is_a_reply, }; @@ -131,9 +151,8 @@ impl HealthcheckMessage { HealthcheckMessage { signature: vec![], data: HealthcheckData { - sender_peer: create_test_peer_address(), sender_public_key: vec![], - expires_at: 0, + expires_at_secs: 0, is_a_reply: false, }, }, @@ -157,49 +176,40 @@ impl HealthcheckMessage { } } - pub(crate) fn is_received_message_valid(&self) -> bool { - let now = Utc::now().timestamp(); - let remaining_expiration_seconds = u64::try_from(self.data.expires_at - now).unwrap_or(0); - - if remaining_expiration_seconds == 0 { - log::debug!( - "Healthcheck message is expired. Current time in UTC: {now}, healthcheck `expires_at` in UTC: {}", - self.data.expires_at - ); - return false; - } else if remaining_expiration_seconds > healthcheck_message_exp_secs() { - log::debug!( - "Healthcheck message have too high expiration time.\nMax allowed expiration seconds: {}\nReceived message expiration seconds: {}", - self.data.expires_at, - remaining_expiration_seconds, - ); - return false; + fn is_received_message_valid(&self) -> Result { + let now_secs = u64::try_from(Utc::now().timestamp()) + .map_err(|e| SignValidationError::Internal { reason: e.to_string() })?; + + let remaining_expiration_secs = self.data.expires_at_secs - now_secs; + + if remaining_expiration_secs == 0 { + return Err(SignValidationError::Expired { + now_secs, + expires_at_secs: self.data.expires_at_secs, + }); + } else if remaining_expiration_secs > healthcheck_message_exp_secs() { + return Err(SignValidationError::LifetimeOverflow { + max_allowed_expiration_secs: healthcheck_message_exp_secs(), + remaining_expiration_secs, + }); } let Ok(public_key) = Libp2pPublic::try_decode_protobuf(&self.data.sender_public_key) else { log::debug!("Couldn't decode public key from the healthcheck message."); - return false + return Err(SignValidationError::InvalidPublicKey); }; - if self.data.sender_peer != public_key.to_peer_id().into() { - log::debug!("`sender_peer` and `sender_public_key` doesn't belong each other."); - - return false; - } - - let Ok(encoded_message) = self.data.encode() else { - log::debug!("Couldn't encode healthcheck data."); - return false - }; + let encoded_message = self + .data + .encode() + .map_err(|e| SignValidationError::Internal { reason: e.to_string() })?; - let res = public_key.verify(&encoded_message, &self.signature); - - if !res { - log::debug!("Healthcheck isn't signed correctly."); + if public_key.verify(&encoded_message, &self.signature) { + Ok(public_key.to_peer_id().into()) + } else { + Err(SignValidationError::FakeSignature) } - - res } #[inline] @@ -210,18 +220,14 @@ impl HealthcheckMessage { #[inline] pub(crate) fn should_reply(&self) -> bool { !self.data.is_a_reply } - - #[inline] - pub(crate) fn sender_peer(&self) -> PeerAddress { self.data.sender_peer } } #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(any(test, target_arch = "wasm32"), derive(PartialEq))] struct HealthcheckData { - sender_peer: PeerAddress, #[serde(deserialize_with = "deserialize_bytes")] sender_public_key: Vec, - expires_at: i64, + expires_at_secs: u64, is_a_reply: bool, } @@ -358,17 +364,17 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li "Couldn't decode healthcheck message" ); - let sender_peer = data.sender_peer(); - let ctx = ctx.clone(); // Pass the remaining work to another thread to free up this one as soon as possible, // so KDF can handle a high amount of healthcheck messages more efficiently. ctx.spawner().spawn(async move { - if !data.is_received_message_valid() { - log::error!("Received an invalid healthcheck message."); - log::debug!("Message context: {:?}", data); - return; + let sender_peer = match data.is_received_message_valid() { + Ok(t) => t, + Err(e) => { + log::error!("Received an invalid healthcheck message. Error: {e}"); + return; + }, }; if data.should_reply() { @@ -400,13 +406,10 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li }); } -fn create_test_peer_address() -> PeerAddress { - let keypair = mm2_libp2p::Keypair::generate_ed25519(); - mm2_libp2p::PeerId::from(keypair.public()).into() -} - #[cfg(any(test, target_arch = "wasm32"))] mod tests { + use std::mem::discriminant; + use super::*; use common::cross_test; use crypto::CryptoCtx; @@ -453,30 +456,48 @@ mod tests { cross_test!(test_valid_message, { let ctx = ctx(); let message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); - assert!(message.is_received_message_valid()); + message.is_received_message_valid().unwrap(); }); cross_test!(test_corrupted_messages, { let ctx = ctx(); let mut message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); - message.data.expires_at += 1; - assert!(!message.is_received_message_valid()); + message.data.expires_at_secs += healthcheck_message_exp_secs() * 3; + assert_eq!( + discriminant(&message.is_received_message_valid().err().unwrap()), + discriminant(&SignValidationError::LifetimeOverflow { + max_allowed_expiration_secs: 0, + remaining_expiration_secs: 0 + }) + ); let mut message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); message.data.is_a_reply = !message.data.is_a_reply; - assert!(!message.is_received_message_valid()); + assert_eq!( + discriminant(&message.is_received_message_valid().err().unwrap()), + discriminant(&SignValidationError::FakeSignature) + ); let mut message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); - message.data.sender_peer = create_test_peer_address(); - assert!(!message.is_received_message_valid()); + message.data.sender_public_key.push(0); + assert_eq!( + discriminant(&message.is_received_message_valid().err().unwrap()), + discriminant(&SignValidationError::InvalidPublicKey) + ); }); cross_test!(test_expired_message, { let ctx = ctx(); let message = HealthcheckMessage::generate_message(&ctx, false).unwrap(); common::executor::Timer::sleep(3.).await; - assert!(!message.is_received_message_valid()); + assert_eq!( + discriminant(&message.is_received_message_valid().err().unwrap()), + discriminant(&SignValidationError::Expired { + now_secs: 0, + expires_at_secs: 0 + }) + ); }); cross_test!(test_encode_decode, {