From 3ff50fff4ccf6236a5d0779c5e81fc42187d9bba Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 6 Jul 2023 15:54:44 -0500 Subject: [PATCH 01/10] VSS client --- mutiny-core/src/auth.rs | 6 +- mutiny-core/src/lib.rs | 1 + mutiny-core/src/vss.rs | 196 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 mutiny-core/src/vss.rs diff --git a/mutiny-core/src/auth.rs b/mutiny-core/src/auth.rs index 2a6ec0ea0..a3ff3b54b 100644 --- a/mutiny-core/src/auth.rs +++ b/mutiny-core/src/auth.rs @@ -75,7 +75,11 @@ impl MutinyAuthClient { self.retrieve_new_jwt().await?; self.authenticated_request(method, url, body).await } - _ => Ok(res), + StatusCode::OK | StatusCode::ACCEPTED | StatusCode::CREATED => Ok(res), + code => { + log_error!(self.logger, "Received unexpected status code: {code}"); + Err(MutinyError::ConnectionFailed) + } } } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 0cc5829c1..c4aebca4d 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -34,6 +34,7 @@ pub mod redshift; pub mod scb; pub mod storage; mod subscription; +pub mod vss; #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs new file mode 100644 index 000000000..fa3bbd9ab --- /dev/null +++ b/mutiny-core/src/vss.rs @@ -0,0 +1,196 @@ +#![allow(dead_code)] +use crate::auth::MutinyAuthClient; +use crate::{error::MutinyError, logging::MutinyLogger}; +use anyhow::anyhow; +use lightning::log_error; +use lightning::util::logger::*; +use reqwest::{Method, Url}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::sync::Arc; + +pub struct MutinyVssClient { + auth_client: Arc, + url: String, + logger: Arc, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct VssKeyValueItem { + pub key: String, + pub value: Option, + pub version: u32, +} + +impl MutinyVssClient { + pub(crate) fn new( + auth_client: Arc, + url: String, + logger: Arc, + ) -> Self { + Self { + auth_client, + url, + logger, + } + } + + pub async fn put_objects(&self, items: Vec) -> Result<(), MutinyError> { + let url = Url::parse(&format!("{}/putObjects", self.url)).map_err(|e| { + log_error!(self.logger, "Error parsing put objects url: {e}"); + MutinyError::Other(anyhow!("Error parsing put objects url: {e}")) + })?; + + // todo do we need global version here? + let body = json!({ "transaction_items": items }); + + self.auth_client + .request(Method::PUT, url, Some(body)) + .await?; + + Ok(()) + } + + pub async fn get_objects(&self, key: &str) -> Result { + let url = Url::parse(&format!("{}/getObject", self.url)).map_err(|e| { + log_error!(self.logger, "Error parsing get objects url: {e}"); + MutinyError::Other(anyhow!("Error parsing get objects url: {e}")) + })?; + + let body = json!({ "key": key }); + let result = self + .auth_client + .request(Method::POST, url, Some(body)) + .await? + .json() + .await + .map_err(|e| { + log_error!(self.logger, "Error parsing get objects response: {e}"); + MutinyError::Other(anyhow!("Error parsing get objects response: {e}")) + })?; + + Ok(result) + } + + pub async fn list_key_versions( + &self, + key_prefix: Option, + ) -> Result, MutinyError> { + let url = Url::parse(&format!("{}/listKeyVersions", self.url)).map_err(|e| { + log_error!(self.logger, "Error parsing list key versions url: {e}"); + MutinyError::Other(anyhow!("Error parsing list key versions url: {e}")) + })?; + + let body = json!({ "key_prefix": key_prefix }); + let result = self + .auth_client + .request(Method::POST, url, Some(body)) + .await? + .json() + .await + .map_err(|e| { + log_error!(self.logger, "Error parsing list key versions response: {e}"); + MutinyError::Other(anyhow!("Error parsing list key versions response: {e}")) + })?; + + Ok(result) + } +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use super::*; + use crate::auth::MutinyAuthClient; + use crate::logging::MutinyLogger; + use crate::test_utils::*; + use std::sync::Arc; + + async fn create_client() -> MutinyVssClient { + // Set up test auth client + let auth_manager = create_manager(); + let lnurl_client = Arc::new( + lnurl::Builder::default() + .build_async() + .expect("failed to make lnurl client"), + ); + let logger = Arc::new(MutinyLogger::default()); + let url = "https://auth-staging.mutinywallet.com"; + + let auth_client = + MutinyAuthClient::new(auth_manager, lnurl_client, logger.clone(), url.to_string()); + + // Test authenticate method + match auth_client.authenticate().await { + Ok(_) => assert!(auth_client.is_authenticated().is_some()), + Err(e) => panic!("Authentication failed with error: {:?}", e), + }; + + MutinyVssClient::new( + Arc::new(auth_client), + "https://storage-staging.mutinywallet.com".to_string(), + logger, + ) + } + + #[tokio::test] + async fn test_vss() { + let client = create_client().await; + + let key = "hello".to_string(); + let value = "world".to_string(); + let obj = VssKeyValueItem { + key: key.clone(), + value: Some(value.clone()), + version: 0, + }; + + client.put_objects(vec![obj.clone()]).await.unwrap(); + + let result = client.get_objects(&key).await.unwrap(); + assert_eq!(obj, result); + + let result = client.list_key_versions(None).await.unwrap(); + let key_version = VssKeyValueItem { + key, + value: None, + version: 0, + }; + + assert_eq!(vec![key_version], result); + assert_eq!(result.len(), 1); + } + + #[tokio::test] + async fn test_vss_versions() { + let client = create_client().await; + + let key = "hello".to_string(); + let value = "world1".to_string(); + let obj = VssKeyValueItem { + key: key.clone(), + value: Some(value.clone()), + version: 0, + }; + + client.put_objects(vec![obj.clone()]).await.unwrap(); + let result = client.get_objects(&key).await.unwrap(); + assert_eq!(obj.clone(), result); + + let value1 = "new world".to_string(); + let obj1 = VssKeyValueItem { + key: key.clone(), + value: Some(value1.clone()), + version: 1, + }; + + client.put_objects(vec![obj1.clone()]).await.unwrap(); + let result = client.get_objects(&key).await.unwrap(); + assert_eq!(obj1, result); + + // check we get version 1 + client.put_objects(vec![obj]).await.unwrap(); + let result = client.get_objects(&key).await.unwrap(); + assert_eq!(obj1, result); + } +} From ae11a3bbac43b42977e14102e446ceee14b236f8 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Fri, 7 Jul 2023 15:59:30 -0500 Subject: [PATCH 02/10] Add encryption to VSS --- mutiny-core/src/vss.rs | 101 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index fa3bbd9ab..f3bd995ec 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -1,36 +1,96 @@ #![allow(dead_code)] use crate::auth::MutinyAuthClient; use crate::{error::MutinyError, logging::MutinyLogger}; +use aes::cipher::block_padding::Pkcs7; +use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use aes::Aes256; use anyhow::anyhow; +use bitcoin::secp256k1; +use bitcoin::secp256k1::SecretKey; +use cbc::{Decryptor, Encryptor}; use lightning::log_error; use lightning::util::logger::*; use reqwest::{Method, Url}; use serde::{Deserialize, Serialize}; -use serde_json::json; +use serde_json::{json, Value}; use std::sync::Arc; +type Aes256CbcEnc = Encryptor; +type Aes256CbcDec = Decryptor; + pub struct MutinyVssClient { auth_client: Arc, url: String, + encryption_key: SecretKey, logger: Arc, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct VssKeyValueItem { pub key: String, - pub value: Option, + pub value: Option, + pub version: u32, +} + +impl VssKeyValueItem { + /// Encrypts the value of the item using the encryption key + /// and returns an encrypted version of the item + pub(crate) fn encrypt(self, encryption_key: &SecretKey) -> EncryptedVssKeyValueItem { + // should we handle this unwrap better? + let bytes = self.value.unwrap().to_string().into_bytes(); + let iv: [u8; 16] = secp256k1::rand::random(); + + let cipher = Aes256CbcEnc::new(&encryption_key.secret_bytes().into(), &iv.into()); + let mut encrypted: Vec = cipher.encrypt_padded_vec_mut::(&bytes); + encrypted.extend(iv); + let encrypted_value = base64::encode(encrypted); + + EncryptedVssKeyValueItem { + key: self.key, + value: encrypted_value, + version: self.version, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct EncryptedVssKeyValueItem { + pub key: String, + pub value: String, pub version: u32, } +impl EncryptedVssKeyValueItem { + pub(crate) fn decrypt(self, encryption_key: &SecretKey) -> VssKeyValueItem { + let bytes = base64::decode(self.value).unwrap(); + // split last 16 bytes off as iv + let iv = &bytes[bytes.len() - 16..]; + let bytes = &bytes[..bytes.len() - 16]; + + let cipher = Aes256CbcDec::new(&encryption_key.secret_bytes().into(), iv.into()); + let decrypted: Vec = cipher.decrypt_padded_vec_mut::(bytes).unwrap(); + let decrypted_value = String::from_utf8(decrypted).unwrap(); + let value = serde_json::from_str(&decrypted_value).unwrap(); + + VssKeyValueItem { + key: self.key, + value: Some(value), + version: self.version, + } + } +} + impl MutinyVssClient { pub(crate) fn new( auth_client: Arc, url: String, + encryption_key: SecretKey, logger: Arc, ) -> Self { Self { auth_client, url, + encryption_key, logger, } } @@ -41,6 +101,20 @@ impl MutinyVssClient { MutinyError::Other(anyhow!("Error parsing put objects url: {e}")) })?; + // check value is defined for all items + for item in &items { + if item.value.is_none() { + return Err(MutinyError::Other(anyhow!( + "Value must be defined for all items" + ))); + } + } + + let items = items + .into_iter() + .map(|item| item.encrypt(&self.encryption_key)) + .collect::>(); + // todo do we need global version here? let body = json!({ "transaction_items": items }); @@ -51,14 +125,14 @@ impl MutinyVssClient { Ok(()) } - pub async fn get_objects(&self, key: &str) -> Result { + pub async fn get_object(&self, key: &str) -> Result { let url = Url::parse(&format!("{}/getObject", self.url)).map_err(|e| { log_error!(self.logger, "Error parsing get objects url: {e}"); MutinyError::Other(anyhow!("Error parsing get objects url: {e}")) })?; let body = json!({ "key": key }); - let result = self + let result: EncryptedVssKeyValueItem = self .auth_client .request(Method::POST, url, Some(body)) .await? @@ -69,7 +143,7 @@ impl MutinyVssClient { MutinyError::Other(anyhow!("Error parsing get objects response: {e}")) })?; - Ok(result) + Ok(result.decrypt(&self.encryption_key)) } pub async fn list_key_versions( @@ -126,9 +200,12 @@ mod tests { Err(e) => panic!("Authentication failed with error: {:?}", e), }; + let encryption_key = SecretKey::from_slice(&[2; 32]).unwrap(); + MutinyVssClient::new( Arc::new(auth_client), "https://storage-staging.mutinywallet.com".to_string(), + encryption_key, logger, ) } @@ -138,7 +215,7 @@ mod tests { let client = create_client().await; let key = "hello".to_string(); - let value = "world".to_string(); + let value: Value = serde_json::from_str("\"world\"").unwrap(); let obj = VssKeyValueItem { key: key.clone(), value: Some(value.clone()), @@ -147,7 +224,7 @@ mod tests { client.put_objects(vec![obj.clone()]).await.unwrap(); - let result = client.get_objects(&key).await.unwrap(); + let result = client.get_object(&key).await.unwrap(); assert_eq!(obj, result); let result = client.list_key_versions(None).await.unwrap(); @@ -166,7 +243,7 @@ mod tests { let client = create_client().await; let key = "hello".to_string(); - let value = "world1".to_string(); + let value: Value = serde_json::from_str("\"world\"").unwrap(); let obj = VssKeyValueItem { key: key.clone(), value: Some(value.clone()), @@ -174,10 +251,10 @@ mod tests { }; client.put_objects(vec![obj.clone()]).await.unwrap(); - let result = client.get_objects(&key).await.unwrap(); + let result = client.get_object(&key).await.unwrap(); assert_eq!(obj.clone(), result); - let value1 = "new world".to_string(); + let value1: Value = serde_json::from_str("\"new world\"").unwrap(); let obj1 = VssKeyValueItem { key: key.clone(), value: Some(value1.clone()), @@ -185,12 +262,12 @@ mod tests { }; client.put_objects(vec![obj1.clone()]).await.unwrap(); - let result = client.get_objects(&key).await.unwrap(); + let result = client.get_object(&key).await.unwrap(); assert_eq!(obj1, result); // check we get version 1 client.put_objects(vec![obj]).await.unwrap(); - let result = client.get_objects(&key).await.unwrap(); + let result = client.get_object(&key).await.unwrap(); assert_eq!(obj1, result); } } From 93c6fbf2d61e3cb3d5306d79fde29b7b502eed46 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Sat, 8 Jul 2023 18:35:22 -0500 Subject: [PATCH 03/10] Integrate VSS into wallet --- mutiny-core/src/auth.rs | 4 +- mutiny-core/src/gossip.rs | 12 ++-- mutiny-core/src/keymanager.rs | 14 ++-- mutiny-core/src/labels.rs | 42 +++++------ mutiny-core/src/ldkstorage.rs | 38 +++++++--- mutiny-core/src/lib.rs | 91 ++++++++++++++---------- mutiny-core/src/logging.rs | 2 +- mutiny-core/src/node.rs | 6 +- mutiny-core/src/nodemanager.rs | 124 ++++++++++----------------------- mutiny-core/src/nostr/mod.rs | 15 ++-- mutiny-core/src/nostr/nwc.rs | 2 +- mutiny-core/src/onchain.rs | 9 ++- mutiny-core/src/redshift.rs | 2 +- mutiny-core/src/storage.rs | 44 ++++++++---- mutiny-core/src/vss.rs | 2 +- mutiny-wasm/Cargo.toml | 1 + mutiny-wasm/src/indexed_db.rs | 103 ++++++++++++++++++++++----- mutiny-wasm/src/lib.rs | 102 +++++++++++++++++++++------ 18 files changed, 376 insertions(+), 237 deletions(-) diff --git a/mutiny-core/src/auth.rs b/mutiny-core/src/auth.rs index a3ff3b54b..a4db26595 100644 --- a/mutiny-core/src/auth.rs +++ b/mutiny-core/src/auth.rs @@ -21,8 +21,8 @@ struct CustomClaims { sub: String, } -pub(crate) struct MutinyAuthClient { - auth: AuthManager, +pub struct MutinyAuthClient { + pub auth: AuthManager, lnurl_client: Arc, url: String, http_client: Client, diff --git a/mutiny-core/src/gossip.rs b/mutiny-core/src/gossip.rs index 4d9d9994f..dc8482a62 100644 --- a/mutiny-core/src/gossip.rs +++ b/mutiny-core/src/gossip.rs @@ -104,10 +104,10 @@ fn write_gossip_data( network_graph: &NetworkGraph, ) -> Result<(), MutinyError> { // Save the last sync timestamp - storage.set_data(GOSSIP_SYNC_TIME_KEY, last_sync_timestamp)?; + storage.set_data(GOSSIP_SYNC_TIME_KEY, last_sync_timestamp, None)?; // Save the network graph - storage.set_data(NETWORK_GRAPH_KEY, network_graph.encode().to_hex())?; + storage.set_data(NETWORK_GRAPH_KEY, network_graph.encode().to_hex(), None)?; Ok(()) } @@ -362,7 +362,7 @@ pub(crate) fn save_peer_connection_info( }, }; - storage.set_data(key, new_info)?; + storage.set_data(key, new_info, None)?; Ok(()) } @@ -388,7 +388,7 @@ pub(crate) fn set_peer_label( }, }; - storage.set_data(key, new_info)?; + storage.set_data(key, new_info, None)?; Ok(()) } @@ -406,7 +406,7 @@ pub(crate) fn delete_peer_info( if current.nodes.is_empty() { storage.delete(&[key])?; } else { - storage.set_data(key, current)?; + storage.set_data(key, current, None)?; } } @@ -426,7 +426,7 @@ pub(crate) fn save_ln_peer_info( // if the new info is different than the current info, we should to save it if !current.is_some_and(|c| c == new_info) { - storage.set_data(key, new_info)?; + storage.set_data(key, new_info, None)?; } Ok(()) diff --git a/mutiny-core/src/keymanager.rs b/mutiny-core/src/keymanager.rs index de5e6b40d..4ad555c2c 100644 --- a/mutiny-core/src/keymanager.rs +++ b/mutiny-core/src/keymanager.rs @@ -196,14 +196,12 @@ pub fn generate_seed(num_words: u8) -> Result { // key secret will be derived from `m/0'`. pub(crate) fn create_keys_manager( wallet: Arc>, - mnemonic: &Mnemonic, + xprivkey: ExtendedPrivKey, child_index: u32, logger: Arc, ) -> Result, MutinyError> { let context = Secp256k1::new(); - let seed = mnemonic.to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(wallet.network, &seed)?; let shared_key = xprivkey.derive_priv( &context, &DerivationPath::from(vec![ChildNumber::from_hardened_idx(0)?]), @@ -250,6 +248,7 @@ mod tests { use crate::onchain::OnChainWallet; use crate::storage::MemoryStorage; use bip39::Mnemonic; + use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; use esplora_client::Builder; use std::str::FromStr; @@ -277,10 +276,11 @@ mod tests { logger.clone(), )); let stop = Arc::new(AtomicBool::new(false)); + let xpriv = ExtendedPrivKey::new_master(Network::Testnet, &mnemonic.to_seed("")).unwrap(); let wallet = Arc::new( OnChainWallet::new( - &mnemonic, + xpriv, db, Network::Testnet, esplora, @@ -291,21 +291,21 @@ mod tests { .unwrap(), ); - let km = create_keys_manager(wallet.clone(), &mnemonic, 1, logger.clone()).unwrap(); + let km = create_keys_manager(wallet.clone(), xpriv, 1, logger.clone()).unwrap(); let pubkey = pubkey_from_keys_manager(&km); assert_eq!( "02cae09cf2c8842ace44068a5bf3117a494ebbf69a99e79712483c36f97cdb7b54", pubkey.to_string() ); - let km = create_keys_manager(wallet.clone(), &mnemonic, 2, logger.clone()).unwrap(); + let km = create_keys_manager(wallet.clone(), xpriv, 2, logger.clone()).unwrap(); let second_pubkey = pubkey_from_keys_manager(&km); assert_eq!( "03fcc9eaaf0b84946ea7935e3bc4f2b498893c2f53e5d2994d6877d149601ce553", second_pubkey.to_string() ); - let km = create_keys_manager(wallet, &mnemonic, 2, logger).unwrap(); + let km = create_keys_manager(wallet, xpriv, 2, logger).unwrap(); let second_pubkey_again = pubkey_from_keys_manager(&km); assert_eq!(second_pubkey, second_pubkey_again); diff --git a/mutiny-core/src/labels.rs b/mutiny-core/src/labels.rs index f8816c601..bc668445e 100644 --- a/mutiny-core/src/labels.rs +++ b/mutiny-core/src/labels.rs @@ -121,7 +121,7 @@ impl LabelStorage for S { // update the labels map let mut address_labels = self.get_address_labels()?; address_labels.insert(address.to_string(), labels.clone()); - self.set_data(ADDRESS_LABELS_MAP_KEY, address_labels)?; + self.set_data(ADDRESS_LABELS_MAP_KEY, address_labels, None)?; // update the label items let now = crate::utils::now().as_secs(); @@ -138,7 +138,7 @@ impl LabelStorage for S { // Update the last used timestamp label_item.last_used_time = now; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; } None => { // Create a new label item @@ -147,7 +147,7 @@ impl LabelStorage for S { invoices: vec![], last_used_time: now, }; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; } } } @@ -159,7 +159,7 @@ impl LabelStorage for S { // update the labels map let mut invoice_labels = self.get_invoice_labels()?; invoice_labels.insert(invoice.clone(), labels.clone()); - self.set_data(INVOICE_LABELS_MAP_KEY, invoice_labels)?; + self.set_data(INVOICE_LABELS_MAP_KEY, invoice_labels, None)?; // update the label items let now = crate::utils::now().as_secs(); @@ -176,7 +176,7 @@ impl LabelStorage for S { // Update the last used timestamp label_item.last_used_time = now; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; } None => { // Create a new label item @@ -185,7 +185,7 @@ impl LabelStorage for S { invoices: vec![invoice.to_string()], last_used_time: now, }; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; } } } @@ -224,7 +224,7 @@ impl LabelStorage for S { // convert label into a uuid for uniqueness let id = Uuid::new_v4().to_string(); // create label item - self.set_data(get_label_item_key(&id), current)?; + self.set_data(get_label_item_key(&id), current, None)?; // replace label in address_labels with new uuid let addr_labels = self.get_address_labels()?; @@ -241,7 +241,7 @@ impl LabelStorage for S { updated.insert(addr, new_labels); } } - self.set_data(ADDRESS_LABELS_MAP_KEY, updated)?; + self.set_data(ADDRESS_LABELS_MAP_KEY, updated, None)?; // replace label in invoice_labels with new uuid let invoice_labels = self.get_invoice_labels()?; @@ -258,11 +258,11 @@ impl LabelStorage for S { updated.insert(inv, new_labels); } } - self.set_data(INVOICE_LABELS_MAP_KEY, updated)?; + self.set_data(INVOICE_LABELS_MAP_KEY, updated, None)?; // create the contact let key = get_contact_key(&id); - self.set_data(key, contact)?; + self.set_data(key, contact, None)?; // delete old label item self.delete(&[get_label_item_key(&label)])?; @@ -276,14 +276,14 @@ impl LabelStorage for S { // generate a uuid, this will be the "label" that we use to store the contact let id = Uuid::new_v4().to_string(); let key = get_contact_key(&id); - self.set_data(key, contact)?; + self.set_data(key, contact, None)?; let key = get_label_item_key(&id); let label_item = LabelItem { last_used_time: crate::utils::now().as_secs(), ..Default::default() }; - self.set_data(key, label_item)?; + self.set_data(key, label_item, None)?; Ok(id) } @@ -291,13 +291,13 @@ impl LabelStorage for S { let contact = self.get_contact(&id)?; if let Some(mut contact) = contact { contact.archived = Some(true); - self.set(get_contact_key(&id), contact)?; + self.set(get_contact_key(&id), contact, None)?; } Ok(()) } fn edit_contact(&self, id: impl AsRef, contact: Contact) -> Result<(), MutinyError> { - self.set(get_contact_key(&id), contact) + self.set(get_contact_key(&id), contact, None) } fn get_tag_items(&self) -> Result, MutinyError> { @@ -509,7 +509,7 @@ mod tests { let storage = MemoryStorage::default(); let labels_map = create_test_address_labels_map(); storage - .set_data(ADDRESS_LABELS_MAP_KEY, labels_map.clone()) + .set_data(ADDRESS_LABELS_MAP_KEY, labels_map.clone(), None) .unwrap(); let result = storage.get_address_labels(); @@ -524,7 +524,7 @@ mod tests { let storage = MemoryStorage::default(); let labels_map = create_test_invoice_labels_map(); storage - .set_data(INVOICE_LABELS_MAP_KEY, labels_map.clone()) + .set_data(INVOICE_LABELS_MAP_KEY, labels_map.clone(), None) .unwrap(); let result = storage.get_invoice_labels(); @@ -541,7 +541,7 @@ mod tests { let labels = create_test_labels(); for (label, label_item) in labels.clone() { storage - .set_data(get_label_item_key(label), label_item) + .set_data(get_label_item_key(label), label_item, None) .unwrap(); } @@ -566,7 +566,7 @@ mod tests { let labels = create_test_labels(); for (label, label_item) in labels.clone() { storage - .set_data(get_label_item_key(label), label_item) + .set_data(get_label_item_key(label), label_item, None) .unwrap(); } @@ -618,7 +618,9 @@ mod tests { let contacts = create_test_contacts(); for (id, contact) in contacts.clone() { - storage.set_data(get_contact_key(id), contact).unwrap(); + storage + .set_data(get_contact_key(id), contact, None) + .unwrap(); } let result = storage.get_contacts().unwrap(); @@ -796,7 +798,7 @@ mod tests { let labels = create_test_labels(); for (label, label_item) in labels { storage - .set_data(get_label_item_key(label.clone()), label_item.clone()) + .set_data(get_label_item_key(label.clone()), label_item.clone(), None) .unwrap(); expected_tag_items.push(TagItem::Label((label, label_item))); } diff --git a/mutiny-core/src/ldkstorage.rs b/mutiny-core/src/ldkstorage.rs index 5aaf6b933..afd7d9b45 100644 --- a/mutiny-core/src/ldkstorage.rs +++ b/mutiny-core/src/ldkstorage.rs @@ -87,10 +87,11 @@ impl MutinyNodePersister { &self, key: &str, object: &W, + version: Option, ) -> Result<(), lightning::io::Error> { let key_with_node = self.get_key(key); self.storage - .set_data(key_with_node, object.encode()) + .set_data(key_with_node, object.encode(), version) .map_err(|e| { match e { MutinyError::PersistenceFailed { source } => { @@ -273,7 +274,7 @@ impl MutinyNodePersister { ) -> io::Result<()> { let key = self.get_key(payment_key(inbound, payment_hash).as_str()); self.storage - .set_data(key, payment_info) + .set_data(key, payment_info, None) .map_err(io::Error::other) } @@ -322,7 +323,7 @@ impl MutinyNodePersister { "{CHANNEL_CLOSURE_PREFIX}{}", user_channel_id.to_be_bytes().to_hex() )); - self.storage.set_data(key, closure)?; + self.storage.set_data(key, closure, None)?; Ok(()) } @@ -381,7 +382,7 @@ impl MutinyNodePersister { // add the new descriptors descriptors.extend(failed_hex); - self.storage.set_data(key, descriptors)?; + self.storage.set_data(key, descriptors, None)?; Ok(()) } @@ -422,7 +423,7 @@ impl MutinyNodePersister { params: ChannelOpenParams, ) -> Result<(), MutinyError> { let key = self.get_key(&channel_open_params_key(id)); - self.storage.set_data(key, params) + self.storage.set_data(key, params, None) } pub(crate) fn get_channel_open_params( @@ -516,12 +517,13 @@ impl &self, channel_manager: &PhantomChannelManager, ) -> Result<(), lightning::io::Error> { - self.persist_local_storage(CHANNEL_MANAGER_KEY, channel_manager) + // fixme add version + self.persist_local_storage(CHANNEL_MANAGER_KEY, channel_manager, None) } fn persist_graph(&self, network_graph: &NetworkGraph) -> Result<(), lightning::io::Error> { self.storage - .set_data(NETWORK_GRAPH_KEY, network_graph.encode().to_hex()) + .set_data(NETWORK_GRAPH_KEY, network_graph.encode().to_hex(), None) .map_err(|_| lightning::io::ErrorKind::Other.into()) } @@ -531,7 +533,7 @@ impl ) -> Result<(), lightning::io::Error> { let scorer_str = scorer.encode().to_hex(); self.storage - .set_data(PROB_SCORER_KEY, scorer_str) + .set_data(PROB_SCORER_KEY, scorer_str, None) .map_err(|_| lightning::io::ErrorKind::Other.into()) } } @@ -550,7 +552,15 @@ impl Persist= u32::MAX as u64 { + u32::MAX + } else { + update_id as u32 + }; + + match self.persist_local_storage(&key, monitor, Some(version)) { Ok(()) => chain::ChannelMonitorUpdateStatus::Completed, Err(_) => chain::ChannelMonitorUpdateStatus::PermanentFailure, } @@ -568,7 +578,15 @@ impl Persist= u32::MAX as u64 { + u32::MAX + } else { + update_id as u32 + }; + + match self.persist_local_storage(&key, monitor, Some(version)) { Ok(()) => chain::ChannelMonitorUpdateStatus::Completed, Err(_) => chain::ChannelMonitorUpdateStatus::PermanentFailure, } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index c4aebca4d..ff802687a 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -10,7 +10,7 @@ // background file is mostly an LDK copy paste mod background; -mod auth; +pub mod auth; mod chain; pub mod encrypt; pub mod error; @@ -21,7 +21,7 @@ mod gossip; mod keymanager; pub mod labels; mod ldkstorage; -mod lnurlauth; +pub mod lnurlauth; pub mod logging; mod lspclient; mod networking; @@ -44,6 +44,7 @@ pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY}; +use crate::auth::MutinyAuthClient; use crate::storage::MutinyStorage; use crate::{error::MutinyError, nostr::ReservedProfile}; use crate::{nodemanager::NodeManager, nostr::ProfileType}; @@ -63,14 +64,14 @@ use std::sync::Arc; #[derive(Clone)] pub struct MutinyWalletConfig { - mnemonic: Option, + xprivkey: ExtendedPrivKey, #[cfg(target_arch = "wasm32")] websocket_proxy_addr: Option, - network: Option, + network: Network, user_esplora_url: Option, user_rgs_url: Option, lsp_url: Option, - auth_url: Option, + auth_client: Option>, subscription_url: Option, do_not_connect_peers: bool, } @@ -78,24 +79,24 @@ pub struct MutinyWalletConfig { impl MutinyWalletConfig { #[allow(clippy::too_many_arguments)] pub fn new( - mnemonic: Option, + xprivkey: ExtendedPrivKey, #[cfg(target_arch = "wasm32")] websocket_proxy_addr: Option, - network: Option, + network: Network, user_esplora_url: Option, user_rgs_url: Option, lsp_url: Option, - auth_url: Option, + auth_client: Option>, subscription_url: Option, ) -> Self { Self { - mnemonic, + xprivkey, #[cfg(target_arch = "wasm32")] websocket_proxy_addr, network, user_esplora_url, user_rgs_url, lsp_url, - auth_url, + auth_client, subscription_url, do_not_connect_peers: false, } @@ -136,9 +137,10 @@ impl MutinyWallet { NodeManager::start_sync(node_manager.clone()); // create nostr manager - let seed = node_manager.show_seed().to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(node_manager.get_network(), &seed)?; - let nostr = Arc::new(NostrManager::from_mnemonic(xprivkey, storage.clone())?); + let nostr = Arc::new(NostrManager::from_mnemonic( + node_manager.xprivkey, + storage.clone(), + )?); let mw = Self { config, @@ -373,14 +375,15 @@ impl MutinyWallet { #[cfg(test)] mod tests { use crate::{ - encrypt::encryption_key_from_pass, nodemanager::NodeManager, MutinyWallet, + encrypt::encryption_key_from_pass, generate_seed, nodemanager::NodeManager, MutinyWallet, MutinyWalletConfig, }; + use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; use crate::test_utils::*; - use crate::storage::MemoryStorage; + use crate::storage::{MemoryStorage, MutinyStorage}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; wasm_bindgen_test_configure!(run_in_browser); @@ -390,24 +393,28 @@ mod tests { let test_name = "create_mutiny_wallet"; log!("{}", test_name); + let mnemonic = generate_seed(12).unwrap(); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &mnemonic.to_seed("")).unwrap(); + let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher)); assert!(!NodeManager::has_node_manager(storage.clone())); let config = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, None, None, ); - MutinyWallet::new(storage.clone(), config) + let mw = MutinyWallet::new(storage.clone(), config) .await .expect("mutiny wallet should initialize"); + mw.storage.insert_mnemonic(mnemonic).unwrap(); assert!(NodeManager::has_node_manager(storage)); } @@ -415,16 +422,17 @@ mod tests { async fn restart_mutiny_wallet() { let test_name = "restart_mutiny_wallet"; log!("{}", test_name); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &[0; 32]).unwrap(); let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher)); assert!(!NodeManager::has_node_manager(storage.clone())); let config = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -434,13 +442,12 @@ mod tests { let mut mw = MutinyWallet::new(storage.clone(), config) .await .expect("mutiny wallet should initialize"); - assert!(NodeManager::has_node_manager(storage)); - let first_seed = mw.node_manager.show_seed(); + let first_seed = mw.node_manager.xprivkey; assert!(mw.stop().await.is_ok()); assert!(mw.start().await.is_ok()); - assert_eq!(first_seed, mw.node_manager.show_seed()); + assert_eq!(first_seed, mw.node_manager.xprivkey); } #[test] @@ -448,16 +455,18 @@ mod tests { let test_name = "restart_mutiny_wallet_with_nodes"; log!("{}", test_name); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &[0; 32]).unwrap(); + let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher)); assert!(!NodeManager::has_node_manager(storage.clone())); let config = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -467,7 +476,6 @@ mod tests { let mut mw = MutinyWallet::new(storage.clone(), config) .await .expect("mutiny wallet should initialize"); - assert!(NodeManager::has_node_manager(storage)); assert_eq!(mw.node_manager.list_nodes().await.unwrap().len(), 1); assert!(mw.node_manager.new_node().await.is_ok()); @@ -482,16 +490,18 @@ mod tests { async fn restore_mutiny_mnemonic() { let test_name = "restore_mutiny_mnemonic"; log!("{}", test_name); + let mnemonic = generate_seed(12).unwrap(); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &mnemonic.to_seed("")).unwrap(); let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher)); assert!(!NodeManager::has_node_manager(storage.clone())); let config = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -501,19 +511,20 @@ mod tests { let mw = MutinyWallet::new(storage.clone(), config) .await .expect("mutiny wallet should initialize"); - let seed = mw.node_manager.show_seed(); - assert_ne!(seed.to_string(), ""); + let seed = mw.node_manager.xprivkey; + assert!(!seed.private_key.is_empty()); // create a second mw and make sure it has a different seed let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage2 = MemoryStorage::new(Some(pass), Some(cipher)); assert!(!NodeManager::has_node_manager(storage2.clone())); - let config2 = MutinyWalletConfig::new( - None, + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &[0; 32]).unwrap(); + let mut config2 = MutinyWalletConfig::new( + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -523,8 +534,8 @@ mod tests { let mw2 = MutinyWallet::new(storage2.clone(), config2.clone()) .await .expect("mutiny wallet should initialize"); - let seed2 = mw2.node_manager.show_seed(); - assert_ne!(seed.to_string(), seed2.to_string()); + let seed2 = mw2.node_manager.xprivkey; + assert_ne!(seed, seed2); // now restore the first seed into the 2nd mutiny node mw2.stop().await.expect("should stop"); @@ -533,14 +544,18 @@ mod tests { let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); let storage3 = MemoryStorage::new(Some(pass), Some(cipher)); - MutinyWallet::restore_mnemonic(storage3.clone(), seed.clone()) + MutinyWallet::restore_mnemonic(storage3.clone(), mnemonic.clone()) .await .expect("mutiny wallet should restore"); + config2.xprivkey = { + let seed = storage3.get_mnemonic().unwrap().unwrap(); + ExtendedPrivKey::new_master(Network::Regtest, &seed.to_seed("")).unwrap() + }; let mw2 = MutinyWallet::new(storage3, config2) .await .expect("mutiny wallet should initialize"); - let restored_seed = mw2.node_manager.show_seed(); - assert_eq!(seed.to_string(), restored_seed.to_string()); + let restored_seed = mw2.node_manager.xprivkey; + assert_eq!(seed, restored_seed); } } diff --git a/mutiny-core/src/logging.rs b/mutiny-core/src/logging.rs index 8f1c22587..96852f641 100644 --- a/mutiny-core/src/logging.rs +++ b/mutiny-core/src/logging.rs @@ -141,7 +141,7 @@ async fn write_logging_data( } // Save the logs - storage.set_data(LOGGING_KEY, &existing_logs)?; + storage.set_data(LOGGING_KEY, &existing_logs, None)?; Ok(()) } diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index 652832c51..3093738c9 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -26,7 +26,6 @@ use crate::{lspclient::FeeRequest, storage::MutinyStorage}; use anyhow::{anyhow, Context}; use bdk::FeeRate; use bdk_esplora::esplora_client::AsyncClient; -use bip39::Mnemonic; use bitcoin::hashes::{hex::ToHex, sha256::Hash as Sha256}; use bitcoin::secp256k1::rand; use bitcoin::{hashes::Hash, secp256k1::PublicKey, BlockHash, Network, OutPoint}; @@ -40,6 +39,7 @@ use lightning::{ util::config::ChannelConfig, }; +use bitcoin::util::bip32::ExtendedPrivKey; use lightning::sign::{EntropySource, InMemorySigner}; use lightning::{ chain::{chainmonitor, Filter, Watch}, @@ -164,7 +164,7 @@ impl Node { pub(crate) async fn new( uuid: String, node_index: &NodeIndex, - mnemonic: &Mnemonic, + xprivkey: ExtendedPrivKey, storage: S, gossip_sync: Arc, scorer: Arc>, @@ -186,7 +186,7 @@ impl Node { let keys_manager = Arc::new(create_keys_manager( wallet.clone(), - mnemonic, + xprivkey, node_index.child_index, logger.clone(), )?); diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index cda201560..20d6e21d6 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -3,6 +3,7 @@ use lightning::sign::{NodeSigner, Recipient}; use std::sync::atomic::{AtomicBool, Ordering}; use std::{collections::HashMap, ops::Deref, sync::Arc}; +use crate::gossip::*; use crate::lnurlauth::AuthManager; use crate::logging::LOGGING_KEY; use crate::redshift::{RedshiftManager, RedshiftStatus, RedshiftStorage}; @@ -13,13 +14,12 @@ use crate::scb::{ use crate::storage::{MutinyStorage, KEYCHAIN_STORE_KEY}; use crate::utils::sleep; use crate::MutinyWalletConfig; -use crate::{auth::MutinyAuthClient, gossip::*}; use crate::{ chain::MutinyChain, error::MutinyError, esplora::EsploraSyncClient, fees::MutinyFeeEstimator, - gossip, keymanager, + gossip, logging::MutinyLogger, lspclient::LspClient, node::{Node, ProbScorer, PubkeyConnectionInfo, RapidGossipSync}, @@ -35,7 +35,6 @@ use crate::{labels::LabelStorage, subscription::MutinySubscriptionClient}; use bdk::chain::{BlockId, ConfirmationTime}; use bdk::{wallet::AddressIndex, LocalUtxo}; use bdk_esplora::esplora_client::AsyncClient; -use bip39::Mnemonic; use bitcoin::blockdata::script; use bitcoin::hashes::hex::ToHex; use bitcoin::hashes::{sha256, Hash}; @@ -500,7 +499,7 @@ pub struct Plan { /// services provided by Mutiny. pub struct NodeManager { pub(crate) stop: Arc, - mnemonic: Mnemonic, + pub(crate) xprivkey: ExtendedPrivKey, network: Network, #[cfg(target_arch = "wasm32")] websocket_proxy_addr: String, @@ -540,26 +539,9 @@ impl NodeManager { .websocket_proxy_addr .unwrap_or_else(|| String::from("wss://p.mutinywallet.com")); - let network: Network = c.network.unwrap_or(Network::Bitcoin); - - let mnemonic = match c.mnemonic { - Some(seed) => storage.insert_mnemonic(seed)?, - None => match storage.get_mnemonic() { - Ok(Some(mnemonic)) => mnemonic, - Ok(None) => { - let seed = keymanager::generate_seed(12)?; - storage.insert_mnemonic(seed)? - } - Err(_) => { - // if we get an error, then we have the wrong password - return Err(MutinyError::IncorrectPassword); - } - }, - }; - let logger = Arc::new(MutinyLogger::with_writer(stop.clone(), storage.clone())); - let esplora_server_url = get_esplora_url(network, c.user_esplora_url); + let esplora_server_url = get_esplora_url(c.network, c.user_esplora_url); let tx_sync = Arc::new(EsploraSyncClient::new(esplora_server_url, logger.clone())); let esplora = Arc::new(tx_sync.client().clone()); @@ -570,9 +552,9 @@ impl NodeManager { )); let wallet = Arc::new(OnChainWallet::new( - &mnemonic, + c.xprivkey, storage.clone(), - network, + c.network, esplora.clone(), fee_estimator.clone(), stop.clone(), @@ -582,7 +564,7 @@ impl NodeManager { let chain = Arc::new(MutinyChain::new(tx_sync, wallet.clone(), logger.clone())); let (gossip_sync, scorer) = - gossip::get_gossip_sync(&storage, c.user_rgs_url, network, logger.clone()).await?; + get_gossip_sync(&storage, c.user_rgs_url, c.network, logger.clone()).await?; let scorer = Arc::new(utils::Mutex::new(scorer)); @@ -627,14 +609,14 @@ impl NodeManager { let node = Node::new( node_item.0, &node_item.1, - &mnemonic, + c.xprivkey, storage.clone(), gossip_sync.clone(), scorer.clone(), chain.clone(), fee_estimator.clone(), wallet.clone(), - network, + c.network, esplora.clone(), &lsp_clients, logger.clone(), @@ -671,47 +653,33 @@ impl NodeManager { let nodes = Arc::new(Mutex::new(nodes_map)); - let seed = mnemonic.to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(network, &seed)?; - let auth = AuthManager::new(xprivkey)?; - let lnurl_client = Arc::new( lnurl::Builder::default() .build_async() .expect("failed to make lnurl client"), ); - let auth_client = if let Some(auth_url) = c.auth_url { - let a = Arc::new(MutinyAuthClient::new( - auth.clone(), - lnurl_client.clone(), - logger.clone(), - auth_url, - )); - Some(a) - } else { - None - }; - - let subscription_client = if let Some(auth_client) = auth_client { + let (subscription_client, auth) = if let Some(auth_client) = c.auth_client { if let Some(subscription_url) = c.subscription_url { + let auth = auth_client.auth.clone(); let s = Arc::new(MutinySubscriptionClient::new( auth_client, subscription_url, logger.clone(), )); - Some(s) + (Some(s), auth) } else { - None + (None, auth_client.auth.clone()) } } else { - None + let auth_manager = AuthManager::new(c.xprivkey)?; + (None, auth_manager) }; let nm = NodeManager { stop, - mnemonic, - network, + xprivkey: c.xprivkey, + network: c.network, wallet, gossip_sync, scorer, @@ -894,11 +862,6 @@ impl NodeManager { self.wallet.broadcast_transaction(tx).await } - /// Returns the mnemonic seed phrase for the wallet. - pub fn show_seed(&self) -> Mnemonic { - self.mnemonic.clone() - } - /// Returns the network of the wallet. pub fn get_network(&self) -> Network { self.network @@ -1933,12 +1896,13 @@ impl NodeManager { } fn get_scb_key(&self) -> SecretKey { - let seed = self.mnemonic.to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(self.network, &seed).unwrap(); let path = DerivationPath::from_str(SCB_ENCRYPTION_KEY_DERIVATION_PATH).unwrap(); let context = Secp256k1::new(); - xprivkey.derive_priv(&context, &path).unwrap().private_key + self.xprivkey + .derive_priv(&context, &path) + .unwrap() + .private_key } /// Creates a static channel backup for all the nodes in the node manager. @@ -2020,7 +1984,7 @@ impl NodeManager { let new_node = Node::new( uuid, &node_index, - &self.mnemonic, + self.xprivkey, self.storage.clone(), self.gossip_sync.clone(), self.scorer.clone(), @@ -2348,7 +2312,7 @@ pub(crate) async fn create_new_node_from_node_manager( let new_node_res = Node::new( next_node_uuid.clone(), &next_node, - &node_manager.mnemonic, + node_manager.xprivkey, node_manager.storage.clone(), node_manager.gossip_sync.clone(), node_manager.scorer.clone(), @@ -2398,6 +2362,7 @@ mod tests { use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::PublicKey; + use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Network, PackedLockTime, Transaction, TxOut, Txid}; use lightning::ln::PaymentHash; use lightning_invoice::Invoice; @@ -2406,7 +2371,7 @@ mod tests { use crate::test_utils::*; use crate::event::{HTLCStatus, MillisatAmount, PaymentInfo}; - use crate::storage::MemoryStorage; + use crate::storage::{MemoryStorage, MutinyStorage}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; wasm_bindgen_test_configure!(run_in_browser); @@ -2417,6 +2382,8 @@ mod tests { async fn create_node_manager() { let test_name = "create_node_manager"; log!("{}", test_name); + let seed = generate_seed(12).unwrap(); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &seed.to_seed("")).unwrap(); let pass = uuid::Uuid::new_v4().to_string(); let cipher = encryption_key_from_pass(&pass).unwrap(); @@ -2424,10 +2391,10 @@ mod tests { assert!(!NodeManager::has_node_manager(storage.clone())); let c = MutinyWalletConfig::new( - None, + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -2437,31 +2404,10 @@ mod tests { NodeManager::new(c, storage.clone()) .await .expect("node manager should initialize"); + storage.insert_mnemonic(seed).unwrap(); assert!(NodeManager::has_node_manager(storage)); } - #[test] - async fn correctly_show_seed() { - let test_name = "correctly_show_seed"; - log!("{}", test_name); - - let seed = generate_seed(12).expect("Failed to gen seed"); - let c = MutinyWalletConfig::new( - Some(seed.clone()), - #[cfg(target_arch = "wasm32")] - None, - Some(Network::Regtest), - None, - None, - None, - None, - None, - ); - let nm = NodeManager::new(c, ()).await.unwrap(); - - assert_eq!(seed, nm.show_seed()); - } - #[test] async fn created_new_nodes() { let test_name = "created_new_nodes"; @@ -2471,11 +2417,12 @@ mod tests { let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher)); let seed = generate_seed(12).expect("Failed to gen seed"); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &seed.to_seed("")).unwrap(); let c = MutinyWalletConfig::new( - Some(seed), + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Regtest), + Network::Regtest, None, None, None, @@ -2519,11 +2466,12 @@ mod tests { let cipher = encryption_key_from_pass(&pass).unwrap(); let storage = MemoryStorage::new(Some(pass), Some(cipher)); let seed = generate_seed(12).expect("Failed to gen seed"); + let xpriv = ExtendedPrivKey::new_master(Network::Regtest, &seed.to_seed("")).unwrap(); let c = MutinyWalletConfig::new( - Some(seed), + xpriv, #[cfg(target_arch = "wasm32")] None, - Some(Network::Signet), + Network::Signet, None, None, None, diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 1ba03c938..868d3e0b0 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -132,7 +132,7 @@ impl NostrManager { .iter() .map(|x| x.profile.clone()) .collect::>(); - self.storage.set_data(NWC_STORAGE_KEY, profiles)?; + self.storage.set_data(NWC_STORAGE_KEY, profiles, None)?; } Ok(nwc_profile) @@ -181,7 +181,7 @@ impl NostrManager { .iter() .map(|x| x.profile.clone()) .collect::>(); - self.storage.set_data(NWC_STORAGE_KEY, profiles)?; + self.storage.set_data(NWC_STORAGE_KEY, profiles, None)?; } Ok(nwc.nwc_profile()) @@ -302,7 +302,8 @@ impl NostrManager { // remove from storage pending.retain(|x| x.invoice.payment_hash() != &hash); - self.storage.set_data(PENDING_NWC_EVENTS_KEY, pending)?; + self.storage + .set_data(PENDING_NWC_EVENTS_KEY, pending, None)?; Ok(event_id) } @@ -323,7 +324,8 @@ impl NostrManager { // remove the invoice invoices.retain(|x| x.invoice.payment_hash() != hash); - self.storage.set_data(PENDING_NWC_EVENTS_KEY, invoices)?; + self.storage + .set_data(PENDING_NWC_EVENTS_KEY, invoices, None)?; Ok(()) } @@ -338,7 +340,8 @@ impl NostrManager { // remove expired invoices invoices.retain(|x| !x.is_expired()); - self.storage.set_data(PENDING_NWC_EVENTS_KEY, invoices)?; + self.storage + .set_data(PENDING_NWC_EVENTS_KEY, invoices, None)?; Ok(()) } @@ -621,7 +624,7 @@ mod test { // add dummy to storage nostr_manager .storage - .set_data(PENDING_NWC_EVENTS_KEY, vec![inv.clone()]) + .set_data(PENDING_NWC_EVENTS_KEY, vec![inv.clone()], None) .unwrap(); let pending = nostr_manager.get_pending_nwc_invoices().unwrap(); diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 483c4875f..c1b3f405b 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -181,7 +181,7 @@ impl NostrWalletConnect { node_manager .storage - .set_data(PENDING_NWC_EVENTS_KEY, current)?; + .set_data(PENDING_NWC_EVENTS_KEY, current, None)?; return Ok(None); } else { diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index c030a50e9..0a6b19495 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -9,7 +9,6 @@ use bdk::psbt::PsbtUtils; use bdk::template::DescriptorTemplateOut; use bdk::{FeeRate, LocalUtxo, SignOptions, TransactionDetails, Wallet}; use bdk_esplora::{esplora_client, EsploraAsyncExt}; -use bip39::Mnemonic; use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}; use bitcoin::{Address, Network, OutPoint, Script, Transaction, Txid}; @@ -38,7 +37,7 @@ pub struct OnChainWallet { impl OnChainWallet { pub fn new( - mnemonic: &Mnemonic, + xprivkey: ExtendedPrivKey, db: S, network: Network, esplora: Arc, @@ -46,8 +45,6 @@ impl OnChainWallet { stop: Arc, logger: Arc, ) -> Result, MutinyError> { - let seed = mnemonic.to_seed(""); - let xprivkey = ExtendedPrivKey::new_master(network, &seed)?; let account_number = 0; let (receive_descriptor_template, change_descriptor_template) = get_tr_descriptors_for_extended_key(xprivkey, network, account_number)?; @@ -555,6 +552,7 @@ mod tests { use super::*; use crate::test_utils::*; use crate::{encrypt::encryption_key_from_pass, storage::MemoryStorage}; + use bip39::Mnemonic; use bitcoin::Address; use esplora_client::Builder; use std::str::FromStr; @@ -578,8 +576,9 @@ mod tests { logger.clone(), )); let stop = Arc::new(AtomicBool::new(false)); + let xpriv = ExtendedPrivKey::new_master(Network::Testnet, &mnemonic.to_seed("")).unwrap(); - OnChainWallet::new(&mnemonic, db, Network::Testnet, esplora, fees, stop, logger).unwrap() + OnChainWallet::new(xpriv, db, Network::Testnet, esplora, fees, stop, logger).unwrap() } #[test] diff --git a/mutiny-core/src/redshift.rs b/mutiny-core/src/redshift.rs index 42b1d7e34..83ab09fcc 100644 --- a/mutiny-core/src/redshift.rs +++ b/mutiny-core/src/redshift.rs @@ -126,7 +126,7 @@ impl RedshiftStorage for S { } fn persist_redshift(&self, redshift: Redshift) -> Result<(), MutinyError> { - self.set_data(get_redshift_key(&redshift.id), redshift) + self.set_data(get_redshift_key(&redshift.id), redshift, None) } } diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 977ee97ea..9a8ce8ba6 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -67,12 +67,22 @@ pub trait MutinyStorage: Clone + Sized + 'static { fn cipher(&self) -> Option; /// Set a value in the storage, the value will already be encrypted if needed - fn set(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> + fn set( + &self, + key: impl AsRef, + value: T, + version: Option, + ) -> Result<(), MutinyError> where T: Serialize; /// Set a value in the storage, the function will encrypt the value if needed - fn set_data(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> + fn set_data( + &self, + key: impl AsRef, + value: T, + version: Option, + ) -> Result<(), MutinyError> where T: Serialize, { @@ -82,7 +92,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { let json: Value = encrypt_value(key.as_ref(), data, self.cipher())?; - self.set(key, json) + self.set(key, json, version) } /// Get a value from the storage, use get_data if you want the value to be decrypted @@ -141,7 +151,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { /// Insert a mnemonic into the storage fn insert_mnemonic(&self, mnemonic: Mnemonic) -> Result { - self.set_data(MNEMONIC_KEY, &mnemonic)?; + self.set_data(MNEMONIC_KEY, &mnemonic, None)?; Ok(mnemonic) } @@ -190,7 +200,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { // encrypt all of the values for (key, value) in values.iter() { - self.set_data(key, value)?; + self.set_data(key, value, None)?; } Ok(()) @@ -213,7 +223,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { /// Inserts the node indexes into storage fn insert_nodes(&self, nodes: NodeStorage) -> Result<(), MutinyError> { - self.set_data(NODES_KEY, nodes) + self.set_data(NODES_KEY, nodes, None) } /// Get the current fee estimates from storage @@ -225,7 +235,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { /// Inserts the fee estimates into storage /// The key is block target, the value is the fee in satoshis per byte fn insert_fee_estimates(&self, fees: HashMap) -> Result<(), MutinyError> { - self.set_data(FEE_ESTIMATES_KEY, fees) + self.set_data(FEE_ESTIMATES_KEY, fees, None) } fn has_done_first_sync(&self) -> Result { @@ -234,7 +244,7 @@ pub trait MutinyStorage: Clone + Sized + 'static { } fn set_done_first_sync(&self) -> Result<(), MutinyError> { - self.set_data(FIRST_SYNC_KEY, true) + self.set_data(FIRST_SYNC_KEY, true, None) } } @@ -270,7 +280,12 @@ impl MutinyStorage for MemoryStorage { self.cipher.to_owned() } - fn set(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> + fn set( + &self, + key: impl AsRef, + value: T, + _version: Option, + ) -> Result<(), MutinyError> where T: Serialize, { @@ -374,7 +389,12 @@ impl MutinyStorage for () { None } - fn set(&self, _key: impl AsRef, _value: T) -> Result<(), MutinyError> + fn set( + &self, + _key: impl AsRef, + _value: T, + _version: Option, + ) -> Result<(), MutinyError> where T: Serialize, { @@ -441,9 +461,9 @@ where match self.0.get_data::(KEYCHAIN_STORE_KEY)? { Some(mut keychain_store) => { keychain_store.append(changeset.clone()); - self.0.set_data(KEYCHAIN_STORE_KEY, keychain_store) + self.0.set_data(KEYCHAIN_STORE_KEY, keychain_store, None) } - None => self.0.set_data(KEYCHAIN_STORE_KEY, changeset), + None => self.0.set_data(KEYCHAIN_STORE_KEY, changeset, None), } } diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index f3bd995ec..ce55ecbae 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -81,7 +81,7 @@ impl EncryptedVssKeyValueItem { } impl MutinyVssClient { - pub(crate) fn new( + pub fn new( auth_client: Arc, url: String, encryption_key: SecretKey, diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index a7487aab9..67e9e4a1e 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -38,6 +38,7 @@ gloo-utils = { version = "0.1.6", features = ["serde"] } web-sys = { version = "0.3.60", features = ["console"] } bip39 = { version = "2.0.0" } getrandom = { version = "0.2", features = ["js"] } +futures = "0.3.25" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index b2577bd7f..f845848b9 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -2,10 +2,11 @@ use anyhow::anyhow; use gloo_storage::{LocalStorage, Storage}; use gloo_utils::format::JsValueSerdeExt; use lightning::util::logger::Logger; -use lightning::{log_debug, log_error}; +use lightning::{log_debug, log_error, log_trace}; use log::error; use mutiny_core::logging::MutinyLogger; use mutiny_core::storage::{MutinyStorage, KEYCHAIN_STORE_KEY}; +use mutiny_core::vss::*; use mutiny_core::*; use mutiny_core::{ encrypt::Cipher, @@ -31,6 +32,7 @@ pub struct IndexedDbStorage { /// This is a RwLock because we want to be able to read from it without blocking memory: Arc>>, pub(crate) indexed_db: Arc>>, + vss: Option>, logger: Arc, } @@ -38,11 +40,12 @@ impl IndexedDbStorage { pub async fn new( password: Option, cipher: Option, + vss: Option>, logger: Arc, ) -> Result { let indexed_db = Arc::new(RwLock::new(Some(Self::build_indexed_db_database().await?))); - let map = Self::read_all(&indexed_db, &logger).await?; + let map = Self::read_all(&indexed_db, vss.as_deref(), &logger).await?; let memory = Arc::new(RwLock::new(map)); let password = password.filter(|p| !p.is_empty()); @@ -51,6 +54,7 @@ impl IndexedDbStorage { cipher, memory, indexed_db, + vss, logger, }) } @@ -142,6 +146,7 @@ impl IndexedDbStorage { pub(crate) async fn read_all( indexed_db: &Arc>>, + vss: Option<&MutinyVssClient>, logger: &MutinyLogger, ) -> Result, MutinyError> { let store = { @@ -199,7 +204,46 @@ impl IndexedDbStorage { } } - Ok(map) + match vss { + None => Ok(map), + Some(vss) => { + log_debug!(logger, "Reading from vss"); + let keys = vss.list_key_versions(None).await?; + let mut vss_map = HashMap::new(); + // todo do this in parallel + // todo only get keys we want (probably will filter out scb) + for key in keys { + let obj = vss.get_object(&key.key).await?; + if let Some(value) = obj.value { + log_trace!( + logger, + "Found vss key {} with version {}", + key.key, + key.version + ); + + // we can get versions from monitors, so we should compare + if key.key.contains("monitor") { + if let Some(current) = map.get(&key.key) { + let bytes: Vec = serde_json::from_value(current.clone())?; + // check first byte is 1, then take u64 from next 8 bytes + let current_version = + u64::from_be_bytes(bytes[1..9].try_into().unwrap()); + // if the current version is less than the version from vss, then we want to use the vss version + if current_version < key.version as u64 { + vss_map.insert(key.key, value); + } + continue; + } + } + + vss_map.insert(key.key, value); + } + } + map.extend(vss_map); + Ok(map) + } + } } async fn build_indexed_db_database() -> Result { @@ -217,7 +261,7 @@ impl IndexedDbStorage { #[cfg(test)] pub(crate) async fn reload_from_indexed_db(&self) -> Result<(), MutinyError> { - let map = Self::read_all(&self.indexed_db, &self.logger).await?; + let map = Self::read_all(&self.indexed_db, self.vss.as_deref(), &self.logger).await?; let mut memory = self .memory .try_write() @@ -259,7 +303,12 @@ impl MutinyStorage for IndexedDbStorage { self.cipher.to_owned() } - fn set(&self, key: impl AsRef, value: T) -> Result<(), MutinyError> + fn set( + &self, + key: impl AsRef, + value: T, + version: Option, + ) -> Result<(), MutinyError> where T: Serialize, { @@ -272,10 +321,30 @@ impl MutinyStorage for IndexedDbStorage { let key_clone = key.clone(); let data_clone = data.clone(); let logger = self.logger.clone(); + let vss = self.vss.clone(); spawn_local(async move { - if let Err(e) = Self::save_to_indexed_db(&indexed_db, &key_clone, &data_clone).await { + if let (Some(vss), Some(version)) = (vss, version) { + let item = VssKeyValueItem { + key: key_clone.clone(), + value: Some(data_clone.clone()), + version, + }; + let vss_fut = vss.put_objects(vec![item]); + let db_fut = Self::save_to_indexed_db(&indexed_db, &key_clone, &data_clone); + let (vss_result, db_result) = futures::join!(vss_fut, db_fut); + + if let Err(e) = vss_result { + log_error!(logger, "Failed to save ({key_clone}) to vss: {e}"); + } + + if let Err(e) = db_result { + log_error!(logger, "Failed to save ({key_clone}) to indexed db: {e}"); + } + } else if let Err(e) = + Self::save_to_indexed_db(&indexed_db, &key_clone, &data_clone).await + { log_error!(logger, "Failed to save ({key_clone}) to indexed db: {e}"); - } + }; }); // Some values we want to write to local storage as well as indexed db @@ -370,7 +439,7 @@ impl MutinyStorage for IndexedDbStorage { self.indexed_db.clone() }; - let map = Self::read_all(&indexed_db, &self.logger).await?; + let map = Self::read_all(&indexed_db, self.vss.as_deref(), &self.logger).await?; let memory = Arc::new(RwLock::new(map)); self.indexed_db = indexed_db; self.memory = memory; @@ -496,7 +565,7 @@ mod tests { log!("{test_name}"); let logger = Arc::new(MutinyLogger::default()); - let storage = IndexedDbStorage::new(Some("".to_string()), None, logger) + let storage = IndexedDbStorage::new(Some("".to_string()), None, None, logger) .await .unwrap(); @@ -514,14 +583,14 @@ mod tests { let logger = Arc::new(MutinyLogger::default()); let password = "password".to_string(); let cipher = encryption_key_from_pass(&password).unwrap(); - let storage = IndexedDbStorage::new(Some(password), Some(cipher), logger) + let storage = IndexedDbStorage::new(Some(password), Some(cipher), None, logger) .await .unwrap(); let result: Option = storage.get(key).unwrap(); assert_eq!(result, None); - storage.set(key, value).unwrap(); + storage.set(key, value, None).unwrap(); let result: Option = storage.get(key).unwrap(); assert_eq!(result, Some(value.to_string())); @@ -566,7 +635,7 @@ mod tests { let logger = Arc::new(MutinyLogger::default()); let password = "password".to_string(); let cipher = encryption_key_from_pass(&password).unwrap(); - let storage = IndexedDbStorage::new(Some(password), Some(cipher), logger) + let storage = IndexedDbStorage::new(Some(password), Some(cipher), None, logger) .await .unwrap(); @@ -591,11 +660,11 @@ mod tests { let logger = Arc::new(MutinyLogger::default()); let password = "password".to_string(); let cipher = encryption_key_from_pass(&password).unwrap(); - let storage = IndexedDbStorage::new(Some(password), Some(cipher), logger) + let storage = IndexedDbStorage::new(Some(password), Some(cipher), None, logger) .await .unwrap(); - storage.set(key, value).unwrap(); + storage.set(key, value, None).unwrap(); IndexedDbStorage::clear().await.unwrap(); @@ -616,7 +685,9 @@ mod tests { let seed = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").expect("could not generate"); let logger = Arc::new(MutinyLogger::default()); - let storage = IndexedDbStorage::new(None, None, logger).await.unwrap(); + let storage = IndexedDbStorage::new(None, None, None, logger) + .await + .unwrap(); let mnemonic = storage.insert_mnemonic(seed).unwrap(); let stored_mnemonic = storage.get_mnemonic().unwrap(); @@ -636,7 +707,7 @@ mod tests { let logger = Arc::new(MutinyLogger::default()); let password = "password".to_string(); let cipher = encryption_key_from_pass(&password).unwrap(); - let storage = IndexedDbStorage::new(Some(password), Some(cipher), logger) + let storage = IndexedDbStorage::new(Some(password), Some(cipher), None, logger) .await .unwrap(); diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 33fbcffa8..181435571 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -18,18 +18,22 @@ use bitcoin::consensus::deserialize; use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::sha256; use bitcoin::secp256k1::PublicKey; +use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{Address, Network, OutPoint, Transaction, Txid}; use gloo_utils::format::JsValueSerdeExt; use lightning::routing::gossip::NodeId; use lightning_invoice::Invoice; use lnurl::lnurl::LnUrl; +use mutiny_core::auth::MutinyAuthClient; +use mutiny_core::lnurlauth::AuthManager; use mutiny_core::redshift::RedshiftManager; +use mutiny_core::redshift::RedshiftRecipient; use mutiny_core::scb::EncryptedSCB; use mutiny_core::storage::MutinyStorage; -use mutiny_core::{encrypt::encryption_key_from_pass, nostr::nwc::NwcProfile}; +use mutiny_core::vss::MutinyVssClient; +use mutiny_core::{encrypt::encryption_key_from_pass, generate_seed, nostr::nwc::NwcProfile}; use mutiny_core::{labels::LabelStorage, nodemanager::NodeManager}; use mutiny_core::{logging::MutinyLogger, nostr::ProfileType}; -use mutiny_core::{nodemanager, redshift::RedshiftRecipient}; use std::str::FromStr; use std::sync::Arc; use std::{ @@ -40,6 +44,7 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct MutinyWallet { + mnemonic: Mnemonic, inner: mutiny_core::MutinyWallet, } @@ -67,33 +72,86 @@ impl MutinyWallet { lsp_url: Option, auth_url: Option, subscription_url: Option, + storage_url: Option, do_not_connect_peers: Option, ) -> Result { utils::set_panic_hook(); - - let network: Option = network_str.map(|s| s.parse().expect("Invalid network")); - - let mnemonic = match mnemonic_str { - Some(m) => Some(Mnemonic::from_str(&m).map_err(|_| MutinyJsError::InvalidMnemonic)?), - None => None, - }; - let logger = Arc::new(MutinyLogger::default()); + let cipher = password .as_ref() .filter(|p| !p.is_empty()) .map(|p| encryption_key_from_pass(p)) .transpose()?; - let storage = IndexedDbStorage::new(password, cipher, logger).await?; + + let network: Network = network_str + .map(|s| s.parse().expect("Invalid network")) + .unwrap_or(Network::Bitcoin); + + let storage = + IndexedDbStorage::new(password.clone(), cipher.clone(), None, logger.clone()).await?; + + let mnemonic = match mnemonic_str { + Some(m) => { + let seed = Mnemonic::from_str(&m).map_err(|_| MutinyJsError::InvalidMnemonic)?; + storage.insert_mnemonic(seed)? + } + None => match storage.get_mnemonic() { + Ok(Some(mnemonic)) => mnemonic, + Ok(None) => { + let seed = generate_seed(12)?; + storage.insert_mnemonic(seed)? + } + Err(_) => { + // if we get an error, then we have the wrong password + return Err(MutinyJsError::IncorrectPassword); + } + }, + }; + + let seed = mnemonic.to_seed(""); + let xprivkey = ExtendedPrivKey::new_master(network, &seed).unwrap(); + + let (auth_client, vss_client) = if let Some(auth_url) = auth_url.clone() { + let auth_manager = AuthManager::new(xprivkey).unwrap(); + + let lnurl_client = Arc::new( + lnurl::Builder::default() + .build_async() + .expect("failed to make lnurl client"), + ); + + let auth_client = Arc::new(MutinyAuthClient::new( + auth_manager, + lnurl_client, + logger.clone(), + auth_url, + )); + + let vss = storage_url.map(|url| { + Arc::new(MutinyVssClient::new( + auth_client.clone(), + url, + xprivkey.private_key, + logger.clone(), + )) + }); + + (Some(auth_client), vss) + } else { + (None, None) + }; + + let storage = IndexedDbStorage::new(password, cipher, vss_client, logger.clone()).await?; let mut config = mutiny_core::MutinyWalletConfig::new( - mnemonic, + xprivkey, websocket_proxy_addr, network, user_esplora_url, user_rgs_url, lsp_url, - auth_url, + auth_client, subscription_url, ); @@ -102,7 +160,7 @@ impl MutinyWallet { } let inner = mutiny_core::MutinyWallet::new(storage, config).await?; - Ok(MutinyWallet { inner }) + Ok(MutinyWallet { mnemonic, inner }) } /// Returns if there is a saved wallet in storage. @@ -119,10 +177,10 @@ impl MutinyWallet { Ok(c) => c, Err(_) => return false, }; - let storage = IndexedDbStorage::new(password, cipher, logger) + let storage = IndexedDbStorage::new(password, cipher, None, logger) .await .expect("Failed to init"); - nodemanager::NodeManager::has_node_manager(storage) + NodeManager::has_node_manager(storage) } /// Starts up all the nodes again. @@ -153,7 +211,7 @@ impl MutinyWallet { /// Returns the mnemonic seed phrase for the wallet. #[wasm_bindgen] pub fn show_seed(&self) -> String { - self.inner.node_manager.show_seed().to_string() + self.mnemonic.to_string() } /// Returns the network of the wallet. @@ -972,7 +1030,7 @@ impl MutinyWallet { pub async fn get_logs() -> Result> */, MutinyJsError> { let logger = Arc::new(MutinyLogger::default()); // Password should not be required for logs - let storage = IndexedDbStorage::new(None, None, logger.clone()).await?; + let storage = IndexedDbStorage::new(None, None, None, logger.clone()).await?; let stop = Arc::new(AtomicBool::new(false)); let logger = Arc::new(MutinyLogger::with_writer(stop.clone(), storage.clone())); let res = JsValue::from_serde(&NodeManager::get_logs(storage, logger)?)?; @@ -1123,7 +1181,8 @@ impl MutinyWallet { .filter(|p| !p.is_empty()) .map(|p| encryption_key_from_pass(p)) .transpose()?; - let storage = IndexedDbStorage::new(password, cipher, logger).await?; + // todo init vss + let storage = IndexedDbStorage::new(password, cipher, None, logger).await?; if storage.get_mnemonic().is_err() { // if we get an error, then we have the wrong password return Err(MutinyJsError::IncorrectPassword); @@ -1155,7 +1214,7 @@ impl MutinyWallet { .filter(|p| !p.is_empty()) .map(|p| encryption_key_from_pass(p)) .transpose()?; - let storage = IndexedDbStorage::new(password, cipher, logger).await?; + let storage = IndexedDbStorage::new(password, cipher, None, logger).await?; mutiny_core::MutinyWallet::::restore_mnemonic( storage, Mnemonic::from_str(&m).map_err(|_| MutinyJsError::InvalidMnemonic)?, @@ -1226,6 +1285,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize"); @@ -1256,6 +1316,7 @@ mod tests { None, None, None, + None, ) .await .unwrap(); @@ -1289,6 +1350,7 @@ mod tests { None, None, None, + None, ) .await .expect("mutiny wallet should initialize"); From 26e2f855c3e513c47fa97ee6a117215d11476f00 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 10 Jul 2023 04:59:08 -0500 Subject: [PATCH 04/10] Remove optional --- mutiny-core/src/vss.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index ce55ecbae..49b3bd1a5 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -25,10 +25,16 @@ pub struct MutinyVssClient { logger: Arc, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct KeyVersion { + pub key: String, + pub version: u32, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct VssKeyValueItem { pub key: String, - pub value: Option, + pub value: Value, pub version: u32, } @@ -37,7 +43,7 @@ impl VssKeyValueItem { /// and returns an encrypted version of the item pub(crate) fn encrypt(self, encryption_key: &SecretKey) -> EncryptedVssKeyValueItem { // should we handle this unwrap better? - let bytes = self.value.unwrap().to_string().into_bytes(); + let bytes = self.value.to_string().into_bytes(); let iv: [u8; 16] = secp256k1::rand::random(); let cipher = Aes256CbcEnc::new(&encryption_key.secret_bytes().into(), &iv.into()); @@ -74,7 +80,7 @@ impl EncryptedVssKeyValueItem { VssKeyValueItem { key: self.key, - value: Some(value), + value, version: self.version, } } @@ -101,15 +107,6 @@ impl MutinyVssClient { MutinyError::Other(anyhow!("Error parsing put objects url: {e}")) })?; - // check value is defined for all items - for item in &items { - if item.value.is_none() { - return Err(MutinyError::Other(anyhow!( - "Value must be defined for all items" - ))); - } - } - let items = items .into_iter() .map(|item| item.encrypt(&self.encryption_key)) @@ -149,7 +146,7 @@ impl MutinyVssClient { pub async fn list_key_versions( &self, key_prefix: Option, - ) -> Result, MutinyError> { + ) -> Result, MutinyError> { let url = Url::parse(&format!("{}/listKeyVersions", self.url)).map_err(|e| { log_error!(self.logger, "Error parsing list key versions url: {e}"); MutinyError::Other(anyhow!("Error parsing list key versions url: {e}")) From 06a364f776583e3e2d4b95948fb01a1345ad908b Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 10 Jul 2023 05:16:41 -0500 Subject: [PATCH 05/10] Unify encryption methods --- mutiny-core/src/encrypt.rs | 42 ++++++++++++++++++++++++++++++--- mutiny-core/src/scb/mod.rs | 32 ++++++------------------- mutiny-core/src/storage.rs | 4 ++-- mutiny-core/src/vss.rs | 27 ++++----------------- mutiny-wasm/src/indexed_db.rs | 44 +++++++++++++++++------------------ 5 files changed, 74 insertions(+), 75 deletions(-) diff --git a/mutiny-core/src/encrypt.rs b/mutiny-core/src/encrypt.rs index b71599d1c..101a546aa 100644 --- a/mutiny-core/src/encrypt.rs +++ b/mutiny-core/src/encrypt.rs @@ -1,4 +1,7 @@ use crate::error::MutinyError; +use aes::cipher::block_padding::Pkcs7; +use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use aes::Aes256; use aes_gcm::Aes256Gcm; use aes_gcm::{ aead::{generic_array::GenericArray, Aead}, @@ -6,9 +9,15 @@ use aes_gcm::{ }; use argon2::Argon2; use base64; +use bitcoin::secp256k1; +use bitcoin::secp256k1::SecretKey; +use cbc::{Decryptor, Encryptor}; use getrandom::getrandom; use std::sync::Arc; +type Aes256CbcEnc = Encryptor; +type Aes256CbcDec = Decryptor; + #[derive(Clone)] pub struct Cipher { key: Arc, @@ -47,7 +56,7 @@ pub fn encrypt(content: &str, c: Cipher) -> Result { Ok(base64::encode(&result)) } -pub fn decrypt(encrypted: &str, password: &str) -> Result { +pub fn decrypt_with_password(encrypted: &str, password: &str) -> Result { let encrypted = base64::decode(encrypted).map_err(|_| MutinyError::IncorrectPassword)?; if encrypted.len() < 12 + 16 { return Err(MutinyError::IncorrectPassword); @@ -86,9 +95,36 @@ fn argon2() -> Argon2<'static> { Argon2::from(params.build().expect("valid params")) } +pub fn encrypt_with_key(encryption_key: &SecretKey, bytes: &[u8]) -> Vec { + let iv: [u8; 16] = secp256k1::rand::random(); + + let cipher = Aes256CbcEnc::new(&encryption_key.secret_bytes().into(), &iv.into()); + let mut encrypted: Vec = cipher.encrypt_padded_vec_mut::(bytes); + encrypted.extend(iv); + + encrypted +} + +pub fn decrypt_with_key( + encryption_key: &SecretKey, + bytes: Vec, +) -> Result, MutinyError> { + if bytes.len() < 16 { + return Err(MutinyError::IncorrectPassword); + } + // split last 16 bytes off as iv + let iv = &bytes[bytes.len() - 16..]; + let bytes = &bytes[..bytes.len() - 16]; + + let cipher = Aes256CbcDec::new(&encryption_key.secret_bytes().into(), iv.into()); + let decrypted: Vec = cipher.decrypt_padded_vec_mut::(bytes).unwrap(); + + Ok(decrypted) +} + #[cfg(test)] mod tests { - use crate::encrypt::{decrypt, encrypt, encryption_key_from_pass}; + use crate::encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass}; #[test] fn test_encryption() { @@ -99,7 +135,7 @@ mod tests { let encrypted = encrypt(content, cipher).unwrap(); println!("{encrypted}"); - let decrypted = decrypt(&encrypted, password).unwrap(); + let decrypted = decrypt_with_password(&encrypted, password).unwrap(); println!("{decrypted}"); assert_eq!(content, decrypted); } diff --git a/mutiny-core/src/scb/mod.rs b/mutiny-core/src/scb/mod.rs index e2353fff0..2ca498aca 100644 --- a/mutiny-core/src/scb/mod.rs +++ b/mutiny-core/src/scb/mod.rs @@ -1,15 +1,12 @@ pub mod message_handler; +use crate::encrypt::{decrypt_with_key, encrypt_with_key}; use crate::error::MutinyError; use crate::nodemanager::NodeIndex; -use aes::cipher::block_padding::Pkcs7; -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; -use aes::Aes256; use bitcoin::bech32::{FromBase32, ToBase32, Variant}; use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::{bech32, secp256k1, OutPoint}; -use cbc::{Decryptor, Encryptor}; +use bitcoin::{bech32, OutPoint}; use lightning::io::{Cursor, Read}; use lightning::ln::msgs::DecodeError; use lightning::util::ser::{Readable, Writeable, Writer}; @@ -17,9 +14,6 @@ use std::collections::HashMap; use std::fmt::Formatter; use std::str::FromStr; -type Aes256CbcEnc = Encryptor; -type Aes256CbcDec = Decryptor; - pub const SCB_ENCRYPTION_KEY_DERIVATION_PATH: &str = "m/444'/444'/444'"; /// A static channel backup is a backup for the channels for a given node. @@ -84,12 +78,9 @@ pub struct StaticChannelBackupStorage { impl StaticChannelBackupStorage { pub(crate) fn encrypt(&self, secret_key: &SecretKey) -> EncryptedSCB { let bytes = self.encode(); - let iv: [u8; 16] = secp256k1::rand::random(); - - let cipher = Aes256CbcEnc::new(&secret_key.secret_bytes().into(), &iv.into()); - let encrypted_scb: Vec = cipher.encrypt_padded_vec_mut::(&bytes); + let encrypted_scb = encrypt_with_key(secret_key, &bytes); - EncryptedSCB { encrypted_scb, iv } + EncryptedSCB { encrypted_scb } } } @@ -160,7 +151,6 @@ impl Readable for StaticChannelBackupStorage { #[derive(Clone, PartialEq, Eq, Debug)] pub struct EncryptedSCB { pub(crate) encrypted_scb: Vec, - pub(crate) iv: [u8; 16], } impl EncryptedSCB { @@ -168,13 +158,8 @@ impl EncryptedSCB { &self, secret_key: &SecretKey, ) -> Result { - let cipher = - Aes256CbcDec::new(&secret_key.secret_bytes().into(), self.iv.as_slice().into()); - let result = cipher - .decrypt_padded_vec_mut::(&self.encrypted_scb) - .map_err(|_| MutinyError::InvalidMnemonic)?; - - let mut cursor = Cursor::new(result); + let bytes = decrypt_with_key(secret_key, self.encrypted_scb.clone())?; + let mut cursor = Cursor::new(bytes); Ok(StaticChannelBackupStorage::read(&mut cursor).expect("decoding succeeds")) } } @@ -184,7 +169,6 @@ impl Writeable for EncryptedSCB { let len = self.encrypted_scb.len() as u32; writer.write_all(&len.to_be_bytes())?; writer.write_all(&self.encrypted_scb)?; - writer.write_all(&self.iv)?; Ok(()) } } @@ -194,9 +178,7 @@ impl Readable for EncryptedSCB { let len: u32 = Readable::read(reader)?; let mut encrypted_scb = vec![0u8; len as usize]; reader.read_exact(&mut encrypted_scb)?; - let mut iv = [0u8; 16]; - reader.read_exact(&mut iv)?; - Ok(Self { encrypted_scb, iv }) + Ok(Self { encrypted_scb }) } } diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 9a8ce8ba6..e6b9aa6a6 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -1,4 +1,4 @@ -use crate::encrypt::{decrypt, encrypt, encryption_key_from_pass, Cipher}; +use crate::encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass, Cipher}; use crate::error::{MutinyError, MutinyStorageError}; use crate::ldkstorage::CHANNEL_MANAGER_KEY; use crate::nodemanager::NodeStorage; @@ -50,7 +50,7 @@ pub fn decrypt_value( let json: Value = match password { Some(pw) if needs_encryption(key.as_ref()) => { let str: String = serde_json::from_value(value)?; - let ciphertext = decrypt(&str, pw)?; + let ciphertext = decrypt_with_password(&str, pw)?; serde_json::from_str(&ciphertext)? } _ => value, diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index 49b3bd1a5..e059a6491 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -1,13 +1,8 @@ -#![allow(dead_code)] use crate::auth::MutinyAuthClient; +use crate::encrypt::{decrypt_with_key, encrypt_with_key}; use crate::{error::MutinyError, logging::MutinyLogger}; -use aes::cipher::block_padding::Pkcs7; -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; -use aes::Aes256; use anyhow::anyhow; -use bitcoin::secp256k1; use bitcoin::secp256k1::SecretKey; -use cbc::{Decryptor, Encryptor}; use lightning::log_error; use lightning::util::logger::*; use reqwest::{Method, Url}; @@ -15,9 +10,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::sync::Arc; -type Aes256CbcEnc = Encryptor; -type Aes256CbcDec = Decryptor; - pub struct MutinyVssClient { auth_client: Arc, url: String, @@ -44,16 +36,12 @@ impl VssKeyValueItem { pub(crate) fn encrypt(self, encryption_key: &SecretKey) -> EncryptedVssKeyValueItem { // should we handle this unwrap better? let bytes = self.value.to_string().into_bytes(); - let iv: [u8; 16] = secp256k1::rand::random(); - let cipher = Aes256CbcEnc::new(&encryption_key.secret_bytes().into(), &iv.into()); - let mut encrypted: Vec = cipher.encrypt_padded_vec_mut::(&bytes); - encrypted.extend(iv); - let encrypted_value = base64::encode(encrypted); + let value = base64::encode(encrypt_with_key(encryption_key, &bytes)); EncryptedVssKeyValueItem { key: self.key, - value: encrypted_value, + value, version: self.version, } } @@ -68,13 +56,8 @@ pub struct EncryptedVssKeyValueItem { impl EncryptedVssKeyValueItem { pub(crate) fn decrypt(self, encryption_key: &SecretKey) -> VssKeyValueItem { - let bytes = base64::decode(self.value).unwrap(); - // split last 16 bytes off as iv - let iv = &bytes[bytes.len() - 16..]; - let bytes = &bytes[..bytes.len() - 16]; - - let cipher = Aes256CbcDec::new(&encryption_key.secret_bytes().into(), iv.into()); - let decrypted: Vec = cipher.decrypt_padded_vec_mut::(bytes).unwrap(); + let bytes = base64::decode(&self.value).unwrap(); + let decrypted = decrypt_with_key(encryption_key, bytes).unwrap(); let decrypted_value = String::from_utf8(decrypted).unwrap(); let value = serde_json::from_str(&decrypted_value).unwrap(); diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index f845848b9..2da8fc836 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -214,31 +214,29 @@ impl IndexedDbStorage { // todo only get keys we want (probably will filter out scb) for key in keys { let obj = vss.get_object(&key.key).await?; - if let Some(value) = obj.value { - log_trace!( - logger, - "Found vss key {} with version {}", - key.key, - key.version - ); - - // we can get versions from monitors, so we should compare - if key.key.contains("monitor") { - if let Some(current) = map.get(&key.key) { - let bytes: Vec = serde_json::from_value(current.clone())?; - // check first byte is 1, then take u64 from next 8 bytes - let current_version = - u64::from_be_bytes(bytes[1..9].try_into().unwrap()); - // if the current version is less than the version from vss, then we want to use the vss version - if current_version < key.version as u64 { - vss_map.insert(key.key, value); - } - continue; + log_trace!( + logger, + "Found vss key {} with version {}", + key.key, + key.version + ); + + // we can get versions from monitors, so we should compare + if key.key.contains("monitor") { + if let Some(current) = map.get(&key.key) { + let bytes: Vec = serde_json::from_value(current.clone())?; + // check first byte is 1, then take u64 from next 8 bytes + let current_version = + u64::from_be_bytes(bytes[1..9].try_into().unwrap()); + // if the current version is less than the version from vss, then we want to use the vss version + if current_version < key.version as u64 { + vss_map.insert(key.key, obj.value); } + continue; } - - vss_map.insert(key.key, value); } + + vss_map.insert(key.key, obj.value); } map.extend(vss_map); Ok(map) @@ -326,7 +324,7 @@ impl MutinyStorage for IndexedDbStorage { if let (Some(vss), Some(version)) = (vss, version) { let item = VssKeyValueItem { key: key_clone.clone(), - value: Some(data_clone.clone()), + value: data_clone.clone(), version, }; let vss_fut = vss.put_objects(vec![item]); From a9f8d9db064f1c1a3b9477f0088bb974dd5f6bf0 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 10 Jul 2023 05:21:28 -0500 Subject: [PATCH 06/10] fix --- mutiny-core/src/vss.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index e059a6491..d9a7f3852 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -198,7 +198,7 @@ mod tests { let value: Value = serde_json::from_str("\"world\"").unwrap(); let obj = VssKeyValueItem { key: key.clone(), - value: Some(value.clone()), + value: value.clone(), version: 0, }; @@ -208,11 +208,7 @@ mod tests { assert_eq!(obj, result); let result = client.list_key_versions(None).await.unwrap(); - let key_version = VssKeyValueItem { - key, - value: None, - version: 0, - }; + let key_version = KeyVersion { key, version: 0 }; assert_eq!(vec![key_version], result); assert_eq!(result.len(), 1); @@ -226,7 +222,7 @@ mod tests { let value: Value = serde_json::from_str("\"world\"").unwrap(); let obj = VssKeyValueItem { key: key.clone(), - value: Some(value.clone()), + value: value.clone(), version: 0, }; @@ -237,7 +233,7 @@ mod tests { let value1: Value = serde_json::from_str("\"new world\"").unwrap(); let obj1 = VssKeyValueItem { key: key.clone(), - value: Some(value1.clone()), + value: value1.clone(), version: 1, }; From aeba11ab9f8b2b05dc7a316abf7fdcfbb057ce90 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 10 Jul 2023 16:54:04 -0500 Subject: [PATCH 07/10] Versioned channel manager --- mutiny-core/src/ldkstorage.rs | 272 ++++++++++++++++++++++++++++++---- mutiny-core/src/storage.rs | 6 + mutiny-wasm/src/indexed_db.rs | 18 ++- 3 files changed, 264 insertions(+), 32 deletions(-) diff --git a/mutiny-core/src/ldkstorage.rs b/mutiny-core/src/ldkstorage.rs index afd7d9b45..73c334d70 100644 --- a/mutiny-core/src/ldkstorage.rs +++ b/mutiny-core/src/ldkstorage.rs @@ -8,7 +8,7 @@ use crate::logging::MutinyLogger; use crate::node::{default_user_config, ChainMonitor, ProbScorer}; use crate::node::{NetworkGraph, Router}; use crate::nodemanager::ChannelClosure; -use crate::storage::MutinyStorage; +use crate::storage::{MutinyStorage, VersionedValue}; use crate::utils; use anyhow::anyhow; use bdk_esplora::esplora_client::AsyncClient; @@ -36,7 +36,7 @@ use lightning::{ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::io; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; pub const CHANNEL_MANAGER_KEY: &str = "manager"; pub const MONITORS_PREFIX_KEY: &str = "monitors/"; @@ -61,6 +61,7 @@ pub(crate) type PhantomChannelManager = LdkChannelManager< pub struct MutinyNodePersister { node_id: String, pub(crate) storage: S, + manager_version: Arc>, logger: Arc, } @@ -75,10 +76,16 @@ impl MutinyNodePersister { MutinyNodePersister { node_id, storage, + manager_version: Arc::new(RwLock::new(0)), logger, } } + #[cfg(test)] + pub(crate) fn manager_version(&self) -> u32 { + *self.manager_version.read().unwrap() + } + fn get_key(&self, key: &str) -> String { format!("{}_{}", key, self.node_id) } @@ -164,38 +171,32 @@ impl MutinyNodePersister { mutiny_logger: Arc, keys_manager: Arc>, router: Arc, - mut channel_monitors: Vec<(BlockHash, ChannelMonitor)>, + channel_monitors: Vec<(BlockHash, ChannelMonitor)>, esplora: Arc, ) -> Result, MutinyError> { - match self.read_value(CHANNEL_MANAGER_KEY) { - Ok(kv_value) => { - let mut channel_monitor_mut_references = Vec::new(); - for (_, channel_monitor) in channel_monitors.iter_mut() { - channel_monitor_mut_references.push(channel_monitor); - } - let read_args = ChannelManagerReadArgs::new( - keys_manager.clone(), - keys_manager.clone(), - keys_manager, - fee_estimator, + let key = self.get_key(CHANNEL_MANAGER_KEY); + match self.storage.get_data::(&key) { + Ok(Some(versioned_value)) => { + // new encoding is in hex + let hex: String = serde_json::from_value(versioned_value.value.clone())?; + let bytes = FromHex::from_hex(&hex)?; + let res = Self::parse_channel_manager( + bytes, chain_monitor, mutiny_chain, - router, + fee_estimator, mutiny_logger, - default_user_config(), - channel_monitor_mut_references, - ); - let mut readable_kv_value = Cursor::new(kv_value); - let Ok((_, channel_manager)) = <(BlockHash, PhantomChannelManager)>::read(&mut readable_kv_value, read_args) else { - return Err(MutinyError::ReadError { source: MutinyStorageError::Other(anyhow!("could not read manager")) }) - }; - Ok(ReadChannelManager { - channel_manager, - is_restarting: true, + keys_manager, + router, channel_monitors, - }) + )?; + + let mut version = self.manager_version.write().unwrap(); + *version = versioned_value.version; + + Ok(res) } - Err(_) => { + Ok(None) => { // no key manager stored, start a new one Self::create_new_channel_manager( @@ -211,9 +212,61 @@ impl MutinyNodePersister { ) .await } + Err(_) => { + // old encoding with no version number and as an array of numbers + let bytes = self.read_value(CHANNEL_MANAGER_KEY)?; + Self::parse_channel_manager( + bytes, + chain_monitor, + mutiny_chain, + fee_estimator, + mutiny_logger, + keys_manager, + router, + channel_monitors, + ) + } } } + #[allow(clippy::too_many_arguments)] + fn parse_channel_manager( + bytes: Vec, + chain_monitor: Arc>, + mutiny_chain: Arc>, + fee_estimator: Arc>, + mutiny_logger: Arc, + keys_manager: Arc>, + router: Arc, + mut channel_monitors: Vec<(BlockHash, ChannelMonitor)>, + ) -> Result, MutinyError> { + let mut channel_monitor_mut_references = Vec::new(); + for (_, channel_monitor) in channel_monitors.iter_mut() { + channel_monitor_mut_references.push(channel_monitor); + } + let read_args = ChannelManagerReadArgs::new( + keys_manager.clone(), + keys_manager.clone(), + keys_manager, + fee_estimator, + chain_monitor, + mutiny_chain, + router, + mutiny_logger, + default_user_config(), + channel_monitor_mut_references, + ); + let mut readable_kv_value = Cursor::new(bytes); + let Ok((_, channel_manager)) = <(BlockHash, PhantomChannelManager)>::read(&mut readable_kv_value, read_args) else { + return Err(MutinyError::ReadError { source: MutinyStorageError::Other(anyhow!("could not read manager")) }) + }; + Ok(ReadChannelManager { + channel_manager, + is_restarting: true, + channel_monitors, + }) + } + #[allow(clippy::too_many_arguments)] pub(crate) async fn create_new_channel_manager( network: Network, @@ -517,8 +570,18 @@ impl &self, channel_manager: &PhantomChannelManager, ) -> Result<(), lightning::io::Error> { - // fixme add version - self.persist_local_storage(CHANNEL_MANAGER_KEY, channel_manager, None) + let mut version = self.manager_version.write().unwrap(); + *version += 1; + let key = self.get_key(CHANNEL_MANAGER_KEY); + + let value = VersionedValue { + version: *version, + value: serde_json::to_value(channel_manager.encode().to_hex()).unwrap(), + }; + + self.storage + .set_data(key, value, Some(*version)) + .map_err(|_| lightning::io::ErrorKind::Other.into()) } fn persist_graph(&self, network_graph: &NetworkGraph) -> Result<(), lightning::io::Error> { @@ -595,12 +658,24 @@ impl Persist> = Arc::new(ChainMonitor::new( + Some(chain.tx_sync.clone()), + chain.clone(), + logger.clone(), + fees.clone(), + persister.clone(), + )); + + let router: Arc = Arc::new(DefaultRouter::new( + network_graph, + logger.clone(), + km.clone().get_secure_random_bytes(), + Arc::new(utils::Mutex::new(scorer)), + ProbabilisticScoringFeeParameters::default(), + )); + + // make sure it correctly reads + let read = persister + .read_channel_manager( + network, + chain_monitor.clone(), + chain.clone(), + fees.clone(), + logger.clone(), + km.clone(), + router.clone(), + vec![], + esplora.clone(), + ) + .await + .unwrap(); + // starts at version 0 + assert_eq!(persister.manager_version(), 0); + assert!(read.is_restarting); + + // persist, should be new version + persister.persist_manager(&read.channel_manager).unwrap(); + assert_eq!(persister.manager_version(), 1); + + // make sure we can read with new encoding + let read = persister + .read_channel_manager( + network, + chain_monitor, + chain.clone(), + fees.clone(), + logger.clone(), + km, + router, + vec![], + esplora, + ) + .await + .unwrap(); + + // should be same version + assert_eq!(persister.manager_version(), 1); + assert!(read.is_restarting); + } } diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index e6b9aa6a6..8d0e55810 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -59,6 +59,12 @@ pub fn decrypt_value( Ok(json) } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VersionedValue { + pub version: u32, + pub value: Value, +} + pub trait MutinyStorage: Clone + Sized + 'static { /// Get the password used to encrypt the storage fn password(&self) -> Option<&str>; diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index 2da8fc836..b3c884acb 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -5,7 +5,7 @@ use lightning::util::logger::Logger; use lightning::{log_debug, log_error, log_trace}; use log::error; use mutiny_core::logging::MutinyLogger; -use mutiny_core::storage::{MutinyStorage, KEYCHAIN_STORE_KEY}; +use mutiny_core::storage::{MutinyStorage, VersionedValue, KEYCHAIN_STORE_KEY}; use mutiny_core::vss::*; use mutiny_core::*; use mutiny_core::{ @@ -221,8 +221,8 @@ impl IndexedDbStorage { key.version ); - // we can get versions from monitors, so we should compare - if key.key.contains("monitor") { + if key.key.contains(MONITORS_PREFIX_KEY) { + // we can get versions from monitors, so we should compare if let Some(current) = map.get(&key.key) { let bytes: Vec = serde_json::from_value(current.clone())?; // check first byte is 1, then take u64 from next 8 bytes @@ -234,6 +234,18 @@ impl IndexedDbStorage { } continue; } + } else if key.key.contains(CHANNEL_MANAGER_KEY) { + // we can get versions from channel manager, so we should compare + if let Some(value) = map.get(&key.key) { + if let Ok(versioned) = + serde_json::from_value::(value.clone()) + { + if versioned.version < key.version { + vss_map.insert(key.key, obj.value); + } + } + continue; + } } vss_map.insert(key.key, obj.value); From 6e3fcb9722d0fef0ffb8b7af29ca1da94b0f35b7 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 10 Jul 2023 18:23:14 -0500 Subject: [PATCH 08/10] Remove unwraps --- mutiny-core/src/encrypt.rs | 4 ++-- mutiny-core/src/error.rs | 20 ++++++++++++++++++++ mutiny-core/src/vss.rs | 21 ++++++++++++--------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/mutiny-core/src/encrypt.rs b/mutiny-core/src/encrypt.rs index 101a546aa..be4845172 100644 --- a/mutiny-core/src/encrypt.rs +++ b/mutiny-core/src/encrypt.rs @@ -57,7 +57,7 @@ pub fn encrypt(content: &str, c: Cipher) -> Result { } pub fn decrypt_with_password(encrypted: &str, password: &str) -> Result { - let encrypted = base64::decode(encrypted).map_err(|_| MutinyError::IncorrectPassword)?; + let encrypted = base64::decode(encrypted)?; if encrypted.len() < 12 + 16 { return Err(MutinyError::IncorrectPassword); } @@ -117,7 +117,7 @@ pub fn decrypt_with_key( let bytes = &bytes[..bytes.len() - 16]; let cipher = Aes256CbcDec::new(&encryption_key.secret_bytes().into(), iv.into()); - let decrypted: Vec = cipher.decrypt_padded_vec_mut::(bytes).unwrap(); + let decrypted: Vec = cipher.decrypt_padded_vec_mut::(bytes)?; Ok(decrypted) } diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index 1c8716c9e..318a91b27 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -1,9 +1,11 @@ use crate::esplora::TxSyncError; +use aes::cipher::block_padding::UnpadError; use bitcoin::Network; use lightning::ln::peer_handler::PeerHandleError; use lightning_invoice::payment::PaymentError; use lightning_invoice::ParseOrSemanticError; use lightning_rapid_gossip_sync::GraphSyncError; +use std::string::FromUtf8Error; use thiserror::Error; #[derive(Error, Debug)] @@ -155,6 +157,24 @@ impl MutinyError { } } +impl From for MutinyError { + fn from(_e: UnpadError) -> Self { + Self::IncorrectPassword + } +} + +impl From for MutinyError { + fn from(_e: base64::DecodeError) -> Self { + Self::IncorrectPassword + } +} + +impl From for MutinyError { + fn from(_e: FromUtf8Error) -> Self { + Self::IncorrectPassword + } +} + impl From for MutinyError { fn from(_: aes_gcm::Error) -> Self { Self::IncorrectPassword diff --git a/mutiny-core/src/vss.rs b/mutiny-core/src/vss.rs index d9a7f3852..acfecd1da 100644 --- a/mutiny-core/src/vss.rs +++ b/mutiny-core/src/vss.rs @@ -55,17 +55,20 @@ pub struct EncryptedVssKeyValueItem { } impl EncryptedVssKeyValueItem { - pub(crate) fn decrypt(self, encryption_key: &SecretKey) -> VssKeyValueItem { - let bytes = base64::decode(&self.value).unwrap(); - let decrypted = decrypt_with_key(encryption_key, bytes).unwrap(); - let decrypted_value = String::from_utf8(decrypted).unwrap(); - let value = serde_json::from_str(&decrypted_value).unwrap(); - - VssKeyValueItem { + pub(crate) fn decrypt( + self, + encryption_key: &SecretKey, + ) -> Result { + let bytes = base64::decode(&self.value)?; + let decrypted = decrypt_with_key(encryption_key, bytes)?; + let decrypted_value = String::from_utf8(decrypted)?; + let value = serde_json::from_str(&decrypted_value)?; + + Ok(VssKeyValueItem { key: self.key, value, version: self.version, - } + }) } } @@ -123,7 +126,7 @@ impl MutinyVssClient { MutinyError::Other(anyhow!("Error parsing get objects response: {e}")) })?; - Ok(result.decrypt(&self.encryption_key)) + result.decrypt(&self.encryption_key) } pub async fn list_key_versions( From 31a12d93b37562417c7b57fa7b4b800d11c0eaa1 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 10 Jul 2023 18:28:41 -0500 Subject: [PATCH 09/10] Encryption test --- mutiny-core/src/encrypt.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/mutiny-core/src/encrypt.rs b/mutiny-core/src/encrypt.rs index be4845172..7b528fd02 100644 --- a/mutiny-core/src/encrypt.rs +++ b/mutiny-core/src/encrypt.rs @@ -124,7 +124,11 @@ pub fn decrypt_with_key( #[cfg(test)] mod tests { - use crate::encrypt::{decrypt_with_password, encrypt, encryption_key_from_pass}; + use crate::encrypt::{ + decrypt_with_key, decrypt_with_password, encrypt, encrypt_with_key, + encryption_key_from_pass, + }; + use bitcoin::secp256k1::SecretKey; #[test] fn test_encryption() { @@ -139,4 +143,15 @@ mod tests { println!("{decrypted}"); assert_eq!(content, decrypted); } + + #[test] + fn test_encryption_with_key() { + let key = SecretKey::from_slice(&[1u8; 32]).unwrap(); + let content = [6u8; 32].to_vec(); + + let encrypted = encrypt_with_key(&key, &content); + + let decrypted = decrypt_with_key(&key, encrypted).unwrap(); + assert_eq!(content, decrypted); + } } From d25867267f9532d12044ce9f446f2d1e68897015 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 10 Jul 2023 18:38:10 -0500 Subject: [PATCH 10/10] AtomicU32 --- mutiny-core/src/ldkstorage.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/mutiny-core/src/ldkstorage.rs b/mutiny-core/src/ldkstorage.rs index 73c334d70..297cf83e7 100644 --- a/mutiny-core/src/ldkstorage.rs +++ b/mutiny-core/src/ldkstorage.rs @@ -36,7 +36,8 @@ use lightning::{ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::io; -use std::sync::{Arc, RwLock}; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; pub const CHANNEL_MANAGER_KEY: &str = "manager"; pub const MONITORS_PREFIX_KEY: &str = "monitors/"; @@ -61,7 +62,7 @@ pub(crate) type PhantomChannelManager = LdkChannelManager< pub struct MutinyNodePersister { node_id: String, pub(crate) storage: S, - manager_version: Arc>, + manager_version: Arc, logger: Arc, } @@ -76,14 +77,14 @@ impl MutinyNodePersister { MutinyNodePersister { node_id, storage, - manager_version: Arc::new(RwLock::new(0)), + manager_version: Arc::new(AtomicU32::new(0)), logger, } } #[cfg(test)] pub(crate) fn manager_version(&self) -> u32 { - *self.manager_version.read().unwrap() + self.manager_version.load(Ordering::Relaxed) } fn get_key(&self, key: &str) -> String { @@ -191,8 +192,8 @@ impl MutinyNodePersister { channel_monitors, )?; - let mut version = self.manager_version.write().unwrap(); - *version = versioned_value.version; + self.manager_version + .swap(versioned_value.version, Ordering::Relaxed); Ok(res) } @@ -570,17 +571,17 @@ impl &self, channel_manager: &PhantomChannelManager, ) -> Result<(), lightning::io::Error> { - let mut version = self.manager_version.write().unwrap(); - *version += 1; + let old = self.manager_version.fetch_add(1, Ordering::Relaxed); + let version = old + 1; let key = self.get_key(CHANNEL_MANAGER_KEY); let value = VersionedValue { - version: *version, + version, value: serde_json::to_value(channel_manager.encode().to_hex()).unwrap(), }; self.storage - .set_data(key, value, Some(*version)) + .set_data(key, value, Some(version)) .map_err(|_| lightning::io::ErrorKind::Other.into()) }