Skip to main content

lexe_api_core/
vfs.rs

1//! Virtual File System ('vfs')
2//!
3//! Our "virtual file system" is a simple way to represent a key-value store
4//! with optional namespacing by "directory". You can think of the `vfs` as a
5//! local directory that can contain files or directories, but where the
6//! directories cannot contain other directories (no nesting).
7//!
8//! Any file can be uniquely identified by its `<dirname>/<filename>`, and all
9//! files exclusively contain only binary data [`Vec<u8>`].
10//!
11//! Singleton objects like the channel manager are stored in the global
12//! namespace, e.g. at `./channel_manager` or `./bdk_wallet_db`
13//!
14//! Growable or shrinkable collections of objects (e.g. channel monitors), are
15//! stored in their own "directory", e.g. `channel_monitors/<funding_txo>`.
16
17use std::{
18    borrow::Cow,
19    fmt::{self, Display},
20    io::Cursor,
21};
22
23use anyhow::{Context, anyhow};
24use async_trait::async_trait;
25use lexe_serde::hexstr_or_bytes;
26use lightning::util::ser::{MaybeReadable, ReadableArgs, Writeable};
27use serde::{Deserialize, Serialize, de::DeserializeOwned};
28use tracing::{debug, warn};
29
30use crate::{error::BackendApiError, types::Empty};
31
32// --- Constants --- //
33
34/// The vfs directory name used by singleton objects.
35pub const SINGLETON_DIRECTORY: &str = ".";
36
37pub const BROADCASTED_TXS_DIR: &str = "broadcasted_txs";
38pub const CHANNEL_MONITORS_ARCHIVE_DIR: &str = "channel_monitors_archive";
39pub const CHANNEL_MONITORS_DIR: &str = "channel_monitors";
40pub const EVENTS_DIR: &str = "events";
41pub const MIGRATIONS_DIR: &str = "migrations";
42pub const UNSWEPT_OUTPUTS_EVENTS: &str = "unswept_outputs-events";
43pub const UNSWEPT_OUTPUTS_TXS: &str = "unswept_outputs-txs";
44
45pub const CHANNEL_MANAGER_FILENAME: &str = "channel_manager";
46pub const PW_ENC_ROOT_SEED_FILENAME: &str = "password_encrypted_root_seed";
47// Filename history:
48// - "bdk_wallet_db" for our pre BDK 1.0 wallet DB.
49// - "bdk_wallet_db_v1" for our BDK 1.0.0-alpha.X wallet DB.
50// - "bdk_wallet_changeset" since BDK 1.0.0-beta.X. (legacy descriptor)
51// - "bdk_wallet_changeset_v2" for > node-v0.9.1 (bip39-compat descriptor)
52pub const WALLET_CHANGESET_LEGACY_FILENAME: &str = "bdk_wallet_changeset";
53pub const WALLET_CHANGESET_V2_FILENAME: &str = "bdk_wallet_changeset_v2";
54
55pub static REVOCABLE_CLIENTS_FILE_ID: VfsFileId =
56    VfsFileId::new_const(SINGLETON_DIRECTORY, "revocable_clients");
57
58// --- Trait --- //
59
60/// Lexe's async persistence interface.
61// TODO(max): We'll eventually move all usage of this to VSS.
62#[async_trait]
63pub trait Vfs {
64    // --- Required methods --- //
65
66    /// Fetch the given [`VfsFile`] from the backend.
67    ///
68    /// Prefer [`Vfs::read_file`] which adds logging and error context.
69    async fn get_file(
70        &self,
71        file_id: &VfsFileId,
72    ) -> Result<Option<VfsFile>, BackendApiError>;
73
74    /// Upsert the given file to the backend with the given # of retries.
75    ///
76    /// Prefer [`Vfs::persist_file`] which adds logging and error context.
77    async fn upsert_file(
78        &self,
79        file_id: &VfsFileId,
80        data: bytes::Bytes,
81        retries: usize,
82    ) -> Result<Empty, BackendApiError>;
83
84    /// Deletes the [`VfsFile`] with the given [`VfsFileId`] from the backend.
85    ///
86    /// Prefer [`Vfs::remove_file`] which adds logging and error context.
87    async fn delete_file(
88        &self,
89        file_id: &VfsFileId,
90    ) -> Result<Empty, BackendApiError>;
91
92    /// List all filenames in the given [`VfsDirectory`].
93    ///
94    /// Returns a [`VfsDirectoryList`] containing the directory name and all
95    /// filenames.
96    async fn list_directory(
97        &self,
98        dir: &VfsDirectory,
99    ) -> Result<VfsDirectoryList, BackendApiError>;
100
101    /// Fetches all files in the given [`VfsDirectory`] from the backend.
102    ///
103    /// Prefer [`Vfs::read_dir_files`] which adds logging and error context.
104    async fn get_directory(
105        &self,
106        dir: &VfsDirectory,
107    ) -> Result<Vec<VfsFile>, BackendApiError> {
108        // Get all filenames in the directory
109        let directory_list = self.list_directory(dir).await?;
110
111        // Fetch all files concurrently
112        let fetch_futs = directory_list.filenames.into_iter().map(|filename| {
113            let file_id =
114                VfsFileId::new(directory_list.dirname.clone(), filename);
115            async move { self.get_file(&file_id).await }
116        });
117
118        // TODO(max): Would be nice to add a semaphore, but don't want
119        // `lexe-api-core` to depend on `tokio`.
120
121        let maybe_files = futures::future::try_join_all(fetch_futs).await?;
122        let files = maybe_files.into_iter().flatten().collect();
123
124        Ok(files)
125    }
126
127    /// Serialize `T` then encrypt it to a file under the VFS master key.
128    fn encrypt_json<T: Serialize>(
129        &self,
130        file_id: VfsFileId,
131        value: &T,
132    ) -> VfsFile;
133
134    /// Serialize a LDK [`Writeable`] then encrypt it under the VFS master key.
135    fn encrypt_ldk_writeable<W: Writeable>(
136        &self,
137        file_id: VfsFileId,
138        writeable: &W,
139    ) -> VfsFile;
140
141    /// Encrypt plaintext bytes to a file under the VFS master key.
142    ///
143    /// Prefer [`Vfs::encrypt_json`] and [`Vfs::encrypt_ldk_writeable`], since
144    /// those avoid the need to write to an intermediate plaintext buffer.
145    fn encrypt_bytes(
146        &self,
147        file_id: VfsFileId,
148        plaintext_bytes: &[u8],
149    ) -> VfsFile;
150
151    /// Decrypt a file previously encrypted under the VFS master key.
152    fn decrypt_file(
153        &self,
154        expected_file_id: &VfsFileId,
155        file: VfsFile,
156    ) -> anyhow::Result<Vec<u8>>;
157
158    // --- Provided methods --- //
159
160    /// Reads, decrypts, and JSON-deserializes a type `T` from the DB.
161    async fn read_json<T: DeserializeOwned>(
162        &self,
163        file_id: &VfsFileId,
164    ) -> anyhow::Result<Option<T>> {
165        let json_bytes = match self.read_bytes(file_id).await? {
166            Some(bytes) => bytes,
167            None => return Ok(None),
168        };
169        let value = serde_json::from_slice(json_bytes.as_slice())
170            .with_context(|| format!("{file_id}"))
171            .context("JSON deserialization failed")?;
172        Ok(Some(value))
173    }
174
175    /// Reads, decrypts, and JSON-deserializes a [`VfsDirectory`] of type `T`.
176    async fn read_dir_json<T: DeserializeOwned>(
177        &self,
178        dir: &VfsDirectory,
179    ) -> anyhow::Result<Vec<(VfsFileId, T)>> {
180        let ids_and_bytes = self.read_dir_bytes(dir).await?;
181        let mut ids_and_values = Vec::with_capacity(ids_and_bytes.len());
182        for (file_id, bytes) in ids_and_bytes {
183            let value = serde_json::from_slice(bytes.as_slice())
184                .with_context(|| format!("{file_id}"))
185                .context("JSON deserialization failed (in dir)")?;
186            ids_and_values.push((file_id, value));
187        }
188        Ok(ids_and_values)
189    }
190
191    /// Reads, decrypts, and deserializes a LDK [`ReadableArgs`] of type `T`
192    /// with read args `A` from the DB.
193    async fn read_readableargs<T, A>(
194        &self,
195        file_id: &VfsFileId,
196        read_args: A,
197    ) -> anyhow::Result<Option<T>>
198    where
199        T: ReadableArgs<A>,
200        A: Send,
201    {
202        let bytes = match self.read_bytes(file_id).await? {
203            Some(b) => b,
204            None => return Ok(None),
205        };
206
207        let value = Self::deser_readableargs(file_id, &bytes, read_args)?;
208
209        Ok(Some(value))
210    }
211
212    /// Reads, decrypts, and deserializes a [`VfsDirectory`] of LDK
213    /// [`MaybeReadable`]s from the DB, along with their [`VfsFileId`]s.
214    /// [`None`] values are omitted from the result.
215    async fn read_dir_maybereadable<T: MaybeReadable>(
216        &self,
217        dir: &VfsDirectory,
218    ) -> anyhow::Result<Vec<(VfsFileId, T)>> {
219        let ids_and_bytes = self.read_dir_bytes(dir).await?;
220        let mut ids_and_values = Vec::with_capacity(ids_and_bytes.len());
221        for (file_id, bytes) in ids_and_bytes {
222            let mut reader = Cursor::new(&bytes);
223            let maybe_value = T::read(&mut reader)
224                .map_err(|e| anyhow!("{e:?}"))
225                .with_context(|| format!("{file_id}"))
226                .context("LDK MaybeReadable deserialization failed (in dir)")?;
227            if let Some(event) = maybe_value {
228                ids_and_values.push((file_id, event));
229            }
230        }
231        Ok(ids_and_values)
232    }
233
234    /// Reads and decrypts [`VfsFile`] bytes from the DB.
235    async fn read_bytes(
236        &self,
237        file_id: &VfsFileId,
238    ) -> anyhow::Result<Option<Vec<u8>>> {
239        match self.read_file(file_id).await? {
240            Some(file) => {
241                let data = self.decrypt_file(file_id, file)?;
242                Ok(Some(data))
243            }
244            None => Ok(None),
245        }
246    }
247
248    /// Reads and decrypts all files in the given [`VfsDirectory`] from the DB,
249    /// returning the [`VfsFileId`] and plaintext bytes for each file.
250    async fn read_dir_bytes(
251        &self,
252        dir: &VfsDirectory,
253    ) -> anyhow::Result<Vec<(VfsFileId, Vec<u8>)>> {
254        let files = self.read_dir_files(dir).await?;
255        let file_ids_and_bytes = files
256            .into_iter()
257            .map(|file| {
258                // Get the expected dirname from params but filename from DB
259                let expected_file_id = VfsFileId::new(
260                    dir.dirname.clone(),
261                    file.id.filename.clone(),
262                );
263                let bytes = self.decrypt_file(&expected_file_id, file)?;
264                Ok((expected_file_id, bytes))
265            })
266            .collect::<anyhow::Result<Vec<(VfsFileId, Vec<u8>)>>>()?;
267        Ok(file_ids_and_bytes)
268    }
269
270    /// Wraps [`Vfs::get_file`] to add logging and error context.
271    async fn read_file(
272        &self,
273        file_id: &VfsFileId,
274    ) -> anyhow::Result<Option<VfsFile>> {
275        debug!("Reading file {file_id}");
276        let result = self
277            .get_file(file_id)
278            .await
279            .with_context(|| format!("Couldn't fetch file from DB: {file_id}"));
280
281        if result.is_ok() {
282            debug!("Done: Read {file_id}");
283        } else {
284            warn!("Error: Failed to read {file_id}");
285        }
286        result
287    }
288
289    /// Wraps [`Vfs::get_directory`] to add logging and error context.
290    async fn read_dir_files(
291        &self,
292        dir: &VfsDirectory,
293    ) -> anyhow::Result<Vec<VfsFile>> {
294        debug!("Reading directory {dir}");
295        let result = self
296            .get_directory(dir)
297            .await
298            .with_context(|| format!("Couldn't fetch VFS dir from DB: {dir}"));
299
300        if result.is_ok() {
301            debug!("Done: Read directory {dir}");
302        } else {
303            warn!("Error: Failed to read directory {dir}");
304        }
305        result
306    }
307
308    /// Deserializes a LDK [`ReadableArgs`] of type `T` from bytes.
309    fn deser_readableargs<T, A>(
310        file_id: &VfsFileId,
311        bytes: &[u8],
312        read_args: A,
313    ) -> anyhow::Result<T>
314    where
315        T: ReadableArgs<A>,
316        A: Send,
317    {
318        let mut reader = Cursor::new(bytes);
319        let value = T::read(&mut reader, read_args)
320            .map_err(|e| anyhow!("{e:?}"))
321            .with_context(|| format!("{file_id}"))
322            .context("LDK ReadableArgs deserialization failed")?;
323        Ok(value)
324    }
325
326    /// JSON-serializes, encrypts, then persists a type `T` to the DB.
327    async fn persist_json<T: Serialize + Send + Sync>(
328        &self,
329        file_id: VfsFileId,
330        value: &T,
331        retries: usize,
332    ) -> anyhow::Result<()> {
333        let file = self.encrypt_json::<T>(file_id, value);
334        self.persist_file(file, retries).await
335    }
336
337    /// Serializes, encrypts, then persists a LDK [`Writeable`] to the DB.
338    async fn persist_ldk_writeable<W: Writeable + Send + Sync>(
339        &self,
340        file_id: VfsFileId,
341        writeable: &W,
342        retries: usize,
343    ) -> anyhow::Result<()> {
344        let file = self.encrypt_ldk_writeable(file_id, writeable);
345        self.persist_file(file, retries).await
346    }
347
348    /// Encrypts plaintext bytes then persists them to the DB.
349    ///
350    /// Prefer [`Vfs::persist_json`] and [`Vfs::persist_ldk_writeable`], since
351    /// those avoid the need to write to an intermediate plaintext buffer.
352    async fn persist_bytes(
353        &self,
354        file_id: VfsFileId,
355        plaintext_bytes: &[u8],
356        retries: usize,
357    ) -> anyhow::Result<()> {
358        let file = self.encrypt_bytes(file_id, plaintext_bytes);
359        self.persist_file(file, retries).await
360    }
361
362    /// Wraps [`Vfs::upsert_file`] to add logging and error context.
363    async fn persist_file(
364        &self,
365        file: VfsFile,
366        retries: usize,
367    ) -> anyhow::Result<()> {
368        let file_id = &file.id;
369        let bytes = file.data.len();
370        debug!("Persisting file {file_id} <{bytes} bytes>");
371
372        let result = self
373            .upsert_file(&file.id, file.data.into(), retries)
374            .await
375            .map(|_| ())
376            .with_context(|| format!("Couldn't persist file to DB: {file_id}"));
377
378        if result.is_ok() {
379            debug!("Done: Persisted {file_id} <{bytes} bytes>");
380        } else {
381            warn!("Error: Failed to persist {file_id}  <{bytes} bytes>");
382        }
383        result
384    }
385
386    /// Wraps [`Vfs::delete_file`] to add logging and error context.
387    async fn remove_file(&self, file_id: &VfsFileId) -> anyhow::Result<()> {
388        debug!("Deleting file {file_id}");
389        let result = self
390            .delete_file(file_id)
391            .await
392            .map(|_| ())
393            .with_context(|| format!("{file_id}"))
394            .context("Couldn't delete file from DB");
395
396        if result.is_ok() {
397            debug!("Done: Deleted {file_id}");
398        } else {
399            warn!("Error: Failed to delete {file_id}");
400        }
401        result
402    }
403}
404
405// --- Types --- //
406
407/// Uniquely identifies a directory in the virtual file system.
408///
409/// This struct exists mainly so that `serde_qs` can use it as a query parameter
410/// struct to fetch files by directory.
411#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
412#[derive(Serialize, Deserialize)]
413pub struct VfsDirectory {
414    pub dirname: Cow<'static, str>,
415}
416
417/// Uniquely identifies a file in the virtual file system.
418///
419/// This struct exists mainly so that `serde_qs` can use it as a query parameter
420/// struct to fetch files by id.
421#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
422#[derive(Serialize, Deserialize)]
423pub struct VfsFileId {
424    // Flattened because serde_qs requires non-nested structs
425    #[serde(flatten)]
426    pub dir: VfsDirectory,
427    pub filename: Cow<'static, str>,
428}
429
430/// Represents a file in the virtual file system. The `data` field is almost
431/// always encrypted.
432#[derive(Clone, Debug, Eq, PartialEq)]
433#[derive(Serialize, Deserialize)]
434pub struct VfsFile {
435    #[serde(flatten)]
436    pub id: VfsFileId,
437    #[serde(with = "hexstr_or_bytes")]
438    pub data: Vec<u8>,
439}
440
441/// An upgradeable version of [`Option<VfsFile>`].
442#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
443pub struct MaybeVfsFile {
444    pub maybe_file: Option<VfsFile>,
445}
446
447/// An upgradeable version of [`Vec<VfsFile>`].
448#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
449pub struct VecVfsFile {
450    pub files: Vec<VfsFile>,
451}
452
453/// A list of all filenames within a [`VfsDirectory`].
454#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
455pub struct VfsDirectoryList {
456    pub dirname: Cow<'static, str>,
457    pub filenames: Vec<String>,
458}
459
460/// An upgradeable version of [`Vec<VfsFileId>`].
461// TODO(max): Use basically VfsDirectory but with a Vec of filenames
462#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
463pub struct VecVfsFileId {
464    pub file_ids: Vec<VfsFileId>,
465}
466
467impl VfsDirectory {
468    pub fn new(dirname: impl Into<Cow<'static, str>>) -> Self {
469        Self {
470            dirname: dirname.into(),
471        }
472    }
473
474    pub const fn new_const(dirname: &'static str) -> Self {
475        Self {
476            dirname: Cow::Borrowed(dirname),
477        }
478    }
479}
480
481impl VfsFileId {
482    pub fn new(
483        dirname: impl Into<Cow<'static, str>>,
484        filename: impl Into<Cow<'static, str>>,
485    ) -> Self {
486        Self {
487            dir: VfsDirectory {
488                dirname: dirname.into(),
489            },
490            filename: filename.into(),
491        }
492    }
493
494    pub const fn new_const(
495        dirname: &'static str,
496        filename: &'static str,
497    ) -> Self {
498        Self {
499            dir: VfsDirectory {
500                dirname: Cow::Borrowed(dirname),
501            },
502            filename: Cow::Borrowed(filename),
503        }
504    }
505}
506
507impl VfsFile {
508    pub fn new(
509        dirname: impl Into<Cow<'static, str>>,
510        filename: impl Into<Cow<'static, str>>,
511        data: Vec<u8>,
512    ) -> Self {
513        Self {
514            id: VfsFileId {
515                dir: VfsDirectory {
516                    dirname: dirname.into(),
517                },
518                filename: filename.into(),
519            },
520            data,
521        }
522    }
523
524    /// Prefer to use this constructor because `Into<Vec<u8>>` may have useful
525    /// optimizations. For example, [`bytes::Bytes`] avoids a copy if the
526    /// refcount is 1, but AIs like to use `bytes.to_vec()` which always copies.
527    pub fn from_parts(id: VfsFileId, data: impl Into<Vec<u8>>) -> Self {
528        Self {
529            id,
530            data: data.into(),
531        }
532    }
533}
534
535impl Display for VfsDirectory {
536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537        write!(f, "{dirname}", dirname = self.dirname)
538    }
539}
540
541impl Display for VfsFileId {
542    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543        let dirname = &self.dir.dirname;
544        let filename = &self.filename;
545        write!(f, "{dirname}/{filename}")
546    }
547}
548
549// --- impl Arbitrary --- //
550
551#[cfg(any(test, feature = "test-utils"))]
552mod prop {
553    use lexe_common::test_utils::arbitrary;
554    use proptest::{
555        arbitrary::{Arbitrary, any},
556        strategy::{BoxedStrategy, Strategy},
557    };
558
559    use super::*;
560
561    impl Arbitrary for VfsDirectory {
562        type Strategy = BoxedStrategy<Self>;
563        type Parameters = ();
564
565        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
566            arbitrary::any_string().prop_map(VfsDirectory::new).boxed()
567        }
568    }
569
570    impl Arbitrary for VfsFileId {
571        type Strategy = BoxedStrategy<Self>;
572        type Parameters = ();
573
574        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
575            (any::<VfsDirectory>(), arbitrary::any_string())
576                .prop_map(|(dir, filename)| VfsFileId {
577                    dir,
578                    filename: filename.into(),
579                })
580                .boxed()
581        }
582    }
583}
584
585#[cfg(test)]
586mod test {
587    use lexe_common::test_utils::roundtrip;
588
589    use super::*;
590
591    #[test]
592    fn vfs_directory_roundtrip() {
593        roundtrip::query_string_roundtrip_proptest::<VfsDirectory>();
594    }
595
596    #[test]
597    fn vfs_file_id_roundtrip() {
598        roundtrip::query_string_roundtrip_proptest::<VfsFileId>();
599    }
600}