1 //! Objects related to [`FilesystemStore`] live here.
2 use crate::utils::{check_namespace_key_validity, is_valid_kvstore_str};
4 use lightning::util::persist::KVStore;
5 use lightning::util::string::PrintableString;
7 use std::collections::HashMap;
9 use std::io::{Read, Write};
10 use std::path::{Path, PathBuf};
11 use std::sync::atomic::{AtomicUsize, Ordering};
12 use std::sync::{Arc, Mutex, RwLock};
14 #[cfg(target_os = "windows")]
15 use {std::ffi::OsStr, std::os::windows::ffi::OsStrExt};
17 #[cfg(target_os = "windows")]
23 Err(std::io::Error::last_os_error())
28 #[cfg(target_os = "windows")]
29 fn path_to_windows_str<T: AsRef<OsStr>>(path: &T) -> Vec<u16> {
30 path.as_ref().encode_wide().chain(Some(0)).collect()
33 // The number of read/write/remove/list operations after which we clean up our `locks` HashMap.
34 const GC_LOCK_INTERVAL: usize = 25;
36 /// A [`KVStore`] implementation that writes to and reads from the file system.
37 pub struct FilesystemStore {
39 tmp_file_counter: AtomicUsize,
40 gc_counter: AtomicUsize,
41 locks: Mutex<HashMap<PathBuf, Arc<RwLock<()>>>>,
44 impl FilesystemStore {
45 /// Constructs a new [`FilesystemStore`].
46 pub fn new(data_dir: PathBuf) -> Self {
47 let locks = Mutex::new(HashMap::new());
48 let tmp_file_counter = AtomicUsize::new(0);
49 let gc_counter = AtomicUsize::new(1);
50 Self { data_dir, tmp_file_counter, gc_counter, locks }
53 /// Returns the data directory.
54 pub fn get_data_dir(&self) -> PathBuf {
58 fn garbage_collect_locks(&self) {
59 let gc_counter = self.gc_counter.fetch_add(1, Ordering::AcqRel);
61 if gc_counter % GC_LOCK_INTERVAL == 0 {
62 // Take outer lock for the cleanup.
63 let mut outer_lock = self.locks.lock().unwrap();
65 // Garbage collect all lock entries that are not referenced anymore.
66 outer_lock.retain(|_, v| Arc::strong_count(&v) > 1);
70 fn get_dest_dir_path(&self, primary_namespace: &str, secondary_namespace: &str) -> std::io::Result<PathBuf> {
71 let mut dest_dir_path = {
72 #[cfg(target_os = "windows")]
74 let data_dir = self.data_dir.clone();
75 fs::create_dir_all(data_dir.clone())?;
76 fs::canonicalize(data_dir)?
78 #[cfg(not(target_os = "windows"))]
84 dest_dir_path.push(primary_namespace);
85 if !secondary_namespace.is_empty() {
86 dest_dir_path.push(secondary_namespace);
93 impl KVStore for FilesystemStore {
94 fn read(&self, primary_namespace: &str, secondary_namespace: &str, key: &str) -> std::io::Result<Vec<u8>> {
95 check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?;
97 let mut dest_file_path = self.get_dest_dir_path(primary_namespace, secondary_namespace)?;
98 dest_file_path.push(key);
100 let mut buf = Vec::new();
102 let inner_lock_ref = {
103 let mut outer_lock = self.locks.lock().unwrap();
104 Arc::clone(&outer_lock.entry(dest_file_path.clone()).or_default())
106 let _guard = inner_lock_ref.read().unwrap();
108 let mut f = fs::File::open(dest_file_path)?;
109 f.read_to_end(&mut buf)?;
112 self.garbage_collect_locks();
117 fn write(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8]) -> std::io::Result<()> {
118 check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?;
120 let mut dest_file_path = self.get_dest_dir_path(primary_namespace, secondary_namespace)?;
121 dest_file_path.push(key);
123 let parent_directory = dest_file_path
127 format!("Could not retrieve parent directory of {}.", dest_file_path.display());
128 std::io::Error::new(std::io::ErrorKind::InvalidInput, msg)
130 fs::create_dir_all(&parent_directory)?;
132 // Do a crazy dance with lots of fsync()s to be overly cautious here...
133 // We never want to end up in a state where we've lost the old data, or end up using the
134 // old data on power loss after we've returned.
135 // The way to atomically write a file on Unix platforms is:
136 // open(tmpname), write(tmpfile), fsync(tmpfile), close(tmpfile), rename(), fsync(dir)
137 let mut tmp_file_path = dest_file_path.clone();
138 let tmp_file_ext = format!("{}.tmp", self.tmp_file_counter.fetch_add(1, Ordering::AcqRel));
139 tmp_file_path.set_extension(tmp_file_ext);
142 let mut tmp_file = fs::File::create(&tmp_file_path)?;
143 tmp_file.write_all(&buf)?;
144 tmp_file.sync_all()?;
148 let inner_lock_ref = {
149 let mut outer_lock = self.locks.lock().unwrap();
150 Arc::clone(&outer_lock.entry(dest_file_path.clone()).or_default())
152 let _guard = inner_lock_ref.write().unwrap();
154 #[cfg(not(target_os = "windows"))]
156 fs::rename(&tmp_file_path, &dest_file_path)?;
157 let dir_file = fs::OpenOptions::new().read(true).open(&parent_directory)?;
158 dir_file.sync_all()?;
162 #[cfg(target_os = "windows")]
164 let res = if dest_file_path.exists() {
166 windows_sys::Win32::Storage::FileSystem::ReplaceFileW(
167 path_to_windows_str(&dest_file_path).as_ptr(),
168 path_to_windows_str(&tmp_file_path).as_ptr(),
170 windows_sys::Win32::Storage::FileSystem::REPLACEFILE_IGNORE_MERGE_ERRORS,
171 std::ptr::null_mut() as *const core::ffi::c_void,
172 std::ptr::null_mut() as *const core::ffi::c_void,
177 windows_sys::Win32::Storage::FileSystem::MoveFileExW(
178 path_to_windows_str(&tmp_file_path).as_ptr(),
179 path_to_windows_str(&dest_file_path).as_ptr(),
180 windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH
181 | windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING,
188 // We fsync the dest file in hopes this will also flush the metadata to disk.
189 let dest_file = fs::OpenOptions::new().read(true).write(true)
190 .open(&dest_file_path)?;
191 dest_file.sync_all()?;
199 self.garbage_collect_locks();
204 fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> std::io::Result<()> {
205 check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?;
207 let mut dest_file_path = self.get_dest_dir_path(primary_namespace, secondary_namespace)?;
208 dest_file_path.push(key);
210 if !dest_file_path.is_file() {
215 let inner_lock_ref = {
216 let mut outer_lock = self.locks.lock().unwrap();
217 Arc::clone(&outer_lock.entry(dest_file_path.clone()).or_default())
219 let _guard = inner_lock_ref.write().unwrap();
222 // If we're lazy we just call remove and be done with it.
223 fs::remove_file(&dest_file_path)?;
225 // If we're not lazy we try our best to persist the updated metadata to ensure
226 // atomicity of this call.
227 #[cfg(not(target_os = "windows"))]
229 fs::remove_file(&dest_file_path)?;
231 let parent_directory = dest_file_path.parent().ok_or_else(|| {
233 format!("Could not retrieve parent directory of {}.", dest_file_path.display());
234 std::io::Error::new(std::io::ErrorKind::InvalidInput, msg)
236 let dir_file = fs::OpenOptions::new().read(true).open(parent_directory)?;
237 // The above call to `fs::remove_file` corresponds to POSIX `unlink`, whose changes
238 // to the inode might get cached (and hence possibly lost on crash), depending on
239 // the target platform and file system.
241 // In order to assert we permanently removed the file in question we therefore
242 // call `fsync` on the parent directory on platforms that support it.
243 dir_file.sync_all()?;
246 #[cfg(target_os = "windows")]
248 // Since Windows `DeleteFile` API is not persisted until the last open file handle
249 // is dropped, and there seemingly is no reliable way to flush the directory
250 // metadata, we here fall back to use a 'recycling bin' model, i.e., first move the
251 // file to be deleted to a temporary trash file and remove the latter file
254 // This should be marginally better, as, according to the documentation,
255 // `MoveFileExW` APIs should offer stronger persistence guarantees,
256 // at least if `MOVEFILE_WRITE_THROUGH`/`MOVEFILE_REPLACE_EXISTING` is set.
257 // However, all this is partially based on assumptions and local experiments, as
258 // Windows API is horribly underdocumented.
259 let mut trash_file_path = dest_file_path.clone();
260 let trash_file_ext = format!("{}.trash",
261 self.tmp_file_counter.fetch_add(1, Ordering::AcqRel));
262 trash_file_path.set_extension(trash_file_ext);
265 windows_sys::Win32::Storage::FileSystem::MoveFileExW(
266 path_to_windows_str(&dest_file_path).as_ptr(),
267 path_to_windows_str(&trash_file_path).as_ptr(),
268 windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH
269 | windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING,
274 // We fsync the trash file in hopes this will also flush the original's file
276 let trash_file = fs::OpenOptions::new().read(true).write(true)
277 .open(&trash_file_path.clone())?;
278 trash_file.sync_all()?;
281 // We're fine if this remove would fail as the trash file will be cleaned up in
283 fs::remove_file(trash_file_path).ok();
288 self.garbage_collect_locks();
293 fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> std::io::Result<Vec<String>> {
294 check_namespace_key_validity(primary_namespace, secondary_namespace, None, "list")?;
296 let prefixed_dest = self.get_dest_dir_path(primary_namespace, secondary_namespace)?;
297 let mut keys = Vec::new();
299 if !Path::new(&prefixed_dest).exists() {
300 return Ok(Vec::new());
303 for entry in fs::read_dir(&prefixed_dest)? {
305 let p = entry.path();
307 if let Some(ext) = p.extension() {
308 #[cfg(target_os = "windows")]
310 // Clean up any trash files lying around.
312 fs::remove_file(p).ok();
321 let metadata = p.metadata()?;
323 // We allow the presence of directories in the empty primary namespace and just skip them.
324 if metadata.is_dir() {
328 // If we otherwise don't find a file at the given path something went wrong.
329 if !metadata.is_file() {
330 debug_assert!(false, "Failed to list keys of {}/{}: file couldn't be accessed.",
331 PrintableString(primary_namespace), PrintableString(secondary_namespace));
332 let msg = format!("Failed to list keys of {}/{}: file couldn't be accessed.",
333 PrintableString(primary_namespace), PrintableString(secondary_namespace));
334 return Err(std::io::Error::new(std::io::ErrorKind::Other, msg));
337 match p.strip_prefix(&prefixed_dest) {
338 Ok(stripped_path) => {
339 if let Some(relative_path) = stripped_path.to_str() {
340 if is_valid_kvstore_str(relative_path) {
341 keys.push(relative_path.to_string())
344 debug_assert!(false, "Failed to list keys of {}/{}: file path is not valid UTF-8",
345 PrintableString(primary_namespace), PrintableString(secondary_namespace));
346 let msg = format!("Failed to list keys of {}/{}: file path is not valid UTF-8",
347 PrintableString(primary_namespace), PrintableString(secondary_namespace));
348 return Err(std::io::Error::new(std::io::ErrorKind::Other, msg));
352 debug_assert!(false, "Failed to list keys of {}/{}: {}",
353 PrintableString(primary_namespace), PrintableString(secondary_namespace), e);
354 let msg = format!("Failed to list keys of {}/{}: {}",
355 PrintableString(primary_namespace), PrintableString(secondary_namespace), e);
356 return Err(std::io::Error::new(std::io::ErrorKind::Other, msg));
361 self.garbage_collect_locks();
370 use crate::test_utils::{do_read_write_remove_list_persist, do_test_store};
372 use bitcoin::hashes::hex::FromHex;
375 use lightning::chain::ChannelMonitorUpdateStatus;
376 use lightning::chain::chainmonitor::Persist;
377 use lightning::chain::transaction::OutPoint;
378 use lightning::check_closed_event;
379 use lightning::events::{ClosureReason, MessageSendEventsProvider};
380 use lightning::ln::functional_test_utils::*;
381 use lightning::util::test_utils;
382 use lightning::util::persist::read_channel_monitors;
384 #[cfg(target_os = "windows")]
386 lightning::get_event_msg,
387 lightning::ln::msgs::ChannelMessageHandler,
390 impl Drop for FilesystemStore {
392 // We test for invalid directory names, so it's OK if directory removal
394 match fs::remove_dir_all(&self.data_dir) {
395 Err(e) => println!("Failed to remove test persister directory: {}", e),
402 fn read_write_remove_list_persist() {
403 let mut temp_path = std::env::temp_dir();
404 temp_path.push("test_read_write_remove_list_persist");
405 let fs_store = FilesystemStore::new(temp_path);
406 do_read_write_remove_list_persist(&fs_store);
410 fn test_if_monitors_is_not_dir() {
411 let store = FilesystemStore::new("test_monitors_is_not_dir".into());
413 fs::create_dir_all(&store.get_data_dir()).unwrap();
414 let mut path = std::path::PathBuf::from(&store.get_data_dir());
415 path.push("monitors");
416 fs::File::create(path).unwrap();
418 let chanmon_cfgs = create_chanmon_cfgs(1);
419 let mut node_cfgs = create_node_cfgs(1, &chanmon_cfgs);
420 let chain_mon_0 = test_utils::TestChainMonitor::new(Some(&chanmon_cfgs[0].chain_source), &chanmon_cfgs[0].tx_broadcaster, &chanmon_cfgs[0].logger, &chanmon_cfgs[0].fee_estimator, &store, node_cfgs[0].keys_manager);
421 node_cfgs[0].chain_monitor = chain_mon_0;
422 let node_chanmgrs = create_node_chanmgrs(1, &node_cfgs, &[None]);
423 let nodes = create_network(1, &node_cfgs, &node_chanmgrs);
425 // Check that read_channel_monitors() returns error if monitors/ is not a
427 assert!(read_channel_monitors(&store, nodes[0].keys_manager, nodes[0].keys_manager).is_err());
431 fn test_filesystem_store() {
432 // Create the nodes, giving them FilesystemStores for data stores.
433 let store_0 = FilesystemStore::new("test_filesystem_store_0".into());
434 let store_1 = FilesystemStore::new("test_filesystem_store_1".into());
435 do_test_store(&store_0, &store_1)
438 // Test that if the store's path to channel data is read-only, writing a
439 // monitor to it results in the store returning an UnrecoverableError.
440 // Windows ignores the read-only flag for folders, so this test is Unix-only.
441 #[cfg(not(target_os = "windows"))]
443 fn test_readonly_dir_perm_failure() {
444 let store = FilesystemStore::new("test_readonly_dir_perm_failure".into());
445 fs::create_dir_all(&store.get_data_dir()).unwrap();
447 // Set up a dummy channel and force close. This will produce a monitor
448 // that we can then use to test persistence.
449 let chanmon_cfgs = create_chanmon_cfgs(2);
450 let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
451 let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
452 let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
453 let chan = create_announced_chan_between_nodes(&nodes, 0, 1);
454 nodes[1].node.force_close_broadcasting_latest_txn(&chan.2, &nodes[0].node.get_our_node_id()).unwrap();
455 check_closed_event!(nodes[1], 1, ClosureReason::HolderForceClosed, [nodes[0].node.get_our_node_id()], 100000);
456 let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap();
457 let update_map = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap();
458 let update_id = update_map.get(&added_monitors[0].0.to_channel_id()).unwrap();
460 // Set the store's directory to read-only, which should result in
461 // returning an unrecoverable failure when we then attempt to persist a
463 let path = &store.get_data_dir();
464 let mut perms = fs::metadata(path).unwrap().permissions();
465 perms.set_readonly(true);
466 fs::set_permissions(path, perms).unwrap();
468 let test_txo = OutPoint {
469 txid: Txid::from_hex("8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be").unwrap(),
472 match store.persist_new_channel(test_txo, &added_monitors[0].1, update_id.2) {
473 ChannelMonitorUpdateStatus::UnrecoverableError => {},
474 _ => panic!("unexpected result from persisting new channel")
477 nodes[1].node.get_and_clear_pending_msg_events();
478 added_monitors.clear();
481 // Test that if a store's directory name is invalid, monitor persistence
483 #[cfg(target_os = "windows")]
485 fn test_fail_on_open() {
486 // Set up a dummy channel and force close. This will produce a monitor
487 // that we can then use to test persistence.
488 let chanmon_cfgs = create_chanmon_cfgs(2);
489 let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
490 let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
491 let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
492 let chan = create_announced_chan_between_nodes(&nodes, 0, 1);
493 nodes[1].node.force_close_broadcasting_latest_txn(&chan.2, &nodes[0].node.get_our_node_id()).unwrap();
494 check_closed_event!(nodes[1], 1, ClosureReason::HolderForceClosed, [nodes[0].node.get_our_node_id()], 100000);
495 let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap();
496 let update_map = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap();
497 let update_id = update_map.get(&added_monitors[0].0.to_channel_id()).unwrap();
499 // Create the store with an invalid directory name and test that the
500 // channel fails to open because the directories fail to be created. There
501 // don't seem to be invalid filename characters on Unix that Rust doesn't
502 // handle, hence why the test is Windows-only.
503 let store = FilesystemStore::new(":<>/".into());
505 let test_txo = OutPoint {
506 txid: Txid::from_hex("8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be").unwrap(),
509 match store.persist_new_channel(test_txo, &added_monitors[0].1, update_id.2) {
510 ChannelMonitorUpdateStatus::UnrecoverableError => {},
511 _ => panic!("unexpected result from persisting new channel")
514 nodes[1].node.get_and_clear_pending_msg_events();
515 added_monitors.clear();
522 use criterion::Criterion;
525 pub fn bench_sends(bench: &mut Criterion) {
526 let store_a = super::FilesystemStore::new("bench_filesystem_store_a".into());
527 let store_b = super::FilesystemStore::new("bench_filesystem_store_b".into());
528 lightning::ln::channelmanager::bench::bench_two_sends(
529 bench, "bench_filesystem_persisted_sends", store_a, store_b);