From 50a389ace35a767bc0d8ce1a1b9ba25e87d75e4e Mon Sep 17 00:00:00 2001 From: "William K. Santiago" Date: Thu, 26 Feb 2026 10:14:32 -0500 Subject: [PATCH 1/5] Add XpubAnnounced desktop handler and fix stale frost_node on reconnect --- keep-desktop/src/frost.rs | 60 ++++++++++++++++++++++++++++++++++--- keep-desktop/src/message.rs | 12 ++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/keep-desktop/src/frost.rs b/keep-desktop/src/frost.rs index bddbf683..04cb1f56 100644 --- a/keep-desktop/src/frost.rs +++ b/keep-desktop/src/frost.rs @@ -122,11 +122,16 @@ pub(crate) struct FrostChannels { } fn push_frost_event(queue: &Mutex>, event: FrostNodeMsg) { - if let Ok(mut q) = queue.lock() { - if q.len() >= MAX_FROST_EVENT_QUEUE { - q.pop_front(); + match queue.lock() { + Ok(mut q) => { + if q.len() >= MAX_FROST_EVENT_QUEUE { + q.pop_front(); + } + q.push_back(event); + } + Err(e) => { + tracing::warn!("frost event queue mutex poisoned, dropping event: {e}"); } - q.push_back(event); } } @@ -532,6 +537,18 @@ pub(crate) async fn frost_event_listener( FrostNodeMsg::DescriptorFailed { session_id, error }, ); } + Ok(KfpNodeEvent::XpubAnnounced { + share_index, + recovery_xpubs, + }) => { + push_frost_event( + &frost_events, + FrostNodeMsg::XpubAnnounced { + share_index, + recovery_xpubs, + }, + ); + } Ok(_) => {} Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {} Err(tokio::sync::broadcast::error::RecvError::Closed) => break, @@ -677,6 +694,28 @@ impl App { self.update_wallet_setup(&session_id, |setup| { setup.phase = SetupPhase::Coordinating(DescriptorProgress::Finalizing); }); + if self.active_coordinations.contains_key(&session_id) { + let Some(node) = self.get_frost_node() else { + return iced::Task::none(); + }; + return iced::Task::perform( + async move { + node.build_and_finalize_descriptor(session_id) + .await + .map_err(|e| format!("{e}")) + }, + move |result| match result { + Ok(()) => Message::WalletDescriptorProgress( + DescriptorProgress::Finalizing, + Some(session_id), + ), + Err(e) => Message::WalletDescriptorProgress( + DescriptorProgress::Failed(e), + Some(session_id), + ), + }, + ); + } } FrostNodeMsg::DescriptorContributed { session_id, .. } => { self.update_wallet_setup(&session_id, |setup| { @@ -717,6 +756,16 @@ impl App { setup.phase = SetupPhase::Coordinating(DescriptorProgress::Failed(error)); }); } + FrostNodeMsg::XpubAnnounced { + share_index, + recovery_xpubs, + } => { + tracing::info!( + share_index, + count = recovery_xpubs.len(), + "Received recovery xpub announcement" + ); + } } iced::Task::none() } @@ -882,6 +931,9 @@ impl App { let _ = tx.try_send(()); } } + if let Ok(mut guard) = self.frost_node.lock() { + *guard = None; + } self.frost_peers.clear(); self.pending_sign_display.clear(); if let Ok(mut guard) = self.pending_sign_requests.lock() { diff --git a/keep-desktop/src/message.rs b/keep-desktop/src/message.rs index 8d73ad99..6ab34010 100644 --- a/keep-desktop/src/message.rs +++ b/keep-desktop/src/message.rs @@ -312,6 +312,10 @@ pub enum FrostNodeMsg { session_id: [u8; 32], error: String, }, + XpubAnnounced { + share_index: u16, + recovery_xpubs: Vec, + }, } impl fmt::Debug for FrostNodeMsg { @@ -358,6 +362,14 @@ impl fmt::Debug for FrostNodeMsg { .field("session_id", &hex::encode(session_id)) .field("error", error) .finish(), + Self::XpubAnnounced { + share_index, + recovery_xpubs, + } => f + .debug_struct("XpubAnnounced") + .field("share_index", share_index) + .field("xpub_count", &recovery_xpubs.len()) + .finish(), } } } From 692364ed9f668e7859e876803d7bfb26a7cec206 Mon Sep 17 00:00:00 2001 From: "William K. Santiago" Date: Thu, 26 Feb 2026 10:52:13 -0500 Subject: [PATCH 2/5] Add DescriptorAcked event and track ack progress in UI --- keep-desktop/src/frost.rs | 36 ++++++++++++++++++++++-- keep-desktop/src/message.rs | 18 ++++++++++++ keep-desktop/src/screen/wallet.rs | 11 ++------ keep-frost-net/src/descriptor_session.rs | 8 ++++++ keep-frost-net/src/node/descriptor.rs | 27 ++++++++++++++---- keep-frost-net/src/node/mod.rs | 18 ++++++++++++ 6 files changed, 101 insertions(+), 17 deletions(-) diff --git a/keep-desktop/src/frost.rs b/keep-desktop/src/frost.rs index 04cb1f56..0832c5d0 100644 --- a/keep-desktop/src/frost.rs +++ b/keep-desktop/src/frost.rs @@ -517,6 +517,22 @@ pub(crate) async fn frost_event_listener( }, ); } + Ok(KfpNodeEvent::DescriptorAcked { + session_id, + share_index, + ack_count, + expected_acks, + }) => { + push_frost_event( + &frost_events, + FrostNodeMsg::DescriptorAcked { + session_id, + share_index, + ack_count, + expected_acks, + }, + ); + } Ok(KfpNodeEvent::DescriptorNacked { session_id, share_index, @@ -705,8 +721,11 @@ impl App { .map_err(|e| format!("{e}")) }, move |result| match result { - Ok(()) => Message::WalletDescriptorProgress( - DescriptorProgress::Finalizing, + Ok(expected_acks) => Message::WalletDescriptorProgress( + DescriptorProgress::WaitingAcks { + received: 0, + expected: expected_acks, + }, Some(session_id), ), Err(e) => Message::WalletDescriptorProgress( @@ -728,6 +747,19 @@ impl App { } }); } + FrostNodeMsg::DescriptorAcked { + session_id, + ack_count, + expected_acks, + .. + } => { + self.update_wallet_setup(&session_id, |setup| { + setup.phase = SetupPhase::Coordinating(DescriptorProgress::WaitingAcks { + received: ack_count, + expected: expected_acks, + }); + }); + } FrostNodeMsg::DescriptorComplete { session_id, external_descriptor, diff --git a/keep-desktop/src/message.rs b/keep-desktop/src/message.rs index 6ab34010..e418edd4 100644 --- a/keep-desktop/src/message.rs +++ b/keep-desktop/src/message.rs @@ -303,6 +303,12 @@ pub enum FrostNodeMsg { external_descriptor: String, internal_descriptor: String, }, + DescriptorAcked { + session_id: [u8; 32], + share_index: u16, + ack_count: usize, + expected_acks: usize, + }, DescriptorNacked { session_id: [u8; 32], share_index: u16, @@ -347,6 +353,18 @@ impl fmt::Debug for FrostNodeMsg { .field("external_descriptor", &"***") .field("internal_descriptor", &"***") .finish(), + Self::DescriptorAcked { + session_id, + share_index, + ack_count, + expected_acks, + } => f + .debug_struct("DescriptorAcked") + .field("session_id", &hex::encode(session_id)) + .field("share_index", share_index) + .field("ack_count", ack_count) + .field("expected_acks", expected_acks) + .finish(), Self::DescriptorNacked { session_id, share_index, diff --git a/keep-desktop/src/screen/wallet.rs b/keep-desktop/src/screen/wallet.rs index 5c74b27a..aab3811f 100644 --- a/keep-desktop/src/screen/wallet.rs +++ b/keep-desktop/src/screen/wallet.rs @@ -55,17 +55,10 @@ impl Default for TierConfig { #[derive(Debug, Clone)] pub enum DescriptorProgress { - WaitingContributions { - received: usize, - expected: usize, - }, + WaitingContributions { received: usize, expected: usize }, Contributed, Finalizing, - #[allow(dead_code)] - WaitingAcks { - received: usize, - expected: usize, - }, + WaitingAcks { received: usize, expected: usize }, Complete, Failed(String), } diff --git a/keep-frost-net/src/descriptor_session.rs b/keep-frost-net/src/descriptor_session.rs index 2655dea9..477c3b5a 100644 --- a/keep-frost-net/src/descriptor_session.rs +++ b/keep-frost-net/src/descriptor_session.rs @@ -285,6 +285,14 @@ impl DescriptorSession { self.expected_acks.iter().all(|idx| self.acks.contains(idx)) } + pub fn ack_count(&self) -> usize { + self.acks.len() + } + + pub fn expected_ack_count(&self) -> usize { + self.expected_acks.len() + } + pub fn descriptor(&self) -> Option<&FinalizedDescriptor> { self.descriptor.as_ref() } diff --git a/keep-frost-net/src/node/descriptor.rs b/keep-frost-net/src/node/descriptor.rs index 17d6504d..a808c565 100644 --- a/keep-frost-net/src/node/descriptor.rs +++ b/keep-frost-net/src/node/descriptor.rs @@ -789,7 +789,7 @@ impl KfpNode { .ok_or_else(|| FrostNetError::UntrustedPeer(sender.to_string()))? }; - let is_complete = { + let (is_complete, ack_count, expected_acks) = { let mut sessions = self.descriptor_sessions.write(); let session = sessions .get_session_mut(&payload.session_id) @@ -800,16 +800,29 @@ impl KfpNode { payload.descriptor_hash, &payload.key_proof_psbt, )?; - session.is_complete() + ( + session.is_complete(), + session.ack_count(), + session.expected_ack_count(), + ) }; info!( session_id = %hex::encode(payload.session_id), share_index, + ack_count, + expected_acks, complete = is_complete, "Received descriptor ACK" ); + let _ = self.event_tx.send(KfpNodeEvent::DescriptorAcked { + session_id: payload.session_id, + share_index, + ack_count, + expected_acks, + }); + if is_complete { let sessions = self.descriptor_sessions.read(); if let Some(session) = sessions.get_session(&payload.session_id) { @@ -827,25 +840,27 @@ impl KfpNode { Ok(()) } - pub async fn build_and_finalize_descriptor(&self, session_id: [u8; 32]) -> Result<()> { - let (external, internal, policy_hash) = { + pub async fn build_and_finalize_descriptor(&self, session_id: [u8; 32]) -> Result { + let (external, internal, policy_hash, expected_acks) = { let sessions = self.descriptor_sessions.read(); let session = sessions .get_session(&session_id) .ok_or_else(|| FrostNetError::Session("unknown descriptor session".into()))?; let policy_hash = derive_policy_hash(session.policy()); + let expected_acks = session.expected_ack_count(); let (external, internal) = reconstruct_descriptor( session.group_pubkey(), session.policy(), session.contributions(), session.network(), )?; - (external, internal, policy_hash) + (external, internal, policy_hash, expected_acks) }; self.finalize_descriptor(session_id, &external, &internal, policy_hash) - .await + .await?; + Ok(expected_acks) } pub fn cancel_descriptor_session(&self, session_id: &[u8; 32]) { diff --git a/keep-frost-net/src/node/mod.rs b/keep-frost-net/src/node/mod.rs index 5ac325d3..fc28b560 100644 --- a/keep-frost-net/src/node/mod.rs +++ b/keep-frost-net/src/node/mod.rs @@ -164,6 +164,12 @@ pub enum KfpNodeEvent { internal_descriptor: String, network: String, }, + DescriptorAcked { + session_id: [u8; 32], + share_index: u16, + ack_count: usize, + expected_acks: usize, + }, DescriptorNacked { session_id: [u8; 32], share_index: u16, @@ -242,6 +248,18 @@ impl std::fmt::Debug for KfpNodeEvent { .debug_struct("DescriptorComplete") .field("session_id", &hex::encode(session_id)) .finish(), + Self::DescriptorAcked { + session_id, + share_index, + ack_count, + expected_acks, + } => f + .debug_struct("DescriptorAcked") + .field("session_id", &hex::encode(session_id)) + .field("share_index", share_index) + .field("ack_count", ack_count) + .field("expected_acks", expected_acks) + .finish(), Self::DescriptorNacked { session_id, share_index, From 3ee637b972b6a8e7f1a281fcf20d1794a36d9551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 26 Feb 2026 12:04:57 -0500 Subject: [PATCH 3/5] fix: harden session matching, zeroize intermediate, and validate propose sender --- keep-desktop/src/frost.rs | 6 +++--- keep-frost-net/src/node/descriptor.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/keep-desktop/src/frost.rs b/keep-desktop/src/frost.rs index 0832c5d0..42835420 100644 --- a/keep-desktop/src/frost.rs +++ b/keep-desktop/src/frost.rs @@ -656,9 +656,7 @@ impl App { ) { if let Screen::Wallet(ws) = &mut self.screen { if let Some(setup) = &mut ws.setup { - let matches = - setup.session_id.as_ref() == Some(session_id) || setup.session_id.is_none(); - if matches { + if setup.session_id.as_ref() == Some(session_id) { f(setup); } } @@ -817,6 +815,7 @@ impl App { }; if !keep_frost_net::VALID_NETWORKS.contains(&network.as_str()) { + tracing::warn!(network = %network, "Ignoring descriptor contribution for invalid network"); return iced::Task::none(); } @@ -937,6 +936,7 @@ impl App { let _ = entry.response_tx.try_send(false); } } + self.active_coordinations.clear(); if let Some(s) = self.relay_screen_mut() { s.status = ConnectionStatus::Disconnected; s.peers.clear(); diff --git a/keep-frost-net/src/node/descriptor.rs b/keep-frost-net/src/node/descriptor.rs index a808c565..d91cc0e8 100644 --- a/keep-frost-net/src/node/descriptor.rs +++ b/keep-frost-net/src/node/descriptor.rs @@ -170,6 +170,7 @@ impl KfpNode { peer.share_index }; + self.verify_peer_share_index(sender, sender_share_index)?; self.check_proposer_authorized(sender_share_index)?; info!( @@ -900,9 +901,7 @@ impl KfpNode { return Ok(()); } if seen.len() >= 10_000 { - if let Some(&oldest) = seen.iter().min_by_key(|(_, ts, _)| *ts) { - seen.remove(&oldest); - } + seen.clear(); } seen.insert(dedup_key); } @@ -963,7 +962,8 @@ impl KfpNode { .key_package() .map_err(|e| FrostNetError::Crypto(format!("key package: {e}")))?; let signing_share = key_package.signing_share(); - let bytes = <[u8; 32]>::try_from(signing_share.serialize().as_slice()) + let serialized = Zeroizing::new(signing_share.serialize()); + let bytes = <[u8; 32]>::try_from(serialized.as_slice()) .map_err(|_| FrostNetError::Crypto("Invalid signing share length".into()))?; Ok(Zeroizing::new(bytes)) } From 1cfd5190090d9ed347a4e3daf732d57188d26ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 26 Feb 2026 12:16:04 -0500 Subject: [PATCH 4/5] Reject xpub announces when dedup set at capacity --- keep-frost-net/src/node/descriptor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keep-frost-net/src/node/descriptor.rs b/keep-frost-net/src/node/descriptor.rs index d91cc0e8..9e9d4d0f 100644 --- a/keep-frost-net/src/node/descriptor.rs +++ b/keep-frost-net/src/node/descriptor.rs @@ -901,7 +901,8 @@ impl KfpNode { return Ok(()); } if seen.len() >= 10_000 { - seen.clear(); + tracing::warn!("seen_xpub_announces at capacity, dropping announce"); + return Ok(()); } seen.insert(dedup_key); } From c8f16e4232942c8a914a34f556cad177b9e57e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 26 Feb 2026 12:23:02 -0500 Subject: [PATCH 5/5] Clear active_coordinations on reconnect, evict oldest xpub dedup entry at capacity --- keep-desktop/src/frost.rs | 1 + keep-frost-net/src/node/descriptor.rs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/keep-desktop/src/frost.rs b/keep-desktop/src/frost.rs index 42835420..36f63af4 100644 --- a/keep-desktop/src/frost.rs +++ b/keep-desktop/src/frost.rs @@ -968,6 +968,7 @@ impl App { } self.frost_peers.clear(); self.pending_sign_display.clear(); + self.active_coordinations.clear(); if let Ok(mut guard) = self.pending_sign_requests.lock() { for entry in guard.drain(..) { let _ = entry.response_tx.try_send(false); diff --git a/keep-frost-net/src/node/descriptor.rs b/keep-frost-net/src/node/descriptor.rs index 9e9d4d0f..03ef4c24 100644 --- a/keep-frost-net/src/node/descriptor.rs +++ b/keep-frost-net/src/node/descriptor.rs @@ -901,8 +901,10 @@ impl KfpNode { return Ok(()); } if seen.len() >= 10_000 { - tracing::warn!("seen_xpub_announces at capacity, dropping announce"); - return Ok(()); + tracing::warn!("seen_xpub_announces at capacity, evicting oldest entry"); + if let Some(&oldest) = seen.iter().min_by_key(|&(_, ts, _)| ts) { + seen.remove(&oldest); + } } seen.insert(dedup_key); }