Compare commits
4 Commits
a6a80dc224
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a178c8d08 | |||
| b780587d26 | |||
| ae059a7238 | |||
| 43966f6aae |
@@ -132,8 +132,16 @@ impl Command for ClearCommand {
|
|||||||
"Clear the wallet state, rolling back the wallet to an empty state.".to_string()
|
"Clear the wallet state, rolling back the wallet to an empty state.".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
|
||||||
lightclient.clear_state();
|
if !args.is_empty() {
|
||||||
|
if let Ok(height) = args[0].parse::<u64>() {
|
||||||
|
lightclient.clear_state_from(height);
|
||||||
|
} else {
|
||||||
|
return format!("Error: invalid height '{}'", args[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lightclient.clear_state();
|
||||||
|
}
|
||||||
|
|
||||||
let result = object!{ "result" => "success" };
|
let result = object!{ "result" => "success" };
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use crate::lightwallet::LightWallet;
|
use crate::lightwallet::LightWallet;
|
||||||
|
|
||||||
use std::sync::{Arc, RwLock, Mutex, mpsc::channel};
|
use std::sync::{Arc, RwLock, Mutex, mpsc::channel};
|
||||||
use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::collections::{HashSet, HashMap};
|
use std::collections::{HashSet, HashMap};
|
||||||
@@ -307,6 +307,7 @@ pub struct LightClient {
|
|||||||
|
|
||||||
sync_lock : Mutex<()>,
|
sync_lock : Mutex<()>,
|
||||||
sync_status : Arc<RwLock<WalletStatus>>, // The current syncing status of the Wallet.
|
sync_status : Arc<RwLock<WalletStatus>>, // The current syncing status of the Wallet.
|
||||||
|
pub shutdown_flag : Arc<AtomicBool>, // Signal mempool threads to stop
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LightClient {
|
impl LightClient {
|
||||||
@@ -392,6 +393,7 @@ impl LightClient {
|
|||||||
sapling_spend : vec![],
|
sapling_spend : vec![],
|
||||||
sync_lock : Mutex::new(()),
|
sync_lock : Mutex::new(()),
|
||||||
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
||||||
|
shutdown_flag : Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
l.set_wallet_initial_state(0);
|
l.set_wallet_initial_state(0);
|
||||||
@@ -420,6 +422,7 @@ impl LightClient {
|
|||||||
sapling_spend : vec![],
|
sapling_spend : vec![],
|
||||||
sync_lock : Mutex::new(()),
|
sync_lock : Mutex::new(()),
|
||||||
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
||||||
|
shutdown_flag : Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
l.set_wallet_initial_state(latest_block);
|
l.set_wallet_initial_state(latest_block);
|
||||||
@@ -447,6 +450,7 @@ impl LightClient {
|
|||||||
sapling_spend : vec![],
|
sapling_spend : vec![],
|
||||||
sync_lock : Mutex::new(()),
|
sync_lock : Mutex::new(()),
|
||||||
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
||||||
|
shutdown_flag : Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// println!("Setting birthday to {}", birthday);
|
// println!("Setting birthday to {}", birthday);
|
||||||
@@ -464,13 +468,38 @@ impl LightClient {
|
|||||||
|
|
||||||
pub fn read_from_disk(config: &LightClientConfig) -> io::Result<Self> {
|
pub fn read_from_disk(config: &LightClientConfig) -> io::Result<Self> {
|
||||||
if !config.wallet_exists() {
|
if !config.wallet_exists() {
|
||||||
return Err(Error::new(ErrorKind::AlreadyExists,
|
// Try to recover from backup
|
||||||
format!("Cannot read wallet. No file at {}", config.get_wallet_path().display())));
|
let bak_path = config.get_wallet_path().with_extension("dat.bak");
|
||||||
|
if bak_path.exists() {
|
||||||
|
warn!("Wallet file missing but backup found, attempting recovery from {:?}", bak_path);
|
||||||
|
std::fs::copy(&bak_path, config.get_wallet_path())?;
|
||||||
|
info!("Wallet recovered from backup");
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(ErrorKind::AlreadyExists,
|
||||||
|
format!("Cannot read wallet. No file at {}", config.get_wallet_path().display())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?);
|
// Try to open the wallet file; if it fails (corrupted/truncated), try the backup
|
||||||
|
let wallet = match File::open(config.get_wallet_path())
|
||||||
let wallet = LightWallet::read(&mut file_buffer, config)?;
|
.and_then(|f| {
|
||||||
|
let mut file_buffer = BufReader::new(f);
|
||||||
|
LightWallet::read(&mut file_buffer, config)
|
||||||
|
}) {
|
||||||
|
Ok(w) => w,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to read wallet file: {}, trying backup", e);
|
||||||
|
let bak_path = config.get_wallet_path().with_extension("dat.bak");
|
||||||
|
if bak_path.exists() {
|
||||||
|
std::fs::copy(&bak_path, config.get_wallet_path())?;
|
||||||
|
let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?);
|
||||||
|
LightWallet::read(&mut file_buffer, config)?
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut lc = LightClient {
|
let mut lc = LightClient {
|
||||||
wallet : Arc::new(RwLock::new(wallet)),
|
wallet : Arc::new(RwLock::new(wallet)),
|
||||||
config : config.clone(),
|
config : config.clone(),
|
||||||
@@ -478,6 +507,7 @@ impl LightClient {
|
|||||||
sapling_spend : vec![],
|
sapling_spend : vec![],
|
||||||
sync_lock : Mutex::new(()),
|
sync_lock : Mutex::new(()),
|
||||||
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
||||||
|
shutdown_flag : Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "embed_params")]
|
#[cfg(feature = "embed_params")]
|
||||||
@@ -682,22 +712,54 @@ impl LightClient {
|
|||||||
// Prevent any overlapping syncs during save, and don't save in the middle of a sync
|
// Prevent any overlapping syncs during save, and don't save in the middle of a sync
|
||||||
let _lock = self.sync_lock.lock().unwrap();
|
let _lock = self.sync_lock.lock().unwrap();
|
||||||
|
|
||||||
|
let wallet_path = self.config.get_wallet_path();
|
||||||
|
let tmp_path = wallet_path.with_extension("dat.tmp");
|
||||||
|
let bak_path = wallet_path.with_extension("dat.bak");
|
||||||
|
|
||||||
|
// Write to a temporary file first (atomic save)
|
||||||
let wallet = self.wallet.write().unwrap();
|
let wallet = self.wallet.write().unwrap();
|
||||||
let mut file_buffer = BufWriter::with_capacity(
|
let mut file_buffer = BufWriter::with_capacity(
|
||||||
1_000_000, // 1 MB write buffer
|
1_000_000, // 1 MB write buffer
|
||||||
File::create(self.config.get_wallet_path()).unwrap());
|
File::create(&tmp_path).map_err(|e| format!("Failed to create temp file: {}", e))?);
|
||||||
|
|
||||||
r = match wallet.write(&mut file_buffer) {
|
r = match wallet.write(&mut file_buffer) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err = format!("ERR: {}", e);
|
let err = format!("ERR: {}", e);
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
|
// Clean up temp file on write failure
|
||||||
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
Err(e.to_string())
|
Err(e.to_string())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
file_buffer.flush().map_err(|e| format!("{}", e))?;
|
file_buffer.flush().map_err(|e| {
|
||||||
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
|
format!("{}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Only proceed with rename if the write succeeded
|
||||||
|
if r.is_ok() {
|
||||||
|
// Create backup of existing wallet (if it exists)
|
||||||
|
if wallet_path.exists() {
|
||||||
|
if let Err(e) = std::fs::copy(&wallet_path, &bak_path) {
|
||||||
|
warn!("Failed to create wallet backup: {}", e);
|
||||||
|
// Non-fatal: continue with the save
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomically replace the wallet file with the new one
|
||||||
|
if let Err(e) = std::fs::rename(&tmp_path, &wallet_path) {
|
||||||
|
error!("Failed to rename temp wallet file: {}", e);
|
||||||
|
// Try direct copy as fallback (rename can fail across filesystems)
|
||||||
|
if let Err(e2) = std::fs::copy(&tmp_path, &wallet_path) {
|
||||||
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
|
return Err(format!("Failed to save wallet: {} / {}", e, e2));
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r
|
r
|
||||||
}
|
}
|
||||||
@@ -1071,12 +1133,17 @@ impl LightClient {
|
|||||||
pub fn start_mempool_monitor(lc: Arc<LightClient>) -> Result<(), String> {
|
pub fn start_mempool_monitor(lc: Arc<LightClient>) -> Result<(), String> {
|
||||||
let config = lc.config.clone();
|
let config = lc.config.clone();
|
||||||
let uri = config.server.clone();
|
let uri = config.server.clone();
|
||||||
|
let shutdown = lc.shutdown_flag.clone();
|
||||||
|
|
||||||
let (incoming_mempool_tx, incoming_mempool_rx) = std::sync::mpsc::channel::<RawTransaction>();
|
let (incoming_mempool_tx, incoming_mempool_rx) = std::sync::mpsc::channel::<RawTransaction>();
|
||||||
|
|
||||||
// Thread for reveive transactions
|
// Thread for receive transactions
|
||||||
|
let shutdown_rx = lc.shutdown_flag.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
while let Ok(rtx) = incoming_mempool_rx.recv() {
|
while let Ok(rtx) = incoming_mempool_rx.recv() {
|
||||||
|
if shutdown_rx.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
if let Ok(tx) = Transaction::read(
|
if let Ok(tx) = Transaction::read(
|
||||||
&rtx.data[..])
|
&rtx.data[..])
|
||||||
{
|
{
|
||||||
@@ -1091,6 +1158,11 @@ pub fn start_mempool_monitor(lc: Arc<LightClient>) -> Result<(), String> {
|
|||||||
let mut rt = Runtime::new().unwrap();
|
let mut rt = Runtime::new().unwrap();
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
loop {
|
loop {
|
||||||
|
if shutdown.load(Ordering::Relaxed) {
|
||||||
|
info!("Mempool monitor shutting down");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
let incoming_mempool_tx_clone = incoming_mempool_tx.clone();
|
let incoming_mempool_tx_clone = incoming_mempool_tx.clone();
|
||||||
let send_closure = move |rtx: RawTransaction| {
|
let send_closure = move |rtx: RawTransaction| {
|
||||||
incoming_mempool_tx_clone.send(rtx).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
incoming_mempool_tx_clone.send(rtx).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
@@ -1101,13 +1173,26 @@ pub fn start_mempool_monitor(lc: Arc<LightClient>) -> Result<(), String> {
|
|||||||
Err(e) => warn!("Mempool monitor returned {:?}, will restart listening", e),
|
Err(e) => warn!("Mempool monitor returned {:?}, will restart listening", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
std::thread::sleep(Duration::from_secs(10));
|
// Sleep in 1-second increments so we can check shutdown flag
|
||||||
|
for _ in 0..10 {
|
||||||
|
if shutdown.load(Ordering::Relaxed) {
|
||||||
|
info!("Mempool monitor shutting down during sleep");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Signal all background threads to stop
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
self.shutdown_flag.store(true, Ordering::Relaxed);
|
||||||
|
info!("Shutdown flag set");
|
||||||
|
}
|
||||||
/// Convinence function to determine what type of key this is and import it
|
/// Convinence function to determine what type of key this is and import it
|
||||||
pub fn do_import_key(&self, key: String, birthday: u64) -> Result<JsonValue, String> {
|
pub fn do_import_key(&self, key: String, birthday: u64) -> Result<JsonValue, String> {
|
||||||
if key.starts_with(self.config.hrp_sapling_private_key()) {
|
if key.starts_with(self.config.hrp_sapling_private_key()) {
|
||||||
@@ -1189,12 +1274,16 @@ pub fn start_mempool_monitor(lc: Arc<LightClient>) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_state(&self) {
|
pub fn clear_state(&self) {
|
||||||
|
self.clear_state_from(self.wallet.read().unwrap().get_birthday());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_state_from(&self, height: u64) {
|
||||||
// First, clear the state from the wallet
|
// First, clear the state from the wallet
|
||||||
self.wallet.read().unwrap().clear_blocks();
|
self.wallet.read().unwrap().clear_blocks();
|
||||||
|
|
||||||
// Then set the initial block
|
// Then set the initial block
|
||||||
self.set_wallet_initial_state(self.wallet.read().unwrap().get_birthday());
|
self.set_wallet_initial_state(height);
|
||||||
info!("Cleared wallet state");
|
info!("Cleared wallet state to height {}", height);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_rescan(&self) -> Result<JsonValue, String> {
|
pub fn do_rescan(&self) -> Result<JsonValue, String> {
|
||||||
@@ -1714,6 +1803,7 @@ pub mod tests {
|
|||||||
sapling_spend : vec![],
|
sapling_spend : vec![],
|
||||||
sync_lock : Mutex::new(()),
|
sync_lock : Mutex::new(()),
|
||||||
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
|
||||||
|
shutdown_flag : Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
let addresses = lc.do_address();
|
let addresses = lc.do_address();
|
||||||
|
|||||||
@@ -1452,11 +1452,12 @@ pub fn scan_full_tx(&self, tx: &Transaction, height: i32, datetime: u64) {
|
|||||||
// Also scan the output to see if it can be decoded with our OutgoingViewKey
|
// Also scan the output to see if it can be decoded with our OutgoingViewKey
|
||||||
// If it can, then we sent this transaction, so we should be able to get
|
// If it can, then we sent this transaction, so we should be able to get
|
||||||
// the memo and value for our records
|
// the memo and value for our records
|
||||||
// First, collect all our z addresses, to check for change
|
|
||||||
// Collect z addresses
|
// Collect IVKs to detect change outputs to diversified addresses
|
||||||
let z_addresses = self.zkeys.read().unwrap().iter().map( |zk| {
|
let ivks_for_change: Vec<_> = self.zkeys.read().unwrap().iter()
|
||||||
encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress)
|
.map(|zk| zk.extfvk.fvk.vk.ivk())
|
||||||
}).collect::<HashSet<String>>();
|
.collect();
|
||||||
|
|
||||||
// Search all ovks that we have
|
// Search all ovks that we have
|
||||||
let ovks: Vec<_> = self.zkeys.read().unwrap().iter()
|
let ovks: Vec<_> = self.zkeys.read().unwrap().iter()
|
||||||
.map(|zk| zk.extfvk.fvk.ovk.clone())
|
.map(|zk| zk.extfvk.fvk.ovk.clone())
|
||||||
@@ -1472,14 +1473,26 @@ pub fn scan_full_tx(&self, tx: &Transaction, height: i32, datetime: u64) {
|
|||||||
Some((note, payment_address, memo)) => {
|
Some((note, payment_address, memo)) => {
|
||||||
let address = encode_payment_address(self.config.hrp_sapling_address(),
|
let address = encode_payment_address(self.config.hrp_sapling_address(),
|
||||||
&payment_address);
|
&payment_address);
|
||||||
// Check if this is change, and if it also doesn't have a memo, don't add
|
|
||||||
// to the outgoing metadata.
|
// Check if this output belongs to our wallet using IVK decryption.
|
||||||
// If this is change (i.e., funds sent to ourself) AND has a memo, then
|
// This correctly detects change sent to diversified addresses,
|
||||||
// presumably the users is writing a memo to themself, so we will add it to
|
// not just the wallet's default z-address.
|
||||||
// the outgoing metadata, even though it might be confusing in the UI, but hopefully
|
let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap();
|
||||||
// the user can make sense of it.
|
let is_to_self = ivks_for_change.iter().any(|ivk| {
|
||||||
if z_addresses.contains(&address) && memo.to_utf8().is_none() {
|
try_sapling_note_decryption(ivk, &epk_prime, &output.cmu, &output.enc_ciphertext).is_some()
|
||||||
continue;
|
});
|
||||||
|
|
||||||
|
// If this is change (funds to ourself) without a meaningful memo, skip it.
|
||||||
|
// memo.to_utf8() returns Some(Ok("")) for empty memos (all-zero bytes),
|
||||||
|
// so we must also check for empty strings, not just None.
|
||||||
|
if is_to_self {
|
||||||
|
let has_memo = match memo.to_utf8() {
|
||||||
|
Some(Ok(ref s)) if !s.is_empty() => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if !has_memo {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Update the WalletTx
|
// Update the WalletTx
|
||||||
// Do it in a short scope because of the write lock.
|
// Do it in a short scope because of the write lock.
|
||||||
|
|||||||
Reference in New Issue
Block a user