diff --git a/keep-agent/src/client.rs b/keep-agent/src/client.rs index 02f4d87a..a991c795 100644 --- a/keep-agent/src/client.rs +++ b/keep-agent/src/client.rs @@ -5,6 +5,8 @@ use std::time::Duration; use nostr_sdk::prelude::*; +use keep_core::relay::TIMESTAMP_TWEAK_RANGE; + use crate::error::{AgentError, Result}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -31,24 +33,13 @@ impl PendingSession { let client = Client::new(client_keys.clone()); client - .add_relay(&relay_url) + .pool() + .add_relay(&relay_url, default_relay_opts()) .await .map_err(|e| AgentError::Connection(e.to_string()))?; client.connect().await; - - tokio::time::timeout(timeout, async { - loop { - if let Ok(relay) = client.relay(&relay_url).await { - if matches!(relay.status(), RelayStatus::Connected) { - break; - } - } - tokio::time::sleep(Duration::from_millis(100)).await; - } - }) - .await - .map_err(|_| AgentError::Connection("Relay connection timeout".into()))?; + wait_for_relay_connection(&client, &relay_url, timeout).await?; let request_id = generate_uuid(); @@ -98,7 +89,7 @@ impl PendingSession { let tags = vec![Tag::public_key(self.signer_pubkey)]; let unsigned = UnsignedEvent::new( self.client_keys.public_key(), - Timestamp::now(), + Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE), Kind::NostrConnect, tags, encrypted, @@ -123,51 +114,49 @@ impl PendingSession { .kind(Kind::NostrConnect) .author(self.signer_pubkey) .pubkey(self.client_keys.public_key()) - .since(Timestamp::now()); + .since(Timestamp::now() - Duration::from_secs(10)); - let sub_output = self + let mut stream = self .client - .subscribe(filter, None) + .pool() + .stream_events(filter, timeout, ReqExitPolicy::WaitForEventsAfterEOSE(1)) .await .map_err(|e| AgentError::Nostr(e.to_string()))?; - let sub_id = sub_output.id(); - let mut notifications = self.client.notifications(); - let result = tokio::time::timeout(timeout, async { - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if event.kind == Kind::NostrConnect && event.pubkey == self.signer_pubkey { - if let Ok(decrypted) = nip44::decrypt( - self.client_keys.secret_key(), - &self.signer_pubkey, - &event.content, - ) { - let parsed: serde_json::Value = serde_json::from_str(&decrypted) - .map_err(|e| AgentError::Serialization(e.to_string()))?; - - if let Some(id) = parsed.get("id").and_then(|v| v.as_str()) { - if id == self.request_id { - if let Some(error) = parsed.get("error") { - if !error.is_null() { - return Ok(ApprovalStatus::Denied); - } - } - if parsed.get("result").is_some() { - return Ok(ApprovalStatus::Approved); - } - } - } - } + while let Some(event) = stream.next().await { + if event.kind != Kind::NostrConnect || event.pubkey != self.signer_pubkey { + continue; + } + let Ok(decrypted) = nip44::decrypt( + self.client_keys.secret_key(), + &self.signer_pubkey, + &event.content, + ) else { + continue; + }; + let parsed: serde_json::Value = serde_json::from_str(&decrypted) + .map_err(|e| AgentError::Serialization(e.to_string()))?; + + let Some(id) = parsed.get("id").and_then(|v| v.as_str()) else { + continue; + }; + if id != self.request_id { + continue; + } + if let Some(error) = parsed.get("error") { + if !error.is_null() { + return Ok(ApprovalStatus::Denied); } } + if parsed.get("result").is_some() { + return Ok(ApprovalStatus::Approved); + } } Ok(ApprovalStatus::Pending) }) .await; - self.client.unsubscribe(sub_id).await; - match result { Ok(inner) => inner, Err(_) => Ok(ApprovalStatus::Pending), @@ -220,24 +209,13 @@ impl AgentClient { let client = Client::new(client_keys.clone()); client - .add_relay(&relay_url) + .pool() + .add_relay(&relay_url, default_relay_opts()) .await .map_err(|e| AgentError::Connection(e.to_string()))?; client.connect().await; - - tokio::time::timeout(timeout, async { - loop { - if let Ok(relay) = client.relay(&relay_url).await { - if matches!(relay.status(), RelayStatus::Connected) { - break; - } - } - tokio::time::sleep(Duration::from_millis(100)).await; - } - }) - .await - .map_err(|_| AgentError::Connection("Relay connection timeout".into()))?; + wait_for_relay_connection(&client, &relay_url, timeout).await?; let mut agent_client = Self { signer_pubkey, @@ -412,7 +390,7 @@ impl AgentClient { } let relays: Vec = if result.is_string() { - serde_json::from_str(result.as_str().unwrap()) + serde_json::from_str(result.as_str().expect("guarded by is_string check")) .map_err(|e| AgentError::Serialization(format!("Invalid relay list: {e}")))? } else if result.is_array() { serde_json::from_value(result.clone()) @@ -438,9 +416,16 @@ impl AgentClient { self.client.disconnect().await; self.client.remove_all_relays().await; + let relay_opts = default_relay_opts(); let mut added = Vec::new(); for relay in &valid_relays { - if self.client.add_relay(relay).await.is_ok() { + if self + .client + .pool() + .add_relay(relay, relay_opts.clone()) + .await + .is_ok() + { added.push(relay.clone()); } } @@ -450,6 +435,7 @@ impl AgentClient { )); } self.client.connect().await; + wait_for_any_relay_connection(&self.client, &added, Duration::from_secs(10)).await?; self.relay_url = added[0].clone(); @@ -477,7 +463,7 @@ impl AgentClient { let tags = vec![Tag::public_key(self.signer_pubkey)]; let unsigned = UnsignedEvent::new( self.client_keys.public_key(), - Timestamp::now(), + Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE), Kind::NostrConnect, tags, encrypted, @@ -496,47 +482,44 @@ impl AgentClient { .kind(Kind::NostrConnect) .author(self.signer_pubkey) .pubkey(self.client_keys.public_key()) - .since(Timestamp::now()); + .since(Timestamp::now() - Duration::from_secs(10)); - let sub_output = self + let mut stream = self .client - .subscribe(filter, None) + .pool() + .stream_events( + filter, + Duration::from_secs(30), + ReqExitPolicy::WaitForEventsAfterEOSE(5), + ) .await .map_err(|e| AgentError::Nostr(e.to_string()))?; - let sub_id = sub_output.id(); - let mut notifications = self.client.notifications(); - let result = tokio::time::timeout(Duration::from_secs(30), async { - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Event { event, .. } = notification { - if event.kind == Kind::NostrConnect && event.pubkey == self.signer_pubkey { - if let Ok(decrypted) = nip44::decrypt( - self.client_keys.secret_key(), - &self.signer_pubkey, - &event.content, - ) { - if let Some(ref expected_id) = request_id { - if let Ok(resp) = - serde_json::from_str::(&decrypted) - { - if resp.get("id").and_then(|v| v.as_str()) != Some(expected_id) - { - continue; - } - } - } - return Ok(decrypted); + while let Some(event) = stream.next().await { + if event.kind != Kind::NostrConnect || event.pubkey != self.signer_pubkey { + continue; + } + let Ok(decrypted) = nip44::decrypt( + self.client_keys.secret_key(), + &self.signer_pubkey, + &event.content, + ) else { + continue; + }; + if let Some(ref expected_id) = request_id { + if let Ok(resp) = serde_json::from_str::(&decrypted) { + if resp.get("id").and_then(|v| v.as_str()) != Some(expected_id) { + continue; } } } + return Ok(decrypted); } Err(AgentError::Connection("No response received".into())) }) .await; - self.client.unsubscribe(sub_id).await; - match result { Ok(inner) => inner, Err(_) => Err(AgentError::Connection("Response timeout".into())), @@ -557,7 +540,7 @@ impl AgentClient { .get("result") .map(|v| { if v.is_string() { - v.as_str().unwrap().to_string() + v.as_str().expect("guarded by is_string check").to_string() } else { v.to_string() } @@ -578,6 +561,56 @@ impl AgentClient { } } +async fn wait_for_relay_connection( + client: &Client, + relay_url: &str, + timeout: Duration, +) -> Result<()> { + tokio::time::timeout(timeout, async { + loop { + if let Ok(relay) = client.relay(relay_url).await { + if matches!(relay.status(), RelayStatus::Connected) { + return; + } + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .map_err(|_| AgentError::Connection("Relay connection timeout".into())) +} + +async fn wait_for_any_relay_connection( + client: &Client, + relays: &[String], + timeout: Duration, +) -> Result<()> { + tokio::time::timeout(timeout, async { + loop { + for relay in relays { + if let Ok(r) = client.relay(relay).await { + if matches!(r.status(), RelayStatus::Connected) { + return; + } + } + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .map_err(|_| AgentError::Connection("Relay connection timeout".into())) +} + +fn default_relay_opts() -> RelayOptions { + RelayOptions::default() + .reconnect(true) + .ping(true) + .retry_interval(Duration::from_secs(10)) + .adjust_retry_interval(true) + .ban_relay_on_mismatch(true) + .max_avg_latency(Some(Duration::from_secs(3))) +} + fn generate_uuid() -> String { uuid::Uuid::new_v4().to_string() } diff --git a/keep-core/src/relay.rs b/keep-core/src/relay.rs index 9dea3469..b4c660dc 100644 --- a/keep-core/src/relay.rs +++ b/keep-core/src/relay.rs @@ -11,6 +11,9 @@ pub const MAX_RELAYS: usize = 10; /// Maximum length of a relay URL. pub const MAX_RELAY_URL_LENGTH: usize = 256; +/// Range of seconds to randomly tweak event timestamps for privacy. +pub const TIMESTAMP_TWEAK_RANGE: std::ops::Range = 0..5; + /// Relay configuration for a FROST share, keyed by group public key. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RelayConfig { diff --git a/keep-frost-net/Cargo.toml b/keep-frost-net/Cargo.toml index 21a58635..3a296cef 100644 --- a/keep-frost-net/Cargo.toml +++ b/keep-frost-net/Cargo.toml @@ -28,7 +28,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" hex = "0.4" base64 = "0.22" -chrono = "0.4" rustls = "0.23" rustls-pki-types = "1" diff --git a/keep-frost-net/src/event.rs b/keep-frost-net/src/event.rs index 4467a66a..66a53d31 100644 --- a/keep-frost-net/src/event.rs +++ b/keep-frost-net/src/event.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use nostr_sdk::prelude::*; +use keep_core::relay::TIMESTAMP_TWEAK_RANGE; + use crate::error::{FrostNetError, Result}; use crate::proof; use crate::protocol::*; @@ -17,7 +19,7 @@ impl KfpEventBuilder { verifying_share: &[u8; 33], name: Option<&str>, ) -> Result { - let timestamp = chrono::Utc::now().timestamp() as u64; + let timestamp = Timestamp::now().as_secs(); let proof_signature = proof::sign_proof( signing_share, group_pubkey, @@ -41,6 +43,7 @@ impl KfpEventBuilder { let content = msg.to_json()?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), content) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::custom( TagKind::custom("g"), [hex::encode(group_pubkey)], @@ -62,6 +65,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom( TagKind::custom("g"), @@ -88,6 +92,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom( TagKind::custom("s"), @@ -110,6 +115,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom( TagKind::custom("s"), @@ -132,6 +138,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom( TagKind::custom("s"), @@ -151,6 +158,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom(TagKind::custom("t"), ["ping"])) .sign_with_keys(keys) @@ -160,7 +168,7 @@ impl KfpEventBuilder { pub fn pong(keys: &Keys, recipient: &PublicKey, challenge: [u8; 32]) -> Result { let payload = PongPayload { challenge, - timestamp: chrono::Utc::now().timestamp() as u64, + timestamp: Timestamp::now().as_secs(), }; let msg = KfpMessage::Pong(payload); let content = msg.to_json()?; @@ -169,6 +177,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom(TagKind::custom("t"), ["pong"])) .sign_with_keys(keys) @@ -194,6 +203,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; let mut builder = EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom(TagKind::custom("t"), ["error"])); @@ -218,6 +228,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom( TagKind::custom("g"), @@ -244,6 +255,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom( TagKind::custom("s"), @@ -267,6 +279,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom(TagKind::custom("s"), [hex::encode(session_id)])) .tag(Tag::custom(TagKind::custom("t"), ["ecdh_complete"])) @@ -287,6 +300,7 @@ impl KfpEventBuilder { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom( TagKind::custom("g"), @@ -305,13 +319,16 @@ impl KfpEventBuilder { )); } - let is_addressed_to_us = event.tags.iter().any(|t| { - if let Some(TagStandard::PublicKey { public_key, .. }) = t.as_standardized() { - public_key == &keys.public_key() - } else { - false - } - }); + let is_addressed_to_us = + event + .tags + .filter(TagKind::p()) + .any(|t| match t.as_standardized() { + Some(TagStandard::PublicKey { public_key, .. }) => { + public_key == &keys.public_key() + } + _ => false, + }); let content = if is_addressed_to_us { nip44::decrypt(keys.secret_key(), &event.pubkey, &event.content) @@ -330,40 +347,28 @@ impl KfpEventBuilder { } pub fn get_message_type(event: &Event) -> Option { - event.tags.iter().find_map(|t| { - let tag = t.as_slice(); - if tag.first()? == "t" { - tag.get(1).map(|s| s.to_string()) - } else { - None - } - }) + event + .tags + .find(TagKind::custom("t")) + .and_then(|t| t.as_slice().get(1).map(|s| s.to_string())) } pub fn get_session_id(event: &Event) -> Option<[u8; 32]> { - event.tags.iter().find_map(|t| { - let tag = t.as_slice(); - if tag.first()? == "s" { - let hex_str = tag.get(1)?; - let bytes = hex::decode(hex_str).ok()?; - bytes.try_into().ok() - } else { - None - } - }) + event + .tags + .find(TagKind::custom("s")) + .and_then(|t| t.as_slice().get(1)) + .and_then(|hex_str| hex::decode(hex_str).ok()) + .and_then(|bytes| bytes.try_into().ok()) } pub fn get_group_pubkey(event: &Event) -> Option<[u8; 32]> { - event.tags.iter().find_map(|t| { - let tag = t.as_slice(); - if tag.first()? == "g" { - let hex_str = tag.get(1)?; - let bytes = hex::decode(hex_str).ok()?; - bytes.try_into().ok() - } else { - None - } - }) + event + .tags + .find(TagKind::custom("g")) + .and_then(|t| t.as_slice().get(1)) + .and_then(|hex_str| hex::decode(hex_str).ok()) + .and_then(|bytes| bytes.try_into().ok()) } } diff --git a/keep-frost-net/src/node/descriptor.rs b/keep-frost-net/src/node/descriptor.rs index 49337959..38d5e164 100644 --- a/keep-frost-net/src/node/descriptor.rs +++ b/keep-frost-net/src/node/descriptor.rs @@ -14,6 +14,7 @@ use crate::descriptor_session::{ }; use crate::error::{FrostNetError, Result}; use crate::protocol::*; +use keep_core::relay::TIMESTAMP_TWEAK_RANGE; use super::{KfpNode, KfpNodeEvent}; @@ -47,7 +48,7 @@ impl KfpNode { self.check_proposer_authorized(our_index)?; - let created_at = chrono::Utc::now().timestamp().max(0) as u64; + let created_at = Timestamp::now().as_secs(); let session_id = derive_descriptor_session_id(&self.group_pubkey, &policy, created_at); let expected_contributors = participant_indices(&policy); let we_are_contributor = expected_contributors.contains(&our_index); @@ -128,6 +129,7 @@ impl KfpNode { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; let event = EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*pubkey)) .tag(Tag::custom( TagKind::custom("g"), @@ -327,6 +329,7 @@ impl KfpNode { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; let event = EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*initiator_pubkey)) .tag(Tag::custom( TagKind::custom("g"), @@ -455,6 +458,7 @@ impl KfpNode { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; let event = EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*pubkey)) .tag(Tag::custom( TagKind::custom("g"), @@ -645,6 +649,7 @@ impl KfpNode { .map_err(|e| FrostNetError::Crypto(e.to_string()))?; let event = EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(sender)) .tag(Tag::custom( TagKind::custom("g"), @@ -713,6 +718,7 @@ impl KfpNode { }; let event = match EventBuilder::new(Kind::Custom(KFP_EVENT_KIND), encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(*recipient)) .tag(Tag::custom( TagKind::custom("g"), @@ -936,7 +942,7 @@ impl KfpNode { } const MAX_SEEN_XPUB_ANNOUNCES: usize = 10_000; if seen.len() > MAX_SEEN_XPUB_ANNOUNCES { - let now = chrono::Utc::now().timestamp().max(0) as u64; + let now = Timestamp::now().as_secs(); let window = self .replay_window_secs .saturating_add(super::MAX_FUTURE_SKEW_SECS); diff --git a/keep-frost-net/src/node/mod.rs b/keep-frost-net/src/node/mod.rs index 547b6145..dbc95bdc 100644 --- a/keep-frost-net/src/node/mod.rs +++ b/keep-frost-net/src/node/mod.rs @@ -391,21 +391,34 @@ impl KfpNode { None => Client::new(keys.clone()), }; + let relay_opts = default_relay_opts(); + for relay in &relays { - client.add_relay(relay).await.map_err(|e| { - FrostNetError::Transport(format!("Failed to add relay {relay}: {e}")) - })?; + client + .pool() + .add_relay(relay, relay_opts.clone()) + .await + .map_err(|e| { + FrostNetError::Transport(format!("Failed to add relay {relay}: {e}")) + })?; } client.connect().await; - tokio::time::sleep(Duration::from_millis(500)).await; - let connected_relays = client.relays().await; - if connected_relays.is_empty() { - return Err(FrostNetError::Transport( - "Failed to connect to any relays".into(), - )); - } + tokio::time::timeout(Duration::from_secs(10), async { + loop { + let relay_map = client.relays().await; + let any_connected = relay_map + .values() + .any(|r| matches!(r.status(), RelayStatus::Connected)); + if any_connected { + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .map_err(|_| FrostNetError::Transport("Timed out waiting for relay connection".into()))?; let group_pubkey = *share.group_pubkey(); let our_index = share.metadata.identifier; @@ -744,10 +757,9 @@ impl KfpNode { let matching: Vec<_> = events .into_iter() .filter(|e| { - e.tags.iter().any(|t| { - t.kind() == TagKind::custom("g") - && t.content().map(|c| c == group_hex).unwrap_or(false) - }) + e.tags + .filter(TagKind::custom("g")) + .any(|t| t.content() == Some(&group_hex)) }) .collect(); debug!(count = matching.len(), "Fetched historical events"); @@ -790,7 +802,7 @@ impl KfpNode { }); } { - let now = chrono::Utc::now().timestamp().max(0) as u64; + let now = Timestamp::now().as_secs(); let window = self.replay_window_secs + MAX_FUTURE_SKEW_SECS; self.seen_xpub_announces.write().retain(|&(_, ts, _)| { now.saturating_sub(window) <= ts @@ -933,7 +945,7 @@ impl KfpNode { return Ok(()); } - let now = chrono::Utc::now().timestamp() as u64; + let now = Timestamp::now().as_secs(); if payload.timestamp + ANNOUNCE_MAX_AGE_SECS < now { debug!( timestamp = payload.timestamp, @@ -1162,6 +1174,16 @@ impl KfpNode { } } +fn default_relay_opts() -> RelayOptions { + RelayOptions::default() + .reconnect(true) + .ping(true) + .retry_interval(Duration::from_secs(10)) + .adjust_retry_interval(true) + .ban_relay_on_mismatch(true) + .max_avg_latency(Some(Duration::from_secs(3))) +} + fn derive_keys_from_share(share: &SharePackage) -> Result { let mut hasher = Sha256::new(); hasher.update(b"keep-frost-node-identity-v2"); @@ -1188,6 +1210,9 @@ mod tests { #[tokio::test] async fn test_node_creation() { + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .ok(); let config = ThresholdConfig::two_of_three(); let dealer = TrustedDealer::new(config); let (mut shares, _) = dealer.generate("test").unwrap(); diff --git a/keep-frost-net/src/protocol.rs b/keep-frost-net/src/protocol.rs index a83bf7dc..6f063f55 100644 --- a/keep-frost-net/src/protocol.rs +++ b/keep-frost-net/src/protocol.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use std::collections::HashSet; +use nostr_sdk::Timestamp; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; @@ -41,7 +42,7 @@ pub const VALID_NETWORKS: &[&str] = &["bitcoin", "testnet", "signet", "regtest"] pub(crate) const MAX_FUTURE_SKEW_SECS: u64 = 30; fn within_replay_window(created_at: u64, window_secs: u64) -> bool { - let now = chrono::Utc::now().timestamp().max(0) as u64; + let now = Timestamp::now().as_secs(); let min_valid = now.saturating_sub(window_secs); let max_valid = now.saturating_add(MAX_FUTURE_SKEW_SECS); created_at >= min_valid && created_at <= max_valid @@ -560,7 +561,7 @@ impl XpubAnnouncePayload { group_pubkey, share_index, recovery_xpubs, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -598,7 +599,7 @@ impl SignRequestPayload { message, message_type: message_type.to_string(), participants, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), metadata: None, } } @@ -682,7 +683,7 @@ impl PingPayload { pub fn new() -> Self { Self { challenge: keep_core::crypto::random_bytes::<32>(), - timestamp: chrono::Utc::now().timestamp() as u64, + timestamp: Timestamp::now().as_secs(), } } } @@ -704,7 +705,7 @@ impl PongPayload { pub fn from_ping(ping: &PingPayload) -> Self { Self { challenge: ping.challenge, - timestamp: chrono::Utc::now().timestamp() as u64, + timestamp: Timestamp::now().as_secs(), } } } @@ -758,7 +759,7 @@ impl EcdhRequestPayload { group_pubkey, recipient_pubkey, participants, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -828,7 +829,7 @@ impl RefreshRequestPayload { session_id, group_pubkey, participants, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -853,7 +854,7 @@ impl RefreshRound1Payload { session_id, share_index, package, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -896,7 +897,7 @@ impl RefreshRound2Payload { share_index, target_index, package, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -932,7 +933,7 @@ impl RefreshCompletePayload { session_id, share_index, success, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -1033,7 +1034,7 @@ impl DescriptorContributePayload { share_index, account_xpub: account_xpub.to_string(), fingerprint: fingerprint.to_string(), - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -1073,7 +1074,7 @@ impl DescriptorFinalizePayload { internal_descriptor: internal_descriptor.to_string(), policy_hash, contributions, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -1107,7 +1108,7 @@ impl DescriptorAckPayload { group_pubkey, descriptor_hash, key_proof_psbt, - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -1132,7 +1133,7 @@ impl DescriptorNackPayload { session_id, group_pubkey, reason: reason.to_string(), - created_at: chrono::Utc::now().timestamp() as u64, + created_at: Timestamp::now().as_secs(), } } @@ -1493,16 +1494,16 @@ mod tests { assert!(payload.is_within_replay_window(1)); let mut old_payload = payload.clone(); - old_payload.created_at = chrono::Utc::now().timestamp() as u64 - 400; + old_payload.created_at = Timestamp::now().as_secs() - 400; assert!(!old_payload.is_within_replay_window(DEFAULT_REPLAY_WINDOW_SECS)); assert!(old_payload.is_within_replay_window(500)); let mut slight_future = payload.clone(); - slight_future.created_at = chrono::Utc::now().timestamp() as u64 + 10; + slight_future.created_at = Timestamp::now().as_secs() + 10; assert!(slight_future.is_within_replay_window(DEFAULT_REPLAY_WINDOW_SECS)); let mut far_future = payload.clone(); - far_future.created_at = chrono::Utc::now().timestamp() as u64 + 400; + far_future.created_at = Timestamp::now().as_secs() + 400; assert!(!far_future.is_within_replay_window(DEFAULT_REPLAY_WINDOW_SECS)); assert!(!far_future.is_within_replay_window(500)); } diff --git a/keep-nip46/src/server.rs b/keep-nip46/src/server.rs index fa130340..4b2acf01 100644 --- a/keep-nip46/src/server.rs +++ b/keep-nip46/src/server.rs @@ -20,6 +20,7 @@ use crate::handler::SignerHandler; use crate::permissions::PermissionManager; use crate::rate_limit::RateLimitConfig; use crate::types::{LogEvent, Nip46Request, Nip46Response, PartialEvent, ServerCallbacks}; +use keep_core::relay::TIMESTAMP_TWEAK_RANGE; pub struct ServerConfig { pub max_event_json_size: usize, @@ -56,9 +57,16 @@ pub struct Server { } async fn add_relays(client: &Client, relay_urls: &[String]) -> Result<()> { + let opts = nostr_sdk::RelayOptions::default() + .reconnect(true) + .ping(true) + .retry_interval(std::time::Duration::from_secs(10)) + .adjust_retry_interval(true); + for relay_url in relay_urls { client - .add_relay(relay_url) + .pool() + .add_relay(relay_url, opts.clone()) .await .map_err(|e| NetworkError::relay(e.to_string()))?; } @@ -506,6 +514,7 @@ impl Server { .map_err(|e| CryptoError::encryption(e.to_string()))?; let response_event = EventBuilder::new(Kind::NostrConnect, encrypted) + .custom_created_at(Timestamp::tweaked(TIMESTAMP_TWEAK_RANGE)) .tag(Tag::public_key(app_pubkey)) .sign_with_keys(keys) .map_err(|e| CryptoError::invalid_signature(format!("sign response: {e}")))?;