From 0aa75af62634d90fa41cc865b745b052ea269e28 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Mon, 3 Mar 2025 17:30:53 +0800 Subject: [PATCH 01/13] impl vss manager --- mutiny-core/src/vss.rs | 59 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index b739c76cd..e2ff890ed 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -1,16 +1,22 @@ use crate::authclient::MutinyAuthClient; use crate::encrypt::{decrypt_with_key, encrypt_with_key}; +use crate::utils; +use crate::DEVICE_LOCK_INTERVAL_SECS; use crate::{error::MutinyError, logging::MutinyLogger}; use anyhow::anyhow; use bitcoin::secp256k1::{Secp256k1, SecretKey}; +use futures::lock::Mutex; use hex_conservative::DisplayHex; use lightning::util::logger::*; -use lightning::{log_error, log_info}; +use lightning::{log_error, log_info, log_warn}; use reqwest::{Method, Url}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use std::collections::HashMap; use std::sync::Arc; +const VSS_TIMEOUT_DURATION: u64 = DEVICE_LOCK_INTERVAL_SECS * 2 * 3; // 3x the device lock lifetime + pub struct MutinyVssClient { auth_client: Option>, client: Option, @@ -198,3 +204,54 @@ impl MutinyVssClient { Ok(result) } } + +#[derive(Debug)] +struct VssPendingWrite { + start_timestamp: u64, +} + +pub(crate) struct VssManager { + pending_writes: Arc>>, + logger: Arc, +} + +impl VssManager { + pub fn new(logger: Arc) -> Self { + Self { + pending_writes: Arc::new(Mutex::new(HashMap::new())), + logger, + } + } + + pub async fn start_write(&self, key: String, start_timestamp: u64) { + let mut pending_writes = self.pending_writes.lock().await; + pending_writes.insert(key, VssPendingWrite { start_timestamp }); + } + + pub async fn complete_write(&self, key: String) { + let mut pending_writes = self.pending_writes.lock().await; + pending_writes.remove(&key); + } + + pub async fn has_in_progress(&self) -> bool { + self.check_timeout().await; + let writes = self.pending_writes.lock().await; + !writes.is_empty() + } + + pub async fn check_timeout(&self) { + let current_time = utils::now().as_secs(); + let mut writes = self.pending_writes.lock().await; + writes.retain(|key, write| { + let valid = current_time - write.start_timestamp < VSS_TIMEOUT_DURATION; + if !valid { + log_warn!( + self.logger, + "VSS write timeout: {}. VSS Manager will ignoring this record.", + key + ); + } + valid + }); + } +} From 5e6ed92bd8a8e15bda0f83b81c08d82694fb3f00 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Mon, 3 Mar 2025 22:33:00 +0800 Subject: [PATCH 02/13] impl intercepting event SyncToVssStarting and SyncToVssCompleted. --- Cargo.lock | 1 + mutiny-core/Cargo.toml | 1 + mutiny-core/src/lib.rs | 10 ++++- mutiny-core/src/vss.rs | 87 +++++++++++++++++++++++++++++++----------- mutiny-wasm/src/lib.rs | 16 +++++++- 5 files changed, 89 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3eac9d89c..4b9629334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1506,6 +1506,7 @@ dependencies = [ "lightning-transaction-sync", "log", "mockall", + "once_cell", "pbkdf2", "reqwest", "serde", diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index c938f0e87..a151a1105 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -65,6 +65,7 @@ argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] } bincode = "1.3.3" hex-conservative = "0.1.1" async-lock = "3.2.0" +once_cell = "1.18.0" base64 = "0.13.0" pbkdf2 = "0.11" diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index aa00148b2..c679abeb3 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -50,8 +50,8 @@ use crate::nodemanager::NodeManager; use crate::nodemanager::{ChannelClosure, MutinyBip21RawMaterials}; pub use crate::onchain::BroadcastTx1InMultiOut; use crate::storage::get_invoice_by_hash; -use crate::utils::sleep; -use crate::utils::spawn; +use crate::utils::{sleep, spawn}; +use crate::vss::VSS_MANAGER; use crate::{authclient::MutinyAuthClient, logging::MutinyLogger}; use crate::{ event::{HTLCStatus, MillisatAmount, PaymentInfo}, @@ -914,6 +914,12 @@ impl MutinyWalletBuilder { log_error!(logger_clone, "Error setting device lock: {e}"); } + log_debug!( + logger_clone, + "Vss pending writes: {:?}", + VSS_MANAGER.get_pending_writes() + ); + let mut remained_sleep_ms = (DEVICE_LOCK_INTERVAL_SECS * 1000) as i32; while !stop_signal.stopping() && remained_sleep_ms > 0 { let sleep_ms = 300; diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index e2ff890ed..d3e39c04a 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -5,7 +5,6 @@ use crate::DEVICE_LOCK_INTERVAL_SECS; use crate::{error::MutinyError, logging::MutinyLogger}; use anyhow::anyhow; use bitcoin::secp256k1::{Secp256k1, SecretKey}; -use futures::lock::Mutex; use hex_conservative::DisplayHex; use lightning::util::logger::*; use lightning::{log_error, log_info, log_warn}; @@ -14,9 +13,13 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; use std::sync::Arc; +use utils::Mutex; const VSS_TIMEOUT_DURATION: u64 = DEVICE_LOCK_INTERVAL_SECS * 2 * 3; // 3x the device lock lifetime +pub static VSS_MANAGER: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(VssManager::default); + pub struct MutinyVssClient { auth_client: Option>, client: Option, @@ -205,51 +208,91 @@ impl MutinyVssClient { } } -#[derive(Debug)] -struct VssPendingWrite { +#[derive(Debug, Clone)] +pub struct VssPendingWrite { start_timestamp: u64, } -pub(crate) struct VssManager { - pending_writes: Arc>>, - logger: Arc, +pub struct VssManager { + pub pending_writes: Arc>>, + logger: Mutex>>, +} + +impl Default for VssManager { + fn default() -> Self { + Self::new() + } } impl VssManager { - pub fn new(logger: Arc) -> Self { + pub fn new() -> Self { Self { pending_writes: Arc::new(Mutex::new(HashMap::new())), - logger, + logger: Mutex::new(None), } } - pub async fn start_write(&self, key: String, start_timestamp: u64) { - let mut pending_writes = self.pending_writes.lock().await; + pub fn get_pending_writes(&self) -> Vec<(String, VssPendingWrite)> { + let writes = self.pending_writes.lock().expect( + " + Failed to lock pending writes", + ); + writes.iter().map(|(k, v)| (k.clone(), v.clone())).collect() + } + + pub fn set_logger(&self, logger: Arc) { + let mut guard = self.logger.lock().expect("Failed to lock logger"); + *guard = Some(logger); + } + + pub fn start_write(&self, key: String, start_timestamp: u64) { + let mut pending_writes = self + .pending_writes + .lock() + .expect("Failed to lock pending writes"); pending_writes.insert(key, VssPendingWrite { start_timestamp }); } - pub async fn complete_write(&self, key: String) { - let mut pending_writes = self.pending_writes.lock().await; + pub fn complete_write(&self, key: String) { + let mut pending_writes = self.pending_writes.lock().expect( + " + Failed to lock pending writes", + ); pending_writes.remove(&key); } - pub async fn has_in_progress(&self) -> bool { - self.check_timeout().await; - let writes = self.pending_writes.lock().await; + pub fn has_in_progress(&self) -> bool { + self.check_timeout(); + let writes = self.pending_writes.lock().expect( + " + Failed to lock pending writes", + ); !writes.is_empty() } - pub async fn check_timeout(&self) { + pub fn check_timeout(&self) { let current_time = utils::now().as_secs(); - let mut writes = self.pending_writes.lock().await; + let mut writes = self.pending_writes.lock().expect( + " + Failed to lock pending writes", + ); + let logger = { + let guard = self.logger.lock().expect( + " + Failed to lock logger", + ); + guard.clone() + }; writes.retain(|key, write| { let valid = current_time - write.start_timestamp < VSS_TIMEOUT_DURATION; if !valid { - log_warn!( - self.logger, - "VSS write timeout: {}. VSS Manager will ignoring this record.", - key - ); + if let Some(logger) = &logger { + log_warn!( + logger, + "VSS write timeout: {}. VSS Manager will ignoring this record.", + key + ); + } } valid }); diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index c6116b2e7..9e04b52ef 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -31,10 +31,10 @@ use mutiny_core::authclient::MutinyAuthClient; use mutiny_core::authmanager::AuthManager; use mutiny_core::encrypt::decrypt_with_password; use mutiny_core::error::MutinyError; -use mutiny_core::messagehandler::CommonLnEventCallback; +use mutiny_core::messagehandler::{CommonLnEvent, CommonLnEventCallback}; use mutiny_core::storage::{DeviceLock, MutinyStorage, DEVICE_LOCK_KEY}; use mutiny_core::utils::sleep; -use mutiny_core::vss::MutinyVssClient; +use mutiny_core::vss::{MutinyVssClient, VSS_MANAGER}; use mutiny_core::MutinyWalletBuilder; use mutiny_core::{ encrypt::{encrypt, encryption_key_from_pass}, @@ -119,6 +119,16 @@ impl MutinyWallet { let ln_event_callback = ln_event_topic.map(|topic| CommonLnEventCallback { callback: Arc::new(move |event| { + match &event { + CommonLnEvent::SyncToVssStarting { key, timestamp, .. } => { + VSS_MANAGER.start_write(key.clone(), *timestamp); + } + CommonLnEvent::SyncToVssCompleted { key, .. } => { + VSS_MANAGER.complete_write(key.clone()); + } + _ => {} + } + const KEY: &str = "common_ln_event_broadcast_channel"; let global = web_sys::js_sys::global(); let value = web_sys::js_sys::Reflect::get(&global, &(KEY.into())).unwrap(); @@ -211,6 +221,8 @@ impl MutinyWallet { let version = env!("CARGO_PKG_VERSION"); log_info!(logger, "Node version {version}"); + VSS_MANAGER.set_logger(logger.clone()); + let cipher = password .as_ref() .filter(|p| !p.is_empty()) From a35a21a451e0564c3f199600094a16c3a8771f4a Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 4 Mar 2025 20:31:11 +0800 Subject: [PATCH 03/13] impl lndchannel mod --- mutiny-core/src/authclient.rs | 3 +- mutiny-core/src/lsp/lndchannel.rs | 116 ++++++++++++++++++++++++++++++ mutiny-core/src/lsp/mod.rs | 1 + mutiny-core/src/node.rs | 11 +++ mutiny-core/src/nodemanager.rs | 1 + 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 mutiny-core/src/lsp/lndchannel.rs diff --git a/mutiny-core/src/authclient.rs b/mutiny-core/src/authclient.rs index 8b6ea27f2..0a7866e50 100644 --- a/mutiny-core/src/authclient.rs +++ b/mutiny-core/src/authclient.rs @@ -4,8 +4,7 @@ use bitcoin::hashes::{hex::prelude::*, sha256, Hash}; use bitcoin::key::rand::Rng; use bitcoin::secp256k1::rand::thread_rng; use jwt_compact::UntrustedToken; -use lightning::{log_debug, log_error, log_info}; -use lightning::{log_trace, util::logger::*}; +use lightning::{log_debug, log_error, log_info, log_trace, util::logger::*}; use reqwest::Client; use reqwest::{Method, StatusCode, Url}; use serde_json::Value; diff --git a/mutiny-core/src/lsp/lndchannel.rs b/mutiny-core/src/lsp/lndchannel.rs new file mode 100644 index 000000000..33b5482ff --- /dev/null +++ b/mutiny-core/src/lsp/lndchannel.rs @@ -0,0 +1,116 @@ +use crate::logging::MutinyLogger; +use crate::{error::MutinyError, utils}; + +use bitcoin::secp256k1::PublicKey; +use lightning::{log_error, util::logger::*}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +const CHANNELS: &str = "/api/v1/channels"; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ChannelConstraints { + pub csv_delay: u32, + pub chan_reserve_sat: u64, + pub dust_limit_sat: u64, + pub max_pending_amt_msat: u64, + pub min_htlc_msat: u64, + pub max_accepted_htlcs: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LndChannel { + pub active: bool, + pub remote_pubkey: String, + pub channel_point: String, + pub chan_id: String, + pub capacity: u64, + pub local_balance: u64, + pub remote_balance: u64, + pub commit_fee: u64, + pub commit_weight: u64, + pub fee_per_kw: u64, + pub total_satoshis_sent: u64, + pub total_satoshis_received: u64, + pub num_updates: u64, + pub csv_delay: u64, + pub private: bool, + pub initiator: bool, + pub chan_status_flags: String, + pub commitment_type: String, + pub lifetime: u64, + pub uptime: u64, + pub push_amount_sat: u64, + pub alias_scids: Vec, + pub peer_scid_alias: u64, + pub memo: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LndListChannelsResponse { + pub channels: Vec, +} + +async fn get_channels(_pubkey: PublicKey) -> Result, MutinyError> { + let mock_channel = LndChannel { + active: false, + remote_pubkey: "02cabb332ae4505a7326440cf43bd02d6a2917cfffa7d9fb175672e628286879e6" + .to_string(), + channel_point: "2823c1bba2b1c75636549ef12beaa4f8d2b590a3491e6f11ac2671dbffb911cb:0" + .to_string(), + chan_id: "4289150879585599488".to_string(), + capacity: 180875, + local_balance: 5051, + remote_balance: 174879, + commit_fee: 285, + commit_weight: 1116, + fee_per_kw: 253, + total_satoshis_sent: 15506, + total_satoshis_received: 2001, + num_updates: 20, + csv_delay: 288, + private: true, + initiator: true, + chan_status_flags: "ChanStatusDefault".to_string(), + commitment_type: "ANCHORS".to_string(), + lifetime: 564362, + uptime: 17475, + push_amount_sat: 161375, + alias_scids: vec![17592186044416000479], + peer_scid_alias: 1972756956701786116, + memo: "JoyID dual-funded channel".to_string(), + }; + + Ok(vec![mock_channel]) +} + +pub(crate) async fn fetch_lnd_channels( + http_client: &Client, + url: &str, + pubkey: &PublicKey, + logger: &MutinyLogger, +) -> Result, MutinyError> { + let full_url = format!("{}{}/{}", url.trim_end_matches('/'), CHANNELS, pubkey); + + let builder = http_client.get(&full_url); + let request = builder.build().map_err(|_| MutinyError::LspGenericError)?; + + let response = utils::fetch_with_timeout(http_client, request) + .await + .map_err(|e| { + log_error!(logger, "Error fetching channels info: {}", e); + MutinyError::LspGenericError + })?; + + if !response.status().is_success() { + log_error!(logger, "Non-success status code: {}", response.status()); + return Err(MutinyError::LspGenericError); + } + + let channels_response: LndListChannelsResponse = response.json().await.map_err(|e| { + log_error!(logger, "Error parsing channels JSON: {}", e); + MutinyError::LspGenericError + })?; + + Ok(channels_response.channels) +} diff --git a/mutiny-core/src/lsp/mod.rs b/mutiny-core/src/lsp/mod.rs index 48ad899ea..334c27ece 100644 --- a/mutiny-core/src/lsp/mod.rs +++ b/mutiny-core/src/lsp/mod.rs @@ -19,6 +19,7 @@ use serde_json::Value; use std::sync::{atomic::AtomicBool, Arc}; use voltage::LspClient; +pub mod lndchannel; pub mod lsps; pub mod voltage; diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index d964f06fb..8aee3a4f1 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -559,6 +559,17 @@ impl NodeBuilder { } None => (None, None, None), }; + if let Some(lsp_client) = lsp_client.as_ref() { + let lsp_connection_string = lsp_client.get_lsp_connection_string().await; + let lsp_pubkey = lsp_client.get_lsp_pubkey().await; + log_debug!( + logger, + "LSP client is VoltageFlow: {:?}", + !lsp_client.is_lsps() + ); + log_info!(logger, "LSP pubkey: {:?}", lsp_pubkey); + log_info!(logger, "LSP connection string: {:?}", lsp_connection_string); + } log_trace!(logger, "finished creating lsp client"); log_trace!(logger, "creating onion routers"); diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index dc2b5bec1..6746a9246 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -402,6 +402,7 @@ impl NodeManagerBuilder { }, ) }; + log_debug!(logger, "LSP config: {:?}", lsp_config); log_trace!(logger, "finished creating lsp config"); log_trace!(logger, "getting nodes from storage"); From ed06642cd59504c07a994b631469f0f31f2db07b Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Wed, 5 Mar 2025 09:52:53 +0800 Subject: [PATCH 04/13] persist active node id --- mutiny-core/src/ldkstorage.rs | 13 +++++++--- mutiny-core/src/lsp/lndchannel.rs | 41 +++---------------------------- mutiny-core/src/node.rs | 20 +++------------ mutiny-core/src/onchain.rs | 4 +-- mutiny-core/src/storage.rs | 1 + mutiny-core/src/vss.rs | 4 +++ mutiny-wasm/src/error.rs | 2 ++ mutiny-wasm/src/indexed_db.rs | 17 +++++++++++++ mutiny-wasm/src/lib.rs | 15 +++++++++-- 9 files changed, 56 insertions(+), 61 deletions(-) diff --git a/mutiny-core/src/ldkstorage.rs b/mutiny-core/src/ldkstorage.rs index 9d450fede..09b65a627 100644 --- a/mutiny-core/src/ldkstorage.rs +++ b/mutiny-core/src/ldkstorage.rs @@ -8,8 +8,7 @@ use crate::node::Router; use crate::node::{default_user_config, ChainMonitor}; use crate::nodemanager::ChannelClosure; use crate::storage::{IndexItem, MutinyStorage, VersionedValue}; -use crate::utils; -use crate::utils::{sleep, spawn}; +use crate::utils::{self, now, sleep, spawn}; use crate::{chain::MutinyChain, scorer::HubPreferentialScorer}; use anyhow::anyhow; use bitcoin::hashes::hex::FromHex; @@ -47,7 +46,7 @@ const CHANNEL_OPENING_PARAMS_PREFIX: &str = "chan_open_params/"; pub const CHANNEL_CLOSURE_PREFIX: &str = "channel_closure/"; pub const CHANNEL_CLOSURE_BUMP_PREFIX: &str = "channel_closure_bump/"; const FAILED_SPENDABLE_OUTPUT_DESCRIPTOR_KEY: &str = "failed_spendable_outputs"; -pub const BROADCAST_TX_1_IN_MULTI_OUT: &str = "broadcast_tx_1_in_multi_out/"; +pub const ACTIVE_NODE_ID: &str = "active_node_id"; pub(crate) type PhantomChannelManager = LdkChannelManager< Arc>, @@ -464,6 +463,14 @@ impl MutinyNodePersister { Ok(()) } + pub(crate) fn persist_node_id(&self, node_id: String) -> Result<(), MutinyError> { + self.storage.write_data( + ACTIVE_NODE_ID.to_string(), + node_id, + Some(now().as_secs() as u32), + ) + } + /// Persists the failed spendable outputs to storage. /// Previously failed spendable outputs are not overwritten. /// diff --git a/mutiny-core/src/lsp/lndchannel.rs b/mutiny-core/src/lsp/lndchannel.rs index 33b5482ff..b93610492 100644 --- a/mutiny-core/src/lsp/lndchannel.rs +++ b/mutiny-core/src/lsp/lndchannel.rs @@ -1,12 +1,11 @@ use crate::logging::MutinyLogger; use crate::{error::MutinyError, utils}; -use bitcoin::secp256k1::PublicKey; use lightning::{log_error, util::logger::*}; use reqwest::Client; use serde::{Deserialize, Serialize}; -const CHANNELS: &str = "/api/v1/channels"; +const CHANNELS: &str = "/api/v1/ln/channels"; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChannelConstraints { @@ -30,8 +29,7 @@ pub struct LndChannel { pub commit_fee: u64, pub commit_weight: u64, pub fee_per_kw: u64, - pub total_satoshis_sent: u64, - pub total_satoshis_received: u64, + #[serde(default)] pub num_updates: u64, pub csv_delay: u64, pub private: bool, @@ -51,43 +49,10 @@ pub struct LndListChannelsResponse { pub channels: Vec, } -async fn get_channels(_pubkey: PublicKey) -> Result, MutinyError> { - let mock_channel = LndChannel { - active: false, - remote_pubkey: "02cabb332ae4505a7326440cf43bd02d6a2917cfffa7d9fb175672e628286879e6" - .to_string(), - channel_point: "2823c1bba2b1c75636549ef12beaa4f8d2b590a3491e6f11ac2671dbffb911cb:0" - .to_string(), - chan_id: "4289150879585599488".to_string(), - capacity: 180875, - local_balance: 5051, - remote_balance: 174879, - commit_fee: 285, - commit_weight: 1116, - fee_per_kw: 253, - total_satoshis_sent: 15506, - total_satoshis_received: 2001, - num_updates: 20, - csv_delay: 288, - private: true, - initiator: true, - chan_status_flags: "ChanStatusDefault".to_string(), - commitment_type: "ANCHORS".to_string(), - lifetime: 564362, - uptime: 17475, - push_amount_sat: 161375, - alias_scids: vec![17592186044416000479], - peer_scid_alias: 1972756956701786116, - memo: "JoyID dual-funded channel".to_string(), - }; - - Ok(vec![mock_channel]) -} - pub(crate) async fn fetch_lnd_channels( http_client: &Client, url: &str, - pubkey: &PublicKey, + pubkey: &str, logger: &MutinyLogger, ) -> Result, MutinyError> { let full_url = format!("{}{}/{}", url.trim_end_matches('/'), CHANNELS, pubkey); diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index 8aee3a4f1..99d49cf75 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -559,17 +559,6 @@ impl NodeBuilder { } None => (None, None, None), }; - if let Some(lsp_client) = lsp_client.as_ref() { - let lsp_connection_string = lsp_client.get_lsp_connection_string().await; - let lsp_pubkey = lsp_client.get_lsp_pubkey().await; - log_debug!( - logger, - "LSP client is VoltageFlow: {:?}", - !lsp_client.is_lsps() - ); - log_info!(logger, "LSP pubkey: {:?}", lsp_pubkey); - log_info!(logger, "LSP connection string: {:?}", lsp_connection_string); - } log_trace!(logger, "finished creating lsp client"); log_trace!(logger, "creating onion routers"); @@ -910,11 +899,10 @@ impl NodeBuilder { log_trace!(logger, "finished spawning ldk reconnect thread"); } - log_info!( - logger, - "Node started: {}", - keys_manager.get_node_id(Recipient::Node).unwrap() - ); + let node_id = keys_manager.get_node_id(Recipient::Node).unwrap(); + let node_id = node_id.serialize().to_lower_hex_string(); + log_info!(logger, "Node started: {}", node_id); + persister.persist_node_id(node_id)?; let sync_lock = Arc::new(Mutex::new(())); diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index 1da0d0eea..00e008714 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -29,11 +29,11 @@ use serde::{Deserialize, Serialize}; use crate::error::MutinyError; use crate::fees::MutinyFeeEstimator; use crate::labels::*; -use crate::ldkstorage::BROADCAST_TX_1_IN_MULTI_OUT; use crate::logging::MutinyLogger; use crate::messagehandler::{CommonLnEvent, CommonLnEventCallback}; use crate::storage::{ - IndexItem, MutinyStorage, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY, ONCHAIN_PREFIX, + IndexItem, MutinyStorage, BROADCAST_TX_1_IN_MULTI_OUT, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY, + ONCHAIN_PREFIX, }; use crate::utils; use crate::utils::{now, sleep}; diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index d7b2b4138..58a1ad611 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -55,6 +55,7 @@ pub const LAST_DM_SYNC_TIME_KEY: &str = "last_dm_sync_time"; pub const LAST_HERMES_SYNC_TIME_KEY: &str = "last_hermes_sync_time"; pub const NOSTR_PROFILE_METADATA: &str = "nostr_profile_metadata"; pub const NOSTR_CONTACT_LIST: &str = "nostr_contact_list"; +pub const BROADCAST_TX_1_IN_MULTI_OUT: &str = "broadcast_tx_1_in_multi_out/"; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct DelayedKeyValueItem { diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index d3e39c04a..6ad9ad89d 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -206,6 +206,10 @@ impl MutinyVssClient { Ok(result) } + + pub fn get_store_id(&self) -> Option { + self.store_id.clone() + } } #[derive(Debug, Clone)] diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index 616e2961b..2eec03ff5 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -188,6 +188,8 @@ pub enum MutinyJsError { JwtAuthFailure, #[error("Failed to parse VSS value from getObject response.")] FailedParsingVssValue, + #[error("Cannot have more than one node.")] + TooManyNodes, /// Unknown error. #[error("Unknown Error")] UnknownError, diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index b0b020b70..eebf66aa2 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -675,6 +675,23 @@ impl IndexedDbStorage { } } } + } else if key.starts_with(ACTIVE_NODE_ID) { + match current.get_data::(&kv.key)? { + Some(node_id) => { + if node_id != kv.key { + let obj = vss.get_object(&kv.key).await?; + if serde_json::from_value::(obj.value.clone()).is_ok() { + return Ok(Some((kv.key, obj.value))); + } + } + } + None => { + let obj = vss.get_object(&kv.key).await?; + if serde_json::from_value::(obj.value.clone()).is_ok() { + return Ok(Some((kv.key, obj.value))); + } + } + } } } } diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 9e04b52ef..0de36becd 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -24,7 +24,7 @@ use bitcoin::{Address, Network, OutPoint, Txid}; use futures::lock::Mutex; use gloo_utils::format::JsValueSerdeExt; -use lightning::{log_info, log_warn, routing::gossip::NodeId, util::logger::Logger}; +use lightning::{log_debug, log_info, log_warn, routing::gossip::NodeId, util::logger::Logger}; use lightning_invoice::Bolt11Invoice; use mutiny_core::authclient::MutinyAuthClient; @@ -42,7 +42,7 @@ use mutiny_core::{ }; use mutiny_core::{ labels::LabelStorage, - nodemanager::{create_lsp_config, NodeManager}, + nodemanager::{create_lsp_config, NodeIndex, NodeManager}, }; use mutiny_core::{logging::MutinyLogger, lsp::LspConfig}; use web_sys::BroadcastChannel; @@ -329,6 +329,17 @@ impl MutinyWallet { ) .await?; + let nodes = storage.get_nodes()?; + let unarchived_nodes: Vec<(String, NodeIndex)> = nodes + .clone() + .nodes + .into_iter() + .filter(|(_, n)| !n.is_archived()) + .collect(); + if unarchived_nodes.len() > 1 { + return Err(MutinyJsError::TooManyNodes); + } + let mut config_builder = MutinyWalletConfigBuilder::new(xprivkey).with_network(network); if let Some(w) = websocket_proxy_addr { config_builder.with_websocket_proxy_addr(w); From 370bf51df5f68400d2a962b26e5f409258869428 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Wed, 5 Mar 2025 13:49:12 +0800 Subject: [PATCH 05/13] impl lnd snapshot --- mutiny-core/src/error.rs | 2 + mutiny-core/src/lib.rs | 91 +++++++++++++++++++++++++++---- mutiny-core/src/lsp/lndchannel.rs | 38 +++++++++++++ mutiny-core/src/storage.rs | 43 ++++++++++++++- mutiny-core/src/vss.rs | 6 +- mutiny-wasm/src/error.rs | 3 + mutiny-wasm/src/indexed_db.rs | 35 +++++++++++- mutiny-wasm/src/lib.rs | 23 +++++++- 8 files changed, 217 insertions(+), 24 deletions(-) diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index a0ce90a05..dfe93cad0 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -21,6 +21,8 @@ pub enum MutinyError { /// Returned when trying to start Mutiny while it is already running. #[error("Mutiny is already running.")] AlreadyRunning, + #[error("The stored LND snapshot is outdated.")] + LndSnapshotOutdated, /// Returned when trying to stop Mutiny while it is not running. #[error("Mutiny is not running.")] NotRunning, diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index c679abeb3..e6e64ce98 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -43,14 +43,16 @@ use crate::error::MutinyError; pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY}; pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{ - BROADCAST_TX_1_IN_MULTI_OUT, CHANNEL_CLOSURE_BUMP_PREFIX, CHANNEL_CLOSURE_PREFIX, - CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY, + ACTIVE_NODE_ID, CHANNEL_CLOSURE_BUMP_PREFIX, CHANNEL_CLOSURE_PREFIX, CHANNEL_MANAGER_KEY, + MONITORS_PREFIX_KEY, }; +use crate::lsp::lndchannel::fetch_lnd_channels_snapshot; +use crate::messagehandler::CommonLnEventCallback; use crate::nodemanager::NodeManager; use crate::nodemanager::{ChannelClosure, MutinyBip21RawMaterials}; pub use crate::onchain::BroadcastTx1InMultiOut; -use crate::storage::get_invoice_by_hash; -use crate::utils::{sleep, spawn}; +use crate::storage::{get_invoice_by_hash, LND_CHANNELS_SNAPSHOT_KEY}; +use crate::utils::{now, sleep, spawn, spawn_with_handle, StopHandle}; use crate::vss::VSS_MANAGER; use crate::{authclient::MutinyAuthClient, logging::MutinyLogger}; use crate::{ @@ -67,7 +69,7 @@ use crate::{ PAYMENT_INBOUND_PREFIX_KEY, PAYMENT_OUTBOUND_PREFIX_KEY, TRANSACTION_DETAILS_PREFIX_KEY, }, }; -use anyhow::Context; + use bdk_chain::ConfirmationTime; use bip39::Mnemonic; pub use bitcoin; @@ -77,6 +79,7 @@ use bitcoin::{bip32::Xpriv, Transaction}; use bitcoin::{hashes::sha256, Network, Txid}; use bitcoin::{hashes::Hash, Address}; +use anyhow::Context; use futures_util::lock::Mutex; use hex_conservative::{DisplayHex, FromHex}; use itertools::Itertools; @@ -87,10 +90,8 @@ use lightning::util::logger::Logger; use lightning::{log_debug, log_error, log_info, log_trace, log_warn}; pub use lightning_invoice; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; - -use messagehandler::CommonLnEventCallback; +use reqwest::Client; use serde::{Deserialize, Serialize}; -use utils::{spawn_with_handle, StopHandle}; use std::collections::HashMap; use std::collections::HashSet; @@ -99,7 +100,6 @@ use std::sync::Arc; use std::time::Duration; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; - #[cfg(target_arch = "wasm32")] use web_time::Instant; @@ -548,6 +548,7 @@ pub struct MutinyWalletConfigBuilder { skip_device_lock: bool, pub safe_mode: bool, skip_hodl_invoices: bool, + check_lnd_snapshot: bool, } impl MutinyWalletConfigBuilder { @@ -572,6 +573,7 @@ impl MutinyWalletConfigBuilder { skip_device_lock: false, safe_mode: false, skip_hodl_invoices: true, + check_lnd_snapshot: false, } } @@ -647,6 +649,10 @@ impl MutinyWalletConfigBuilder { self.skip_hodl_invoices = false; } + pub fn do_check_lnd_snapshot(&mut self) { + self.check_lnd_snapshot = true; + } + pub fn build(self) -> MutinyWalletConfig { let network = self.network.expect("network is required"); @@ -670,6 +676,7 @@ impl MutinyWalletConfigBuilder { skip_device_lock: self.skip_device_lock, safe_mode: self.safe_mode, skip_hodl_invoices: self.skip_hodl_invoices, + check_lnd_snapshot: false, } } } @@ -695,6 +702,7 @@ pub struct MutinyWalletConfig { skip_device_lock: bool, pub safe_mode: bool, skip_hodl_invoices: bool, + check_lnd_snapshot: bool, } pub struct MutinyWalletBuilder { @@ -817,7 +825,7 @@ impl MutinyWalletBuilder { let network = self .network .map_or_else(|| Err(MutinyError::InvalidArgumentsError), Ok)?; - let config = self.config.unwrap_or( + let config = self.config.clone().unwrap_or( MutinyWalletConfigBuilder::new(self.xprivkey) .with_network(network) .build(), @@ -844,12 +852,15 @@ impl MutinyWalletBuilder { // Need to prevent other devices from running at the same time log_debug!(logger, "checking device lock"); + let lsp_url = config.lsp_url.clone().expect("lsp_url is required"); if !config.skip_device_lock { let start = Instant::now(); if let Some(lock) = self.storage.get_device_lock()? { log_info!(logger, "Current device lock: {lock:?}"); } - self.storage.set_device_lock(&logger)?; + self.storage + .set_device_lock(&logger, &lsp_url, config.check_lnd_snapshot) + .await?; log_debug!( logger, "Device lock set: took {}ms", @@ -910,7 +921,63 @@ impl MutinyWalletBuilder { } break; } - if let Err(e) = storage_clone.set_device_lock(&logger_clone) { + + let config = self.config.as_ref().expect("config is required"); + if let Ok(Some(node_id)) = storage_clone.get_node_id() { + match fetch_lnd_channels_snapshot( + &Client::new(), + &lsp_url, + &node_id, + &logger_clone, + ) + .await + { + Ok(first_lnd_snapshot) => { + log_debug!( + logger_clone, + "First fetched lnd snapshot: {:?}", + first_lnd_snapshot + ); + if !VSS_MANAGER.has_in_progress() { + if let Ok(second_lnd_snapshot) = fetch_lnd_channels_snapshot( + &Client::new(), + &lsp_url, + &node_id, + &logger_clone, + ) + .await + { + log_debug!( + logger_clone, + "Second fetched lnd snapshot: {:?}", + second_lnd_snapshot + ); + if first_lnd_snapshot.snapshot == second_lnd_snapshot.snapshot { + log_debug!(logger_clone, "Saving lnd snapshot"); + if let Err(e) = storage_clone.write_data( + LND_CHANNELS_SNAPSHOT_KEY.to_string(), + &second_lnd_snapshot, + Some(now().as_secs() as u32), + ) { + log_error!( + logger_clone, + "Error saving lnd snapshot: {e}" + ); + } + } + } + } + } + Err(e) => { + log_error!(logger_clone, "Error fetching lnd channels: {e}"); + } + } + } + + if let Err(e) = storage_clone + .set_device_lock(&logger_clone, &lsp_url, config.check_lnd_snapshot) + .await + { log_error!(logger_clone, "Error setting device lock: {e}"); } diff --git a/mutiny-core/src/lsp/lndchannel.rs b/mutiny-core/src/lsp/lndchannel.rs index b93610492..6ce721451 100644 --- a/mutiny-core/src/lsp/lndchannel.rs +++ b/mutiny-core/src/lsp/lndchannel.rs @@ -1,10 +1,13 @@ use crate::logging::MutinyLogger; +use crate::utils::now; use crate::{error::MutinyError, utils}; use lightning::{log_error, util::logger::*}; use reqwest::Client; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + const CHANNELS: &str = "/api/v1/ln/channels"; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -79,3 +82,38 @@ pub(crate) async fn fetch_lnd_channels( Ok(channels_response.channels) } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LndChannelsSnapshot { + pub snapshot: BTreeMap, + pub timestamp: u32, +} + +impl LndChannelsSnapshot { + pub fn from_channels(channels: Vec) -> Self { + let snapshot = channels + .into_iter() + .map(|channel| (channel.chan_id, channel.num_updates)) + .collect::>(); + LndChannelsSnapshot { + snapshot, + timestamp: now().as_secs() as u32, + } + } +} + +impl From> for LndChannelsSnapshot { + fn from(channels: Vec) -> Self { + LndChannelsSnapshot::from_channels(channels) + } +} + +pub(crate) async fn fetch_lnd_channels_snapshot( + http_client: &Client, + url: &str, + pubkey: &str, + logger: &MutinyLogger, +) -> Result { + let channels = fetch_lnd_channels(http_client, url, pubkey, logger).await?; + Ok(channels.into()) +} diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 58a1ad611..81fcda374 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -1,12 +1,13 @@ use crate::ldkstorage::CHANNEL_MANAGER_KEY; use crate::logging::MutinyLogger; +use crate::lsp::lndchannel::{fetch_lnd_channels_snapshot, LndChannelsSnapshot}; use crate::messagehandler::{CommonLnEvent, CommonLnEventCallback}; use crate::nodemanager::{ChannelClosure, NodeStorage}; use crate::utils::{now, spawn, DBTasks, Task}; use crate::vss::{MutinyVssClient, VssKeyValueItem}; use crate::{ encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass, Cipher}, - DEVICE_LOCK_INTERVAL_SECS, + ACTIVE_NODE_ID, DEVICE_LOCK_INTERVAL_SECS, }; use crate::{ error::{MutinyError, MutinyStorageError}, @@ -23,7 +24,8 @@ use bitcoin::Txid; use futures_util::lock::Mutex; use hex_conservative::*; use lightning::{ln::PaymentHash, util::logger::Logger}; -use lightning::{log_debug, log_trace}; +use lightning::{log_debug, log_error, log_trace}; +use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::{BTreeSet, HashMap}; @@ -56,6 +58,7 @@ pub const LAST_HERMES_SYNC_TIME_KEY: &str = "last_hermes_sync_time"; pub const NOSTR_PROFILE_METADATA: &str = "nostr_profile_metadata"; pub const NOSTR_CONTACT_LIST: &str = "nostr_contact_list"; pub const BROADCAST_TX_1_IN_MULTI_OUT: &str = "broadcast_tx_1_in_multi_out/"; +pub const LND_CHANNELS_SNAPSHOT_KEY: &str = "lnd_channels_snapshot"; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct DelayedKeyValueItem { @@ -530,7 +533,20 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { self.get_data(DEVICE_LOCK_KEY) } - fn set_device_lock(&self, logger: &MutinyLogger) -> Result<(), MutinyError> { + fn get_node_id(&self) -> Result, MutinyError> { + self.get_data(ACTIVE_NODE_ID) + } + + fn get_lnd_channels_snapshot(&self) -> Result, MutinyError> { + self.get_data(LND_CHANNELS_SNAPSHOT_KEY) + } + + async fn set_device_lock( + &self, + logger: &MutinyLogger, + lsp_url: &str, + check_lnd_snapshot: bool, + ) -> Result<(), MutinyError> { let device = self.get_device_id()?; if let Some(lock) = self.get_device_lock()? { if lock.is_locked(&device) { @@ -538,6 +554,27 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { log_debug!(logger, "locked device is {}", lock.device); return Err(MutinyError::AlreadyRunning); } + + if check_lnd_snapshot && !lock.is_last_locker(&device) { + if let Ok(Some(node_id)) = self.get_node_id() { + match fetch_lnd_channels_snapshot(&Client::new(), lsp_url, &node_id, logger) + .await + { + Ok(lnd_channels_snapshot) => { + log_debug!(logger, "Fetched lnd channels: {:?}", lnd_channels_snapshot); + if let Some(local) = self.get_lnd_channels_snapshot()? { + if local.snapshot != lnd_channels_snapshot.snapshot { + return Err(MutinyError::LndSnapshotOutdated); + } + } + } + Err(e) => { + log_error!(logger, "Error fetching lnd channels: {e}"); + return Err(MutinyError::LspGenericError); + } + } + } + } } let time = now().as_secs() as u32; diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index 6ad9ad89d..de101a776 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -249,7 +249,7 @@ impl VssManager { *guard = Some(logger); } - pub fn start_write(&self, key: String, start_timestamp: u64) { + pub fn on_start_write(&self, key: String, start_timestamp: u64) { let mut pending_writes = self .pending_writes .lock() @@ -257,7 +257,7 @@ impl VssManager { pending_writes.insert(key, VssPendingWrite { start_timestamp }); } - pub fn complete_write(&self, key: String) { + pub fn on_complete_write(&self, key: String) { let mut pending_writes = self.pending_writes.lock().expect( " Failed to lock pending writes", @@ -274,7 +274,7 @@ impl VssManager { !writes.is_empty() } - pub fn check_timeout(&self) { + fn check_timeout(&self) { let current_time = utils::now().as_secs(); let mut writes = self.pending_writes.lock().expect( " diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index 2eec03ff5..fbb83555d 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -9,6 +9,8 @@ pub enum MutinyJsError { /// Returned when trying to start Mutiny while it is already running. #[error("Mutiny is already running.")] AlreadyRunning, + #[error("The stored LND snapshot is outdated.")] + LndSnapshotOutdated, /// Returned when trying to stop Mutiny while it is not running. #[error("Mutiny is not running.")] NotRunning, @@ -199,6 +201,7 @@ impl From for MutinyJsError { fn from(e: MutinyError) -> Self { match e { MutinyError::AlreadyRunning => MutinyJsError::AlreadyRunning, + MutinyError::LndSnapshotOutdated => MutinyJsError::LndSnapshotOutdated, MutinyError::NotRunning => MutinyJsError::NotRunning, MutinyError::NotFound => MutinyJsError::NotFound, MutinyError::FundingTxCreationFailed => MutinyJsError::FundingTxCreationFailed, diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index eebf66aa2..8170458dd 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -19,6 +19,7 @@ use mutiny_core::*; use mutiny_core::{ encrypt::Cipher, error::{MutinyError, MutinyStorageError}, + lsp::lndchannel::LndChannelsSnapshot, }; use nodemanager::ChannelClosure; use rexie::{ObjectStore, Rexie, TransactionMode}; @@ -678,16 +679,44 @@ impl IndexedDbStorage { } else if key.starts_with(ACTIVE_NODE_ID) { match current.get_data::(&kv.key)? { Some(node_id) => { - if node_id != kv.key { + let obj = vss.get_object(&kv.key).await?; + let node_id_from_vss = + serde_json::from_value::(obj.value.clone()) + .with_context(|| "deserialize node id from vss")?; + if node_id != node_id_from_vss { + log_error!( + logger, + "Node ID from VSS {} does not match current node ID {}", + node_id_from_vss, + node_id + ); + } + return Ok(None); + } + None => { + let obj = vss.get_object(&kv.key).await?; + if serde_json::from_value::(obj.value.clone()).is_ok() { + return Ok(Some((kv.key, obj.value))); + } + } + } + } else if key.starts_with(LND_CHANNELS_SNAPSHOT_KEY) { + match current.get_data::(&kv.key)? { + Some(snapshot) => { + if snapshot.timestamp < kv.version { let obj = vss.get_object(&kv.key).await?; - if serde_json::from_value::(obj.value.clone()).is_ok() { + if serde_json::from_value::(obj.value.clone()) + .is_ok() + { return Ok(Some((kv.key, obj.value))); } } } None => { let obj = vss.get_object(&kv.key).await?; - if serde_json::from_value::(obj.value.clone()).is_ok() { + if serde_json::from_value::(obj.value.clone()) + .is_ok() + { return Ok(Some((kv.key, obj.value))); } } diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 0de36becd..2b1a04f3e 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -24,7 +24,7 @@ use bitcoin::{Address, Network, OutPoint, Txid}; use futures::lock::Mutex; use gloo_utils::format::JsValueSerdeExt; -use lightning::{log_debug, log_info, log_warn, routing::gossip::NodeId, util::logger::Logger}; +use lightning::{log_info, log_warn, routing::gossip::NodeId, util::logger::Logger}; use lightning_invoice::Bolt11Invoice; use mutiny_core::authclient::MutinyAuthClient; @@ -106,6 +106,7 @@ impl MutinyWallet { blind_auth_url: Option, hermes_url: Option, ln_event_topic: Option, + check_lnd_snapshot: bool, ) -> Result { let start = instant::Instant::now(); @@ -121,10 +122,10 @@ impl MutinyWallet { callback: Arc::new(move |event| { match &event { CommonLnEvent::SyncToVssStarting { key, timestamp, .. } => { - VSS_MANAGER.start_write(key.clone(), *timestamp); + VSS_MANAGER.on_start_write(key.clone(), *timestamp); } CommonLnEvent::SyncToVssCompleted { key, .. } => { - VSS_MANAGER.complete_write(key.clone()); + VSS_MANAGER.on_complete_write(key.clone()); } _ => {} } @@ -169,6 +170,7 @@ impl MutinyWallet { blind_auth_url, hermes_url, ln_event_callback, + check_lnd_snapshot, ) .await { @@ -214,6 +216,7 @@ impl MutinyWallet { blind_auth_url: Option, hermes_url: Option, ln_event_callback: Option, + check_lnd_snapshot: bool, ) -> Result { let safe_mode = safe_mode.unwrap_or(false); let logger = Arc::new(MutinyLogger::memory_only()); @@ -390,6 +393,9 @@ impl MutinyWallet { if safe_mode { config_builder.with_safe_mode(); } + if check_lnd_snapshot { + config_builder.do_check_lnd_snapshot(); + } let config = config_builder.build(); let mut mw_builder = MutinyWalletBuilder::new(xprivkey, storage).with_config(config); @@ -1408,6 +1414,7 @@ mod tests { None, None, None, + false, ) .await .expect("mutiny wallet should initialize"); @@ -1452,6 +1459,7 @@ mod tests { None, None, None, + false, ) .await .expect("mutiny wallet should initialize"); @@ -1490,6 +1498,7 @@ mod tests { None, None, None, + false, ) .await; @@ -1541,6 +1550,7 @@ mod tests { None, None, None, + false, ) .await .expect("mutiny wallet should initialize"); @@ -1578,6 +1588,7 @@ mod tests { None, None, None, + false, ) .await; @@ -1632,6 +1643,7 @@ mod tests { None, None, None, + false, ) .await .unwrap(); @@ -1688,6 +1700,7 @@ mod tests { None, None, None, + false, ) .await .unwrap(); @@ -1731,6 +1744,7 @@ mod tests { None, None, None, + false, ) .await; @@ -1774,6 +1788,7 @@ mod tests { None, None, None, + false, ) .await .expect("mutiny wallet should initialize"); @@ -1847,6 +1862,7 @@ mod tests { None, None, None, + false, ) .await .expect("mutiny wallet should initialize"); @@ -1911,6 +1927,7 @@ mod tests { None, None, None, + false, ) .await .expect("mutiny wallet should initialize"); From 4bb4c11f46ca0b95f268da171d13754d2a8fd2ff Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Wed, 5 Mar 2025 13:56:57 +0800 Subject: [PATCH 06/13] check vss task when release device lock --- mutiny-core/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index e6e64ce98..8a8fa8ba5 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -916,6 +916,10 @@ impl MutinyWalletBuilder { loop { if stop_signal.stopping() { log_debug!(logger_clone, "stopping claim device lock"); + while VSS_MANAGER.has_in_progress() { + log_debug!(logger_clone, "waiting for VSS to finish"); + sleep(300).await; + } if let Err(e) = storage_clone.release_device_lock(&logger_clone) { log_error!(logger_clone, "Error releasing device lock: {e}"); } From 46eebe7307ce1dd66da9e5c249933ad5ef9f31e5 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Wed, 5 Mar 2025 14:06:23 +0800 Subject: [PATCH 07/13] bump version to v.14.0 --- Cargo.lock | 2 +- mutiny-wasm/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b9629334..dea816ecd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1525,7 +1525,7 @@ dependencies = [ [[package]] name = "mutiny-wasm" -version = "1.13.0" +version = "1.14.0" dependencies = [ "anyhow", "async-trait", diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index b6162c670..e723d87bd 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = ["per-package-target"] [package] name = "mutiny-wasm" -version = "1.13.0" +version = "1.14.0" edition = "2021" authors = ["utxostack"] forced-target = "wasm32-unknown-unknown" From bfaeae1f8da0489779ca0e978987dd2fe839ac89 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Wed, 5 Mar 2025 15:02:23 +0800 Subject: [PATCH 08/13] fix ci --- mutiny-core/src/lib.rs | 14 ++++++++------ mutiny-core/src/storage.rs | 13 +++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 8a8fa8ba5..7c561f63d 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -852,14 +852,14 @@ impl MutinyWalletBuilder { // Need to prevent other devices from running at the same time log_debug!(logger, "checking device lock"); - let lsp_url = config.lsp_url.clone().expect("lsp_url is required"); + let lsp_url = config.lsp_url.clone(); if !config.skip_device_lock { let start = Instant::now(); if let Some(lock) = self.storage.get_device_lock()? { log_info!(logger, "Current device lock: {lock:?}"); } self.storage - .set_device_lock(&logger, &lsp_url, config.check_lnd_snapshot) + .set_device_lock(&logger, lsp_url.clone(), config.check_lnd_snapshot) .await?; log_debug!( logger, @@ -927,10 +927,12 @@ impl MutinyWalletBuilder { } let config = self.config.as_ref().expect("config is required"); - if let Ok(Some(node_id)) = storage_clone.get_node_id() { + if let (Some(lsp_url), Ok(Some(node_id))) = + (config.lsp_url.as_ref(), storage_clone.get_node_id()) + { match fetch_lnd_channels_snapshot( &Client::new(), - &lsp_url, + lsp_url, &node_id, &logger_clone, ) @@ -945,7 +947,7 @@ impl MutinyWalletBuilder { if !VSS_MANAGER.has_in_progress() { if let Ok(second_lnd_snapshot) = fetch_lnd_channels_snapshot( &Client::new(), - &lsp_url, + lsp_url, &node_id, &logger_clone, ) @@ -979,7 +981,7 @@ impl MutinyWalletBuilder { } if let Err(e) = storage_clone - .set_device_lock(&logger_clone, &lsp_url, config.check_lnd_snapshot) + .set_device_lock(&logger_clone, lsp_url.clone(), config.check_lnd_snapshot) .await { log_error!(logger_clone, "Error setting device lock: {e}"); diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 81fcda374..0312f6c5f 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -544,7 +544,7 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { async fn set_device_lock( &self, logger: &MutinyLogger, - lsp_url: &str, + lsp_url: Option, check_lnd_snapshot: bool, ) -> Result<(), MutinyError> { let device = self.get_device_id()?; @@ -555,10 +555,15 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { return Err(MutinyError::AlreadyRunning); } - if check_lnd_snapshot && !lock.is_last_locker(&device) { + if check_lnd_snapshot && !lock.is_last_locker(&device) && lsp_url.is_some() { if let Ok(Some(node_id)) = self.get_node_id() { - match fetch_lnd_channels_snapshot(&Client::new(), lsp_url, &node_id, logger) - .await + match fetch_lnd_channels_snapshot( + &Client::new(), + &lsp_url.unwrap(), + &node_id, + logger, + ) + .await { Ok(lnd_channels_snapshot) => { log_debug!(logger, "Fetched lnd channels: {:?}", lnd_channels_snapshot); From 5dfcb2ca62b0480fdb047146d75b234f25a07bdb Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Wed, 5 Mar 2025 15:10:45 +0800 Subject: [PATCH 09/13] =?UTF-8?q?refactoring=EF=BC=9Arename=20const=20name?= =?UTF-8?q?(not=20value)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mutiny-core/src/ldkstorage.rs | 4 ++-- mutiny-core/src/lib.rs | 2 +- mutiny-core/src/onchain.rs | 6 +++--- mutiny-core/src/storage.rs | 6 +++--- mutiny-wasm/src/indexed_db.rs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mutiny-core/src/ldkstorage.rs b/mutiny-core/src/ldkstorage.rs index 09b65a627..6856bca19 100644 --- a/mutiny-core/src/ldkstorage.rs +++ b/mutiny-core/src/ldkstorage.rs @@ -46,7 +46,7 @@ const CHANNEL_OPENING_PARAMS_PREFIX: &str = "chan_open_params/"; pub const CHANNEL_CLOSURE_PREFIX: &str = "channel_closure/"; pub const CHANNEL_CLOSURE_BUMP_PREFIX: &str = "channel_closure_bump/"; const FAILED_SPENDABLE_OUTPUT_DESCRIPTOR_KEY: &str = "failed_spendable_outputs"; -pub const ACTIVE_NODE_ID: &str = "active_node_id"; +pub const ACTIVE_NODE_ID_KEY: &str = "active_node_id"; pub(crate) type PhantomChannelManager = LdkChannelManager< Arc>, @@ -465,7 +465,7 @@ impl MutinyNodePersister { pub(crate) fn persist_node_id(&self, node_id: String) -> Result<(), MutinyError> { self.storage.write_data( - ACTIVE_NODE_ID.to_string(), + ACTIVE_NODE_ID_KEY.to_string(), node_id, Some(now().as_secs() as u32), ) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 7c561f63d..8c86d0671 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -43,7 +43,7 @@ use crate::error::MutinyError; pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY}; pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{ - ACTIVE_NODE_ID, CHANNEL_CLOSURE_BUMP_PREFIX, CHANNEL_CLOSURE_PREFIX, CHANNEL_MANAGER_KEY, + ACTIVE_NODE_ID_KEY, CHANNEL_CLOSURE_BUMP_PREFIX, CHANNEL_CLOSURE_PREFIX, CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY, }; use crate::lsp::lndchannel::fetch_lnd_channels_snapshot; diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index 00e008714..4a53baea5 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -32,8 +32,8 @@ use crate::labels::*; use crate::logging::MutinyLogger; use crate::messagehandler::{CommonLnEvent, CommonLnEventCallback}; use crate::storage::{ - IndexItem, MutinyStorage, BROADCAST_TX_1_IN_MULTI_OUT, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY, - ONCHAIN_PREFIX, + IndexItem, MutinyStorage, BROADCAST_TX_1_IN_MULTI_OUT_PREFIX_KEY, KEYCHAIN_STORE_KEY, + NEED_FULL_SYNC_KEY, ONCHAIN_PREFIX, }; use crate::utils; use crate::utils::{now, sleep}; @@ -148,7 +148,7 @@ impl OnChainWallet { input: OutPoint, tx: BroadcastTx1InMultiOut, ) -> Result<(), MutinyError> { - let key = format!("{BROADCAST_TX_1_IN_MULTI_OUT}{}", input); + let key = format!("{BROADCAST_TX_1_IN_MULTI_OUT_PREFIX_KEY}{}", input); self.storage .write_data(key, tx.clone(), Some(tx.timestamp as u32))?; Ok(()) diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 0312f6c5f..4a6533312 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -7,7 +7,7 @@ use crate::utils::{now, spawn, DBTasks, Task}; use crate::vss::{MutinyVssClient, VssKeyValueItem}; use crate::{ encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass, Cipher}, - ACTIVE_NODE_ID, DEVICE_LOCK_INTERVAL_SECS, + ACTIVE_NODE_ID_KEY, DEVICE_LOCK_INTERVAL_SECS, }; use crate::{ error::{MutinyError, MutinyStorageError}, @@ -57,7 +57,7 @@ pub const LAST_DM_SYNC_TIME_KEY: &str = "last_dm_sync_time"; pub const LAST_HERMES_SYNC_TIME_KEY: &str = "last_hermes_sync_time"; pub const NOSTR_PROFILE_METADATA: &str = "nostr_profile_metadata"; pub const NOSTR_CONTACT_LIST: &str = "nostr_contact_list"; -pub const BROADCAST_TX_1_IN_MULTI_OUT: &str = "broadcast_tx_1_in_multi_out/"; +pub const BROADCAST_TX_1_IN_MULTI_OUT_PREFIX_KEY: &str = "broadcast_tx_1_in_multi_out/"; pub const LND_CHANNELS_SNAPSHOT_KEY: &str = "lnd_channels_snapshot"; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -534,7 +534,7 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { } fn get_node_id(&self) -> Result, MutinyError> { - self.get_data(ACTIVE_NODE_ID) + self.get_data(ACTIVE_NODE_ID_KEY) } fn get_lnd_channels_snapshot(&self) -> Result, MutinyError> { diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index 8170458dd..dd6736ba5 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -644,7 +644,7 @@ impl IndexedDbStorage { } } } - } else if key.starts_with(BROADCAST_TX_1_IN_MULTI_OUT) { + } else if key.starts_with(BROADCAST_TX_1_IN_MULTI_OUT_PREFIX_KEY) { match current.get_data::(&kv.key)? { Some(tx) => { if (tx.timestamp as u32) < kv.version { @@ -676,7 +676,7 @@ impl IndexedDbStorage { } } } - } else if key.starts_with(ACTIVE_NODE_ID) { + } else if key.starts_with(ACTIVE_NODE_ID_KEY) { match current.get_data::(&kv.key)? { Some(node_id) => { let obj = vss.get_object(&kv.key).await?; From 9cbe60d096e1f0aa5167721e56b9428b3b9236dc Mon Sep 17 00:00:00 2001 From: Chengxing Yuan Date: Wed, 5 Mar 2025 21:15:02 +0800 Subject: [PATCH 10/13] Update mutiny-core/src/storage.rs add comment Co-authored-by: Flouse <1297478+Flouse@users.noreply.github.com> --- mutiny-core/src/storage.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 4a6533312..ac775885e 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -568,6 +568,7 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { Ok(lnd_channels_snapshot) => { log_debug!(logger, "Fetched lnd channels: {:?}", lnd_channels_snapshot); if let Some(local) = self.get_lnd_channels_snapshot()? { + // After the initialization, local.snapshot == VSS.snapshot if local.snapshot != lnd_channels_snapshot.snapshot { return Err(MutinyError::LndSnapshotOutdated); } From b606a4d0a7cad9ba17dca23e800f4de0bce45d1c Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Thu, 6 Mar 2025 11:02:25 +0800 Subject: [PATCH 11/13] rename const CHANNELS to CHANNELS_LSP_URL_PATH --- mutiny-core/src/lsp/lndchannel.rs | 9 +++++++-- mutiny-core/src/storage.rs | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/mutiny-core/src/lsp/lndchannel.rs b/mutiny-core/src/lsp/lndchannel.rs index 6ce721451..52effeb77 100644 --- a/mutiny-core/src/lsp/lndchannel.rs +++ b/mutiny-core/src/lsp/lndchannel.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -const CHANNELS: &str = "/api/v1/ln/channels"; +const CHANNELS_LSP_URL_PATH: &str = "/api/v1/ln/channels"; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChannelConstraints { @@ -58,7 +58,12 @@ pub(crate) async fn fetch_lnd_channels( pubkey: &str, logger: &MutinyLogger, ) -> Result, MutinyError> { - let full_url = format!("{}{}/{}", url.trim_end_matches('/'), CHANNELS, pubkey); + let full_url = format!( + "{}{}/{}", + url.trim_end_matches('/'), + CHANNELS_LSP_URL_PATH, + pubkey + ); let builder = http_client.get(&full_url); let request = builder.build().map_err(|_| MutinyError::LspGenericError)?; diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index ac775885e..b2c1993e3 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -566,10 +566,16 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { .await { Ok(lnd_channels_snapshot) => { - log_debug!(logger, "Fetched lnd channels: {:?}", lnd_channels_snapshot); + log_debug!( + logger, + "New fetched lnd snapshot: {:?}", + lnd_channels_snapshot + ); if let Some(local) = self.get_lnd_channels_snapshot()? { - // After the initialization, local.snapshot == VSS.snapshot + log_debug!(logger, "Local lnd snapshot: {:?}", local); + // After the initialization, local.snapshot >= VSS.snapshot if local.snapshot != lnd_channels_snapshot.snapshot { + log_error!(logger, "Lnd snapshot outdated"); return Err(MutinyError::LndSnapshotOutdated); } } From aa554a6f4d6b9feee1e4d25a92e29f694f9f8f67 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Thu, 6 Mar 2025 11:13:25 +0800 Subject: [PATCH 12/13] fix MutinyWalletConfig value --- mutiny-core/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 8c86d0671..481853636 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -676,7 +676,7 @@ impl MutinyWalletConfigBuilder { skip_device_lock: self.skip_device_lock, safe_mode: self.safe_mode, skip_hodl_invoices: self.skip_hodl_invoices, - check_lnd_snapshot: false, + check_lnd_snapshot: self.check_lnd_snapshot, } } } From 965e2a1d0870a1285be9fbff24e053f00ad197af Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Thu, 6 Mar 2025 11:26:15 +0800 Subject: [PATCH 13/13] refactoring to: check_lnd_snapshot: Option --- mutiny-wasm/src/lib.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 2b1a04f3e..659af98fa 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -106,7 +106,7 @@ impl MutinyWallet { blind_auth_url: Option, hermes_url: Option, ln_event_topic: Option, - check_lnd_snapshot: bool, + check_lnd_snapshot: Option, ) -> Result { let start = instant::Instant::now(); @@ -216,7 +216,7 @@ impl MutinyWallet { blind_auth_url: Option, hermes_url: Option, ln_event_callback: Option, - check_lnd_snapshot: bool, + check_lnd_snapshot: Option, ) -> Result { let safe_mode = safe_mode.unwrap_or(false); let logger = Arc::new(MutinyLogger::memory_only()); @@ -393,7 +393,7 @@ impl MutinyWallet { if safe_mode { config_builder.with_safe_mode(); } - if check_lnd_snapshot { + if let Some(true) = check_lnd_snapshot { config_builder.do_check_lnd_snapshot(); } let config = config_builder.build(); @@ -1414,7 +1414,7 @@ mod tests { None, None, None, - false, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1459,7 +1459,7 @@ mod tests { None, None, None, - false, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1498,7 +1498,7 @@ mod tests { None, None, None, - false, + None, ) .await; @@ -1550,7 +1550,7 @@ mod tests { None, None, None, - false, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1588,7 +1588,7 @@ mod tests { None, None, None, - false, + None, ) .await; @@ -1643,7 +1643,7 @@ mod tests { None, None, None, - false, + None, ) .await .unwrap(); @@ -1700,7 +1700,7 @@ mod tests { None, None, None, - false, + None, ) .await .unwrap(); @@ -1744,7 +1744,7 @@ mod tests { None, None, None, - false, + None, ) .await; @@ -1788,7 +1788,7 @@ mod tests { None, None, None, - false, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1862,7 +1862,7 @@ mod tests { None, None, None, - false, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1927,7 +1927,7 @@ mod tests { None, None, None, - false, + None, ) .await .expect("mutiny wallet should initialize");