diff --git a/protobuf/generated/rust/qaul.net.messaging.rs b/protobuf/generated/rust/qaul.net.messaging.rs index b1019620a..dbb3c00e1 100644 --- a/protobuf/generated/rust/qaul.net.messaging.rs +++ b/protobuf/generated/rust/qaul.net.messaging.rs @@ -30,7 +30,7 @@ pub struct Envelope { /// envelop payload #[derive(Clone, PartialEq, ::prost::Message)] pub struct EnvelopPayload { - #[prost(oneof = "envelop_payload::Payload", tags = "1, 2")] + #[prost(oneof = "envelop_payload::Payload", tags = "1, 2, 3")] pub payload: ::core::option::Option, } /// Nested message and enum types in `EnvelopPayload`. @@ -43,6 +43,9 @@ pub mod envelop_payload { /// DTN message #[prost(bytes, tag = "2")] Dtn(::prost::alloc::vec::Vec), + /// directed custody routed DTN message + #[prost(message, tag = "3")] + DtnRoutedV2(super::DtnRoutedV2), } } /// encrypted message data @@ -207,7 +210,7 @@ pub struct RtcMessage { /// DTN message #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Dtn { - #[prost(oneof = "dtn::Message", tags = "1, 2")] + #[prost(oneof = "dtn::Message", tags = "1, 2, 3")] pub message: ::core::option::Option, } /// Nested message and enum types in `Dtn`. @@ -220,8 +223,37 @@ pub mod dtn { /// message received response #[prost(message, tag = "2")] Response(super::DtnResponse), + /// directed custody routed DTN message + #[prost(message, tag = "3")] + RoutedV2(super::DtnRoutedV2), } } +/// DTN source routed message (V2) +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct DtnRoutedV2 { + /// the original encrypted container bytes + #[prost(bytes = "vec", tag = "1")] + pub container: ::prost::alloc::vec::Vec, + /// ordered list of custody user IDs + #[prost(bytes = "vec", repeated, tag = "2")] + pub custody_route: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// index of the first custody entry that has not yet taken custody + #[prost(uint32, tag = "3")] + pub next_route_index: u32, + /// signature of the original message (used for dedup) + #[prost(bytes = "vec", tag = "4")] + pub original_signature: ::prost::alloc::vec::Vec, + /// public key of the original sender (protobuf-encoded) + #[prost(bytes = "vec", tag = "5")] + pub sender_public_key: ::prost::alloc::vec::Vec, + /// expiry timestamp (milliseconds since epoch), 0 = no expiry + #[prost(uint64, tag = "6")] + pub expires_at: u64, + /// remaining allowed handoffs before message is dropped + #[prost(uint32, tag = "7")] + pub remaining_handoffs: u32, +} /// DTN response #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct DtnResponse { diff --git a/protobuf/generated/rust/qaul.net.router_net_info.rs b/protobuf/generated/rust/qaul.net.router_net_info.rs index 7cf10990f..1b103ea75 100644 --- a/protobuf/generated/rust/qaul.net.router_net_info.rs +++ b/protobuf/generated/rust/qaul.net.router_net_info.rs @@ -73,9 +73,12 @@ pub struct UserIdTable { /// User information table #[derive(Clone, PartialEq, ::prost::Message)] pub struct UserInfoTable { - /// user info + /// legacy user info (unsigned) #[prost(message, repeated, tag = "1")] pub info: ::prost::alloc::vec::Vec, + /// signed user profiles (preferred when available) + #[prost(message, repeated, tag = "2")] + pub signed_profiles: ::prost::alloc::vec::Vec, } /// User info structure for sending to the neighbours #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -90,6 +93,45 @@ pub struct UserInfo { #[prost(string, tag = "3")] pub name: ::prost::alloc::string::String, } +/// Extended user profile, signed by the user's own key +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct UserProfile { + /// user id (38 byte PeerId) + #[prost(bytes = "vec", tag = "1")] + pub id: ::prost::alloc::vec::Vec, + /// protobuf-encoded Ed25519 public key + #[prost(bytes = "vec", tag = "2")] + pub key: ::prost::alloc::vec::Vec, + /// display name + #[prost(string, tag = "3")] + pub name: ::prost::alloc::string::String, + /// small avatar image (max ~32KB) + #[prost(bytes = "vec", tag = "4")] + pub avatar: ::prost::alloc::vec::Vec, + /// bio / status text + #[prost(string, tag = "5")] + pub bio: ::prost::alloc::string::String, + /// monotonically increasing version counter + #[prost(uint64, tag = "6")] + pub version: u64, + /// timestamp in milliseconds since epoch + #[prost(uint64, tag = "7")] + pub updated_at: u64, + /// preferred custody route for DTN V2 delivery when this user is offline. + /// each entry is a PeerId (38 bytes) of a trusted custodian, in priority order. + #[prost(bytes = "vec", repeated, tag = "8")] + pub preferred_custody_route: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +/// Self-signed user profile wrapper +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SignedUserProfile { + /// protobuf-encoded UserProfile bytes + #[prost(bytes = "vec", tag = "1")] + pub profile: ::prost::alloc::vec::Vec, + /// Ed25519 signature over profile bytes, signed by the user's own key + #[prost(bytes = "vec", tag = "2")] + pub signature: ::prost::alloc::vec::Vec, +} /// List of feed ID's #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct FeedIdsTable { diff --git a/protobuf/generated/rust/qaul.rpc.dtn.rs b/protobuf/generated/rust/qaul.rpc.dtn.rs index 722a24923..a34d03f72 100644 --- a/protobuf/generated/rust/qaul.rpc.dtn.rs +++ b/protobuf/generated/rust/qaul.rpc.dtn.rs @@ -3,7 +3,10 @@ #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Dtn { /// message type - #[prost(oneof = "dtn::Message", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10")] + #[prost( + oneof = "dtn::Message", + tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14" + )] pub message: ::core::option::Option, } /// Nested message and enum types in `DTN`. @@ -41,6 +44,18 @@ pub mod dtn { /// dtn set total size response #[prost(message, tag = "10")] DtnSetTotalSizeResponse(super::DtnSetTotalSizeResponse), + /// dtn send routed request + #[prost(message, tag = "11")] + DtnSendRoutedRequest(super::DtnSendRoutedRequest), + /// dtn send routed response + #[prost(message, tag = "12")] + DtnSendRoutedResponse(super::DtnSendRoutedResponse), + /// dtn set custody enabled request + #[prost(message, tag = "13")] + DtnSetCustodyEnabledRequest(super::DtnSetCustodyEnabledRequest), + /// dtn set custody enabled response + #[prost(message, tag = "14")] + DtnSetCustodyEnabledResponse(super::DtnSetCustodyEnabledResponse), } } /// Dtn State Request @@ -123,3 +138,49 @@ pub struct DtnSetTotalSizeResponse { #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, } +/// Request to send a DTN source routed message +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct DtnSendRoutedRequest { + /// the receiver user ID + #[prost(bytes = "vec", tag = "1")] + pub receiver_id: ::prost::alloc::vec::Vec, + /// the message data to send (signed Container bytes) + #[prost(bytes = "vec", tag = "2")] + pub data: ::prost::alloc::vec::Vec, + /// ordered list of custody user IDs + #[prost(bytes = "vec", repeated, tag = "3")] + pub custody_route: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + /// how many seconds until the message expires (0 = no expiry) + #[prost(uint64, tag = "4")] + pub expiry_seconds: u64, + /// maximum number of custody handoffs allowed + #[prost(uint32, tag = "5")] + pub max_handoffs: u32, +} +/// Response to a directed custody routed DTN send request +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct DtnSendRoutedResponse { + /// whether the request was accepted + #[prost(bool, tag = "1")] + pub status: bool, + /// error or status message + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, +} +/// Request to enable or disable DTN V2 custody acceptance +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct DtnSetCustodyEnabledRequest { + /// whether to enable custody acceptance + #[prost(bool, tag = "1")] + pub enabled: bool, +} +/// Response to custody enable/disable request +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct DtnSetCustodyEnabledResponse { + /// whether the request was successful + #[prost(bool, tag = "1")] + pub status: bool, + /// error or status message + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, +} diff --git a/protobuf/generated/rust/qaul.rpc.user_accounts.rs b/protobuf/generated/rust/qaul.rpc.user_accounts.rs index b1ec60829..2b00ffa39 100644 --- a/protobuf/generated/rust/qaul.rpc.user_accounts.rs +++ b/protobuf/generated/rust/qaul.rpc.user_accounts.rs @@ -2,7 +2,7 @@ /// user account rpc message container #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UserAccounts { - #[prost(oneof = "user_accounts::Message", tags = "1, 2, 3, 4, 5, 6")] + #[prost(oneof = "user_accounts::Message", tags = "1, 2, 3, 4, 5, 6, 7, 8")] pub message: ::core::option::Option, } /// Nested message and enum types in `UserAccounts`. @@ -21,6 +21,10 @@ pub mod user_accounts { SetPasswordRequest(super::SetPasswordRequest), #[prost(message, tag = "6")] SetPasswordResponse(super::SetPasswordResponse), + #[prost(message, tag = "7")] + UpdateProfileRequest(super::UpdateProfileRequest), + #[prost(message, tag = "8")] + UpdateProfileResponse(super::UpdateProfileResponse), } } /// create a new user on this node @@ -53,6 +57,36 @@ pub struct DefaultUserAccount { #[prost(message, optional, tag = "2")] pub my_user_account: ::core::option::Option, } +/// Request to update the local user's profile +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct UpdateProfileRequest { + /// new display name (empty = no change) + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + /// new avatar image bytes (empty = no change) + #[prost(bytes = "vec", tag = "2")] + pub avatar: ::prost::alloc::vec::Vec, + /// new bio / status text (empty = no change) + #[prost(string, tag = "3")] + pub bio: ::prost::alloc::string::String, + /// preferred custody route for DTN V2 delivery (ordered PeerIds). + /// empty = no change; to clear, send a single empty bytes entry. + #[prost(bytes = "vec", repeated, tag = "4")] + pub preferred_custody_route: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +/// Response after profile update +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct UpdateProfileResponse { + /// whether the update succeeded + #[prost(bool, tag = "1")] + pub success: bool, + /// error message if failed + #[prost(string, tag = "2")] + pub error_message: ::prost::alloc::string::String, + /// the new profile version after update + #[prost(uint64, tag = "3")] + pub new_version: u64, +} /// Information about my user #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct MyUserAccount { diff --git a/protobuf/generated/rust/qaul.rpc.users.rs b/protobuf/generated/rust/qaul.rpc.users.rs index aea003129..f4fa0bc46 100644 --- a/protobuf/generated/rust/qaul.rpc.users.rs +++ b/protobuf/generated/rust/qaul.rpc.users.rs @@ -130,6 +130,21 @@ pub struct UserEntry { /// RoutingTableConnection connections = 11; #[prost(message, repeated, tag = "11")] pub connections: ::prost::alloc::vec::Vec, + /// bio / status text + #[prost(string, tag = "12")] + pub bio: ::prost::alloc::string::String, + /// avatar bytes (small image) + #[prost(bytes = "vec", tag = "13")] + pub avatar: ::prost::alloc::vec::Vec, + /// profile version number + #[prost(uint64, tag = "14")] + pub profile_version: u64, + /// profile last updated timestamp (ms since epoch) + #[prost(uint64, tag = "15")] + pub profile_updated_at: u64, + /// preferred custody route for DTN V2 delivery (ordered PeerIds) + #[prost(bytes = "vec", repeated, tag = "16")] + pub preferred_custody_route: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } /// Routing table connection entry. /// This message contains a connection to a specific user. diff --git a/protobuf/proto_definitions/node/user_accounts.proto b/protobuf/proto_definitions/node/user_accounts.proto index 4c99f102b..871f457db 100644 --- a/protobuf/proto_definitions/node/user_accounts.proto +++ b/protobuf/proto_definitions/node/user_accounts.proto @@ -11,6 +11,8 @@ message UserAccounts { MyUserAccount my_user_account = 4; SetPasswordRequest set_password_request = 5; SetPasswordResponse set_password_response = 6; + UpdateProfileRequest update_profile_request = 7; + UpdateProfileResponse update_profile_response = 8; } } @@ -37,6 +39,29 @@ message DefaultUserAccount { MyUserAccount my_user_account = 2; } +// Request to update the local user's profile +message UpdateProfileRequest { + // new display name (empty = no change) + string name = 1; + // new avatar image bytes (empty = no change) + bytes avatar = 2; + // new bio / status text (empty = no change) + string bio = 3; + // preferred custody route for DTN V2 delivery (ordered PeerIds). + // empty = no change; to clear, send a single empty bytes entry. + repeated bytes preferred_custody_route = 4; +} + +// Response after profile update +message UpdateProfileResponse { + // whether the update succeeded + bool success = 1; + // error message if failed + string error_message = 2; + // the new profile version after update + uint64 new_version = 3; +} + // Information about my user message MyUserAccount { string name = 1; diff --git a/protobuf/proto_definitions/router/router_net_info.proto b/protobuf/proto_definitions/router/router_net_info.proto index 6c21a9197..685f126a6 100644 --- a/protobuf/proto_definitions/router/router_net_info.proto +++ b/protobuf/proto_definitions/router/router_net_info.proto @@ -74,8 +74,10 @@ message UserIdTable { // User information table message UserInfoTable { - // user info + // legacy user info (unsigned) repeated UserInfo info = 1; + // signed user profiles (preferred when available) + repeated SignedUserProfile signed_profiles = 2; } // User info structure for sending to the neighbours @@ -88,6 +90,35 @@ message UserInfo { string name = 3; } +// Extended user profile, signed by the user's own key +message UserProfile { + // user id (38 byte PeerId) + bytes id = 1; + // protobuf-encoded Ed25519 public key + bytes key = 2; + // display name + string name = 3; + // small avatar image (max ~32KB) + bytes avatar = 4; + // bio / status text + string bio = 5; + // monotonically increasing version counter + uint64 version = 6; + // timestamp in milliseconds since epoch + uint64 updated_at = 7; + // preferred custody route for DTN V2 delivery when this user is offline. + // each entry is a PeerId (38 bytes) of a trusted custodian, in priority order. + repeated bytes preferred_custody_route = 8; +} + +// Self-signed user profile wrapper +message SignedUserProfile { + // protobuf-encoded UserProfile bytes + bytes profile = 1; + // Ed25519 signature over profile bytes, signed by the user's own key + bytes signature = 2; +} + // List of feed ID's message FeedIdsTable { // feed id diff --git a/protobuf/proto_definitions/router/users.proto b/protobuf/proto_definitions/router/users.proto index f69ecbc00..f9c38bbda 100644 --- a/protobuf/proto_definitions/router/users.proto +++ b/protobuf/proto_definitions/router/users.proto @@ -99,6 +99,16 @@ message UserEntry { // routing connection entries // RoutingTableConnection connections = 11; repeated RoutingTableConnection connections = 11; + // bio / status text + string bio = 12; + // avatar bytes (small image) + bytes avatar = 13; + // profile version number + uint64 profile_version = 14; + // profile last updated timestamp (ms since epoch) + uint64 profile_updated_at = 15; + // preferred custody route for DTN V2 delivery (ordered PeerIds) + repeated bytes preferred_custody_route = 16; } // Connection modules diff --git a/protobuf/proto_definitions/services/dtn/dtn_rpc.proto b/protobuf/proto_definitions/services/dtn/dtn_rpc.proto index 9027fe6d4..06c93957d 100644 --- a/protobuf/proto_definitions/services/dtn/dtn_rpc.proto +++ b/protobuf/proto_definitions/services/dtn/dtn_rpc.proto @@ -25,6 +25,14 @@ message DTN { DtnSetTotalSizeRequest dtn_set_total_size_request = 9; // dtn set total size response DtnSetTotalSizeResponse dtn_set_total_size_response = 10; + // dtn send routed request + DtnSendRoutedRequest dtn_send_routed_request = 11; + // dtn send routed response + DtnSendRoutedResponse dtn_send_routed_response = 12; + // dtn set custody enabled request + DtnSetCustodyEnabledRequest dtn_set_custody_enabled_request = 13; + // dtn set custody enabled response + DtnSetCustodyEnabledResponse dtn_set_custody_enabled_response = 14; } } @@ -93,3 +101,39 @@ message DtnSetTotalSizeResponse { // users string message = 2; } + +// Request to send a DTN source routed message +message DtnSendRoutedRequest { + // the receiver user ID + bytes receiver_id = 1; + // the message data to send (signed Container bytes) + bytes data = 2; + // ordered list of custody user IDs + repeated bytes custody_route = 3; + // how many seconds until the message expires (0 = no expiry) + uint64 expiry_seconds = 4; + // maximum number of custody handoffs allowed + uint32 max_handoffs = 5; +} + +// Response to a directed custody routed DTN send request +message DtnSendRoutedResponse { + // whether the request was accepted + bool status = 1; + // error or status message + string message = 2; +} + +// Request to enable or disable DTN V2 custody acceptance +message DtnSetCustodyEnabledRequest { + // whether to enable custody acceptance + bool enabled = 1; +} + +// Response to custody enable/disable request +message DtnSetCustodyEnabledResponse { + // whether the request was successful + bool status = 1; + // error or status message + string message = 2; +} diff --git a/protobuf/proto_definitions/services/messaging/messaging.proto b/protobuf/proto_definitions/services/messaging/messaging.proto index a28bbb67d..4b3927321 100644 --- a/protobuf/proto_definitions/services/messaging/messaging.proto +++ b/protobuf/proto_definitions/services/messaging/messaging.proto @@ -29,6 +29,8 @@ message EnvelopPayload { Encrypted encrypted = 1; // DTN message bytes dtn = 2; + // directed custody routed DTN message + DtnRoutedV2 dtn_routed_v2 = 3; } } @@ -167,9 +169,29 @@ message Dtn { bytes container = 1; // message received response DtnResponse response = 2; + // directed custody routed DTN message + DtnRoutedV2 routed_v2 = 3; } } +// DTN source routed message (V2) +message DtnRoutedV2 { + // the original encrypted container bytes + bytes container = 1; + // ordered list of custody user IDs + repeated bytes custody_route = 2; + // index of the first custody entry that has not yet taken custody + uint32 next_route_index = 3; + // signature of the original message (used for dedup) + bytes original_signature = 4; + // public key of the original sender (protobuf-encoded) + bytes sender_public_key = 5; + // expiry timestamp (milliseconds since epoch), 0 = no expiry + uint64 expires_at = 6; + // remaining allowed handoffs before message is dropped + uint32 remaining_handoffs = 7; +} + // DTN response message DtnResponse { // the enum definition of the type diff --git a/rust/clients/cli/src/users.rs b/rust/clients/cli/src/users.rs index be5997322..cd1279a3d 100644 --- a/rust/clients/cli/src/users.rs +++ b/rust/clients/cli/src/users.rs @@ -198,6 +198,11 @@ impl Users { verified, blocked, connections: vec![], + bio: String::new(), + avatar: Vec::new(), + profile_version: 0, + profile_updated_at: 0, + preferred_custody_route: Vec::new(), })), }; diff --git a/rust/clients/qauld-ctl/src/commands/users.rs b/rust/clients/qauld-ctl/src/commands/users.rs index 4dee57832..4815fec60 100644 --- a/rust/clients/qauld-ctl/src/commands/users.rs +++ b/rust/clients/qauld-ctl/src/commands/users.rs @@ -31,6 +31,11 @@ fn send_user_update( verified, blocked, connections: vec![], + bio: String::new(), + avatar: Vec::new(), + profile_version: 0, + profile_updated_at: 0, + preferred_custody_route: Vec::new(), })), }; diff --git a/rust/libqaul/src/node/user_accounts.rs b/rust/libqaul/src/node/user_accounts.rs index ccb881436..5418235c6 100644 --- a/rust/libqaul/src/node/user_accounts.rs +++ b/rust/libqaul/src/node/user_accounts.rs @@ -183,8 +183,24 @@ impl UserAccounts { } Configuration::save(); - // add it to users list - crate::router::users::Users::add(id, keys_ed25519.public(), name.clone(), false, false); + let mut initial_user = router::users::User { + id, + key: keys_ed25519.public(), + name: name.clone(), + verified: false, + blocked: false, + bio: String::new(), + avatar: Vec::new(), + version: 1, + updated_at: crate::utilities::timestamp::Timestamp::get_timestamp(), + signed_profile_bytes: Vec::new(), + signed_profile_signature: Vec::new(), + preferred_custody_route: Vec::new(), + }; + let signed = router::users::Users::create_signed_profile(&initial_user, &keys_ed25519); + initial_user.signed_profile_bytes = signed.profile; + initial_user.signed_profile_signature = signed.signature; + crate::router::users::Users::add(initial_user); // add user to routing table / connections table crate::router::connections::ConnectionTable::add_local_user(id); @@ -315,6 +331,13 @@ impl UserAccounts { name: user.name.clone(), verified: false, blocked: false, + bio: String::new(), + avatar: Vec::new(), + version: 0, + updated_at: 0, + signed_profile_bytes: Vec::new(), + signed_profile_signature: Vec::new(), + preferred_custody_route: Vec::new(), }); } @@ -477,6 +500,65 @@ impl UserAccounts { } } } + Some(proto::user_accounts::Message::UpdateProfileRequest(update_req)) => { + let user_peer_id = match PeerId::from_bytes(&user_id) { + Ok(id) => id, + Err(_) => { + Self::send_update_profile_response(false, "invalid user id".to_string(), 0, request_id); + return; + } + }; + + let account = match Self::get_by_id(user_peer_id) { + Some(a) => a, + None => { + Self::send_update_profile_response(false, "user account not found".to_string(), 0, request_id); + return; + } + }; + + let id_bytes = user_peer_id.to_bytes(); + let q8id = id_bytes[6..14].to_vec(); + + let updated_user = match router::users::Users::get_user_snapshot(&q8id) { + Some(user) => { + let new_name = if update_req.name.is_empty() { user.name.clone() } else { update_req.name.clone() }; + let new_bio = if update_req.bio.is_empty() { user.bio.clone() } else { update_req.bio.clone() }; + let new_avatar = if update_req.avatar.is_empty() { user.avatar.clone() } else { update_req.avatar.clone() }; + let new_custody_route = if update_req.preferred_custody_route.is_empty() { user.preferred_custody_route.clone() } else { update_req.preferred_custody_route.clone() }; + let new_version = user.version + 1; + let new_updated_at = crate::utilities::timestamp::Timestamp::get_timestamp(); + + router::users::User { + id: user.id, + key: user.key, + name: new_name, + verified: user.verified, + blocked: user.blocked, + bio: new_bio, + avatar: new_avatar, + version: new_version, + updated_at: new_updated_at, + signed_profile_bytes: Vec::new(), + signed_profile_signature: Vec::new(), + preferred_custody_route: new_custody_route, + } + } + None => { + Self::send_update_profile_response(false, "user not found in users table".to_string(), 0, request_id); + return; + } + }; + + let signed = router::users::Users::create_signed_profile(&updated_user, &account.keys); + let new_version = updated_user.version; + let mut user_to_store = updated_user; + user_to_store.signed_profile_bytes = signed.profile; + user_to_store.signed_profile_signature = signed.signature; + router::users::Users::add(user_to_store); + + Self::send_update_profile_response(true, String::new(), new_version, request_id); + } _ => {} } } @@ -486,6 +568,27 @@ impl UserAccounts { } } + /// send update profile response to client + fn send_update_profile_response(success: bool, error_message: String, new_version: u64, request_id: String) { + let proto_message = proto::UserAccounts { + message: Some(proto::user_accounts::Message::UpdateProfileResponse( + proto::UpdateProfileResponse { + success, + error_message, + new_version, + }, + )), + }; + let mut buf = Vec::with_capacity(proto_message.encoded_len()); + proto_message.encode(&mut buf).expect("Vec provides capacity as needed"); + Rpc::send_message( + buf, + crate::rpc::proto::Modules::Useraccounts.into(), + request_id, + Vec::new(), + ); + } + /// send password operation response ot client fn send_password_response(success: bool, message: String, request_id: String) { let proto_message = proto::UserAccounts { diff --git a/rust/libqaul/src/router/info.rs b/rust/libqaul/src/router/info.rs index 415fb957f..4f801ebd2 100644 --- a/rust/libqaul/src/router/info.rs +++ b/rust/libqaul/src/router/info.rs @@ -466,9 +466,6 @@ impl RouterInfo { match decoding_result { Ok(container) => { - // TODO: check signature - //let signature = container.signature;message.ids - // decode message let message_result = router_net_proto::RouterInfoContent::decode(&container.message[..]); @@ -586,7 +583,14 @@ impl RouterInfo { let message_info = router_net_proto::UserInfoTable::decode(&content.content[..]); if let Ok(message) = message_info { - Users::add_user_info_table(&message.info); + // Process signed profiles first (verified, preferred) + if !message.signed_profiles.is_empty() { + Users::add_signed_user_info_table(&message.signed_profiles); + } + // Fall back to legacy unsigned info for any remaining unknowns + if !message.info.is_empty() { + Users::add_user_info_table(&message.info); + } } } Err(_) => {} diff --git a/rust/libqaul/src/router/users.rs b/rust/libqaul/src/router/users.rs index 4f9de9445..d37cfb674 100644 --- a/rust/libqaul/src/router/users.rs +++ b/rust/libqaul/src/router/users.rs @@ -53,21 +53,58 @@ impl Users { // iterate over all values in db for res in tree.iter() { if let Ok((_vec, user_bytes)) = res { - // decode user bytes - let user: UserData = bincode::deserialize(&user_bytes).unwrap(); + // decode user bytes with migration fallback + let user_data: UserData = match bincode::deserialize(&user_bytes) { + Ok(u) => u, + Err(_) => { + // Try legacy format for backward compat + match bincode::deserialize::(&user_bytes) { + Ok(legacy) => { + let migrated = UserData { + id: legacy.id, + key: legacy.key, + name: legacy.name, + verified: legacy.verified, + blocked: legacy.blocked, + bio: String::new(), + avatar: Vec::new(), + version: 0, + updated_at: 0, + signed_profile_bytes: Vec::new(), + signed_profile_signature: Vec::new(), + preferred_custody_route: Vec::new(), + }; + // Re-save in new format + DbUsers::add_user(migrated.clone()); + migrated + } + Err(e) => { + log::error!("Failed to deserialize user data: {}", e); + continue; + } + } + } + }; // encode values from bytes - let q8id = QaulId::bytes_to_q8id(user.id.clone()); - let id = PeerId::from_bytes(&user.id).unwrap(); - let key = PublicKey::try_decode_protobuf(&user.key).unwrap(); + let q8id = QaulId::bytes_to_q8id(user_data.id.clone()); + let id = PeerId::from_bytes(&user_data.id).unwrap(); + let key = PublicKey::try_decode_protobuf(&user_data.key).unwrap(); // fill result into user table users.users.insert( q8id, User { id, key, - name: user.name, - verified: user.verified, - blocked: user.blocked, + name: user_data.name, + verified: user_data.verified, + blocked: user_data.blocked, + bio: user_data.bio, + avatar: user_data.avatar, + version: user_data.version, + updated_at: user_data.updated_at, + signed_profile_bytes: user_data.signed_profile_bytes, + signed_profile_signature: user_data.signed_profile_signature, + preferred_custody_route: user_data.preferred_custody_route, }, ); } @@ -77,20 +114,25 @@ impl Users { /// add a new user /// /// This user will be added to the users list in memory and to the data base - pub fn add(id: PeerId, key: PublicKey, name: String, verified: bool, blocked: bool) { - let id_bytes = id.to_bytes(); + pub fn add(user: User) { + let id_bytes = user.id.to_bytes(); let q8id = QaulId::bytes_to_q8id(id_bytes.clone()); - // save user to the data base DbUsers::add_user(UserData { id: id_bytes, - key: key.encode_protobuf(), - name: name.clone(), - verified, - blocked, + key: user.key.encode_protobuf(), + name: user.name.clone(), + verified: user.verified, + blocked: user.blocked, + bio: user.bio.clone(), + avatar: user.avatar.clone(), + version: user.version, + updated_at: user.updated_at, + signed_profile_bytes: user.signed_profile_bytes.clone(), + signed_profile_signature: user.signed_profile_signature.clone(), + preferred_custody_route: user.preferred_custody_route.clone(), }); - // add user to the users table let mut users = USERS.get().write().unwrap(); if users.users.len() >= 100_000 { log::warn!( @@ -98,16 +140,7 @@ impl Users { users.users.len() ); } - users.users.insert( - q8id, - User { - id, - key, - name, - verified, - blocked, - }, - ); + users.users.insert(q8id, user); } /// add a new user to the users list, and check whether the @@ -131,8 +164,14 @@ impl Users { return; } } - // add user - Self::add(id, key, name, false, false); + Self::add(User { + id, key, name, + verified: false, blocked: false, + bio: String::new(), avatar: Vec::new(), + version: 0, updated_at: 0, + signed_profile_bytes: Vec::new(), signed_profile_signature: Vec::new(), + preferred_custody_route: Vec::new(), + }); } /// check missed users from ids @@ -162,6 +201,12 @@ impl Users { store.users.get(q8id).map(|user| user.key.clone()) } + /// get a snapshot of a user by q8id + pub fn get_user_snapshot(q8id: &[u8]) -> Option { + let store = USERS.get().read().unwrap(); + store.users.get(q8id).cloned() + } + /// get user by q8id pub fn get_user_id_by_q8id(q8id: &[u8]) -> Option { let store = USERS.get().read().unwrap(); @@ -187,21 +232,30 @@ impl Users { /// RouterInfo message which is sent regularly to neighbours pub fn get_user_info_table_by_q8ids(q8ids: &[Vec]) -> router_net_proto::UserInfoTable { let store = USERS.get().read().unwrap(); - let mut users = router_net_proto::UserInfoTable { + let mut table = router_net_proto::UserInfoTable { info: Vec::with_capacity(q8ids.len()), + signed_profiles: Vec::with_capacity(q8ids.len()), }; for q8id in q8ids { if let Some(value) = store.users.get(q8id) { - let user_info = router_net_proto::UserInfo { + // Always include legacy UserInfo for backward compat + table.info.push(router_net_proto::UserInfo { id: value.id.to_bytes(), key: value.key.encode_protobuf(), name: value.name.clone(), - }; - users.info.push(user_info); + }); + + // Include signed profile if available + if !value.signed_profile_bytes.is_empty() { + table.signed_profiles.push(router_net_proto::SignedUserProfile { + profile: value.signed_profile_bytes.clone(), + signature: value.signed_profile_signature.clone(), + }); + } } } - users + table } /// add new users from the received bytes of a UserInfoTable @@ -217,6 +271,124 @@ impl Users { } } + /// Create a SignedUserProfile for a user, signed with the given keypair. + pub fn create_signed_profile( + user: &User, + keys: &libp2p::identity::Keypair, + ) -> router_net_proto::SignedUserProfile { + let profile = router_net_proto::UserProfile { + id: user.id.to_bytes(), + key: user.key.encode_protobuf(), + name: user.name.clone(), + avatar: user.avatar.clone(), + bio: user.bio.clone(), + version: user.version, + updated_at: user.updated_at, + preferred_custody_route: user.preferred_custody_route.clone(), + }; + + let profile_bytes = profile.encode_to_vec(); + let signature = match keys.sign(&profile_bytes) { + Ok(sig) => sig, + Err(e) => { + log::error!("Failed to sign user profile: {}", e); + return router_net_proto::SignedUserProfile { + profile: profile_bytes, + signature: Vec::new(), + }; + } + }; + + router_net_proto::SignedUserProfile { + profile: profile_bytes, + signature, + } + } + + /// Verify a SignedUserProfile. + /// + /// Checks that: + /// 1. The profile bytes decode to a valid UserProfile + /// 2. The embedded public key can be decoded + /// 3. The signature verifies against the profile bytes + /// 4. The PeerId matches the public key + pub fn verify_signed_profile( + signed: &router_net_proto::SignedUserProfile, + ) -> Result { + let profile = router_net_proto::UserProfile::decode(&signed.profile[..]) + .map_err(|e| format!("failed to decode UserProfile: {}", e))?; + + let key = PublicKey::try_decode_protobuf(&profile.key) + .map_err(|e| format!("failed to decode public key: {}", e))?; + + if !key.verify(&signed.profile, &signed.signature) { + return Err("signature verification failed".to_string()); + } + + let id = PeerId::from_bytes(&profile.id) + .map_err(|e| format!("invalid PeerId: {}", e))?; + if id != key.to_peer_id() { + return Err("PeerId does not match public key".to_string()); + } + + Ok(profile) + } + + /// Process received signed user profiles with version checking. + /// + /// For each profile: verify the signature, then accept only if the + /// version is higher than the currently stored version (or the user + /// is new). + pub fn add_signed_user_info_table(signed_profiles: &[router_net_proto::SignedUserProfile]) { + for signed in signed_profiles { + match Self::verify_signed_profile(signed) { + Ok(profile) => { + let id = match PeerId::from_bytes(&profile.id) { + Ok(id) => id, + Err(_) => continue, + }; + let key = match PublicKey::try_decode_protobuf(&profile.key) { + Ok(k) => k, + Err(_) => continue, + }; + let id_bytes = id.to_bytes(); + let q8id = id_bytes[6..14].to_vec(); + + let (should_update, verified, blocked) = { + let users = USERS.get().read().unwrap(); + match users.users.get(&q8id) { + Some(existing) => { + let dominated = profile.version > existing.version + || (profile.version == existing.version + && profile.updated_at > existing.updated_at); + (dominated, existing.verified, existing.blocked) + } + None => (true, false, false), + } + }; + + if should_update { + Self::add(User { + id, key, + name: profile.name.clone(), + verified, blocked, + bio: profile.bio.clone(), + avatar: profile.avatar.clone(), + version: profile.version, + updated_at: profile.updated_at, + preferred_custody_route: profile.preferred_custody_route.clone(), // wired after proto regen + signed_profile_bytes: signed.profile.clone(), + signed_profile_signature: signed.signature.clone(), + }); + } + } + Err(e) => { + log::warn!("Rejected signed user profile: {}", e); + } + } + } + } + fn compare(a: &[u8], b: &[u8]) -> Ordering { for (ai, bi) in a.iter().zip(b.iter()) { match ai.cmp(&bi) { @@ -304,6 +476,13 @@ impl Users { name: user_result.name.clone(), verified: updated_user.verified, blocked: updated_user.blocked, + bio: user_result.bio.clone(), + avatar: user_result.avatar.clone(), + version: user_result.version, + updated_at: user_result.updated_at, + signed_profile_bytes: user_result.signed_profile_bytes.clone(), + signed_profile_signature: user_result.signed_profile_signature.clone(), + preferred_custody_route: user_result.preferred_custody_route.clone(), }); }, ); @@ -448,12 +627,32 @@ enum UserFilter { } /// user structure +#[derive(Clone)] pub struct User { pub id: PeerId, pub key: PublicKey, pub name: String, pub verified: bool, pub blocked: bool, + pub bio: String, + pub avatar: Vec, + pub version: u64, + pub updated_at: u64, + pub signed_profile_bytes: Vec, + pub signed_profile_signature: Vec, + /// Preferred custody route for DTN V2 delivery when this user is offline. + /// Each entry is a PeerId (38 bytes) of a trusted custodian, in priority order. + pub preferred_custody_route: Vec>, +} + +/// Legacy user structure for backward-compatible deserialization +#[derive(Serialize, Deserialize)] +struct UserDataLegacy { + pub id: Vec, + pub key: Vec, + pub name: String, + pub verified: bool, + pub blocked: bool, } /// user structure for storing it in the data base @@ -464,6 +663,14 @@ pub struct UserData { pub name: String, pub verified: bool, pub blocked: bool, + pub bio: String, + pub avatar: Vec, + pub version: u64, + pub updated_at: u64, + pub signed_profile_bytes: Vec, + pub signed_profile_signature: Vec, + #[serde(default)] + pub preferred_custody_route: Vec>, } fn send_users_rpc_message(message: proto::users::Message, request_id: String) { @@ -523,6 +730,11 @@ fn build_user_entry( verified: user.verified, blocked: user.blocked, connections, + bio: user.bio.clone(), + avatar: user.avatar.clone(), + profile_version: user.version, + profile_updated_at: user.updated_at, + preferred_custody_route: user.preferred_custody_route.clone(), } } @@ -627,6 +839,13 @@ mod tests { name: format!("user_{}", i), verified: false, blocked: false, + bio: String::new(), + avatar: Vec::new(), + version: 0, + updated_at: 0, + signed_profile_bytes: Vec::new(), + signed_profile_signature: Vec::new(), + preferred_custody_route: Vec::new(), }, ); ids.push(id); @@ -900,4 +1119,96 @@ mod tests { assert_eq!(conn.hop_count, 1); assert_eq!(conn.rtt, 10); } + + #[test] + fn sign_verify_profile_roundtrip() { + let kp = Keypair::generate_ed25519(); + let pk = kp.public(); + let id = pk.to_peer_id(); + + let user = User { + id, + key: pk, + name: "test_user".to_string(), + verified: false, + blocked: false, + bio: "hello world".to_string(), + avatar: vec![0xFF, 0xD8], + version: 1, + updated_at: 12345, + signed_profile_bytes: Vec::new(), + signed_profile_signature: Vec::new(), + preferred_custody_route: Vec::new(), + }; + + let signed = Users::create_signed_profile(&user, &kp); + assert!(!signed.profile.is_empty()); + assert!(!signed.signature.is_empty()); + + let verified = Users::verify_signed_profile(&signed); + assert!(verified.is_ok()); + let profile = verified.unwrap(); + assert_eq!(profile.name, "test_user"); + assert_eq!(profile.bio, "hello world"); + assert_eq!(profile.version, 1); + } + + #[test] + fn tampered_profile_rejected() { + let kp = Keypair::generate_ed25519(); + let pk = kp.public(); + let id = pk.to_peer_id(); + + let user = User { + id, + key: pk, + name: "test_user".to_string(), + verified: false, + blocked: false, + bio: String::new(), + avatar: Vec::new(), + version: 1, + updated_at: 0, + signed_profile_bytes: Vec::new(), + signed_profile_signature: Vec::new(), + preferred_custody_route: Vec::new(), + }; + + let mut signed = Users::create_signed_profile(&user, &kp); + // Tamper with the profile bytes + if let Some(byte) = signed.profile.last_mut() { + *byte ^= 0xFF; + } + + let result = Users::verify_signed_profile(&signed); + assert!(result.is_err()); + } + + #[test] + fn wrong_key_profile_rejected() { + let kp1 = Keypair::generate_ed25519(); + let kp2 = Keypair::generate_ed25519(); + let pk1 = kp1.public(); + let id1 = pk1.to_peer_id(); + + let user = User { + id: id1, + key: pk1, + name: "test_user".to_string(), + verified: false, + blocked: false, + bio: String::new(), + avatar: Vec::new(), + version: 1, + updated_at: 0, + signed_profile_bytes: Vec::new(), + signed_profile_signature: Vec::new(), + preferred_custody_route: Vec::new(), + }; + + // Sign with wrong key + let signed = Users::create_signed_profile(&user, &kp2); + let result = Users::verify_signed_profile(&signed); + assert!(result.is_err()); + } } diff --git a/rust/libqaul/src/services/chat/file.rs b/rust/libqaul/src/services/chat/file.rs index dce08e0ef..ca767bfb8 100644 --- a/rust/libqaul/src/services/chat/file.rs +++ b/rust/libqaul/src/services/chat/file.rs @@ -97,7 +97,13 @@ impl UserFiles { /// save file history pub fn save_filehistory(&self, file_id: u64, file_history: FileHistory) { // save file history into data base - let file_history_bytes = bincode::serialize(&file_history).unwrap(); + let file_history_bytes = match bincode::serialize(&file_history) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Error serializing file history: {}", e); + return; + } + }; if let Err(e) = self .histories .insert(file_id.to_be_bytes(), file_history_bytes) @@ -292,7 +298,13 @@ impl ChatFile { // check if user data exists { // get chat state - let all_files = ALLFILES.get().read().unwrap(); + let all_files = match ALLFILES.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("Error reading all_files state: {}", e); + return Self::create_userfiles(user_id); + } + }; // check if user ID is in map if let Some(user_files) = all_files.db_ref.get(&user_id.to_bytes()) { @@ -319,8 +331,26 @@ impl ChatFile { let db = DataBase::get_user_db(user_id.clone()); // open trees - let histories: sled::Tree = db.open_tree("chat_file").unwrap(); - let file_chunks: sled::Tree = db.open_tree("file_chunks").unwrap(); + let histories: sled::Tree = match db.open_tree("chat_file") { + Ok(tree) => tree, + Err(e) => { + log::error!("Error opening chat_file tree: {}", e); + return UserFiles { + histories: db.open_tree("__fallback_chat_file").expect("fallback tree"), + file_chunks: db.open_tree("__fallback_file_chunks").expect("fallback tree"), + }; + } + }; + let file_chunks: sled::Tree = match db.open_tree("file_chunks") { + Ok(tree) => tree, + Err(e) => { + log::error!("Error opening file_chunks tree: {}", e); + return UserFiles { + histories, + file_chunks: db.open_tree("__fallback_file_chunks").expect("fallback tree"), + }; + } + }; let user_files = UserFiles { histories, @@ -328,7 +358,13 @@ impl ChatFile { }; // get chat state for writing - let mut all_files = ALLFILES.get().write().unwrap(); + let mut all_files = match ALLFILES.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("Error writing all_files state: {}", e); + return user_files; + } + }; // add user to state all_files @@ -473,7 +509,13 @@ impl ChatFile { } // check if we collect the result - let file_history: FileHistory = bincode::deserialize(&message).unwrap(); + let file_history: FileHistory = match bincode::deserialize(&message) { + Ok(v) => v, + Err(e) => { + log::error!("Error deserializing file history: {}", e); + continue; + } + }; if counter >= history_req.offset { histories.push(file_history); } @@ -540,7 +582,12 @@ impl ChatFile { } }; - let size = file.metadata().unwrap().len() as u32; + let size = match file.metadata() { + Ok(m) => m.len() as u32, + Err(e) => { + return Err(format!("file metadata error: {}", e)); + } + }; if size == 0 { return Err("file size is zero".to_string()); } @@ -549,13 +596,18 @@ impl ChatFile { let path = Path::new(path_name.as_str()); let mut extension = "".to_string(); - if let Some(ext) = - Self::get_extension_from_filename(path.file_name().unwrap().to_str().unwrap()) - { + let path_file_name = match path.file_name().and_then(|f| f.to_str()) { + Some(name) => name, + None => { + return Err("unable to get file name from path".to_string()); + } + }; + + if let Some(ext) = Self::get_extension_from_filename(path_file_name) { extension = ext.to_string(); } - let file_name = path.file_name().unwrap().to_str().unwrap().to_string(); + let file_name = path_file_name.to_string(); // create file id let user_id_bytes = user_account.id.to_bytes(); @@ -608,7 +660,13 @@ impl ChatFile { ); // create group ID object - let groupid = GroupId::from_bytes(group_id).unwrap(); + let groupid = match GroupId::from_bytes(group_id) { + Ok(id) => id, + Err(e) => { + log::error!("Error parsing group id: {}", e); + return Err("invalid group id".to_string()); + } + }; // save file state to data base let file_history = FileHistory { @@ -632,7 +690,13 @@ impl ChatFile { let db_ref = Self::get_db_ref(&user_account.id); // save file history to data base - let file_history_bytes = bincode::serialize(&file_history).unwrap(); + let file_history_bytes = match bincode::serialize(&file_history) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Error serializing file history: {}", e); + return Err("failed to serialize file history".to_string()); + } + }; if let Err(e) = db_ref .histories .insert(&file_id.to_be_bytes(), file_history_bytes) @@ -1005,13 +1069,25 @@ impl ChatFile { /// Process incoming RPC request messages for file sharing module pub async fn rpc(data: Vec, user_id: Vec, request_id: String) { - let account_id = PeerId::from_bytes(&user_id).unwrap(); + let account_id = match PeerId::from_bytes(&user_id) { + Ok(id) => id, + Err(e) => { + log::error!("Error parsing user id: {:?}", e); + return; + } + }; match proto_rpc::ChatFile::decode(&data[..]) { Ok(chatfile) => { match chatfile.message { Some(proto_rpc::chat_file::Message::SendFileRequest(send_req)) => { - let user_account = UserAccounts::get_by_id(account_id).unwrap(); + let user_account = match UserAccounts::get_by_id(account_id) { + Some(account) => account, + None => { + log::error!("user account not found for file send"); + return; + } + }; if let Err(e) = Self::send( &user_account, diff --git a/rust/libqaul/src/services/chat/message.rs b/rust/libqaul/src/services/chat/message.rs index 20a53acad..847acc071 100644 --- a/rust/libqaul/src/services/chat/message.rs +++ b/rust/libqaul/src/services/chat/message.rs @@ -129,7 +129,13 @@ impl ChatMessage { // send to all group members if let Some(user_account) = UserAccounts::get_by_id(*account_id) { for user_id in group.members.keys() { - let receiver = PeerId::from_bytes(user_id).unwrap(); + let receiver = match PeerId::from_bytes(user_id) { + Ok(id) => id, + Err(e) => { + log::error!("Error parsing receiver user id: {:?}", e); + continue; + } + }; if receiver != *account_id { log::trace!("send message to {}", receiver.to_base58()); if let Err(error) = Self::send(&user_account, &receiver, &common_message) { diff --git a/rust/libqaul/src/services/chat/mod.rs b/rust/libqaul/src/services/chat/mod.rs index 2bf38e758..09495f3f6 100644 --- a/rust/libqaul/src/services/chat/mod.rs +++ b/rust/libqaul/src/services/chat/mod.rs @@ -63,7 +63,13 @@ impl Chat { _internet: Option<&mut Internet>, request_id: String, ) { - let account_id = PeerId::from_bytes(&user_id).unwrap(); + let account_id = match PeerId::from_bytes(&user_id) { + Ok(id) => id, + Err(e) => { + log::error!("Error parsing user id: {:?}", e); + return; + } + }; match rpc_proto::Chat::decode(&data[..]) { Ok(chat) => { diff --git a/rust/libqaul/src/services/chat/storage.rs b/rust/libqaul/src/services/chat/storage.rs index 0cd4374af..8cf5384a9 100644 --- a/rust/libqaul/src/services/chat/storage.rs +++ b/rust/libqaul/src/services/chat/storage.rs @@ -89,7 +89,13 @@ impl ChatStorage { chat_message: &rpc_proto::ChatMessage, flush_mode: FlushMode, ) { - let chat_message_bytes = bincode::serialize(chat_message).unwrap(); + let chat_message_bytes = match bincode::serialize(chat_message) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Error serializing chat message: {}", e); + return; + } + }; if let Err(e) = db_ref.messages.insert(db_key, chat_message_bytes) { log::error!("Error saving chat message to data base: {}", e); return; @@ -122,15 +128,33 @@ impl ChatStorage { flush_mode: FlushMode, mutate: impl FnOnce(&mut rpc_proto::ChatMessage), ) { - let Some(key) = db_ref.message_ids.get(message_id).unwrap() else { + let Some(key) = (match db_ref.message_ids.get(message_id) { + Ok(v) => v, + Err(e) => { + log::error!("Error getting message id from data base: {}", e); + return; + } + }) else { return; }; - let Some(chat_msg_fromdb) = db_ref.messages.get(&key).unwrap() else { + let Some(chat_msg_fromdb) = (match db_ref.messages.get(&key) { + Ok(v) => v, + Err(e) => { + log::error!("Error getting chat message from data base: {}", e); + return; + } + }) else { return; }; let mut chat_message: rpc_proto::ChatMessage = - bincode::deserialize(&chat_msg_fromdb).unwrap(); + match bincode::deserialize(&chat_msg_fromdb) { + Ok(v) => v, + Err(e) => { + log::error!("Error deserializing chat message: {}", e); + return; + } + }; mutate(&mut chat_message); Self::save_chat_message_record(db_ref, &key, &chat_message, flush_mode); @@ -142,7 +166,7 @@ impl ChatStorage { // get data base of user account let db_ref = Self::get_db_ref(user_id.clone()); for id in message_ids { - if !db_ref.message_ids.contains_key(id).unwrap() { + if !db_ref.message_ids.contains_key(id).unwrap_or(false) { return false; } } @@ -253,7 +277,7 @@ impl ChatStorage { // check if message_id already exists // this protects the double saving of incoming messages if !message_id.is_empty() { - if db_ref.message_ids.contains_key(message_id).unwrap() { + if db_ref.message_ids.contains_key(message_id).unwrap_or(false) { log::warn!("chat message already exists"); return; } @@ -421,7 +445,13 @@ impl ChatStorage { match res { Ok((_id, message_bytes)) => { let message: rpc_proto::ChatMessage = - bincode::deserialize(&message_bytes).unwrap(); + match bincode::deserialize(&message_bytes) { + Ok(v) => v, + Err(e) => { + log::error!("Error deserializing chat message: {}", e); + continue; + } + }; message_list.push(message); } Err(e) => { @@ -467,7 +497,13 @@ impl ChatStorage { let result = db_ref.messages.get_lt(search_key); if let Ok(Some((_key, value))) = result { // check if result is really of the same group - let chat_message: rpc_proto::ChatMessage = bincode::deserialize(&value).unwrap(); + let chat_message: rpc_proto::ChatMessage = match bincode::deserialize(&value) { + Ok(v) => v, + Err(e) => { + log::error!("Error deserializing chat message for index lookup: {}", e); + return 0; + } + }; if group_id == chat_message.group_id.as_slice() { return chat_message.index + 1; } @@ -481,7 +517,13 @@ impl ChatStorage { // check if user account data exists { // get chat state - let chat = CHAT.get().read().unwrap(); + let chat = match CHAT.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("Error reading chat state: {}", e); + return Self::create_chatuser(account_id); + } + }; // check if user account ID is in map if let Some(chat_user) = chat.db_ref.get(&account_id.to_bytes()) { @@ -508,8 +550,26 @@ impl ChatStorage { let db = DataBase::get_user_db(account_id); // open trees - let messages: sled::Tree = db.open_tree("chat_messages").unwrap(); - let message_ids: sled::Tree = db.open_tree("chat_message_ids").unwrap(); + let messages: sled::Tree = match db.open_tree("chat_messages") { + Ok(tree) => tree, + Err(e) => { + log::error!("Error opening chat_messages tree: {}", e); + return ChatAccountDb { + messages: db.open_tree("__fallback_chat_messages").expect("fallback tree"), + message_ids: db.open_tree("__fallback_chat_message_ids").expect("fallback tree"), + }; + } + }; + let message_ids: sled::Tree = match db.open_tree("chat_message_ids") { + Ok(tree) => tree, + Err(e) => { + log::error!("Error opening chat_message_ids tree: {}", e); + return ChatAccountDb { + messages, + message_ids: db.open_tree("__fallback_chat_message_ids").expect("fallback tree"), + }; + } + }; let chat_user = ChatAccountDb { messages, @@ -517,7 +577,13 @@ impl ChatStorage { }; // get chat state for writing - let mut chat = CHAT.get().write().unwrap(); + let mut chat = match CHAT.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("Error writing chat state: {}", e); + return chat_user; + } + }; // add user to state chat.db_ref.insert(account_id.to_bytes(), chat_user.clone()); diff --git a/rust/libqaul/src/services/crypto/crypto25519.rs b/rust/libqaul/src/services/crypto/crypto25519.rs index cd510e056..4a56a004a 100644 --- a/rust/libqaul/src/services/crypto/crypto25519.rs +++ b/rust/libqaul/src/services/crypto/crypto25519.rs @@ -48,12 +48,17 @@ impl Crypto25519 { // make sure the transformation was correct and we have the correct secret key { - let retransformed_ed25519_keypair = libp2p::identity::ed25519::Keypair::from( - libp2p::identity::ed25519::SecretKey::try_from_bytes( - ed25519_dalek_secret_bytes, - ) - .unwrap(), - ); + let secret_key = match libp2p::identity::ed25519::SecretKey::try_from_bytes( + ed25519_dalek_secret_bytes, + ) { + Ok(k) => k, + Err(e) => { + log::error!("Failed to reconstruct secret key from bytes: {}", e); + return None; + } + }; + let retransformed_ed25519_keypair = + libp2p::identity::ed25519::Keypair::from(secret_key); assert!( ed25519_dalek_keypair_bytes == retransformed_ed25519_keypair.to_bytes(), "secret key transformation failed" diff --git a/rust/libqaul/src/services/crypto/noise.rs b/rust/libqaul/src/services/crypto/noise.rs index a1d31af1d..d66a67de2 100644 --- a/rust/libqaul/src/services/crypto/noise.rs +++ b/rust/libqaul/src/services/crypto/noise.rs @@ -48,7 +48,13 @@ impl CryptoNoise { } // create crypto state - state = Self::create_crypto_state::(true, user_account.clone(), remote_key); + state = match Self::create_crypto_state::(true, user_account.clone(), remote_key) { + Some(s) => s, + None => { + log::error!("Failed to create crypto state for handshake 1"); + return (None, 0, 0); + } + }; let session_id = state.session_id; log::trace!("new session generated with session_id: {}", session_id); @@ -121,7 +127,13 @@ impl CryptoNoise { Some(U8Array::from_slice(state.s.clone().as_slice())), Some(e), Some(U8Array::from_slice(state.rs.clone().as_slice())), - Some(U8Array::from_slice(state.re.clone().unwrap().as_slice())), + Some(U8Array::from_slice(match state.re.clone() { + Some(re) => re, + None => { + log::error!("Missing remote ephemeral key in crypto state"); + return (None, 0); + } + }.as_slice())), ); // set message index @@ -178,8 +190,15 @@ impl CryptoNoise { nonce = state.index_nonce_out; // create cipher + let cipher_out = match state.clone().cipher_out { + Some(key) => key, + None => { + log::error!("Missing cipher_out key in transport state"); + return (None, 0); + } + }; let mut cipher: CipherState = - CipherState::new(state.clone().cipher_out.unwrap().as_slice(), nonce); + CipherState::new(cipher_out.as_slice(), nonce); // encrypt message message = Some(cipher.encrypt_vec(data.as_slice())); @@ -234,7 +253,13 @@ impl CryptoNoise { } // create initial crypto state - state = Self::create_crypto_state::(false, user_account.clone(), remote_key); + state = match Self::create_crypto_state::(false, user_account.clone(), remote_key) { + Some(s) => s, + None => { + log::error!("Failed to create crypto state for decryption handshake 1"); + return None; + } + }; // save session_id to state state.session_id = session_id; @@ -383,8 +408,15 @@ impl CryptoNoise { log::trace!("Decrypting with full encryption"); // create cipher + let cipher_in = match state.cipher_in.clone() { + Some(key) => key, + None => { + log::error!("Missing cipher_in key in transport state"); + return None; + } + }; let mut cipher: CipherState = - CipherState::new(state.cipher_in.clone().unwrap().as_slice(), nonce); + CipherState::new(cipher_in.as_slice(), nonce); // decrypt message match cipher.decrypt_vec(data.as_slice()) { @@ -409,7 +441,7 @@ impl CryptoNoise { initiator: bool, user_account: UserAccount, remote_key: PublicKey, - ) -> CryptoState + ) -> Option where D: DH, { @@ -418,10 +450,22 @@ impl CryptoNoise { let session_id: u32 = rng.random(); // create private key - let private_key = Crypto25519::private_key_to_montgomery(user_account.keys).unwrap(); + let private_key = match Crypto25519::private_key_to_montgomery(user_account.keys) { + Some(key) => key, + None => { + log::error!("Failed to convert private key to montgomery form"); + return None; + } + }; // create public key - let remote_public_key = Crypto25519::public_key_to_montgomery(remote_key).unwrap(); + let remote_public_key = match Crypto25519::public_key_to_montgomery(remote_key) { + Some(key) => key, + None => { + log::error!("Failed to convert remote public key to montgomery form"); + return None; + } + }; // create new ephemeral key let e = D::genkey(); @@ -451,6 +495,6 @@ impl CryptoNoise { out_of_order_indexes: false, }; - state + Some(state) } } diff --git a/rust/libqaul/src/services/crypto/storage.rs b/rust/libqaul/src/services/crypto/storage.rs index 16d4ee3d4..4853cda70 100644 --- a/rust/libqaul/src/services/crypto/storage.rs +++ b/rust/libqaul/src/services/crypto/storage.rs @@ -87,7 +87,13 @@ impl CryptoAccount { match result { Ok((_key, crypto_state_bytes)) => { let crypto_state: CryptoState = - bincode::deserialize(&crypto_state_bytes).unwrap(); + match bincode::deserialize(&crypto_state_bytes) { + Ok(v) => v, + Err(e) => { + log::error!("Error deserializing crypto state: {}", e); + continue; + } + }; match crypto_state.state { super::CryptoProcessState::HalfOutgoing => { state_option = Some(crypto_state) @@ -111,7 +117,13 @@ impl CryptoAccount { // get result from data base match self.state.get(key) { Ok(Some(crypto_state_bytes)) => { - let crypto_state: CryptoState = bincode::deserialize(&crypto_state_bytes).unwrap(); + let crypto_state: CryptoState = match bincode::deserialize(&crypto_state_bytes) { + Ok(v) => v, + Err(e) => { + log::error!("Error deserializing crypto state by id: {}", e); + return None; + } + }; return Some(crypto_state); } Ok(None) => return None, @@ -127,7 +139,13 @@ impl CryptoAccount { let key = Self::create_state_key(remote_id, session_id); // save message in data base - let crypto_state_bytes = bincode::serialize(&crypto_state).unwrap(); + let crypto_state_bytes = match bincode::serialize(&crypto_state) { + Ok(v) => v, + Err(e) => { + log::error!("Error serializing crypto state: {}", e); + return; + } + }; if let Err(e) = self.state.insert(key, crypto_state_bytes) { log::error!("Error handshake to db: {}", e); } @@ -150,7 +168,13 @@ impl CryptoAccount { let key = Self::create_cache_key(remote_id, session_id, nonce); // save message in data base - let message_bytes = bincode::serialize(&message).unwrap(); + let message_bytes = match bincode::serialize(&message) { + Ok(v) => v, + Err(e) => { + log::error!("Error serializing cache message: {}", e); + return; + } + }; if let Err(e) = self.cache.insert(key, message_bytes) { log::error!("Error handshake to db: {}", e); } @@ -187,7 +211,13 @@ impl CryptoStorage { // check if user account data exists { // get chat state - let crypto_storage = CRYPTOSTORAGE.get().read().unwrap(); + let crypto_storage = match CRYPTOSTORAGE.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("Error reading crypto storage lock: {}", e); + return Self::create_groupaccountdb(account_id); + } + }; // check if user account ID is in map if let Some(crypto_account_db) = crypto_storage.db_ref.get(&account_id.to_bytes()) { @@ -215,13 +245,37 @@ impl CryptoStorage { let db = DataBase::get_user_db(account_id); // open trees - let state: sled::Tree = db.open_tree("crypto_state").unwrap(); - let cache: sled::Tree = db.open_tree("crypto_cache").unwrap(); + let state: sled::Tree = match db.open_tree("crypto_state") { + Ok(tree) => tree, + Err(e) => { + log::error!("failed to open crypto_state tree: {}", e); + return CryptoAccount { + state: db.open_tree("__fallback_crypto_state").expect("fallback tree"), + cache: db.open_tree("__fallback_crypto_cache").expect("fallback tree"), + }; + } + }; + let cache: sled::Tree = match db.open_tree("crypto_cache") { + Ok(tree) => tree, + Err(e) => { + log::error!("failed to open crypto_cache tree: {}", e); + return CryptoAccount { + state, + cache: db.open_tree("__fallback_crypto_cache").expect("fallback tree"), + }; + } + }; let crypto_account = CryptoAccount { state, cache }; // get group storage for writing - let mut crypto_storage = CRYPTOSTORAGE.get().write().unwrap(); + let mut crypto_storage = match CRYPTOSTORAGE.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("failed to write crypto storage lock: {}", e); + return crypto_account; + } + }; // add user to state crypto_storage diff --git a/rust/libqaul/src/services/dtn/mod.rs b/rust/libqaul/src/services/dtn/mod.rs index ca2d241f6..52b348782 100644 --- a/rust/libqaul/src/services/dtn/mod.rs +++ b/rust/libqaul/src/services/dtn/mod.rs @@ -6,18 +6,21 @@ //! The DTN service sends and receives DTN messages into the network. //! They should reach everyone in the network. +use libp2p::identity::PublicKey; use libp2p::PeerId; use prost::Message; use serde::{Deserialize, Serialize}; use sled; use state::InitCell; -use std::{convert::TryInto, fmt, sync::RwLock}; +use std::{fmt, sync::RwLock}; use super::messaging::{proto, MessagingServiceType}; use crate::node::user_accounts::{UserAccount, UserAccounts}; +use crate::router::table::RoutingTable; use crate::rpc::Rpc; use crate::storage::configuration::Configuration; use crate::storage::database::DataBase; +use crate::utilities::timestamp::Timestamp; /// Import protobuf message definition pub use qaul_proto::qaul_rpc_dtn as proto_rpc; @@ -51,6 +54,49 @@ pub struct DtnStorageState { /// mutable state of storge pub static STORAGESTATE: InitCell> = InitCell::new(); +/// DTN V2 routed message entry stored in sled +#[derive(Serialize, Deserialize, Clone)] +pub struct DtnRoutedV2Entry { + /// serialized DtnRoutedV2 protobuf message + pub routed_v2_bytes: Vec, + /// public key of the original sender + pub sender_public_key: Vec, + /// size of the entry in bytes + pub size: u32, + /// timestamp when this entry was accepted + pub accepted_at: u64, + /// the ultimate receiver's user ID + pub receiver_id: Vec, +} + +/// Per-sender quota tracking for V2 DTN messages +#[derive(Default, Serialize, Deserialize, Clone)] +pub struct SenderQuotaEntry { + /// total bytes used by this sender + pub used_bytes: u64, + /// number of messages stored for this sender + pub message_count: u32, +} + +/// V2 DTN storage state +#[derive(Clone)] +pub struct DtnStorageStateV2 { + /// V2 routed messages: original_signature => DtnRoutedV2Entry + pub db_ref_routed_v2: sled::Tree, + /// Per-sender quota tracking: sender_public_key => SenderQuotaEntry + pub db_ref_sender_quotas: sled::Tree, + /// Total used size for V2 messages + pub used_size: u64, + /// Total V2 message count + pub message_count: u32, +} + +/// mutable state of V2 DTN storage +pub static STORAGESTATE_V2: InitCell> = InitCell::new(); + +/// Maximum bytes a single sender can store on this node (10 MB) +const V2_PER_SENDER_QUOTA: u64 = 10 * 1024 * 1024; + /// qaul Delayed /// pub struct Dtn {} @@ -62,16 +108,30 @@ impl Dtn { let db = DataBase::get_node_db(); // open trees - let dtn_messages: sled::Tree = db.open_tree("dtn-messages").unwrap(); - let db_ref_id: sled::Tree = db.open_tree("dtn-messages-ids").unwrap(); + let dtn_messages: sled::Tree = match db.open_tree("dtn-messages") { + Ok(tree) => tree, + Err(e) => { + log::error!("Failed to open dtn-messages tree: {}", e); + return; + } + }; + let db_ref_id: sled::Tree = match db.open_tree("dtn-messages-ids") { + Ok(tree) => tree, + Err(e) => { + log::error!("Failed to open dtn-messages-ids tree: {}", e); + return; + } + }; // calc current used size let mut used_size: u64 = 0; for entry in dtn_messages.iter() { if let Ok((_, message_entry_bytes)) = entry { - let message_entry: DtnMessageEntry = - bincode::deserialize(&message_entry_bytes).unwrap(); - used_size = used_size + (message_entry.size as u64); + if let Ok(message_entry) = + bincode::deserialize::(&message_entry_bytes) + { + used_size = used_size + (message_entry.size as u64); + } } } let storage_state = DtnStorageState { @@ -82,6 +142,41 @@ impl Dtn { }; STORAGESTATE.set(RwLock::new(storage_state)); + + // Initialize V2 storage + let db_v2 = DataBase::get_node_db(); + let db_ref_routed_v2 = match db_v2.open_tree("dtn-routed-v2") { + Ok(tree) => tree, + Err(e) => { + log::error!("Failed to open dtn-routed-v2 tree: {}", e); + return; + } + }; + let db_ref_sender_quotas = match db_v2.open_tree("dtn-sender-quotas") { + Ok(tree) => tree, + Err(e) => { + log::error!("Failed to open dtn-sender-quotas tree: {}", e); + return; + } + }; + + let mut v2_used_size: u64 = 0; + for entry in db_ref_routed_v2.iter() { + if let Ok((_, entry_bytes)) = entry { + if let Ok(v2_entry) = bincode::deserialize::(&entry_bytes) { + v2_used_size += v2_entry.size as u64; + } + } + } + + let v2_state = DtnStorageStateV2 { + message_count: db_ref_routed_v2.len() as u32, + used_size: v2_used_size, + db_ref_routed_v2, + db_ref_sender_quotas, + }; + + STORAGESTATE_V2.set(RwLock::new(v2_state)); } /// Convert Group ID from String to Binary @@ -135,17 +230,22 @@ impl Dtn { org_sig: &Vec, dtn_payload: &Vec, ) -> (i32, i32) { - let mut storage_state = STORAGESTATE.get().write().unwrap(); + let mut storage_state = match STORAGESTATE.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("DTN: failed to acquire write lock: {}", e); + return ( + super::messaging::proto::dtn_response::ResponseType::Rejected as i32, + super::messaging::proto::dtn_response::Reason::None as i32, + ); + } + }; // check already received - if storage_state.db_ref_id.contains_key(org_sig).unwrap() { + if storage_state.db_ref_id.contains_key(org_sig).unwrap_or(false) { return ( - super::messaging::proto::dtn_response::ResponseType::Accepted - .try_into() - .unwrap(), - super::messaging::proto::dtn_response::Reason::None - .try_into() - .unwrap(), + super::messaging::proto::dtn_response::ResponseType::Accepted as i32, + super::messaging::proto::dtn_response::Reason::None as i32, ); } @@ -157,12 +257,8 @@ impl Dtn { None => { log::error!("dtn module: user profile no exists"); return ( - super::messaging::proto::dtn_response::ResponseType::Rejected - .try_into() - .unwrap(), - super::messaging::proto::dtn_response::Reason::UserNotAccepted - .try_into() - .unwrap(), + super::messaging::proto::dtn_response::ResponseType::Rejected as i32, + super::messaging::proto::dtn_response::Reason::UserNotAccepted as i32, ); } } @@ -172,12 +268,8 @@ impl Dtn { let total_limit = (user_profile.storage.size_total as u64) * 1024 * 1024; if new_size > total_limit { return ( - super::messaging::proto::dtn_response::ResponseType::Rejected - .try_into() - .unwrap(), - super::messaging::proto::dtn_response::Reason::OverallQuota - .try_into() - .unwrap(), + super::messaging::proto::dtn_response::ResponseType::Rejected as i32, + super::messaging::proto::dtn_response::Reason::OverallQuota as i32, ); } @@ -203,7 +295,16 @@ impl Dtn { org_sig: org_sig.clone(), size: dtn_payload.len() as u32, }; - let message_entry_bytes = bincode::serialize(&message_entry).unwrap(); + let message_entry_bytes = match bincode::serialize(&message_entry) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("DTN: failed to serialize message entry: {}", e); + return ( + super::messaging::proto::dtn_response::ResponseType::Rejected as i32, + super::messaging::proto::dtn_response::Reason::None as i32, + ); + } + }; // NOTE: The following two tree writes (db_ref and db_ref_id) are not // atomic. If a crash occurs between them, the database could end up in @@ -247,14 +348,9 @@ impl Dtn { } ( - super::messaging::proto::dtn_response::ResponseType::Accepted - .try_into() - .unwrap(), - super::messaging::proto::dtn_response::Reason::None - .try_into() - .unwrap(), + super::messaging::proto::dtn_response::ResponseType::Accepted as i32, + super::messaging::proto::dtn_response::Reason::None as i32, ) - // update storage state } /// this function is called when receive DTN response @@ -299,15 +395,17 @@ impl Dtn { if let Some(user_account) = UserAccounts::get_by_id(*user_id) { match proto::Container::decode(&dtn_payload[..]) { Ok(container) => { - let envelope = container.envelope.as_ref().unwrap(); + let envelope = match container.envelope.as_ref() { + Some(e) => e, + None => { + log::error!("DTN: no envelope in container"); + return; + } + }; let mut res: (i32, i32) = ( - super::messaging::proto::dtn_response::ResponseType::Accepted - .try_into() - .unwrap(), - super::messaging::proto::dtn_response::Reason::None - .try_into() - .unwrap(), + super::messaging::proto::dtn_response::ResponseType::Accepted as i32, + super::messaging::proto::dtn_response::Reason::None as i32, ); //if container.envelope.receiver_id @@ -368,8 +466,20 @@ impl Dtn { match proto_rpc::Dtn::decode(&data[..]) { Ok(dtn) => match dtn.message { Some(proto_rpc::dtn::Message::DtnStateRequest(_req)) => { - let state = STORAGESTATE.get().read().unwrap(); - let unconfirmed = super::messaging::UNCONFIRMED.get().read().unwrap(); + let state = match STORAGESTATE.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("DTN RPC: failed to acquire read lock: {}", e); + return; + } + }; + let unconfirmed = match super::messaging::UNCONFIRMED.get().read() { + Ok(u) => u, + Err(e) => { + log::error!("DTN RPC: failed to acquire unconfirmed read lock: {}", e); + return; + } + }; let unconfrimed_len = unconfirmed.unconfirmed.len(); let proto_message = proto_rpc::Dtn { @@ -500,23 +610,20 @@ impl Dtn { // check if user storage exists let mut idx: Option = None; - for i in 0..user_profile.storage.users.len() { - if *user_profile.storage.users.get(i).unwrap() == user_id_string { + for (i, user) in user_profile.storage.users.iter().enumerate() { + if *user == user_id_string { idx = Some(i); break; } } - match idx { - None => { - status = false; - message = "User does not exist".to_string(); - } - _ => {} + if idx.is_none() { + status = false; + message = "User does not exist".to_string(); } - if status { + if let Some(i) = idx { let mut opt = user_profile.storage.clone(); - opt.users.remove(idx.unwrap()); + opt.users.remove(i); Configuration::update_user_storage(my_user_id.to_string(), &opt); Configuration::save(); } @@ -570,6 +677,12 @@ impl Dtn { } } } + Some(proto_rpc::dtn::Message::DtnSendRoutedRequest(req)) => { + Self::rpc_send_routed(my_user_id, req, request_id); + } + Some(proto_rpc::dtn::Message::DtnSetCustodyEnabledRequest(req)) => { + Self::rpc_set_custody_enabled(my_user_id, req, request_id); + } _ => { log::error!("Unhandled Protobuf DTN RPC message"); } @@ -579,4 +692,1442 @@ impl Dtn { } } } + + /// Handle DtnSendRoutedRequest RPC + fn rpc_send_routed( + my_user_id: PeerId, + req: proto_rpc::DtnSendRoutedRequest, + request_id: String, + ) { + let send_response = |status: bool, message: String| { + let proto_message = proto_rpc::Dtn { + message: Some(proto_rpc::dtn::Message::DtnSendRoutedResponse( + proto_rpc::DtnSendRoutedResponse { status, message }, + )), + }; + Rpc::send_message( + proto_message.encode_to_vec(), + crate::rpc::proto::Modules::Dtn.into(), + request_id.clone(), + Vec::new(), + ); + }; + + // Validate receiver + let receiver_id = match PeerId::from_bytes(&req.receiver_id) { + Ok(id) => id, + Err(_) => { + send_response(false, "invalid receiver_id".to_string()); + return; + } + }; + + // Validate custody route + if req.custody_route.is_empty() { + send_response(false, "at least one custody user is required".to_string()); + return; + } + if req.custody_route.len() > 10 { + send_response(false, "maximum 10 custody users allowed".to_string()); + return; + } + for user_bytes in &req.custody_route { + if let Ok(uid) = PeerId::from_bytes(user_bytes) { + if uid == my_user_id || uid == receiver_id { + send_response(false, "custodians must not include sender or receiver".to_string()); + return; + } + } else { + send_response(false, "invalid custodian user ID".to_string()); + return; + } + } + + // Get user account + let user_account = match UserAccounts::get_by_id(my_user_id) { + Some(ua) => ua, + None => { + send_response(false, "user account not found".to_string()); + return; + } + }; + + // Use the flat custody route directly + let custody_route = req.custody_route.clone(); + + // Calculate expiry + let expires_at = if req.expiry_seconds > 0 { + Timestamp::get_timestamp() + (req.expiry_seconds * 1000) + } else { + 0 + }; + + // Calculate remaining handoffs + let total_custodians: u32 = custody_route.len() as u32; + let remaining_handoffs = if req.max_handoffs > 0 { + req.max_handoffs + } else { + total_custodians * 2 + }; + + // Extract original_signature from the inner Container + let original_signature = match proto::Container::decode(&req.data[..]) { + Ok(container) => { + if container.signature.is_empty() { + send_response(false, "inner container has no signature".to_string()); + return; + } + container.signature + } + Err(e) => { + send_response(false, format!("invalid container data: {}", e)); + return; + } + }; + + // Build the DtnRoutedV2 message + let routed_v2 = proto::DtnRoutedV2 { + container: req.data.clone(), + custody_route, + next_route_index: 0, + original_signature, + sender_public_key: user_account.keys.public().encode_protobuf(), + expires_at, + remaining_handoffs, + }; + + // Find initial target + let target = match Self::select_custody_target(&routed_v2, &receiver_id) { + Some(t) => t, + None => { + send_response(false, "no reachable custodian found".to_string()); + return; + } + }; + + // Send via envelope + match super::messaging::Messaging::send_dtn_routed_v2_message( + &user_account, + &target, + routed_v2.clone(), + ) { + Ok(_sig) => { + // Store in V2 state so on_dtn_response_v2 can clean up + // when the first custodian responds + let entry_size = routed_v2.container.len() as u32; + let v2_entry = DtnRoutedV2Entry { + routed_v2_bytes: routed_v2.encode_to_vec(), + sender_public_key: routed_v2.sender_public_key.clone(), + size: entry_size, + accepted_at: Timestamp::get_timestamp(), + receiver_id: receiver_id.to_bytes(), + }; + if let Ok(entry_bytes) = bincode::serialize(&v2_entry) { + if let Ok(mut state) = STORAGESTATE_V2.get().write() { + let _ = state.db_ref_routed_v2.insert( + routed_v2.original_signature.clone(), + entry_bytes, + ); + let _ = state.db_ref_routed_v2.flush(); + state.used_size += entry_size as u64; + state.message_count += 1; + } + } + send_response(true, "".to_string()); + } + Err(e) => { + send_response(false, e); + } + } + } + + /// Determine where to forward a V2 DTN message. + /// + /// Returns the recipient if online, otherwise scans the custody route + /// forward from `next_route_index`, returning the first reachable user. + /// Handle DtnSetCustodyEnabledRequest RPC + fn rpc_set_custody_enabled( + my_user_id: PeerId, + req: proto_rpc::DtnSetCustodyEnabledRequest, + request_id: String, + ) { + let send_response = |status: bool, message: String| { + let proto_message = proto_rpc::Dtn { + message: Some(proto_rpc::dtn::Message::DtnSetCustodyEnabledResponse( + proto_rpc::DtnSetCustodyEnabledResponse { status, message }, + )), + }; + Rpc::send_message( + proto_message.encode_to_vec(), + crate::rpc::proto::Modules::Dtn.into(), + request_id.clone(), + Vec::new(), + ); + }; + + match Configuration::get_user(my_user_id.to_string()) { + Some(mut user_profile) => { + user_profile.storage.dtn_v2_custody_enabled = req.enabled; + Configuration::update_user_storage( + my_user_id.to_string(), + &user_profile.storage, + ); + Configuration::save(); + send_response(true, "".to_string()); + } + None => { + send_response(false, "user profile not found".to_string()); + } + } + } + + pub fn select_custody_target( + routed_v2: &proto::DtnRoutedV2, + receiver_id: &PeerId, + ) -> Option { + // Check if recipient is directly reachable + if RoutingTable::get_route_to_user(*receiver_id).is_some() { + return Some(*receiver_id); + } + + // Strict forward scan from next_route_index + let start = routed_v2.next_route_index as usize; + let len = routed_v2.custody_route.len(); + if start >= len { + return None; + } + for i in start..len { + if let Ok(custodian_id) = PeerId::from_bytes(&routed_v2.custody_route[i]) { + if RoutingTable::get_route_to_user(custodian_id).is_some() { + return Some(custodian_id); + } + } + } + + None + } + + /// Process a received DtnRoutedV2 message from the network + pub fn net_routed_v2( + user_id: &PeerId, + sender_id: &PeerId, + _signature: &[u8], + routed_v2: proto::DtnRoutedV2, + ) { + log::info!("Received DtnRoutedV2 message from {}", sender_id.to_base58()); + + let user_account = match UserAccounts::get_by_id(*user_id) { + Some(ua) => ua, + None => { + log::error!("DtnRoutedV2: user account not found for {}", user_id.to_base58()); + if let Some(default_user) = UserAccounts::get_default_user() { + Self::send_v2_response( + &default_user, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::UserNotAccepted, + ); + } + return; + } + }; + + // 1. Expiry check + if routed_v2.expires_at > 0 { + let now = Timestamp::get_timestamp(); + if now > routed_v2.expires_at { + log::warn!("DtnRoutedV2 message expired, dropping"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::None, + ); + return; + } + } + + // 2. Handoff check + if routed_v2.remaining_handoffs == 0 { + log::warn!("DtnRoutedV2 message has no remaining handoffs, dropping"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::None, + ); + return; + } + + // 3. Duplicate check + { + let state = match STORAGESTATE_V2.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("DtnRoutedV2: failed to acquire read lock: {}", e); + return; + } + }; + if state + .db_ref_routed_v2 + .contains_key(&routed_v2.original_signature) + .unwrap_or(false) + { + log::info!("DtnRoutedV2 duplicate detected, accepting silently"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Accepted, + proto::dtn_response::Reason::None, + ); + return; + } + } + + // 4. Am I the recipient? + let envelope_receiver = Self::get_receiver_from_container(&routed_v2.container); + if let Some(recv_id) = envelope_receiver { + if recv_id == *user_id { + log::info!("DtnRoutedV2: I am the recipient, processing inner container"); + if let Ok(container) = proto::Container::decode(&routed_v2.container[..]) { + super::messaging::process::MessagingProcess::process_received_message( + user_account.clone(), + container, + ); + } + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Accepted, + proto::dtn_response::Reason::None, + ); + return; + } + } + + // 5. Custody opt-in check + match Configuration::get_user(user_account.id.to_string()) { + Some(user_profile) => { + if !user_profile.storage.dtn_v2_custody_enabled { + log::warn!("DtnRoutedV2: custody not enabled for this node"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::UserNotAccepted, + ); + return; + } + } + None => { + log::error!("DtnRoutedV2: user profile not found for custody check"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::UserNotAccepted, + ); + return; + } + } + + // 6. Sender signature verification + { + let inner_container = match proto::Container::decode(&routed_v2.container[..]) { + Ok(c) => c, + Err(e) => { + log::error!("DtnRoutedV2: failed to decode inner container: {}", e); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::None, + ); + return; + } + }; + let sender_key = match PublicKey::try_decode_protobuf(&routed_v2.sender_public_key) { + Ok(k) => k, + Err(e) => { + log::error!("DtnRoutedV2: invalid sender public key: {}", e); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::None, + ); + return; + } + }; + if let Some(envelope) = inner_container.envelope.as_ref() { + let mut envelope_buf = Vec::with_capacity(envelope.encoded_len()); + envelope + .encode(&mut envelope_buf) + .expect("Vec provides capacity as needed"); + if !sender_key.verify(&envelope_buf, &inner_container.signature) { + log::error!("DtnRoutedV2: inner container signature verification failed"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::UserNotAccepted, + ); + return; + } + } else { + log::error!("DtnRoutedV2: inner container has no envelope"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::None, + ); + return; + } + } + + // 7. Per-sender quota check + { + let state = match STORAGESTATE_V2.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("DtnRoutedV2: failed to acquire read lock for quota check: {}", e); + return; + } + }; + if let Ok(Some(quota_bytes)) = state + .db_ref_sender_quotas + .get(&routed_v2.sender_public_key) + { + if let Ok(quota) = bincode::deserialize::("a_bytes) { + if quota.used_bytes + (routed_v2.container.len() as u64) > V2_PER_SENDER_QUOTA { + log::warn!("DtnRoutedV2: per-sender quota exceeded"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::UserQuota, + ); + return; + } + } + } + } + + // 7. Overall quota check + { + let state = match STORAGESTATE_V2.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("DtnRoutedV2: failed to acquire read lock for overall quota: {}", e); + return; + } + }; + let v1_state = match STORAGESTATE.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("DtnRoutedV2: failed to acquire V1 read lock: {}", e); + return; + } + }; + match Configuration::get_user(user_account.id.to_string()) { + Some(user_profile) => { + let total_limit = (user_profile.storage.size_total as u64) * 1024 * 1024; + let total_used = + v1_state.used_size + state.used_size + (routed_v2.container.len() as u64); + if total_used > total_limit { + log::warn!("DtnRoutedV2: overall quota exceeded"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::OverallQuota, + ); + return; + } + } + None => { + log::error!("DtnRoutedV2: user profile not found"); + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Rejected, + proto::dtn_response::Reason::UserNotAccepted, + ); + return; + } + } + } + + // 8. Accept custody: store in DB + let entry_size = routed_v2.container.len() as u32; + let v2_entry = DtnRoutedV2Entry { + routed_v2_bytes: routed_v2.encode_to_vec(), + sender_public_key: routed_v2.sender_public_key.clone(), + size: entry_size, + accepted_at: Timestamp::get_timestamp(), + receiver_id: envelope_receiver.map(|r| r.to_bytes()).unwrap_or_default(), + }; + let entry_bytes = match bincode::serialize(&v2_entry) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("DtnRoutedV2: failed to serialize entry: {}", e); + return; + } + }; + + { + let mut state = match STORAGESTATE_V2.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("DtnRoutedV2: failed to acquire write lock: {}", e); + return; + } + }; + if let Err(e) = state + .db_ref_routed_v2 + .insert(routed_v2.original_signature.clone(), entry_bytes) + { + log::error!("DtnRoutedV2: storage insert error: {}", e); + } + let _ = state.db_ref_routed_v2.flush(); + + state.used_size += entry_size as u64; + state.message_count += 1; + + // Update sender quota + let mut quota = if let Ok(Some(quota_bytes)) = state + .db_ref_sender_quotas + .get(&routed_v2.sender_public_key) + { + bincode::deserialize::("a_bytes).unwrap_or_default() + } else { + SenderQuotaEntry::default() + }; + quota.used_bytes += entry_size as u64; + quota.message_count += 1; + if let Ok(quota_bytes) = bincode::serialize("a) { + let _ = state + .db_ref_sender_quotas + .insert(routed_v2.sender_public_key.clone(), quota_bytes); + } else { + log::error!("DtnRoutedV2: failed to serialize sender quota"); + } + let _ = state.db_ref_sender_quotas.flush(); + } + + // Send acceptance response + Self::send_v2_response( + &user_account, + sender_id, + &routed_v2.original_signature, + proto::dtn_response::ResponseType::Accepted, + proto::dtn_response::Reason::None, + ); + + // 9. Attempt immediate forward + if let Some(recv_id) = envelope_receiver { + Self::try_forward_v2(&user_account, &routed_v2, &recv_id); + } + } + + /// Try to forward a V2 message to the next custodian or recipient + fn try_forward_v2( + user_account: &UserAccount, + routed_v2: &proto::DtnRoutedV2, + receiver_id: &PeerId, + ) { + if let Some(target) = Self::select_custody_target(routed_v2, receiver_id) { + let mut forwarded = routed_v2.clone(); + forwarded.remaining_handoffs = forwarded.remaining_handoffs.saturating_sub(1); + + // Advance next_route_index past the target + for (i, user_bytes) in forwarded.custody_route.iter().enumerate() { + if let Ok(uid) = PeerId::from_bytes(user_bytes) { + if uid == target && i as u32 >= forwarded.next_route_index { + forwarded.next_route_index = (i as u32) + 1; + break; + } + } + } + + if let Err(e) = super::messaging::Messaging::send_dtn_routed_v2_message( + user_account, + &target, + forwarded, + ) { + log::error!("DtnRoutedV2: forward error: {}", e); + } + } + } + + /// Send a DTN response message for V2 handling + fn send_v2_response( + user_account: &UserAccount, + sender_id: &PeerId, + signature: &[u8], + response_type: proto::dtn_response::ResponseType, + reason: proto::dtn_response::Reason, + ) { + let dtn_response = proto::DtnResponse { + response_type: response_type as i32, + reason: reason as i32, + signature: signature.to_vec(), + }; + let send_message = proto::Messaging { + message: Some(proto::messaging::Message::DtnResponse(dtn_response)), + }; + if let Err(e) = super::messaging::Messaging::pack_and_send_message( + user_account, + sender_id, + send_message.encode_to_vec(), + MessagingServiceType::DtnStored, + &Vec::new(), + false, + ) { + log::error!("DtnRoutedV2: send response error: {}", e); + } + } + + /// Extract the receiver PeerId from a serialized Container + fn get_receiver_from_container(container_bytes: &[u8]) -> Option { + if let Ok(container) = proto::Container::decode(container_bytes) { + if let Some(envelope) = container.envelope { + return PeerId::from_bytes(&envelope.receiver_id).ok(); + } + } + None + } + + /// Handle DTN response for V2 messages + pub fn on_dtn_response_v2(dtn_response: &proto::DtnResponse) { + let mut state = match STORAGESTATE_V2.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("DtnRoutedV2: failed to acquire write lock for response: {}", e); + return; + } + }; + if let Ok(Some(entry_bytes)) = state + .db_ref_routed_v2 + .get(&dtn_response.signature) + { + if let Ok(entry) = bincode::deserialize::(&entry_bytes) { + // Only remove on acceptance + if dtn_response.response_type + == proto::dtn_response::ResponseType::Accepted as i32 + { + // Remove from V2 storage + let _ = state.db_ref_routed_v2.remove(&dtn_response.signature); + let _ = state.db_ref_routed_v2.flush(); + + // Update counts + state.used_size = state.used_size.saturating_sub(entry.size as u64); + if state.message_count > 0 { + state.message_count -= 1; + } + + // Update sender quota + if let Ok(Some(quota_bytes)) = + state.db_ref_sender_quotas.get(&entry.sender_public_key) + { + if let Ok(mut quota) = + bincode::deserialize::("a_bytes) + { + quota.used_bytes = quota.used_bytes.saturating_sub(entry.size as u64); + quota.message_count = quota.message_count.saturating_sub(1); + if let Ok(quota_bytes) = bincode::serialize("a) { + let _ = state + .db_ref_sender_quotas + .insert(entry.sender_public_key.clone(), quota_bytes); + let _ = state.db_ref_sender_quotas.flush(); + } + } + } + } + } + } + } + + /// Process V2 routed messages in the retransmit loop. + /// Called periodically to check if stored V2 messages can be forwarded. + pub fn process_retransmit_v2() { + let state = match STORAGESTATE_V2.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("DtnRoutedV2: failed to acquire read lock for retransmit: {}", e); + return; + } + }; + let now = Timestamp::get_timestamp(); + + let mut to_remove: Vec> = Vec::new(); + let mut to_forward: Vec<(Vec, DtnRoutedV2Entry)> = Vec::new(); + + for entry in state.db_ref_routed_v2.iter() { + if let Ok((sig, entry_bytes)) = entry { + if let Ok(v2_entry) = bincode::deserialize::(&entry_bytes) { + if let Ok(routed_v2) = + proto::DtnRoutedV2::decode(&v2_entry.routed_v2_bytes[..]) + { + // Check expiry + if routed_v2.expires_at > 0 && now > routed_v2.expires_at { + to_remove.push(sig.to_vec()); + continue; + } + + to_forward.push((sig.to_vec(), v2_entry)); + } + } + } + } + drop(state); + + // Remove expired entries + if !to_remove.is_empty() { + let mut state = match STORAGESTATE_V2.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("DtnRoutedV2: failed to acquire write lock for cleanup: {}", e); + return; + } + }; + for sig in &to_remove { + if let Ok(Some(entry_bytes)) = state.db_ref_routed_v2.get(sig) { + if let Ok(entry) = bincode::deserialize::(&entry_bytes) { + state.used_size = state.used_size.saturating_sub(entry.size as u64); + if state.message_count > 0 { + state.message_count -= 1; + } + // Update sender quota + if let Ok(Some(quota_bytes)) = + state.db_ref_sender_quotas.get(&entry.sender_public_key) + { + if let Ok(mut quota) = + bincode::deserialize::("a_bytes) + { + quota.used_bytes = + quota.used_bytes.saturating_sub(entry.size as u64); + quota.message_count = quota.message_count.saturating_sub(1); + if let Ok(quota_bytes) = bincode::serialize("a) { + let _ = state + .db_ref_sender_quotas + .insert(entry.sender_public_key.clone(), quota_bytes); + } + } + } + } + } + let _ = state.db_ref_routed_v2.remove(sig); + } + let _ = state.db_ref_routed_v2.flush(); + let _ = state.db_ref_sender_quotas.flush(); + } + + // Try to forward stored messages + for (_sig, v2_entry) in &to_forward { + if let Ok(routed_v2) = proto::DtnRoutedV2::decode(&v2_entry.routed_v2_bytes[..]) { + if let Ok(recv_id) = PeerId::from_bytes(&v2_entry.receiver_id) { + // We need a user account to send from. Use the first local account. + if let Some(user_account) = UserAccounts::get_default_user() { + Self::try_forward_v2(&user_account, &routed_v2, &recv_id); + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::connections::ConnectionModule; + use crate::router::table::{RoutingConnectionEntry, RoutingTable, RoutingUserEntry}; + use crate::utilities::qaul_id::QaulId; + use libp2p::identity::Keypair; + use prost::Message; + use std::collections::HashMap; + use std::sync::{Mutex, Once}; + + static INIT_ROUTING: Once = Once::new(); + + /// Mutex to serialize tests that share the global routing table. + static ROUTING_TABLE_LOCK: Mutex<()> = Mutex::new(()); + + /// Ensure the global routing table is initialized exactly once. + fn ensure_routing_table() { + INIT_ROUTING.call_once(|| { + RoutingTable::init(); + }); + } + + /// Create a random PeerId from a fresh Ed25519 keypair. + fn random_peer() -> PeerId { + let keys = Keypair::generate_ed25519(); + PeerId::from(keys.public()) + } + + /// Make a user appear "online" by inserting a routing entry. + fn make_online(table: &mut HashMap, RoutingUserEntry>, peer: PeerId) { + let q8id = QaulId::to_q8id(peer); + let neighbour = random_peer(); + table.insert( + q8id.clone(), + RoutingUserEntry { + id: q8id, + pgid: 1, + pgid_update: 0, + pgid_update_hc: 0, + online_time: 0, + connections: vec![RoutingConnectionEntry { + module: ConnectionModule::Lan, + node: neighbour, + rtt: 50, + hc: 1, + lq: 10, + last_update: 0, + }], + }, + ); + } + + /// Set the global routing table to contain exactly the given entries. + fn set_routing_table(table: HashMap, RoutingUserEntry>) { + ensure_routing_table(); + RoutingTable::set(RoutingTable { table }); + } + + /// Build a DtnRoutedV2 with the given custody route and next_route_index. + fn build_routed_v2(custody_route: Vec>, next_route_index: u32) -> proto::DtnRoutedV2 { + proto::DtnRoutedV2 { + container: vec![1, 2, 3], + custody_route, + next_route_index, + original_signature: vec![0xAA], + sender_public_key: vec![0xBB], + expires_at: 0, + remaining_handoffs: 10, + } + } + + // ── Serialization tests ── + + #[test] + fn dtn_routed_v2_round_trip() { + let original = proto::DtnRoutedV2 { + container: vec![10, 20, 30, 40], + custody_route: vec![vec![1, 2, 3], vec![4, 5, 6]], + next_route_index: 0, + original_signature: vec![0xAA, 0xBB], + sender_public_key: vec![0xCC, 0xDD], + expires_at: 1234567890, + remaining_handoffs: 5, + }; + + let encoded = original.encode_to_vec(); + assert!(!encoded.is_empty()); + + let decoded = proto::DtnRoutedV2::decode(&encoded[..]).unwrap(); + assert_eq!(decoded.container, original.container); + assert_eq!(decoded.custody_route.len(), 2); + assert_eq!(decoded.custody_route[0], vec![1, 2, 3]); + assert_eq!(decoded.custody_route[1], vec![4, 5, 6]); + assert_eq!(decoded.next_route_index, 0); + assert_eq!(decoded.original_signature, original.original_signature); + assert_eq!(decoded.sender_public_key, original.sender_public_key); + assert_eq!(decoded.expires_at, original.expires_at); + assert_eq!(decoded.remaining_handoffs, original.remaining_handoffs); + } + + #[test] + fn dtn_routed_v2_serde_round_trip() { + let original = proto::DtnRoutedV2 { + container: vec![10, 20], + custody_route: vec![vec![1, 2, 3]], + next_route_index: 1, + original_signature: vec![0xAA], + sender_public_key: vec![0xCC], + expires_at: 0, + remaining_handoffs: 3, + }; + + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: proto::DtnRoutedV2 = bincode::deserialize(&serialized).unwrap(); + assert_eq!(deserialized.container, original.container); + assert_eq!(deserialized.remaining_handoffs, 3); + } + + #[test] + fn dtn_routed_v2_entry_serde_round_trip() { + let entry = DtnRoutedV2Entry { + routed_v2_bytes: vec![1, 2, 3, 4], + sender_public_key: vec![5, 6], + size: 100, + accepted_at: 999, + receiver_id: vec![7, 8, 9], + }; + + let serialized = bincode::serialize(&entry).unwrap(); + let deserialized: DtnRoutedV2Entry = bincode::deserialize(&serialized).unwrap(); + assert_eq!(deserialized.routed_v2_bytes, entry.routed_v2_bytes); + assert_eq!(deserialized.size, 100); + assert_eq!(deserialized.accepted_at, 999); + } + + #[test] + fn sender_quota_entry_serde_round_trip() { + let entry = SenderQuotaEntry { + used_bytes: 5000, + message_count: 3, + }; + + let serialized = bincode::serialize(&entry).unwrap(); + let deserialized: SenderQuotaEntry = bincode::deserialize(&serialized).unwrap(); + assert_eq!(deserialized.used_bytes, 5000); + assert_eq!(deserialized.message_count, 3); + } + + #[test] + fn envelop_payload_dtn_routed_v2_variant() { + let routed_v2 = proto::DtnRoutedV2 { + container: vec![1, 2, 3], + custody_route: vec![], + next_route_index: 0, + original_signature: vec![], + sender_public_key: vec![], + expires_at: 0, + remaining_handoffs: 1, + }; + + let payload = proto::EnvelopPayload { + payload: Some(proto::envelop_payload::Payload::DtnRoutedV2(routed_v2)), + }; + + let encoded = payload.encode_to_vec(); + let decoded = proto::EnvelopPayload::decode(&encoded[..]).unwrap(); + + match decoded.payload { + Some(proto::envelop_payload::Payload::DtnRoutedV2(v2)) => { + assert_eq!(v2.container, vec![1, 2, 3]); + assert_eq!(v2.remaining_handoffs, 1); + } + _ => panic!("Expected DtnRoutedV2 payload variant"), + } + } + + // ── select_custody_target tests ── + // + // NOTE: These tests share a process-wide global RoutingTable. They run + // sequentially (cargo test runs tests within one binary on separate + // threads, but `RoutingTable::set` replaces the whole table atomically). + // Each test sets its own table before asserting. + + #[test] + fn select_target_returns_recipient_when_online() { + let _lock = ROUTING_TABLE_LOCK.lock().unwrap(); + let recipient = random_peer(); + let custodian = random_peer(); + + let mut table = HashMap::new(); + make_online(&mut table, recipient); + // custodian is NOT online + set_routing_table(table); + + let v2 = build_routed_v2(vec![custodian.to_bytes()], 0); + + let target = Dtn::select_custody_target(&v2, &recipient); + assert_eq!(target, Some(recipient)); + } + + #[test] + fn select_target_returns_first_reachable_custodian() { + let _lock = ROUTING_TABLE_LOCK.lock().unwrap(); + let recipient = random_peer(); + let c1 = random_peer(); + let c2 = random_peer(); + + let mut table = HashMap::new(); + // recipient offline, c1 offline, c2 online + make_online(&mut table, c2); + set_routing_table(table); + + let v2 = build_routed_v2(vec![c1.to_bytes(), c2.to_bytes()], 0); + + let target = Dtn::select_custody_target(&v2, &recipient); + assert_eq!(target, Some(c2)); + } + + #[test] + fn select_target_returns_none_when_nobody_online() { + let _lock = ROUTING_TABLE_LOCK.lock().unwrap(); + let recipient = random_peer(); + let c1 = random_peer(); + let c2 = random_peer(); + + // Empty routing table — nobody online + set_routing_table(HashMap::new()); + + let v2 = build_routed_v2(vec![c1.to_bytes(), c2.to_bytes()], 0); + + let target = Dtn::select_custody_target(&v2, &recipient); + assert_eq!(target, None); + } + + #[test] + fn select_target_respects_next_route_index() { + let _lock = ROUTING_TABLE_LOCK.lock().unwrap(); + let recipient = random_peer(); + let c1 = random_peer(); + let c2 = random_peer(); + + let mut table = HashMap::new(); + // Both custodians online + make_online(&mut table, c1); + make_online(&mut table, c2); + set_routing_table(table); + + // next_route_index = 1 means c1 (index 0) is already done, only c2 eligible + let v2 = build_routed_v2(vec![c1.to_bytes(), c2.to_bytes()], 1); + + let target = Dtn::select_custody_target(&v2, &recipient); + assert_eq!(target, Some(c2)); + } + + #[test] + fn select_target_skips_exhausted_route() { + let _lock = ROUTING_TABLE_LOCK.lock().unwrap(); + let recipient = random_peer(); + let c1 = random_peer(); + + let mut table = HashMap::new(); + make_online(&mut table, c1); + set_routing_table(table); + + // next_route_index == len means the route is exhausted + let v2 = build_routed_v2(vec![c1.to_bytes()], 1); + + let target = Dtn::select_custody_target(&v2, &recipient); + assert_eq!(target, None); + } + + #[test] + fn select_target_picks_first_reachable_in_forward_order() { + let _lock = ROUTING_TABLE_LOCK.lock().unwrap(); + let recipient = random_peer(); + let c1 = random_peer(); // first in route + let c2 = random_peer(); // second in route + + let mut table = HashMap::new(); + // Both online + make_online(&mut table, c1); + make_online(&mut table, c2); + set_routing_table(table); + + let v2 = build_routed_v2(vec![c1.to_bytes(), c2.to_bytes()], 0); + + // Forward scan should pick c1 (index 0) as the first reachable + let target = Dtn::select_custody_target(&v2, &recipient); + assert_eq!(target, Some(c1)); + } + + // ── Route advancement tests ── + + #[test] + fn route_next_route_index_advances_on_forward() { + let c1 = random_peer(); + let c2 = random_peer(); + let target = c2; + + let mut routed = proto::DtnRoutedV2 { + container: vec![], + custody_route: vec![c1.to_bytes(), c2.to_bytes()], + next_route_index: 0, + original_signature: vec![], + sender_public_key: vec![], + expires_at: 0, + remaining_handoffs: 5, + }; + + // Simulate what try_forward_v2 does to the route state + routed.remaining_handoffs = routed.remaining_handoffs.saturating_sub(1); + for (i, user_bytes) in routed.custody_route.iter().enumerate() { + if let Ok(uid) = PeerId::from_bytes(user_bytes) { + if uid == target && i as u32 >= routed.next_route_index { + routed.next_route_index = (i as u32) + 1; + break; + } + } + } + + assert_eq!(routed.remaining_handoffs, 4); + assert_eq!(routed.next_route_index, 2); // c2 is at index 1, so next = 2 + } + + #[test] + fn route_next_route_index_advances_for_middle_custodian() { + let c1 = random_peer(); + let c2 = random_peer(); + let c3 = random_peer(); + let target = c2; + + let mut routed = proto::DtnRoutedV2 { + container: vec![], + custody_route: vec![c1.to_bytes(), c2.to_bytes(), c3.to_bytes()], + next_route_index: 0, + original_signature: vec![], + sender_public_key: vec![], + expires_at: 0, + remaining_handoffs: 5, + }; + + routed.remaining_handoffs = routed.remaining_handoffs.saturating_sub(1); + for (i, user_bytes) in routed.custody_route.iter().enumerate() { + if let Ok(uid) = PeerId::from_bytes(user_bytes) { + if uid == target && i as u32 >= routed.next_route_index { + routed.next_route_index = (i as u32) + 1; + break; + } + } + } + + // c2 at index 1, next_route_index should advance to 2, leaving c3 still eligible + assert_eq!(routed.next_route_index, 2); + } + + // ── V2 storage tests ── + + fn init_v2_storage() { + use std::sync::Once; + static INIT_V2: Once = Once::new(); + INIT_V2.call_once(|| { + let db = sled::Config::new().temporary(true).open().unwrap(); + let db_ref_routed_v2 = db.open_tree("dtn-routed-v2").unwrap(); + let db_ref_sender_quotas = db.open_tree("dtn-sender-quotas").unwrap(); + STORAGESTATE_V2.set(RwLock::new(DtnStorageStateV2 { + db_ref_routed_v2, + db_ref_sender_quotas, + used_size: 0, + message_count: 0, + })); + }); + } + + #[test] + fn v2_storage_insert_and_retrieve() { + init_v2_storage(); + + let sig = vec![0x01, 0x02, 0x03]; + let entry = DtnRoutedV2Entry { + routed_v2_bytes: vec![10, 20, 30], + sender_public_key: vec![0xAA], + size: 3, + accepted_at: 12345, + receiver_id: vec![0xBB], + }; + let entry_bytes = bincode::serialize(&entry).unwrap(); + + { + let mut state = STORAGESTATE_V2.get().write().unwrap(); + state + .db_ref_routed_v2 + .insert(sig.clone(), entry_bytes) + .unwrap(); + state.db_ref_routed_v2.flush().unwrap(); + state.used_size += entry.size as u64; + state.message_count += 1; + } + + // Retrieve + { + let state = STORAGESTATE_V2.get().read().unwrap(); + assert!(state.db_ref_routed_v2.contains_key(&sig).unwrap()); + let stored = state.db_ref_routed_v2.get(&sig).unwrap().unwrap(); + let decoded: DtnRoutedV2Entry = bincode::deserialize(&stored).unwrap(); + assert_eq!(decoded.routed_v2_bytes, vec![10, 20, 30]); + assert_eq!(decoded.size, 3); + } + + // Cleanup + { + let mut state = STORAGESTATE_V2.get().write().unwrap(); + state.db_ref_routed_v2.remove(&sig).unwrap(); + state.used_size = 0; + state.message_count = 0; + } + } + + #[test] + fn v2_storage_duplicate_detection() { + init_v2_storage(); + + let sig = vec![0xDE, 0xAD]; + let entry = DtnRoutedV2Entry { + routed_v2_bytes: vec![1], + sender_public_key: vec![2], + size: 1, + accepted_at: 0, + receiver_id: vec![3], + }; + let entry_bytes = bincode::serialize(&entry).unwrap(); + + { + let state = STORAGESTATE_V2.get().write().unwrap(); + state + .db_ref_routed_v2 + .insert(sig.clone(), entry_bytes) + .unwrap(); + } + + // Should detect duplicate + { + let state = STORAGESTATE_V2.get().read().unwrap(); + assert!(state.db_ref_routed_v2.contains_key(&sig).unwrap()); + } + + // Cleanup + { + let state = STORAGESTATE_V2.get().write().unwrap(); + state.db_ref_routed_v2.remove(&sig).unwrap(); + } + } + + #[test] + fn v2_sender_quota_tracking() { + init_v2_storage(); + + let sender_key = vec![0xCC, 0xDD]; + let quota = SenderQuotaEntry { + used_bytes: 500, + message_count: 2, + }; + let quota_bytes = bincode::serialize("a).unwrap(); + + { + let state = STORAGESTATE_V2.get().write().unwrap(); + state + .db_ref_sender_quotas + .insert(sender_key.clone(), quota_bytes) + .unwrap(); + } + + // Retrieve and check + { + let state = STORAGESTATE_V2.get().read().unwrap(); + let stored = state + .db_ref_sender_quotas + .get(&sender_key) + .unwrap() + .unwrap(); + let decoded: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + assert_eq!(decoded.used_bytes, 500); + assert_eq!(decoded.message_count, 2); + } + + // Simulate removing a message — quota should decrease + { + let state = STORAGESTATE_V2.get().write().unwrap(); + let stored = state + .db_ref_sender_quotas + .get(&sender_key) + .unwrap() + .unwrap(); + let mut decoded: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + decoded.used_bytes = decoded.used_bytes.saturating_sub(200); + decoded.message_count = decoded.message_count.saturating_sub(1); + let updated = bincode::serialize(&decoded).unwrap(); + state + .db_ref_sender_quotas + .insert(sender_key.clone(), updated) + .unwrap(); + } + + { + let state = STORAGESTATE_V2.get().read().unwrap(); + let stored = state + .db_ref_sender_quotas + .get(&sender_key) + .unwrap() + .unwrap(); + let decoded: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + assert_eq!(decoded.used_bytes, 300); + assert_eq!(decoded.message_count, 1); + } + + // Cleanup + { + let state = STORAGESTATE_V2.get().write().unwrap(); + state.db_ref_sender_quotas.remove(&sender_key).unwrap(); + } + } + + #[test] + fn v2_per_sender_quota_limit_enforced() { + init_v2_storage(); + + let sender_key = vec![0xEE, 0xFF]; + // Set quota near the limit + let quota = SenderQuotaEntry { + used_bytes: V2_PER_SENDER_QUOTA - 10, + message_count: 100, + }; + let quota_bytes = bincode::serialize("a).unwrap(); + + { + let state = STORAGESTATE_V2.get().read().unwrap(); + state + .db_ref_sender_quotas + .insert(sender_key.clone(), quota_bytes) + .unwrap(); + } + + // A message of size 11 should exceed the quota + { + let state = STORAGESTATE_V2.get().read().unwrap(); + let stored = state + .db_ref_sender_quotas + .get(&sender_key) + .unwrap() + .unwrap(); + let decoded: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + let new_msg_size: u64 = 11; + assert!(decoded.used_bytes + new_msg_size > V2_PER_SENDER_QUOTA); + } + + // A message of size 5 should be under the quota + { + let state = STORAGESTATE_V2.get().read().unwrap(); + let stored = state + .db_ref_sender_quotas + .get(&sender_key) + .unwrap() + .unwrap(); + let decoded: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + let new_msg_size: u64 = 5; + assert!(decoded.used_bytes + new_msg_size <= V2_PER_SENDER_QUOTA); + } + + // Cleanup + { + let state = STORAGESTATE_V2.get().read().unwrap(); + state.db_ref_sender_quotas.remove(&sender_key).unwrap(); + } + } + + // ── Signature verification tests ── + + /// Helper: create a properly signed inner Container with a real keypair. + /// Returns (keypair, container_bytes, container_signature). + fn build_signed_container(receiver: &PeerId) -> (Keypair, Vec, Vec) { + let keys = Keypair::generate_ed25519(); + let sender = PeerId::from(keys.public()); + + let envelope = proto::Envelope { + sender_id: sender.to_bytes(), + receiver_id: receiver.to_bytes(), + payload: vec![0xDE, 0xAD], + }; + + let mut envelope_buf = Vec::with_capacity(envelope.encoded_len()); + envelope.encode(&mut envelope_buf).unwrap(); + + let signature = keys.sign(&envelope_buf).unwrap(); + + let container = proto::Container { + signature: signature.clone(), + envelope: Some(envelope), + }; + + (keys, container.encode_to_vec(), signature) + } + + #[test] + fn signature_verification_accepts_valid_signature() { + let receiver = random_peer(); + let (keys, container_bytes, _sig) = build_signed_container(&receiver); + + // Decode the inner container + let inner = proto::Container::decode(&container_bytes[..]).unwrap(); + let sender_key = keys.public(); + + // Re-encode envelope and verify + let envelope = inner.envelope.as_ref().unwrap(); + let mut envelope_buf = Vec::with_capacity(envelope.encoded_len()); + envelope.encode(&mut envelope_buf).unwrap(); + + assert!(sender_key.verify(&envelope_buf, &inner.signature)); + } + + #[test] + fn signature_verification_rejects_wrong_key() { + let receiver = random_peer(); + let (_keys, container_bytes, _sig) = build_signed_container(&receiver); + + // Use a different key to verify + let wrong_keys = Keypair::generate_ed25519(); + let wrong_key = wrong_keys.public(); + + let inner = proto::Container::decode(&container_bytes[..]).unwrap(); + let envelope = inner.envelope.as_ref().unwrap(); + let mut envelope_buf = Vec::with_capacity(envelope.encoded_len()); + envelope.encode(&mut envelope_buf).unwrap(); + + assert!(!wrong_key.verify(&envelope_buf, &inner.signature)); + } + + #[test] + fn signature_verification_rejects_tampered_envelope() { + let receiver = random_peer(); + let (keys, container_bytes, _sig) = build_signed_container(&receiver); + + let mut inner = proto::Container::decode(&container_bytes[..]).unwrap(); + // Tamper with the envelope + inner.envelope.as_mut().unwrap().payload = vec![0xFF, 0xFF]; + + let sender_key = keys.public(); + let envelope = inner.envelope.as_ref().unwrap(); + let mut envelope_buf = Vec::with_capacity(envelope.encoded_len()); + envelope.encode(&mut envelope_buf).unwrap(); + + // Original signature should not verify against tampered envelope + assert!(!sender_key.verify(&envelope_buf, &inner.signature)); + } + + #[test] + fn original_signature_extracted_from_inner_container() { + let receiver = random_peer(); + let (_keys, container_bytes, expected_sig) = build_signed_container(&receiver); + + // Simulate what rpc_send_routed does: decode Container to get signature + let inner = proto::Container::decode(&container_bytes[..]).unwrap(); + assert_eq!(inner.signature, expected_sig); + assert!(!inner.signature.is_empty()); + } + + #[test] + fn public_key_protobuf_round_trip() { + let keys = Keypair::generate_ed25519(); + let pub_key = keys.public(); + + // Encode to protobuf bytes (as stored in sender_public_key) + let encoded = pub_key.encode_protobuf(); + assert!(!encoded.is_empty()); + + // Decode back + let decoded = libp2p::identity::PublicKey::try_decode_protobuf(&encoded).unwrap(); + assert_eq!(decoded, pub_key); + } } diff --git a/rust/libqaul/src/services/feed/mod.rs b/rust/libqaul/src/services/feed/mod.rs index b6cd43310..76b5e2a72 100644 --- a/rust/libqaul/src/services/feed/mod.rs +++ b/rust/libqaul/src/services/feed/mod.rs @@ -78,8 +78,20 @@ impl Feed { pub fn init() { // get database and initialize tree let db = DataBase::get_node_db(); - let tree: sled::Tree = db.open_tree("feed").unwrap(); - let tree_ids: sled::Tree = db.open_tree("feed_id").unwrap(); + let tree: sled::Tree = match db.open_tree("feed") { + Ok(tree) => tree, + Err(e) => { + log::error!("Failed to open feed tree: {}", e); + return; + } + }; + let tree_ids: sled::Tree = match db.open_tree("feed_id") { + Ok(tree) => tree, + Err(e) => { + log::error!("Failed to open feed_id tree: {}", e); + return; + } + }; // get last key let last_message: u64; @@ -156,16 +168,14 @@ impl Feed { Self::save_message(container.signature.clone(), msg); // flood via floodsub - if lan.is_some() { - lan.unwrap() - .swarm + if let Some(lan) = lan { + lan.swarm .behaviour_mut() .floodsub .publish(Node::get_topic(), buf.clone()); } - if internet.is_some() { + if let Some(internet) = internet { internet - .unwrap() .swarm .behaviour_mut() .floodsub @@ -207,7 +217,13 @@ impl Feed { let mut new_message = true; { - let feed = FEED.get().read().unwrap(); + let feed = match FEED.get().read() { + Ok(f) => f, + Err(e) => { + log::error!("Failed to acquire feed read lock: {}", e); + return; + } + }; if feed.messages.contains_key(&feed_container.signature) { new_message = false; @@ -256,8 +272,14 @@ impl Feed { //Save message by sync pub fn save_message_by_sync(message_id: &[u8], sender_id: &[u8], content: String, time: u64) { - let mut feed = FEED.get().write().unwrap(); - if let Some(_index) = feed.tree_ids.get(&message_id[..]).unwrap() { + let mut feed = match FEED.get().write() { + Ok(f) => f, + Err(e) => { + log::error!("Failed to acquire feed write lock: {}", e); + return; + } + }; + if feed.tree_ids.get(&message_id[..]).unwrap_or(None).is_some() { return; } @@ -287,7 +309,13 @@ impl Feed { }; // save to data base - let message_data_bytes = bincode::serialize(&message_data).unwrap(); + let message_data_bytes = match bincode::serialize(&message_data) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Failed to serialize feed message data: {}", e); + return; + } + }; if let Err(e) = feed .tree .insert(&last_message.to_be_bytes(), message_data_bytes) @@ -299,7 +327,13 @@ impl Feed { } } - let last_message_bytes = bincode::serialize(&last_message).unwrap(); + let last_message_bytes = match bincode::serialize(&last_message) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Failed to serialize last_message index: {}", e); + return; + } + }; if let Err(e) = feed.tree_ids.insert(&message_id[..], last_message_bytes) { log::error!("Error saving feed id to data base: {}", e); } else { @@ -317,7 +351,13 @@ impl Feed { /// This function saves a new message in the data base and in the in-memory BTreeMap fn save_message(signature: Vec, message: proto_net::FeedMessageContent) { // open feed map for writing - let mut feed = FEED.get().write().unwrap(); + let mut feed = match FEED.get().write() { + Ok(f) => f, + Err(e) => { + log::error!("Failed to acquire feed write lock: {}", e); + return; + } + }; let sender_id = message.sender.clone(); let content = message.content.clone(); @@ -343,7 +383,13 @@ impl Feed { }; // save to data base - let message_data_bytes = bincode::serialize(&message_data).unwrap(); + let message_data_bytes = match bincode::serialize(&message_data) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Failed to serialize feed message data: {}", e); + return; + } + }; if let Err(e) = feed .tree .insert(&last_message.to_be_bytes(), message_data_bytes) @@ -355,7 +401,13 @@ impl Feed { } } - let last_message_bytes = bincode::serialize(&last_message).unwrap(); + let last_message_bytes = match bincode::serialize(&last_message) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Failed to serialize last_message index: {}", e); + return; + } + }; if let Err(e) = feed .tree_ids .insert(&message_data.message_id[..], last_message_bytes) @@ -373,7 +425,13 @@ impl Feed { pub fn get_latest_message_ids(count: usize) -> Vec> { // get feed message store - let feed = FEED.get().read().unwrap(); + let feed = match FEED.get().read() { + Ok(f) => f, + Err(e) => { + log::error!("Failed to acquire feed read lock: {}", e); + return Vec::new(); + } + }; let mut msg_count: usize = count; if feed.last_message < (count as u64) { msg_count = feed.last_message as usize; @@ -385,8 +443,10 @@ impl Feed { for res in feed.tree.range(first_message_bytes.as_slice()..) { match res { Ok((_id, message_bytes)) => { - let message: FeedMessageData = bincode::deserialize(&message_bytes).unwrap(); - ids.push(message.message_id.clone()); + match bincode::deserialize::(&message_bytes) { + Ok(message) => ids.push(message.message_id.clone()), + Err(e) => log::error!("Failed to deserialize feed message: {}", e), + } } Err(e) => { log::error!("Error retrieving feed message from data base: {}", e); @@ -400,10 +460,16 @@ impl Feed { pub fn process_received_feed_ids(ids: &[Vec]) -> Vec> { let mut missing_ids = Vec::with_capacity(ids.len()); - let feed = FEED.get().read().unwrap(); + let feed = match FEED.get().read() { + Ok(f) => f, + Err(e) => { + log::error!("Failed to acquire feed read lock: {}", e); + return missing_ids; + } + }; for id in ids { - match feed.tree_ids.get(&id[..]).unwrap() { - Some(_index) => {} + match feed.tree_ids.get(&id[..]) { + Ok(Some(_index)) => {} _ => { missing_ids.push(id.clone()); } @@ -414,18 +480,26 @@ impl Feed { pub fn get_messges_by_ids(ids: &[Vec]) -> Vec<(Vec, Vec, String, u64)> { let mut res = Vec::with_capacity(ids.len()); - let feed = FEED.get().read().unwrap(); + let feed = match FEED.get().read() { + Ok(f) => f, + Err(e) => { + log::error!("Failed to acquire feed read lock: {}", e); + return res; + } + }; for id in ids { - if let Some(index_bytes) = feed.tree_ids.get(&id[..]).unwrap() { - let index: u64 = bincode::deserialize(&index_bytes).unwrap(); - if let Some(message_bytes) = feed.tree.get(index.to_be_bytes()).unwrap() { - let message: FeedMessageData = bincode::deserialize(&message_bytes).unwrap(); - res.push(( - id.clone(), - message.sender_id.clone(), - message.content.clone(), - message.timestamp_sent, - )); + if let Ok(Some(index_bytes)) = feed.tree_ids.get(&id[..]) { + if let Ok(index) = bincode::deserialize::(&index_bytes) { + if let Ok(Some(message_bytes)) = feed.tree.get(index.to_be_bytes()) { + if let Ok(message) = bincode::deserialize::(&message_bytes) { + res.push(( + id.clone(), + message.sender_id.clone(), + message.content.clone(), + message.timestamp_sent, + )); + } + } } } } @@ -438,7 +512,16 @@ impl Feed { /// that are newer then the last message. fn get_messages(last_message: u64) -> proto::FeedMessageList { // get feed message store - let feed = FEED.get().read().unwrap(); + let feed = match FEED.get().read() { + Ok(f) => f, + Err(e) => { + log::error!("Failed to acquire feed read lock: {}", e); + return proto::FeedMessageList { + feed_message: Vec::new(), + pagination: None, + }; + } + }; let mut feed_list = proto::FeedMessageList { feed_message: Vec::with_capacity( feed.last_message.saturating_sub(last_message) as usize @@ -455,8 +538,13 @@ impl Feed { for res in feed.tree.range(first_message_bytes.as_slice()..) { match res { Ok((_id, message_bytes)) => { - let message: FeedMessageData = - bincode::deserialize(&message_bytes).unwrap(); + let message: FeedMessageData = match bincode::deserialize(&message_bytes) { + Ok(m) => m, + Err(e) => { + log::error!("Failed to deserialize feed message: {}", e); + continue; + } + }; if feed.messages.contains_key(&message.message_id) { log::trace!("key exist"); @@ -504,14 +592,29 @@ impl Feed { /// Get messages from database using pagination fn get_paginated_messages(offset: u32, limit: u32) -> proto::FeedMessageList { - let feed = FEED.get().read().unwrap(); + let feed = match FEED.get().read() { + Ok(f) => f, + Err(e) => { + log::error!("Failed to acquire feed read lock: {}", e); + return proto::FeedMessageList { + feed_message: Vec::new(), + pagination: None, + }; + } + }; build_feed_list_from(&feed.tree, offset, limit) } /// Sign a message with the private key /// The signature can be validated with the corresponding public key. pub fn sign_message(buf: &[u8], keys: &Keypair) -> Vec { - keys.sign(buf).unwrap() + match keys.sign(buf) { + Ok(sig) => sig, + Err(e) => { + log::error!("Failed to sign feed message: {}", e); + Vec::new() + } + } } /// validate a message via the public key of the sender @@ -635,7 +738,13 @@ fn build_feed_list_from(tree: &sled::Tree, offset: u32, limit: u32) -> proto::Fe for res in iter { match res { Ok((_key, message_bytes)) => { - let message: FeedMessageData = bincode::deserialize(&message_bytes).unwrap(); + let message: FeedMessageData = match bincode::deserialize(&message_bytes) { + Ok(m) => m, + Err(e) => { + log::error!("Failed to deserialize feed message: {}", e); + continue; + } + }; let sender_id_base58 = bs58::encode(&message.sender_id).into_string(); diff --git a/rust/libqaul/src/services/group/manage.rs b/rust/libqaul/src/services/group/manage.rs index a8dcbe708..81dccdb8d 100644 --- a/rust/libqaul/src/services/group/manage.rs +++ b/rust/libqaul/src/services/group/manage.rs @@ -102,11 +102,9 @@ impl GroupManage { account_id.to_bytes(), super::GroupMember { user_id: account_id.to_bytes(), - role: super::proto_rpc::GroupMemberRole::Admin.try_into().unwrap(), + role: super::proto_rpc::GroupMemberRole::Admin as i32, joined_at: Timestamp::get_timestamp(), - state: super::proto_rpc::GroupMemberState::Activated - .try_into() - .unwrap(), + state: super::proto_rpc::GroupMemberState::Activated as i32, last_message_index: 0, }, ); @@ -114,11 +112,9 @@ impl GroupManage { user_id.to_bytes(), super::GroupMember { user_id: user_id.to_bytes(), - role: super::proto_rpc::GroupMemberRole::Admin.try_into().unwrap(), + role: super::proto_rpc::GroupMemberRole::Admin as i32, joined_at: Timestamp::get_timestamp(), - state: super::proto_rpc::GroupMemberState::Activated - .try_into() - .unwrap(), + state: super::proto_rpc::GroupMemberState::Activated as i32, last_message_index: 0, }, ); @@ -142,7 +138,7 @@ impl GroupManage { account_id.to_bytes(), super::GroupMember { user_id: account_id.to_bytes(), - role: super::proto_rpc::GroupMemberRole::Admin.try_into().unwrap(), + role: super::proto_rpc::GroupMemberRole::Admin as i32, joined_at: Timestamp::get_timestamp(), state: super::proto_rpc::GroupMemberState::Activated as i32, last_message_index: 0, @@ -164,9 +160,16 @@ impl GroupManage { )), }; + let group_id = match GroupId::from_bytes(&group.id) { + Ok(id) => id, + Err(e) => { + log::error!("failed to parse group id: {}", e); + return group.id; + } + }; ChatStorage::save_message( account_id, - &GroupId::from_bytes(&group.id).unwrap(), + &group_id, account_id, &Vec::new(), Timestamp::get_timestamp(), @@ -261,7 +264,13 @@ impl GroupManage { for entry in db_ref.groups.iter() { match entry { Ok((_, group_bytes)) => { - let group: Group = bincode::deserialize(&group_bytes).unwrap(); + let group: Group = match bincode::deserialize(&group_bytes) { + Ok(v) => v, + Err(e) => { + log::error!("failed to deserialize group in list: {}", e); + continue; + } + }; let mut members = Vec::with_capacity(group.members.len()); for m in group.members.values() { members.push(Self::to_rpc_group_member(m)); @@ -299,7 +308,13 @@ impl GroupManage { for entry in db_ref.invited.iter() { match entry { Ok((_, invite_bytes)) => { - let invite: GroupInvited = bincode::deserialize(&invite_bytes).unwrap(); + let invite: GroupInvited = match bincode::deserialize(&invite_bytes) { + Ok(v) => v, + Err(e) => { + log::error!("failed to deserialize group invite in list: {}", e); + continue; + } + }; let mut members = Vec::with_capacity(invite.group.members.len()); for (_, member) in invite.group.members { members.push(Self::to_rpc_group_member(&member)); diff --git a/rust/libqaul/src/services/group/member.rs b/rust/libqaul/src/services/group/member.rs index bff010703..e9680a8e6 100644 --- a/rust/libqaul/src/services/group/member.rs +++ b/rust/libqaul/src/services/group/member.rs @@ -132,11 +132,9 @@ impl Member { // save new user let member = super::GroupMember { user_id: user_id_bytes.clone(), - role: super::proto_rpc::GroupMemberRole::User.try_into().unwrap(), + role: super::proto_rpc::GroupMemberRole::User as i32, joined_at: timestamp::Timestamp::get_timestamp(), - state: super::proto_rpc::GroupMemberState::Invited - .try_into() - .unwrap(), + state: super::proto_rpc::GroupMemberState::Invited as i32, last_message_index: 0, }; @@ -196,13 +194,20 @@ impl Member { GroupStorage::flush_account(account_id); if accept { - Self::save_group_event_message( - account_id, - &GroupId::from_bytes(group_id).unwrap(), - account_id, - chat::rpc_proto::GroupEventType::InviteAccepted, - account_id.to_bytes(), - ); + match GroupId::from_bytes(group_id) { + Ok(id) => { + Self::save_group_event_message( + account_id, + &id, + account_id, + chat::rpc_proto::GroupEventType::InviteAccepted, + account_id.to_bytes(), + ); + } + Err(e) => { + log::error!("failed to parse group id in reply_invite: {}", e); + } + } } Ok(true) @@ -266,13 +271,20 @@ impl Member { Group::send_notify_message(&user_account, user_id, proto_message.encode_to_vec()); // save group event - Self::save_group_event_message( - account_id, - &GroupId::from_bytes(group_id).unwrap(), - user_id, - chat::rpc_proto::GroupEventType::Left, - user_id.to_bytes(), - ); + match GroupId::from_bytes(group_id) { + Ok(id) => { + Self::save_group_event_message( + account_id, + &id, + user_id, + chat::rpc_proto::GroupEventType::Left, + user_id.to_bytes(), + ); + } + Err(e) => { + log::error!("failed to parse group id in remove: {}", e); + } + } Ok(true) } @@ -347,7 +359,14 @@ impl Member { group.revision += 1; // save group - let group_id = GroupId::from_bytes(&group.id).unwrap(); + let group_id = match GroupId::from_bytes(&group.id) { + Ok(id) => id, + Err(e) => { + log::error!("failed to parse group id in on_accepted_invite: {}", e); + GroupStorage::save_group(account_id.to_owned(), group); + return Ok(true); + } + }; GroupStorage::save_group(account_id.to_owned(), group); // save event @@ -449,7 +468,14 @@ impl Member { group.status = super::proto_rpc::GroupStatus::Deactivated as i32; // save group - let group_id = GroupId::from_bytes(&group.id).unwrap(); + let group_id = match GroupId::from_bytes(&group.id) { + Ok(id) => id, + Err(e) => { + log::error!("failed to parse group id in on_removed: {}", e); + GroupStorage::save_group(account_id.to_owned(), group); + return Ok(true); + } + }; GroupStorage::save_group(account_id.to_owned(), group); // save event diff --git a/rust/libqaul/src/services/group/message.rs b/rust/libqaul/src/services/group/message.rs index f5eb6b4bb..09f6b9dcd 100644 --- a/rust/libqaul/src/services/group/message.rs +++ b/rust/libqaul/src/services/group/message.rs @@ -23,10 +23,22 @@ impl ParsedGroupMessageId { return Err("invalid group message id".to_string()); } + let group_crc = message_id[0..8] + .try_into() + .map(u64::from_be_bytes) + .map_err(|_| "invalid group_crc slice".to_string())?; + let sender_crc = message_id[8..16] + .try_into() + .map(u64::from_be_bytes) + .map_err(|_| "invalid sender_crc slice".to_string())?; + let sender_msg_index = message_id[16..20] + .try_into() + .map(u32::from_be_bytes) + .map_err(|_| "invalid sender_msg_index slice".to_string())?; Ok(Self { - group_crc: u64::from_be_bytes(message_id[0..8].try_into().unwrap()), - sender_crc: u64::from_be_bytes(message_id[8..16].try_into().unwrap()), - sender_msg_index: u32::from_be_bytes(message_id[16..20].try_into().unwrap()), + group_crc, + sender_crc, + sender_msg_index, }) } diff --git a/rust/libqaul/src/services/group/mod.rs b/rust/libqaul/src/services/group/mod.rs index d50e6d237..aff792163 100644 --- a/rust/libqaul/src/services/group/mod.rs +++ b/rust/libqaul/src/services/group/mod.rs @@ -164,7 +164,13 @@ impl Group { error_context: &str, ) { for member in group.members.values() { - let receiver = PeerId::from_bytes(&member.user_id).unwrap(); + let receiver = match PeerId::from_bytes(&member.user_id) { + Ok(id) => id, + Err(e) => { + log::error!("failed to parse member peer id: {}", e); + continue; + } + }; if receiver == user_account.id { continue; } @@ -385,7 +391,13 @@ impl Group { /// Process incoming RPC request messages for group chat module pub fn rpc(data: Vec, user_id: Vec, request_id: String) { - let my_user_id = PeerId::from_bytes(&user_id).unwrap(); + let my_user_id = match PeerId::from_bytes(&user_id) { + Ok(id) => id, + Err(e) => { + log::error!("failed to parse user id in group rpc: {}", e); + return; + } + }; match proto_rpc::Group::decode(&data[..]) { Ok(group) => { @@ -502,10 +514,17 @@ impl Group { Some(proto_rpc::group::Message::GroupInviteMemberRequest(invite_req)) => { let mut status = true; let mut message: String = "".to_string(); + let invite_user_id = match PeerId::from_bytes(&invite_req.user_id) { + Ok(id) => id, + Err(e) => { + log::error!("failed to parse invite user id: {}", e); + return; + } + }; if let Err(err) = Member::invite( &my_user_id, &invite_req.group_id, - &PeerId::from_bytes(&invite_req.user_id).unwrap(), + &invite_user_id, ) { status = false; message = err.clone(); @@ -561,10 +580,17 @@ impl Group { let mut status = true; let mut message: String = "".to_string(); + let remove_user_id = match PeerId::from_bytes(&remove_req.user_id) { + Ok(id) => id, + Err(e) => { + log::error!("failed to parse remove user id: {}", e); + return; + } + }; if let Err(err) = Member::remove( &my_user_id, &remove_req.group_id, - &PeerId::from_bytes(&remove_req.user_id).unwrap(), + &remove_user_id, ) { status = false; message = err.clone(); diff --git a/rust/libqaul/src/services/group/storage.rs b/rust/libqaul/src/services/group/storage.rs index 3fb63c144..06b7c3a4c 100644 --- a/rust/libqaul/src/services/group/storage.rs +++ b/rust/libqaul/src/services/group/storage.rs @@ -56,14 +56,19 @@ impl GroupStorage { // check if user account data exists { // get chat state - let group_storage = GROUPSTORAGE.get().read().unwrap(); - - // check if user account ID is in map - if let Some(group_account_db) = group_storage.db_ref.get(&account_id.to_bytes()) { - return GroupAccountDb { - groups: group_account_db.groups.clone(), - invited: group_account_db.invited.clone(), - }; + match GROUPSTORAGE.get().read() { + Ok(group_storage) => { + // check if user account ID is in map + if let Some(group_account_db) = group_storage.db_ref.get(&account_id.to_bytes()) { + return GroupAccountDb { + groups: group_account_db.groups.clone(), + invited: group_account_db.invited.clone(), + }; + } + } + Err(e) => { + log::error!("failed to read group storage lock: {}", e); + } } } @@ -90,18 +95,34 @@ impl GroupStorage { let db = DataBase::get_user_db(account_id); // open trees - let groups: sled::Tree = db.open_tree("groups").unwrap(); - let invited: sled::Tree = db.open_tree("invited").unwrap(); + let groups: sled::Tree = match db.open_tree("groups") { + Ok(tree) => tree, + Err(e) => { + log::error!("failed to open groups tree: {}", e); + db.open_tree("__fallback_groups").expect("critical: cannot open fallback groups tree") + } + }; + let invited: sled::Tree = match db.open_tree("invited") { + Ok(tree) => tree, + Err(e) => { + log::error!("failed to open invited tree: {}", e); + db.open_tree("__fallback_invited").expect("critical: cannot open fallback invited tree") + } + }; let group_account_db = GroupAccountDb { groups, invited }; // get group storage for writing - let mut group_storage = GROUPSTORAGE.get().write().unwrap(); - - // add user to state - group_storage - .db_ref - .insert(account_id.to_bytes(), group_account_db.clone()); + match GROUPSTORAGE.get().write() { + Ok(mut group_storage) => { + group_storage + .db_ref + .insert(account_id.to_bytes(), group_account_db.clone()); + } + Err(e) => { + log::error!("failed to write group storage lock: {}", e); + } + } // return structure group_account_db @@ -115,7 +136,13 @@ impl GroupStorage { // get group match db_ref.groups.get(group_id) { Ok(Some(group_bytes)) => { - let group: Group = bincode::deserialize(&group_bytes).unwrap(); + let group: Group = match bincode::deserialize(&group_bytes) { + Ok(v) => v, + Err(e) => { + log::error!("failed to deserialize group: {}", e); + return None; + } + }; return Some(group); } Ok(None) => return None, @@ -192,7 +219,13 @@ impl GroupStorage { let db_ref = Self::get_db_ref(account_id); // save group in data base - let group_bytes = bincode::serialize(&group).unwrap(); + let group_bytes = match bincode::serialize(&group) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("failed to serialize group: {}", e); + return; + } + }; if let Err(e) = db_ref.groups.insert(group.id.clone(), group_bytes) { log::error!("Error saving group to data base: {}", e); } @@ -283,7 +316,13 @@ impl GroupStorage { // get invite match db_ref.invited.get(group_id) { Ok(Some(invite_bytes)) => { - let invite: GroupInvited = bincode::deserialize(&invite_bytes).unwrap(); + let invite: GroupInvited = match bincode::deserialize(&invite_bytes) { + Ok(v) => v, + Err(e) => { + log::error!("failed to deserialize group invite: {}", e); + return None; + } + }; return Some(invite); } Ok(None) => return None, @@ -312,7 +351,13 @@ impl GroupStorage { let db_ref = Self::get_db_ref(account_id); // save group invite in data base - let invite_bytes = bincode::serialize(&invite).unwrap(); + let invite_bytes = match bincode::serialize(&invite) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("failed to serialize group invite: {}", e); + return; + } + }; if let Err(e) = db_ref.invited.insert(invite.group.id.clone(), invite_bytes) { log::error!("Error saving group invite to data base: {}", e); } diff --git a/rust/libqaul/src/services/messaging/mod.rs b/rust/libqaul/src/services/messaging/mod.rs index baaa7d082..2de1438d9 100644 --- a/rust/libqaul/src/services/messaging/mod.rs +++ b/rust/libqaul/src/services/messaging/mod.rs @@ -125,7 +125,13 @@ impl Messaging { let db = DataBase::get_node_db(); // open trees - let unconfirmed: sled::Tree = db.open_tree("unconfirmed").unwrap(); + let unconfirmed: sled::Tree = match db.open_tree("unconfirmed") { + Ok(tree) => tree, + Err(e) => { + log::error!("Failed to open unconfirmed tree: {}", e); + return; + } + }; let unconfirmed_messages = UnConfirmedMessages { unconfirmed }; UNCONFIRMED.set(RwLock::new(unconfirmed_messages)); } @@ -149,10 +155,22 @@ impl Messaging { scheduled_dtn: false, is_dtn, }; - let unconfirmed = UNCONFIRMED.get().write().unwrap(); + let unconfirmed = match UNCONFIRMED.get().write() { + Ok(u) => u, + Err(e) => { + log::error!("Failed to acquire unconfirmed write lock: {}", e); + return; + } + }; // insert message to data base - let new_entry_bytes = bincode::serialize(&new_entry).unwrap(); + let new_entry_bytes = match bincode::serialize(&new_entry) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Failed to serialize unconfirmed entry: {}", e); + return; + } + }; if let Err(e) = unconfirmed .unconfirmed .insert(container.signature.clone(), new_entry_bytes) @@ -180,7 +198,13 @@ impl Messaging { bs58::encode(signature).into_string() ); - let unconfirmed = UNCONFIRMED.get().write().unwrap(); + let unconfirmed = match UNCONFIRMED.get().write() { + Ok(u) => u, + Err(e) => { + log::error!("Failed to acquire unconfirmed write lock: {}", e); + return; + } + }; // check and remove unconfirmed from DB match unconfirmed.unconfirmed.remove(signature) { @@ -191,8 +215,13 @@ impl Messaging { match v { Some(unconfirmed_bytes) => { - let unconfirmed: UnConfirmedMessage = - bincode::deserialize(&unconfirmed_bytes).unwrap(); + let unconfirmed: UnConfirmedMessage = match bincode::deserialize(&unconfirmed_bytes) { + Ok(u) => u, + Err(e) => { + log::error!("Failed to deserialize unconfirmed message: {}", e); + return; + } + }; // check message and decide what to do match unconfirmed.message_type { @@ -259,19 +288,36 @@ impl Messaging { } fn on_scheduled_message(signature: &[u8]) { - let unconfirmed = UNCONFIRMED.get().write().unwrap(); - let Some(unconfirmed_message_bytes) = unconfirmed.unconfirmed.get(signature).unwrap() - else { - return; + let unconfirmed = match UNCONFIRMED.get().write() { + Ok(u) => u, + Err(e) => { + log::error!("Failed to acquire unconfirmed write lock: {}", e); + return; + } + }; + let unconfirmed_message_bytes = match unconfirmed.unconfirmed.get(signature) { + Ok(Some(bytes)) => bytes, + _ => return, + }; + let mut unconfirmed_message: UnConfirmedMessage = match bincode::deserialize(&unconfirmed_message_bytes) { + Ok(u) => u, + Err(e) => { + log::error!("Failed to deserialize unconfirmed message: {}", e); + return; + } }; - let mut unconfirmed_message: UnConfirmedMessage = - bincode::deserialize(&unconfirmed_message_bytes).unwrap(); if unconfirmed_message.scheduled { return; } unconfirmed_message.scheduled = true; - let unconfirmed_message_todb = bincode::serialize(&unconfirmed_message).unwrap(); + let unconfirmed_message_todb = match bincode::serialize(&unconfirmed_message) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Failed to serialize unconfirmed message: {}", e); + return; + } + }; if let Err(_e) = unconfirmed .unconfirmed .insert(signature.to_vec(), unconfirmed_message_todb) @@ -285,19 +331,36 @@ impl Messaging { } fn on_scheduled_as_dtn_message(signature: &[u8]) { - let unconfirmed = UNCONFIRMED.get().write().unwrap(); - let Some(unconfirmed_message_bytes) = unconfirmed.unconfirmed.get(signature).unwrap() - else { - return; + let unconfirmed = match UNCONFIRMED.get().write() { + Ok(u) => u, + Err(e) => { + log::error!("Failed to acquire unconfirmed write lock: {}", e); + return; + } + }; + let unconfirmed_message_bytes = match unconfirmed.unconfirmed.get(signature) { + Ok(Some(bytes)) => bytes, + _ => return, + }; + let mut unconfirmed_message: UnConfirmedMessage = match bincode::deserialize(&unconfirmed_message_bytes) { + Ok(u) => u, + Err(e) => { + log::error!("Failed to deserialize unconfirmed message: {}", e); + return; + } }; - let mut unconfirmed_message: UnConfirmedMessage = - bincode::deserialize(&unconfirmed_message_bytes).unwrap(); if unconfirmed_message.scheduled { return; } unconfirmed_message.scheduled_dtn = true; - let unconfirmed_message_todb = bincode::serialize(&unconfirmed_message).unwrap(); + let unconfirmed_message_todb = match bincode::serialize(&unconfirmed_message) { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Failed to serialize unconfirmed message: {}", e); + return; + } + }; if let Err(_e) = unconfirmed .unconfirmed .insert(signature.to_vec(), unconfirmed_message_todb) @@ -469,6 +532,51 @@ impl Messaging { } } + /// Pack, sign and schedule a DtnRoutedV2 message for sending + pub fn send_dtn_routed_v2_message( + user_account: &UserAccount, + target_id: &PeerId, + routed_v2: proto::DtnRoutedV2, + ) -> Result, String> { + // Create envelope payload with DtnRoutedV2 + let dtn_payload = proto::EnvelopPayload { + payload: Some(proto::envelop_payload::Payload::DtnRoutedV2(routed_v2)), + }; + let envelope = proto::Envelope { + sender_id: user_account.id.to_bytes(), + receiver_id: target_id.to_bytes(), + payload: dtn_payload.encode_to_vec(), + }; + + if let Ok(signature) = user_account.keys.sign(&envelope.encode_to_vec()) { + let container = proto::Container { + signature: signature.clone(), + envelope: Some(envelope), + }; + + Self::save_unconfirmed_message( + MessagingServiceType::DtnStored, + &[], + target_id, + &container, + true, + ); + + Self::schedule_message( + target_id.clone(), + container, + true, + false, + true, + true, + ); + + Ok(signature) + } else { + Err("dtn v2 messaging signing error".to_string()) + } + } + /// schedule a message /// /// schedule a message for sending. @@ -523,7 +631,13 @@ impl Messaging { // get scheduled messaging buffer { - let mut messaging = MESSAGING.get().write().unwrap(); + let mut messaging = match MESSAGING.get().write() { + Ok(m) => m, + Err(e) => { + log::error!("Failed to acquire messaging write lock: {}", e); + return None; + } + }; message_item = messaging.to_send.pop_front(); } @@ -546,23 +660,25 @@ impl Messaging { && message.is_common { // get storage node id - if let Ok(my_user_id) = - PeerId::from_bytes(&message.container.envelope.as_ref().unwrap().sender_id) - { - if let Some(storage_node_id) = - super::dtn::Dtn::get_storage_user(&my_user_id) + if let Some(envelope) = message.container.envelope.as_ref() { + if let Ok(my_user_id) = + PeerId::from_bytes(&envelope.sender_id) { - if let Some(user_account) = UserAccounts::get_by_id(my_user_id) { - if let Err(_e) = Self::send_dtn_message( - &user_account, - &storage_node_id, - &message.container, - ) { - log::error!("DTN scheduling error!"); - } else { - log::error!("DTN scheduled..."); - // update unconfirmed table - Self::on_scheduled_as_dtn_message(&message.container.signature); + if let Some(storage_node_id) = + super::dtn::Dtn::get_storage_user(&my_user_id) + { + if let Some(user_account) = UserAccounts::get_by_id(my_user_id) { + if let Err(_e) = Self::send_dtn_message( + &user_account, + &storage_node_id, + &message.container, + ) { + log::error!("DTN scheduling error!"); + } else { + log::error!("DTN scheduled..."); + // update unconfirmed table + Self::on_scheduled_as_dtn_message(&message.container.signature); + } } } } diff --git a/rust/libqaul/src/services/messaging/network_emul.rs b/rust/libqaul/src/services/messaging/network_emul.rs index 8f97b0566..fa0e45249 100644 --- a/rust/libqaul/src/services/messaging/network_emul.rs +++ b/rust/libqaul/src/services/messaging/network_emul.rs @@ -23,7 +23,13 @@ impl NetworkEmulator { } pub fn is_lost() -> bool { - let mut state = STATE.get().write().unwrap(); + let mut state = match STATE.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("Error writing network emulator state lock: {}", e); + return false; + } + }; state.total_message = state.total_message + 1; let lost_rate = state.total_drop * 100 / state.total_message; diff --git a/rust/libqaul/src/services/messaging/process.rs b/rust/libqaul/src/services/messaging/process.rs index 2b41724a4..21a5bfad3 100644 --- a/rust/libqaul/src/services/messaging/process.rs +++ b/rust/libqaul/src/services/messaging/process.rs @@ -78,8 +78,10 @@ impl MessagingProcess { } } Some(super::proto::messaging::Message::DtnResponse(dtn_response)) => { - // update DTN state + // update DTN V1 state dtn::Dtn::on_dtn_response(&dtn_response); + // update DTN V2 state + dtn::Dtn::on_dtn_response_v2(&dtn_response); // update unconfirmed table super::Messaging::on_confirmed_message( @@ -313,6 +315,9 @@ impl MessagingProcess { Some(super::proto::envelop_payload::Payload::Dtn(dtn)) => { dtn::Dtn::net(&receiver_id, &sender_id, &container.signature, &dtn); } + Some(super::proto::envelop_payload::Payload::DtnRoutedV2(routed_v2)) => { + dtn::Dtn::net_routed_v2(&receiver_id, &sender_id, &container.signature, routed_v2); + } _ => { log::error!("unknown envelop payload"); return; diff --git a/rust/libqaul/src/services/messaging/retransmit.rs b/rust/libqaul/src/services/messaging/retransmit.rs index 9c4f19b31..75e579142 100644 --- a/rust/libqaul/src/services/messaging/retransmit.rs +++ b/rust/libqaul/src/services/messaging/retransmit.rs @@ -10,6 +10,7 @@ use prost::Message; use super::UnConfirmedMessage; use crate::router; +use crate::services::dtn; use crate::utilities::qaul_id::QaulId; use crate::utilities::timestamp::Timestamp; @@ -20,7 +21,13 @@ impl MessagingRetransmit { /// process retransmission pub fn process() { // get unconfirmed table - let unconfirmed = super::UNCONFIRMED.get().write().unwrap(); + let unconfirmed = match super::UNCONFIRMED.get().write() { + Ok(u) => u, + Err(e) => { + log::error!("Failed to acquire unconfirmed write lock: {}", e); + return; + } + }; if unconfirmed.unconfirmed.len() == 0 { // there are no message to retransmit return; @@ -34,7 +41,13 @@ impl MessagingRetransmit { for entry in unconfirmed.unconfirmed.iter() { if let Ok((signature, unconfirmed_message_bytes)) = entry { let mut unconfirmed_message: UnConfirmedMessage = - bincode::deserialize(&unconfirmed_message_bytes).unwrap(); + match bincode::deserialize(&unconfirmed_message_bytes) { + Ok(u) => u, + Err(e) => { + log::error!("Failed to deserialize unconfirmed message: {}", e); + continue; + } + }; // let's assume message transmit in 3 seconds if cur_time < (unconfirmed_message.last_sent + 3000) { @@ -73,8 +86,13 @@ impl MessagingRetransmit { if let Ok(container) = super::proto::Container::decode(&unconfirmed_message.container[..]) { - let receiver = - PeerId::from_bytes(&unconfirmed_message.receiver_id).unwrap(); + let receiver = match PeerId::from_bytes(&unconfirmed_message.receiver_id) { + Ok(r) => r, + Err(e) => { + log::error!("Failed to parse receiver PeerId: {}", e); + continue; + } + }; log::trace!( "retrans message, signature: {}", @@ -126,5 +144,8 @@ impl MessagingRetransmit { log::error!("updating unconfirmed table error!"); } } + + // Process V2 DTN routed messages + dtn::Dtn::process_retransmit_v2(); } } diff --git a/rust/libqaul/src/services/rtc/mod.rs b/rust/libqaul/src/services/rtc/mod.rs index d26b6c7bc..1eb943a97 100644 --- a/rust/libqaul/src/services/rtc/mod.rs +++ b/rust/libqaul/src/services/rtc/mod.rs @@ -69,22 +69,40 @@ impl Rtc { /// get session from session_id pub fn get_session_from_id(group_id: &Vec) -> Option { - let sessions = RTCSESSIONS.get().read().unwrap(); - if sessions.sessions.contains_key(group_id) { - return Some(sessions.sessions.get(group_id).unwrap().clone()); + let sessions = match RTCSESSIONS.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("Error reading RTC sessions lock: {}", e); + return None; + } + }; + if let Some(session) = sessions.sessions.get(group_id) { + return Some(session.clone()); } None } /// get session from session_id pub fn update_session(session: RtcSession) { - let mut sessions = RTCSESSIONS.get().write().unwrap(); + let mut sessions = match RTCSESSIONS.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("Error writing RTC sessions lock: {}", e); + return; + } + }; sessions.sessions.insert(session.group_id.clone(), session); } /// remove session on the storage pub fn remove_session(session_id: &Vec) { - let mut sessions = RTCSESSIONS.get().write().unwrap(); + let mut sessions = match RTCSESSIONS.get().write() { + Ok(s) => s, + Err(e) => { + log::error!("Error writing RTC sessions lock for removal: {}", e); + return; + } + }; sessions.sessions.remove(session_id); } @@ -198,7 +216,13 @@ impl Rtc { /// Process incoming RPC request messages pub fn rpc(data: Vec, user_id: Vec, request_id: String) { - let my_user_id = PeerId::from_bytes(&user_id).unwrap(); + let my_user_id = match PeerId::from_bytes(&user_id) { + Ok(id) => id, + Err(e) => { + log::error!("Error parsing PeerId from bytes in RTC rpc: {}", e); + return; + } + }; match proto_rpc::RtcRpc::decode(&data[..]) { Ok(rtc_rpc) => { diff --git a/rust/libqaul/src/services/rtc/rtc_managing.rs b/rust/libqaul/src/services/rtc/rtc_managing.rs index 069ac6b31..c7b5d16ce 100644 --- a/rust/libqaul/src/services/rtc/rtc_managing.rs +++ b/rust/libqaul/src/services/rtc/rtc_managing.rs @@ -10,7 +10,13 @@ impl RtcManaging { pub fn session_list(_my_user_id: &PeerId) -> super::proto_rpc::RtcSessionListResponse { let mut res = super::proto_rpc::RtcSessionListResponse { sessions: vec![] }; - let sessions = super::RTCSESSIONS.get().read().unwrap(); + let sessions = match super::RTCSESSIONS.get().read() { + Ok(s) => s, + Err(e) => { + log::error!("Error reading RTC sessions lock in session_list: {}", e); + return res; + } + }; for (_id, session) in sessions.sessions.iter() { let entry = super::proto_rpc::RtcSession { group_id: session.group_id.clone(), @@ -55,7 +61,12 @@ impl RtcManaging { .expect("Vec provides capacity as needed"); if let Some(user_account) = UserAccounts::get_by_id(*my_user_id) { - let receiver = PeerId::from_bytes(&req.group_id).unwrap(); + let receiver = match PeerId::from_bytes(&req.group_id) { + Ok(id) => id, + Err(e) => { + return Err(format!("Error parsing PeerId from group_id: {}", e)); + } + }; super::Rtc::send_rtc_message_through_message(&user_account, receiver, &message_buff); } else { return Err("user account has problem".to_string()); @@ -85,7 +96,12 @@ impl RtcManaging { .expect("Vec provides capacity as needed"); if let Some(user_account) = UserAccounts::get_by_id(*my_user_id) { - let receiver = PeerId::from_bytes(group_id).unwrap(); + let receiver = match PeerId::from_bytes(group_id) { + Ok(id) => id, + Err(e) => { + return Err(format!("Error parsing PeerId from group_id: {}", e)); + } + }; super::Rtc::send_rtc_message_through_message(&user_account, receiver, &message_buff); } else { return Err("user account has problem".to_string()); diff --git a/rust/libqaul/src/services/rtc/rtc_messaging.rs b/rust/libqaul/src/services/rtc/rtc_messaging.rs index 52cee105e..492eb2aa9 100644 --- a/rust/libqaul/src/services/rtc/rtc_messaging.rs +++ b/rust/libqaul/src/services/rtc/rtc_messaging.rs @@ -35,7 +35,12 @@ impl RtcMessaging { let message_id: Vec = Vec::new(); if let Some(user_account) = UserAccounts::get_by_id(*my_user_id) { - let receiver = PeerId::from_bytes(&req.group_id).unwrap(); + let receiver = match PeerId::from_bytes(&req.group_id) { + Ok(id) => id, + Err(e) => { + return Err(format!("Error parsing PeerId from group_id: {}", e)); + } + }; if let Err(e) = messaging::Messaging::pack_and_send_message( &user_account, &receiver, diff --git a/rust/libqaul/src/storage/configuration.rs b/rust/libqaul/src/storage/configuration.rs index bae935cd6..a6a95f986 100644 --- a/rust/libqaul/src/storage/configuration.rs +++ b/rust/libqaul/src/storage/configuration.rs @@ -209,6 +209,9 @@ pub struct StorageOptions { pub users: Vec, //Sending the table every 10 seconds to direct neighbours. pub size_total: u32, + // Whether this node accepts DTN V2 custody requests + #[serde(default)] + pub dtn_v2_custody_enabled: bool, } impl Default for StorageOptions { @@ -216,6 +219,7 @@ impl Default for StorageOptions { StorageOptions { users: vec![], size_total: 1024, //1024 MB + dtn_v2_custody_enabled: false, } } } diff --git a/rust/libqaul/src/utilities/upgrade/v2_0_0_rc_5/mod.rs b/rust/libqaul/src/utilities/upgrade/v2_0_0_rc_5/mod.rs index f813c2ca4..b45c3ad91 100644 --- a/rust/libqaul/src/utilities/upgrade/v2_0_0_rc_5/mod.rs +++ b/rust/libqaul/src/utilities/upgrade/v2_0_0_rc_5/mod.rs @@ -114,6 +114,7 @@ impl VersionUpgrade { storage: crate::storage::configuration::StorageOptions { users: user.storage.users.clone(), size_total: user.storage.size_total, + dtn_v2_custody_enabled: false, }, }); } diff --git a/rust/libqaul/tests/dtn_routed_v2.rs b/rust/libqaul/tests/dtn_routed_v2.rs new file mode 100644 index 000000000..1151c6d97 --- /dev/null +++ b/rust/libqaul/tests/dtn_routed_v2.rs @@ -0,0 +1,421 @@ +// Copyright (c) 2023 Open Community Project Association https://ocpa.ch +// This software is published under the AGPLv3 license. + +//! # DTN Routed V2 Integration Tests +//! +//! Tests the directed custody routing message types end-to-end: +//! - Protobuf encode/decode through the full envelope chain +//! - V2 storage entry serialization via bincode (as used in sled) +//! - Expiry and handoff validation logic +//! - Single flat route message construction and round-trip +//! - Duplicate detection via sled (temporary DB) +//! - Quota tracking via sled (temporary DB) + +use libp2p::identity::Keypair; +use libp2p::PeerId; +use prost::Message; +use serde::{Deserialize, Serialize}; + +/// Protobuf types from qaul-proto (public crate) +use qaul_proto::qaul_net_messaging as proto; + +/// Mirror of DtnRoutedV2Entry from libqaul (not publicly exported). +/// We redefine it here to test the sled storage layer independently. +#[derive(Serialize, Deserialize, Clone)] +struct DtnRoutedV2Entry { + routed_v2_bytes: Vec, + sender_public_key: Vec, + size: u32, + accepted_at: u64, + receiver_id: Vec, +} + +/// Mirror of SenderQuotaEntry. +#[derive(Default, Serialize, Deserialize, Clone)] +struct SenderQuotaEntry { + used_bytes: u64, + message_count: u32, +} + +/// Per-sender quota limit (same as in libqaul). +const V2_PER_SENDER_QUOTA: u64 = 10 * 1024 * 1024; + +fn random_peer() -> PeerId { + let keys = Keypair::generate_ed25519(); + PeerId::from(keys.public()) +} + +/// Build a DtnRoutedV2 with a properly signed inner Container. +fn build_test_v2( + receiver: &PeerId, + custodians: Vec, + expires_at: u64, + remaining_handoffs: u32, +) -> proto::DtnRoutedV2 { + let keys = Keypair::generate_ed25519(); + let sender = PeerId::from(keys.public()); + let envelope = proto::Envelope { + sender_id: sender.to_bytes(), + receiver_id: receiver.to_bytes(), + payload: vec![], + }; + + // Sign the envelope properly + let mut envelope_buf = Vec::with_capacity(envelope.encoded_len()); + envelope.encode(&mut envelope_buf).unwrap(); + let signature = keys.sign(&envelope_buf).unwrap(); + + let container = proto::Container { + signature: signature.clone(), + envelope: Some(envelope), + }; + + let custody_route: Vec> = custodians.iter().map(|c| c.to_bytes()).collect(); + + proto::DtnRoutedV2 { + container: container.encode_to_vec(), + custody_route, + next_route_index: 0, + original_signature: signature, + sender_public_key: keys.public().encode_protobuf(), + expires_at, + remaining_handoffs, + } +} + +// ── Full envelope chain tests ── + +#[test] +fn v2_message_survives_full_envelope_chain() { + let receiver = random_peer(); + let sender = random_peer(); + let custodian = random_peer(); + + let v2 = build_test_v2(&receiver, vec![custodian], 0, 5); + + // Wrap in EnvelopPayload + let payload = proto::EnvelopPayload { + payload: Some(proto::envelop_payload::Payload::DtnRoutedV2(v2.clone())), + }; + let payload_bytes = payload.encode_to_vec(); + + // Wrap in Envelope + let envelope = proto::Envelope { + sender_id: sender.to_bytes(), + receiver_id: receiver.to_bytes(), + payload: payload_bytes, + }; + + // Wrap in Container + let container = proto::Container { + signature: vec![0xDE, 0xAD], + envelope: Some(envelope), + }; + let container_bytes = container.encode_to_vec(); + + // Now decode the whole chain + let decoded_container = proto::Container::decode(&container_bytes[..]).unwrap(); + let decoded_envelope = decoded_container.envelope.unwrap(); + assert_eq!( + PeerId::from_bytes(&decoded_envelope.receiver_id).unwrap(), + receiver + ); + + let decoded_payload = proto::EnvelopPayload::decode(&decoded_envelope.payload[..]).unwrap(); + match decoded_payload.payload { + Some(proto::envelop_payload::Payload::DtnRoutedV2(decoded_v2)) => { + assert_eq!(decoded_v2.remaining_handoffs, 5); + assert_eq!(decoded_v2.custody_route.len(), 1); + assert_eq!(decoded_v2.custody_route[0], custodian.to_bytes()); + + // Decode the inner container to get the ultimate receiver + let inner = proto::Container::decode(&decoded_v2.container[..]).unwrap(); + let inner_recv = + PeerId::from_bytes(&inner.envelope.unwrap().receiver_id).unwrap(); + assert_eq!(inner_recv, receiver); + } + _ => panic!("Expected DtnRoutedV2 payload"), + } +} + +#[test] +fn v2_message_in_dtn_oneof() { + let receiver = random_peer(); + let v2 = build_test_v2(&receiver, vec![random_peer()], 0, 3); + + // Wrap in Dtn message (the other transport path) + let dtn = proto::Dtn { + message: Some(proto::dtn::Message::RoutedV2(v2.clone())), + }; + let encoded = dtn.encode_to_vec(); + let decoded = proto::Dtn::decode(&encoded[..]).unwrap(); + + match decoded.message { + Some(proto::dtn::Message::RoutedV2(decoded_v2)) => { + assert_eq!(decoded_v2.remaining_handoffs, 3); + } + _ => panic!("Expected RoutedV2 variant"), + } +} + +// ── Sled storage tests ── + +#[test] +fn v2_sled_store_and_retrieve() { + let db = sled::Config::new().temporary(true).open().unwrap(); + let tree = db.open_tree("v2-messages").unwrap(); + + let receiver = random_peer(); + let v2 = build_test_v2(&receiver, vec![random_peer()], 0, 5); + let sig = v2.original_signature.clone(); + let v2_bytes = v2.encode_to_vec(); + + let entry = DtnRoutedV2Entry { + routed_v2_bytes: v2_bytes.clone(), + sender_public_key: v2.sender_public_key.clone(), + size: v2_bytes.len() as u32, + accepted_at: 12345, + receiver_id: receiver.to_bytes(), + }; + + // Store + tree.insert(&sig, bincode::serialize(&entry).unwrap()) + .unwrap(); + tree.flush().unwrap(); + + // Retrieve and decode + let stored = tree.get(&sig).unwrap().unwrap(); + let decoded: DtnRoutedV2Entry = bincode::deserialize(&stored).unwrap(); + assert_eq!(decoded.size, v2_bytes.len() as u32); + assert_eq!(decoded.accepted_at, 12345); + + let inner_v2 = proto::DtnRoutedV2::decode(&decoded.routed_v2_bytes[..]).unwrap(); + assert_eq!(inner_v2.remaining_handoffs, 5); +} + +#[test] +fn v2_sled_duplicate_detection() { + let db = sled::Config::new().temporary(true).open().unwrap(); + let tree = db.open_tree("v2-dedup").unwrap(); + + let sig = vec![0xDE, 0xAD, 0xBE, 0xEF]; + + // First message accepted + tree.insert(&sig, b"message-data").unwrap(); + assert!(tree.contains_key(&sig).unwrap()); + + // Second message with same sig should be detected + let is_dup = tree.contains_key(&sig).unwrap(); + assert!(is_dup, "duplicate should be detected"); +} + +#[test] +fn v2_sled_quota_tracking_lifecycle() { + let db = sled::Config::new().temporary(true).open().unwrap(); + let quotas = db.open_tree("v2-quotas").unwrap(); + let messages = db.open_tree("v2-msgs").unwrap(); + + let sender_key = vec![0xAA, 0xBB]; + let sig1 = vec![0x01]; + let sig2 = vec![0x02]; + + // Accept message 1 (size: 100) + let entry1 = DtnRoutedV2Entry { + routed_v2_bytes: vec![0; 100], + sender_public_key: sender_key.clone(), + size: 100, + accepted_at: 1000, + receiver_id: vec![], + }; + messages + .insert(&sig1, bincode::serialize(&entry1).unwrap()) + .unwrap(); + let quota = SenderQuotaEntry { + used_bytes: 100, + message_count: 1, + }; + quotas + .insert(&sender_key, bincode::serialize("a).unwrap()) + .unwrap(); + + // Accept message 2 (size: 200) + let entry2 = DtnRoutedV2Entry { + routed_v2_bytes: vec![0; 200], + sender_public_key: sender_key.clone(), + size: 200, + accepted_at: 2000, + receiver_id: vec![], + }; + messages + .insert(&sig2, bincode::serialize(&entry2).unwrap()) + .unwrap(); + let stored = quotas.get(&sender_key).unwrap().unwrap(); + let mut quota: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + quota.used_bytes += 200; + quota.message_count += 1; + quotas + .insert(&sender_key, bincode::serialize("a).unwrap()) + .unwrap(); + + // Verify quota + let stored = quotas.get(&sender_key).unwrap().unwrap(); + let quota: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + assert_eq!(quota.used_bytes, 300); + assert_eq!(quota.message_count, 2); + + // Forward message 1 (simulate acceptance) — remove and update quota + let removed = messages.remove(&sig1).unwrap().unwrap(); + let removed_entry: DtnRoutedV2Entry = bincode::deserialize(&removed).unwrap(); + let stored = quotas.get(&sender_key).unwrap().unwrap(); + let mut quota: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + quota.used_bytes -= removed_entry.size as u64; + quota.message_count -= 1; + quotas + .insert(&sender_key, bincode::serialize("a).unwrap()) + .unwrap(); + + // Verify updated quota + let stored = quotas.get(&sender_key).unwrap().unwrap(); + let quota: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + assert_eq!(quota.used_bytes, 200); + assert_eq!(quota.message_count, 1); +} + +#[test] +fn v2_per_sender_quota_enforcement() { + let db = sled::Config::new().temporary(true).open().unwrap(); + let quotas = db.open_tree("v2-quota-enforce").unwrap(); + + let sender_key = vec![0xCC]; + + // Sender near quota limit + let quota = SenderQuotaEntry { + used_bytes: V2_PER_SENDER_QUOTA - 100, + message_count: 50, + }; + quotas + .insert(&sender_key, bincode::serialize("a).unwrap()) + .unwrap(); + + // Small message should fit + let stored = quotas.get(&sender_key).unwrap().unwrap(); + let q: SenderQuotaEntry = bincode::deserialize(&stored).unwrap(); + assert!( + q.used_bytes + 50 <= V2_PER_SENDER_QUOTA, + "50-byte message should fit" + ); + + // Large message should not fit + assert!( + q.used_bytes + 200 > V2_PER_SENDER_QUOTA, + "200-byte message should be rejected" + ); +} + +// ── Expiry and handoff validation tests ── + +#[test] +fn v2_expired_messages_detected_in_storage_scan() { + let db = sled::Config::new().temporary(true).open().unwrap(); + let tree = db.open_tree("v2-expiry").unwrap(); + + let receiver = random_peer(); + + // Expired message + let expired_v2 = build_test_v2(&receiver, vec![random_peer()], 1, 5); + let expired_entry = DtnRoutedV2Entry { + routed_v2_bytes: expired_v2.encode_to_vec(), + sender_public_key: expired_v2.sender_public_key.clone(), + size: 10, + accepted_at: 0, + receiver_id: receiver.to_bytes(), + }; + tree.insert( + &expired_v2.original_signature, + bincode::serialize(&expired_entry).unwrap(), + ) + .unwrap(); + + // Non-expired message + let valid_v2 = build_test_v2(&receiver, vec![random_peer()], u64::MAX, 5); + let valid_entry = DtnRoutedV2Entry { + routed_v2_bytes: valid_v2.encode_to_vec(), + sender_public_key: valid_v2.sender_public_key.clone(), + size: 10, + accepted_at: 0, + receiver_id: receiver.to_bytes(), + }; + tree.insert( + &valid_v2.original_signature, + bincode::serialize(&valid_entry).unwrap(), + ) + .unwrap(); + + // Scan and classify + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + let mut expired_sigs = Vec::new(); + let mut valid_count = 0; + + for entry in tree.iter() { + let (sig, bytes) = entry.unwrap(); + let stored: DtnRoutedV2Entry = bincode::deserialize(&bytes).unwrap(); + let v2 = proto::DtnRoutedV2::decode(&stored.routed_v2_bytes[..]).unwrap(); + if v2.expires_at > 0 && now > v2.expires_at { + expired_sigs.push(sig.to_vec()); + } else { + valid_count += 1; + } + } + + assert_eq!(expired_sigs.len(), 1); + assert_eq!(valid_count, 1); + + // Remove expired + for sig in &expired_sigs { + tree.remove(sig).unwrap(); + } + assert_eq!(tree.len(), 1); +} + +#[test] +fn v2_flat_route_construction_and_advancement() { + let c1 = random_peer(); + let c2 = random_peer(); + let c3 = random_peer(); + let _receiver = random_peer(); + + let mut v2 = proto::DtnRoutedV2 { + container: vec![], + custody_route: vec![c1.to_bytes(), c2.to_bytes(), c3.to_bytes()], + next_route_index: 0, + original_signature: vec![0xAA], + sender_public_key: vec![], + expires_at: 0, + remaining_handoffs: 6, + }; + + // Simulate forwarding to c2 (index 1) + let target = c2; + v2.remaining_handoffs = v2.remaining_handoffs.saturating_sub(1); + for (i, user_bytes) in v2.custody_route.iter().enumerate() { + if let Ok(uid) = PeerId::from_bytes(user_bytes) { + if uid == target && i as u32 >= v2.next_route_index { + v2.next_route_index = (i as u32) + 1; + break; + } + } + } + + assert_eq!(v2.remaining_handoffs, 5); + assert_eq!(v2.next_route_index, 2); // past c2, c3 still eligible + + // Encode and decode — verify state persists + let encoded = v2.encode_to_vec(); + let decoded = proto::DtnRoutedV2::decode(&encoded[..]).unwrap(); + assert_eq!(decoded.next_route_index, 2); + assert_eq!(decoded.remaining_handoffs, 5); +} diff --git a/rust/qaul-proto/build.rs b/rust/qaul-proto/build.rs index aa4947357..6669229f1 100644 --- a/rust/qaul-proto/build.rs +++ b/rust/qaul-proto/build.rs @@ -58,6 +58,12 @@ fn main() { ); prost_build.type_attribute("Data", "#[derive(serde::Serialize, serde::Deserialize)]"); + // make DTN V2 messages serializable for sled storage + prost_build.type_attribute( + "DtnRoutedV2", + "#[derive(serde::Serialize, serde::Deserialize)]", + ); + // compile these protobuf files match prost_build.compile_protos( &[