Browse Source

event_graph: replaced sled-overlay with kvdb-overlay

x 4 days ago
parent
commit
77971baf09

+ 3 - 3
Cargo.lock

@@ -2291,6 +2291,7 @@ dependencies = [
  "easy-parallel",
  "futures",
  "futures-rustls",
+ "kvdb-overlay",
  "libc",
  "rand 0.8.6",
  "rustls-pemfile",
@@ -2298,7 +2299,6 @@ dependencies = [
  "serde",
  "signal-hook",
  "signal-hook-async-std",
- "sled-overlay",
  "smol",
  "structopt",
  "structopt-toml",
@@ -4678,7 +4678,7 @@ dependencies = [
 [[package]]
 name = "kvdb-overlay"
 version = "0.1.0"
-source = "git+https://git.dark.fi/darkrenaissance/kvdb-overlay#b59057e55ffa8c8e54d578f6a5b4c169f2c1f7ef"
+source = "git+https://git.dark.fi/darkrenaissance/kvdb-overlay#52ff2bf06e800e098b32089acff7e2fa1dcfa30d"
 dependencies = [
  "fjall",
  "tempfile",
@@ -8151,12 +8151,12 @@ dependencies = [
  "darkfi-serial",
  "easy-parallel",
  "futures",
+ "kvdb-overlay",
  "libc",
  "rand 0.8.6",
  "serde",
  "signal-hook",
  "signal-hook-async-std",
- "sled-overlay",
  "smol",
  "structopt",
  "structopt-toml",

+ 1 - 1
Cargo.toml

@@ -185,8 +185,8 @@ geode = [
 event-graph = [
     "async-std",
     "blake3",
+    "kvdb-overlay",
     "num-bigint",
-    "sled-overlay",
     "smol",
     "tinyjson",
     "bs58",

+ 1 - 1
bin/darkirc/Cargo.toml

@@ -30,7 +30,7 @@ libc = "0.2.186"
 tikv-jemallocator = "0.5.4"
 
 # Event Graph DB
-sled-overlay = "0.1.20"
+kvdb-overlay = {git = "https://git.dark.fi/darkrenaissance/kvdb-overlay", version = "0.1.0"}
 
 # TLS
 async-trait = "0.1.89"

+ 1 - 7
bin/darkirc/darkirc_config.toml

@@ -26,9 +26,6 @@
 ## Sets Datastore Path
 #datastore = "~/.local/share/darkfi/darkirc/darkirc_db"
 
-## Sled cache capacity for the DarkIRC/EventGraph datastore, in MiB
-#sled_cache_mb = 64
-
 ## Enable RLN proof generation and verification. Disabled by default to skip
 ## RLN key loading, identity SMT state, proof generation, and proof verification.
 #rln_enabled = false
@@ -36,13 +33,10 @@
 ## Datastore path for RLN proving and verifying keys
 #zk_key_datastore = "~/.local/share/darkfi/darkirc/zk_keys"
 
-## Sled cache capacity for the RLN key datastore, in MiB
-#zk_key_sled_cache_mb = 16
-
 ## Sets DB logs replay datastore path
 #replay_datastore = "~/.local/share/darkfi/darkirc/replayed_darkirc_db"
 
-## Run in replay mode to store Sled DB instructions
+## Run in replay mode to store KVDB instructions
 ## (for eventgraph debugging tool)
 #replay_mode = false
 

+ 9 - 9
bin/darkirc/src/irc/client.rs

@@ -32,7 +32,7 @@ use darkfi::{
 };
 use darkfi_serial::{deserialize_async_partial, serialize_async};
 use futures::{FutureExt, StreamExt};
-use sled_overlay::sled;
+use kvdb_overlay::{Batch, Tree};
 use smol::{
     io::{self, AsyncBufRead, AsyncBufReadExt, AsyncWriteExt, BufReader},
     lock::{OnceCell, RwLock},
@@ -179,7 +179,7 @@ pub struct Client {
     pub caps: RwLock<HashMap<String, bool>>,
     /// Set of seen messages for the user
     /// TODO: It grows indefinitely, needs to be pruned.
-    pub seen: OnceCell<sled::Tree>,
+    pub seen: OnceCell<Tree>,
     /// NickServ instance
     pub nickserv: Arc<NickServ>,
 }
@@ -693,31 +693,31 @@ impl Client {
 
     /// Atomically mark a message as seen for this client.
     pub async fn mark_seen(&self, event_id: &blake3::Hash) -> Result<()> {
-        let db = self
+        let tree = self
             .seen
             .get_or_init(|| async {
                 let u = self.username.read().await.to_string();
-                self.server.darkirc.sled.open_tree(format!("darkirc_user_{u}")).unwrap()
+                self.server.darkirc.kvdb.open_tree_default(&format!("darkirc_user_{u}")).unwrap()
             })
             .await;
 
         debug!("Marking event {event_id} as seen");
-        let mut batch = sled::Batch::default();
+        let mut batch = Batch::default();
         batch.insert(event_id.as_bytes(), &[]);
-        Ok(db.apply_batch(batch)?)
+        Ok(self.server.darkirc.kvdb.atomic_write(&[(tree, &batch)])?)
     }
 
     /// Check if a message was already marked seen for this client.
     pub async fn is_seen(&self, event_id: &blake3::Hash) -> Result<bool> {
-        let db = self
+        let tree = self
             .seen
             .get_or_init(|| async {
                 let u = self.username.read().await.to_string();
-                self.server.darkirc.sled.open_tree(format!("darkirc_user_{u}")).unwrap()
+                self.server.darkirc.kvdb.open_tree_default(&format!("darkirc_user_{u}")).unwrap()
             })
             .await;
 
-        Ok(db.contains_key(event_id.as_bytes())?)
+        Ok(tree.contains_key(event_id.as_bytes())?)
     }
 }
 

+ 1 - 1
bin/darkirc/src/irc/command.rs

@@ -379,7 +379,7 @@ impl Client {
 
         let nick = self.nickname.read().await.to_string();
 
-        let tree = self.server.darkirc.sled.open_tree(SEEN_CHANNELS_TREE)?;
+        let tree = self.server.darkirc.kvdb.open_tree_default(SEEN_CHANNELS_TREE)?;
 
         let mut replies =
             vec![ReplyType::Server((RPL_LISTSTART, format!("{nick} Channel :Users  Name")))];

+ 36 - 39
bin/darkirc/src/irc/server.rs

@@ -38,7 +38,7 @@ use futures_rustls::{
     },
     TlsAcceptor,
 };
-use sled_overlay::sled;
+use kvdb_overlay::{Batch, Database};
 use smol::{
     fs,
     lock::{Mutex, RwLock},
@@ -67,7 +67,7 @@ pub const MAX_NICK_LEN: usize = 24;
 /// Max message length
 pub const MAX_MSG_LEN: usize = 512;
 
-/// Sled tree storing every public (`#`-prefixed) IRC channel we have
+/// Kvdb tree storing every public (`#`-prefixed) IRC channel we have
 /// observed on the p2p network. Keys are channel names; values are empty.
 pub const SEEN_CHANNELS_TREE: &str = "darkirc_seen_channels";
 
@@ -82,26 +82,24 @@ pub enum RlnMessageReservation {
 }
 
 /// Persist the active RLN counter to the default mirror and matching account tree.
-async fn persist_rln_identity_counter(sled_db: &sled::Db, identity: &RlnIdentity) -> Result<()> {
+async fn persist_rln_identity_counter(kvdb: &Database, identity: &RlnIdentity) -> Result<()> {
     let encoded = serialize_async(identity).await;
     let active_commitment = identity.commitment();
     let mut updated_account = false;
 
-    for raw in sled_db.tree_names() {
-        let bytes: &[u8] = raw.as_ref();
-        let Ok(name) = std::str::from_utf8(bytes) else { continue };
+    for name in kvdb.tree_names()? {
         let Some(account_name) = name.strip_prefix(ACCOUNTS_DB_PREFIX) else { continue };
         if account_name == "default" || account_name.is_empty() {
             continue
         }
 
-        let tree = sled_db.open_tree(name)?;
+        let tree = kvdb.open_tree_default(&name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else { continue };
         let Ok(stored): std::result::Result<RlnIdentity, _> = deserialize_async(&blob).await else {
             continue
         };
         if stored.commitment() == active_commitment {
-            tree.insert(ACCOUNTS_KEY_RLN_IDENTITY, encoded.clone())?;
+            tree.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded)?;
             updated_account = true;
         }
     }
@@ -113,15 +111,15 @@ async fn persist_rln_identity_counter(sled_db: &sled::Db, identity: &RlnIdentity
         );
     }
 
-    let default_db = sled_db.open_tree(ACCOUNTS_DEFAULT_TREE)?;
-    default_db.insert(ACCOUNTS_KEY_RLN_IDENTITY, encoded)?;
-    sled_db.flush_async().await?;
+    let default_db = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
+    default_db.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded)?;
+    kvdb.flush_default_mode_async().await?;
     Ok(())
 }
 
 /// Reserve the next RLN message ID and persist it before proof creation.
 pub(crate) async fn reserve_rln_message_id_in_store(
-    sled_db: &sled::Db,
+    kvdb: &Database,
     active: &mut Option<RlnIdentity>,
     now_millis: u64,
 ) -> Result<RlnMessageReservation> {
@@ -132,7 +130,7 @@ pub(crate) async fn reserve_rln_message_id_in_store(
         return Ok(RlnMessageReservation::BudgetExhausted)
     };
 
-    persist_rln_identity_counter(sled_db, &updated).await?;
+    persist_rln_identity_counter(kvdb, &updated).await?;
     *current = updated;
 
     Ok(RlnMessageReservation::Reserved { identity: updated, message_id })
@@ -188,10 +186,10 @@ fn load_tls_acceptor(tls_cert: &str, tls_secret: &str) -> Result<TlsAcceptor> {
     tls_acceptor_from_pem(&mut cert_reader, &mut secret_reader)
 }
 
-async fn load_default_rln_identity(sled_db: &sled::Db) -> Result<Option<RlnIdentity>> {
-    let default_db = sled_db.open_tree(ACCOUNTS_DEFAULT_TREE)?;
+async fn load_default_rln_identity(kvdb: &Database) -> Result<Option<RlnIdentity>> {
+    let default_db = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
     let Some(blob) = default_db.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
-        if default_db.is_empty() {
+        if default_db.is_empty()? {
             return Ok(None)
         }
 
@@ -236,7 +234,7 @@ impl IrcServer {
     /// Reserve and persist the next RLN message slot before proof creation.
     pub async fn reserve_rln_message_id(&self, now_millis: u64) -> Result<RlnMessageReservation> {
         let mut active = self.rln_identity.write().await;
-        reserve_rln_message_id_in_store(&self.darkirc.sled, &mut active, now_millis).await
+        reserve_rln_message_id_in_store(&self.darkirc.kvdb, &mut active, now_millis).await
     }
 
     /// Instantiate a new IRC server. This function will try to bind a TCP socket,
@@ -282,7 +280,7 @@ impl IrcServer {
         // Set the default RLN account if any. When RLN is disabled, avoid
         // loading account state that cannot affect outbound messages.
         let rln_identity = if darkirc.event_graph.rln_enabled() {
-            let rln_identity = load_default_rln_identity(&darkirc.sled).await?;
+            let rln_identity = load_default_rln_identity(&darkirc.kvdb).await?;
             if rln_identity.is_some() {
                 info!("Default RLN account set");
             }
@@ -487,7 +485,7 @@ impl IrcServer {
             }
         }
 
-        let tree = self.darkirc.sled.open_tree(SEEN_CHANNELS_TREE)?;
+        let tree = self.darkirc.kvdb.open_tree_default(SEEN_CHANNELS_TREE)?;
         if tree.insert(channel.as_bytes(), &[])?.is_none() {
             debug!(
                 target: "darkirc::irc::server",
@@ -506,8 +504,8 @@ impl IrcServer {
     /// channels carry base58 ciphertext and are skipped.
     pub async fn populate_seen_channels(&self) -> Result<usize> {
         let events = self.darkirc.event_graph.order_events().await?;
-        let tree = self.darkirc.sled.open_tree(SEEN_CHANNELS_TREE)?;
-        let mut batch = sled::Batch::default();
+        let tree = self.darkirc.kvdb.open_tree_default(SEEN_CHANNELS_TREE)?;
+        let mut batch = Batch::default();
         let mut count = 0usize;
         for event in events.iter() {
             let Ok((privmsg, _)) = deserialize_async_partial::<Privmsg>(event.content()).await
@@ -520,7 +518,7 @@ impl IrcServer {
             }
         }
         if count > 0 {
-            tree.apply_batch(batch)?;
+            self.darkirc.kvdb.atomic_write(&[(&tree, &batch)])?;
         }
         Ok(count)
     }
@@ -663,9 +661,9 @@ mod tests {
     #[test]
     fn load_default_rln_identity_returns_none_for_empty_tree() {
         smol::block_on(async {
-            let sled_db = sled::Config::new().temporary(true).open().unwrap();
+            let (kvdb, _kvdb_folder) = Database::open_temp().unwrap();
 
-            let identity = load_default_rln_identity(&sled_db).await.unwrap();
+            let identity = load_default_rln_identity(&kvdb).await.unwrap();
 
             assert!(identity.is_none());
         })
@@ -674,11 +672,11 @@ mod tests {
     #[test]
     fn load_default_rln_identity_rejects_missing_identity_record() {
         smol::block_on(async {
-            let sled_db = sled::Config::new().temporary(true).open().unwrap();
-            let default = sled_db.open_tree(ACCOUNTS_DEFAULT_TREE).unwrap();
+            let (kvdb, _kvdb_folder) = Database::open_temp().unwrap();
+            let default = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE).unwrap();
             default.insert(b"other", b"value").unwrap();
 
-            let err = match load_default_rln_identity(&sled_db).await {
+            let err = match load_default_rln_identity(&kvdb).await {
                 Ok(_) => panic!("expected missing identity record error"),
                 Err(e) => e,
             };
@@ -693,11 +691,11 @@ mod tests {
     #[test]
     fn load_default_rln_identity_rejects_corrupted_identity_record() {
         smol::block_on(async {
-            let sled_db = sled::Config::new().temporary(true).open().unwrap();
-            let default = sled_db.open_tree(ACCOUNTS_DEFAULT_TREE).unwrap();
+            let (kvdb, _kvdb_folder) = Database::open_temp().unwrap();
+            let default = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE).unwrap();
             default.insert(ACCOUNTS_KEY_RLN_IDENTITY, b"not an identity").unwrap();
 
-            let err = match load_default_rln_identity(&sled_db).await {
+            let err = match load_default_rln_identity(&kvdb).await {
                 Ok(_) => panic!("expected corrupted identity record error"),
                 Err(e) => e,
             };
@@ -709,18 +707,18 @@ mod tests {
     #[test]
     fn rln_message_reservation_persists_default_and_account_counters() {
         smol::block_on(async {
-            let sled_db = sled::Config::new().temporary(true).open().unwrap();
-            let account = sled_db.open_tree(format!("{ACCOUNTS_DB_PREFIX}alice")).unwrap();
-            let default = sled_db.open_tree(ACCOUNTS_DEFAULT_TREE).unwrap();
+            let (kvdb, _kvdb_folder) = Database::open_temp().unwrap();
+            let account = kvdb.open_tree_default(&format!("{ACCOUNTS_DB_PREFIX}alice")).unwrap();
+            let default = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE).unwrap();
             let identity = test_identity(2);
             let encoded = serialize_async(&identity).await;
-            account.insert(ACCOUNTS_KEY_RLN_IDENTITY, encoded.clone()).unwrap();
-            default.insert(ACCOUNTS_KEY_RLN_IDENTITY, encoded).unwrap();
+            account.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded).unwrap();
+            default.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded).unwrap();
 
             let now = 1_704_067_800_000;
             let mut active = Some(identity);
             let reservation =
-                reserve_rln_message_id_in_store(&sled_db, &mut active, now).await.unwrap();
+                reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap();
             let RlnMessageReservation::Reserved { identity: reserved, message_id } = reservation
             else {
                 panic!("expected reservation")
@@ -743,14 +741,13 @@ mod tests {
             assert_eq!(stored_account.last_epoch, epoch_of(now));
 
             let reservation =
-                reserve_rln_message_id_in_store(&sled_db, &mut active, now).await.unwrap();
+                reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap();
             let RlnMessageReservation::Reserved { message_id, .. } = reservation else {
                 panic!("expected second reservation")
             };
             assert_eq!(message_id, 1);
 
-            let exhausted =
-                reserve_rln_message_id_in_store(&sled_db, &mut active, now).await.unwrap();
+            let exhausted = reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap();
             assert!(matches!(exhausted, RlnMessageReservation::BudgetExhausted));
         })
     }

+ 25 - 27
bin/darkirc/src/irc/services/nickserv.rs

@@ -19,7 +19,7 @@
 //! NickServ - account management for DarkIRC.
 //!
 //! Each registered account is one RLN identity stored under its own
-//! sled tree, named `darkirc_account_<name>`. A separate sled tree
+//! kvdb tree, named `darkirc_account_<name>`. A separate kvdb tree
 //! `darkirc_account_default` mirrors whichever identity is currently
 //! active; on startup, `IrcServer::new` reads from that tree to
 //! populate `IrcServer::rln_identity`, which is the field every
@@ -39,7 +39,7 @@
 //! - `SET <name>` - swap the active identity to `<name>`. The
 //!   change is persisted (next restart will load the same one) and
 //!   takes effect for the next outbound message.
-//! - `DEREGISTER <name>` - drop the account's sled tree. Refuses
+//! - `DEREGISTER <name>` - drop the account's kvdb tree. Refuses
 //!   if `<name>` is currently active (the user must SET away first
 //!   so they don't accidentally orphan their session). Local-only:
 //!   the on-network registration is unaffected, so the same
@@ -77,7 +77,7 @@ pub const ACCOUNTS_DB_PREFIX: &str = "darkirc_account_";
 pub const ACCOUNTS_KEY_RLN_IDENTITY: &[u8] = b"rln_identity";
 const MAX_ACCOUNT_NAME_LEN: usize = MAX_NICK_LEN;
 
-/// Name of the sled tree that mirrors the currently-active identity.
+/// Name of the kvdb tree that mirrors the currently-active identity.
 /// `IrcServer::new` reads this on startup.
 pub const ACCOUNTS_DEFAULT_TREE: &str = "darkirc_account_default";
 
@@ -303,12 +303,7 @@ impl NickServ {
             self.server.rln_identity.read().await.as_ref().map(|id| id.commitment());
 
         let mut accounts: Vec<(String, RlnIdentity)> = Vec::new();
-        for raw in self.server.darkirc.sled.tree_names() {
-            // `raw` is a sled IVec; coerce to bytes via AsRef so
-            // this compiles regardless of which sled fork the
-            // workspace pulls in.
-            let bytes: &[u8] = raw.as_ref();
-            let Ok(name) = std::str::from_utf8(bytes) else { continue };
+        for name in self.server.darkirc.kvdb.tree_names()? {
             // Skip the `default` mirror tree and anything that
             // isn't an account tree. Note we strip the prefix once
             // and reject the literal "default" suffix - we do NOT
@@ -319,7 +314,7 @@ impl NickServ {
                 continue
             }
 
-            let tree = self.server.darkirc.sled.open_tree(name)?;
+            let tree = self.server.darkirc.kvdb.open_tree_default(&name)?;
             let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else { continue };
             // If a tree exists but the blob is malformed, skip
             // rather than failing the whole listing.
@@ -365,7 +360,7 @@ impl NickServ {
             return Ok(vec![notice(nick, "Invalid account name.")])
         }
 
-        let tree = self.server.darkirc.sled.open_tree(&tree_name)?;
+        let tree = self.server.darkirc.kvdb.open_tree_default(&tree_name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
             return Ok(vec![notice(nick, format!("No such account: \"{account_name}\""))])
         };
@@ -493,27 +488,30 @@ impl NickServ {
             )])
         }
 
-        // Open the per-account sled tree only after the identity has
+        // Open the per-account kvdb tree only after the identity has
         // passed the pregenerated-admission checks. Rejected identities
         // must not leave account state behind or become active locally.
-        let db =
-            self.server.darkirc.sled.open_tree(format!("{ACCOUNTS_DB_PREFIX}{account_name}"))?;
+        let db = self
+            .server
+            .darkirc
+            .kvdb
+            .open_tree_default(&format!("{ACCOUNTS_DB_PREFIX}{account_name}"))?;
 
-        if !db.is_empty() {
+        if !db.is_empty()? {
             return Ok(vec![notice(nick, "This account name is already registered.")])
         }
 
         // Store account.
-        db.insert(ACCOUNTS_KEY_RLN_IDENTITY, serialize_async(&new_rln_identity).await)?;
+        db.insert(ACCOUNTS_KEY_RLN_IDENTITY, &serialize_async(&new_rln_identity).await)?;
 
         // First-ever registration also becomes the active one. We
         // check the in-memory active identity (not the default
         // tree) because that's the source of truth at runtime.
         let became_active = self.server.rln_identity.read().await.is_none();
         if became_active {
-            let db_default = self.server.darkirc.sled.open_tree(ACCOUNTS_DEFAULT_TREE)?;
+            let db_default = self.server.darkirc.kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
             db_default
-                .insert(ACCOUNTS_KEY_RLN_IDENTITY, serialize_async(&new_rln_identity).await)?;
+                .insert(ACCOUNTS_KEY_RLN_IDENTITY, &serialize_async(&new_rln_identity).await)?;
             *self.server.rln_identity.write().await = Some(new_rln_identity);
         }
 
@@ -556,7 +554,7 @@ impl NickServ {
         // Look up the account's commitment so we can compare
         // against the in-memory active identity.
         let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
-        let tree = self.server.darkirc.sled.open_tree(&tree_name)?;
+        let tree = self.server.darkirc.kvdb.open_tree_default(&tree_name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
             return Ok(vec![notice(nick, format!("No such account: \"{account_name}\""))])
         };
@@ -567,7 +565,7 @@ impl NickServ {
                 // tree name. We can't tell whether it's active, so
                 // err on the safe side and refuse if there IS an
                 // active one. The only way out from a corrupted
-                // active account is to manually surgery sled.
+                // active account is to manually surgery kvdb.
                 if self.server.rln_identity.read().await.is_some() {
                     return Ok(vec![notice(
                         nick,
@@ -578,7 +576,7 @@ impl NickServ {
                         ),
                     )])
                 }
-                self.server.darkirc.sled.drop_tree(&tree_name)?;
+                self.server.darkirc.kvdb.drop_tree(&tree_name)?;
                 return Ok(vec![notice(
                     nick,
                     format!("Dropped corrupted account \"{account_name}\"."),
@@ -602,7 +600,7 @@ impl NickServ {
         }
 
         // Drop the tree.
-        self.server.darkirc.sled.drop_tree(&tree_name)?;
+        self.server.darkirc.kvdb.drop_tree(&tree_name)?;
 
         Ok(vec![notice(nick, format!("Successfully deregistered account \"{account_name}\""))])
     }
@@ -627,7 +625,7 @@ impl NickServ {
         }
 
         let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
-        let tree = self.server.darkirc.sled.open_tree(&tree_name)?;
+        let tree = self.server.darkirc.kvdb.open_tree_default(&tree_name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
             return Ok(notices(
                 nick,
@@ -666,7 +664,7 @@ impl NickServ {
         // counter state if it were the previously-active one)
         // because the default tree is meant to mirror an account
         // tree exactly.
-        let db_default = self.server.darkirc.sled.open_tree(ACCOUNTS_DEFAULT_TREE)?;
+        let db_default = self.server.darkirc.kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
         db_default.insert(ACCOUNTS_KEY_RLN_IDENTITY, blob.as_ref())?;
 
         // Swap in-memory. The loaded identity includes any persisted
@@ -754,7 +752,7 @@ impl NickServ {
 
         // Load the account.
         let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
-        let tree = self.server.darkirc.sled.open_tree(&tree_name)?;
+        let tree = self.server.darkirc.kvdb.open_tree_default(&tree_name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
             return Ok(vec![notice(nick, format!("No such account: \"{account_name}\""))])
         };
@@ -832,7 +830,7 @@ impl NickServ {
         // the account unusable anyway, so keeping the tree around
         // would be misleading (it would show up in INFO as if it
         // were still registered).
-        self.server.darkirc.sled.drop_tree(&tree_name)?;
+        self.server.darkirc.kvdb.drop_tree(&tree_name)?;
 
         Ok(notices(
             nick,
@@ -889,7 +887,7 @@ fn is_account_name_char(byte: u8) -> bool {
     byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')
 }
 
-/// Return true when a local account name is safe as a sled tree suffix.
+/// Return true when a local account name is safe as a kvdb tree suffix.
 fn is_valid_account_name(account_name: &str) -> bool {
     account_name != "default" &&
         !account_name.is_empty() &&

+ 10 - 9
bin/darkirc/src/lib.rs

@@ -16,10 +16,11 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use sled_overlay::sled;
-use smol::lock::Mutex;
 use std::{collections::HashSet, path::PathBuf};
 
+use kvdb_overlay::Database;
+use smol::lock::Mutex;
+
 use darkfi::{
     event_graph::EventGraphPtr, net::P2pPtr, rpc::jsonrpc::JsonSubscriber, system::StoppableTaskPtr,
 };
@@ -56,8 +57,8 @@ pub struct Privmsg {
 pub struct DarkIrc {
     /// P2P network pointer
     p2p: P2pPtr,
-    /// Sled DB (also used in event_graph and for RLN)
-    sled: sled::Db,
+    /// KVDB (also used in event_graph and for RLN)
+    kvdb: Database,
     /// Event Graph instance
     event_graph: EventGraphPtr,
     /// JSON-RPC connection tracker
@@ -75,7 +76,7 @@ pub struct DarkIrc {
 impl DarkIrc {
     pub fn new(
         p2p: P2pPtr,
-        sled: sled::Db,
+        kvdb: Database,
         event_graph: EventGraphPtr,
         dnet_sub: JsonSubscriber,
         deg_sub: JsonSubscriber,
@@ -84,7 +85,7 @@ impl DarkIrc {
     ) -> Self {
         Self {
             p2p,
-            sled,
+            kvdb,
             event_graph,
             rpc_connections: Mutex::new(HashSet::new()),
             dnet_sub,
@@ -132,7 +133,7 @@ mod tests {
     };
     use darkfi_serial::{deserialize_async_partial, serialize_async};
     use easy_parallel::Parallel;
-    use sled_overlay::sled;
+    use kvdb_overlay::Database;
     use smol::{channel, future, Executor};
     use url::Url;
 
@@ -190,9 +191,9 @@ mod tests {
         };
 
         let p2p = P2p::new(settings, ex.clone()).await.unwrap();
-        let sled_db = sled::Config::new().temporary(true).open().unwrap();
+        let (kvdb, _kvdb_folder) = Database::open_temp().unwrap();
         let event_graph =
-            EventGraph::new(p2p.clone(), sled_db, "/tmp".into(), false, history_test_config(), ex)
+            EventGraph::new(p2p.clone(), kvdb, "/tmp".into(), false, history_test_config(), ex)
                 .await
                 .unwrap();
         event_graph.synced.store(true, Ordering::Release);

+ 21 - 66
bin/darkirc/src/main.rs

@@ -55,8 +55,8 @@ use irc2::{
     DarkIrc,
 };
 
+use kvdb_overlay::Database;
 use rand::rngs::OsRng;
-use sled_overlay::sled;
 use smol::{fs, stream::StreamExt, Executor};
 use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
 use tracing::{debug, error, info};
@@ -85,23 +85,11 @@ const DARKIRC_HOURS_ROTATION: u64 = 1;
 /// deployment never appear valid on another.
 const DARKIRC_GENESIS_CONTENTS: &[u8] = b"darkirc-v1";
 
-const BYTES_PER_MIB: u64 = 1024 * 1024;
-
 /// Per-epoch limit printed by `--gen-rln-identity`.
 fn generated_rln_identity_user_msg_limit() -> u64 {
     GENESIS_USER_MSG_LIMIT
 }
 
-fn sled_cache_capacity_bytes(name: &str, cache_mb: u64) -> Result<u64> {
-    if cache_mb == 0 {
-        return Err(Error::Custom(format!("{name} must be greater than 0")))
-    }
-
-    cache_mb
-        .checked_mul(BYTES_PER_MIB)
-        .ok_or_else(|| Error::Custom(format!("{name} overflows bytes")))
-}
-
 fn history_retention_limit(
     dags_count: usize,
     history_retention_dags: usize,
@@ -181,18 +169,10 @@ struct Args {
     /// Datastore (DB) path
     datastore: String,
 
-    #[structopt(long, default_value = "64")]
-    /// Sled cache capacity for the datastore, in MiB
-    sled_cache_mb: u64,
-
     #[structopt(long, default_value = "~/.local/share/darkfi/darkirc/zk_keys")]
     /// Datastore path for RLN proving and verifying keys
     zk_key_datastore: String,
 
-    #[structopt(long, default_value = "16")]
-    /// Sled cache capacity for the RLN key datastore, in MiB
-    zk_key_sled_cache_mb: u64,
-
     #[structopt(long)]
     /// Enable RLN proof generation and verification
     rln_enabled: Option<bool>,
@@ -202,7 +182,7 @@ struct Args {
     replay_datastore: String,
 
     #[structopt(long)]
-    /// Flag to store Sled DB instructions
+    /// Flag to store KVDB instructions
     replay_mode: bool,
 
     #[structopt(long)]
@@ -515,38 +495,25 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
         );
     }
 
-    let sled_cache_capacity = sled_cache_capacity_bytes("sled_cache_mb", args.sled_cache_mb)?;
-    info!("Instantiating event DAG with {} MiB sled cache", args.sled_cache_mb);
-    let sled_db = match sled::Config::new()
-        .path(datastore.clone())
-        .cache_capacity(sled_cache_capacity)
-        .open()
-    {
+    info!("Instantiating event DAG");
+    let kvdb = match Database::open_default(&datastore) {
         Ok(v) => v,
         Err(e) => {
             error!("Failed to open datastore database `{datastore:?}`: {e}");
             return Err(e.into());
         }
     };
-    log_memory("after sled open");
+    log_memory("after kvdb open");
 
     let zk_key_db = if let Some(zk_key_datastore) = zk_key_datastore.as_ref() {
-        let zk_key_sled_cache_capacity =
-            sled_cache_capacity_bytes("zk_key_sled_cache_mb", args.zk_key_sled_cache_mb)?;
-        info!("Opening RLN key datastore with {} MiB sled cache", args.zk_key_sled_cache_mb);
-        Some(
-            match sled::Config::new()
-                .path(zk_key_datastore.clone())
-                .cache_capacity(zk_key_sled_cache_capacity)
-                .open()
-            {
-                Ok(v) => v,
-                Err(e) => {
-                    error!("Failed to open RLN key datastore `{zk_key_datastore:?}`: {e}");
-                    return Err(e.into());
-                }
-            },
-        )
+        info!("Opening RLN key datastore");
+        Some(match Database::open_default(zk_key_datastore) {
+            Ok(v) => v,
+            Err(e) => {
+                error!("Failed to open RLN key datastore `{zk_key_datastore:?}`: {e}");
+                return Err(e.into());
+            }
+        })
     } else {
         None
     };
@@ -576,7 +543,7 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     let event_graph = match if let Some(zk_key_db) = zk_key_db.clone() {
         EventGraph::new_with_zk_key_db(
             p2p.clone(),
-            sled_db.clone(),
+            kvdb.clone(),
             zk_key_db,
             replay_datastore.clone(),
             replay_mode,
@@ -587,7 +554,7 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     } else {
         EventGraph::new(
             p2p.clone(),
-            sled_db.clone(),
+            kvdb.clone(),
             replay_datastore.clone(),
             replay_mode,
             eg_config,
@@ -695,7 +662,7 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     let rpc_settings: RpcSettings = args.rpc.into();
     let darkirc = Arc::new(DarkIrc::new(
         p2p.clone(),
-        sled_db.clone(),
+        kvdb.clone(),
         event_graph.clone(),
         dnet_sub,
         deg_sub,
@@ -863,14 +830,12 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     drain_task.stop().await;
     prune_task.stop().await;
 
-    info!("Flushing sled database...");
-    let flushed_bytes = sled_db.flush_async().await?;
-    info!("Flushed {flushed_bytes} bytes");
+    info!("Flushing kvdb database...");
+    kvdb.flush_default_mode_async().await?;
 
     if let Some(zk_key_db) = zk_key_db {
-        info!("Flushing RLN key sled database...");
-        let flushed_key_bytes = zk_key_db.flush_async().await?;
-        info!("Flushed {flushed_key_bytes} RLN key bytes");
+        info!("Flushing RLN key kvdb database...");
+        zk_key_db.flush_default_mode_async().await?;
     }
 
     info!("Shut down successfully");
@@ -1010,7 +975,7 @@ mod tests {
     use darkfi::event_graph::rln::MAX_MSG_LIMIT;
     use rand::rngs::OsRng;
 
-    use super::{history_retention_limit, sled_cache_capacity_bytes, RlnIdentity, BYTES_PER_MIB};
+    use super::{history_retention_limit, RlnIdentity};
 
     #[test]
     fn generated_rln_identity_limit_matches_genesis_budget() {
@@ -1021,16 +986,6 @@ mod tests {
         assert_eq!(identity.user_message_limit, super::generated_rln_identity_user_msg_limit());
     }
 
-    #[test]
-    fn sled_cache_capacity_rejects_zero() {
-        assert!(sled_cache_capacity_bytes("test_cache_mb", 0).is_err());
-    }
-
-    #[test]
-    fn sled_cache_capacity_converts_mib() {
-        assert_eq!(sled_cache_capacity_bytes("test_cache_mb", 64).unwrap(), 64 * BYTES_PER_MIB);
-    }
-
     #[test]
     fn history_window_rejects_zero_startup_sync() {
         assert!(history_retention_limit(0, 24, false).is_err());

+ 1 - 1
bin/tau/taud/Cargo.toml

@@ -29,7 +29,7 @@ darkfi-sdk = {path = "../../../src/sdk", features = ["async"]}
 darkfi-serial = {path = "../../../src/serial"}
 
 # Event Graph DB
-sled-overlay = "0.1.20"
+kvdb-overlay = {git = "https://git.dark.fi/darkrenaissance/kvdb-overlay", version = "0.1.0"}
 
 # Crypto
 blake3 = "1.8.5"

+ 3 - 4
bin/tau/taud/src/error.rs

@@ -17,7 +17,6 @@
  */
 
 use darkfi::rpc::jsonrpc::{ErrorCode, JsonError, JsonResponse, JsonResult};
-use sled_overlay::sled;
 use tinyjson::JsonValue;
 
 #[derive(Debug, thiserror::Error)]
@@ -38,8 +37,8 @@ pub enum TaudError {
     DecryptionError(String),
     #[error("IO Error: `{0}`")]
     IoError(String),
-    #[error("Sled error: `{0}`")]
-    Sled(#[from] sled::Error),
+    #[error("KVDB error: `{0}`")]
+    Kvdb(#[from] kvdb_overlay::Error),
 }
 
 pub type TaudResult<T> = std::result::Result<T, TaudError>;
@@ -79,7 +78,7 @@ pub fn to_json_result(res: TaudResult<JsonValue>, id: i64) -> JsonResult {
                 JsonError::new(ErrorCode::InternalError, Some(e.to_string()), id).into()
             }
             TaudError::IoError(e) => JsonError::new(ErrorCode::InternalError, Some(e), id).into(),
-            TaudError::Sled(e) => {
+            TaudError::Kvdb(e) => {
                 JsonError::new(ErrorCode::InternalError, Some(e.to_string()), id).into()
             }
         },

+ 21 - 21
bin/tau/taud/src/jsonrpc.rs

@@ -24,7 +24,7 @@ use std::{
 };
 
 use async_trait::async_trait;
-use sled_overlay::sled;
+use kvdb_overlay::Database;
 use smol::lock::{Mutex, MutexGuard, RwLock};
 use tinyjson::JsonValue;
 use tracing::{debug, info, warn};
@@ -72,7 +72,7 @@ pub struct JsonRpcInterface {
     dnet_sub: JsonSubscriber,
     deg_sub: JsonSubscriber,
     rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
-    sled_db: sled::Db,
+    kvdb: Database,
     rln_identity: Arc<RwLock<Option<RlnIdentity>>>,
 }
 
@@ -138,7 +138,7 @@ impl JsonRpcInterface {
         event_graph: EventGraphPtr,
         dnet_sub: JsonSubscriber,
         deg_sub: JsonSubscriber,
-        sled_db: sled::Db,
+        kvdb: Database,
         rln_identity: Arc<RwLock<Option<RlnIdentity>>>,
     ) -> Self {
         let workspace = Mutex::new(workspace);
@@ -153,7 +153,7 @@ impl JsonRpcInterface {
             rpc_connections: Mutex::new(HashSet::new()),
             dnet_sub,
             deg_sub,
-            sled_db,
+            kvdb,
             rln_identity,
         }
     }
@@ -905,22 +905,22 @@ impl JsonRpcInterface {
             )]))
         }
 
-        // Open the per-account sled tree only after the identity has
+        // Open the per-account kvdb tree only after the identity has
         // passed the pregenerated-admission checks.
-        let db = self.sled_db.open_tree(format!("{ACCOUNTS_DB_PREFIX}{account_name}"))?;
-        if !db.is_empty() {
+        let db = self.kvdb.open_tree_default(&format!("{ACCOUNTS_DB_PREFIX}{account_name}"))?;
+        if !db.is_empty()? {
             return Ok(strings_to_json(vec!["This account name is already registered.".to_string()]))
         }
 
         // Store account.
-        db.insert(ACCOUNTS_KEY_RLN_IDENTITY, serialize_async(&new_rln_identity).await)?;
+        db.insert(ACCOUNTS_KEY_RLN_IDENTITY, &serialize_async(&new_rln_identity).await)?;
 
         // First-ever registration also becomes the active one.
         let became_active = self.rln_identity.read().await.is_none();
         if became_active {
-            let db_default = self.sled_db.open_tree(ACCOUNTS_DEFAULT_TREE)?;
+            let db_default = self.kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
             db_default
-                .insert(ACCOUNTS_KEY_RLN_IDENTITY, serialize_async(&new_rln_identity).await)?;
+                .insert(ACCOUNTS_KEY_RLN_IDENTITY, &serialize_async(&new_rln_identity).await)?;
             *self.rln_identity.write().await = Some(new_rln_identity);
         }
 
@@ -958,7 +958,7 @@ impl JsonRpcInterface {
         let active_commitment = self.rln_identity.read().await.as_ref().map(|id| id.commitment());
 
         let mut accounts: Vec<(String, RlnIdentity)> = Vec::new();
-        for raw in self.sled_db.tree_names() {
+        for raw in self.kvdb.tree_names()? {
             let bytes: &[u8] = raw.as_ref();
             let Ok(name) = std::str::from_utf8(bytes) else { continue };
             // Skip the `default` mirror tree and anything that isn't an
@@ -968,7 +968,7 @@ impl JsonRpcInterface {
                 continue
             }
 
-            let tree = self.sled_db.open_tree(name)?;
+            let tree = self.kvdb.open_tree_default(name)?;
             let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else { continue };
             let Ok(identity): std::result::Result<RlnIdentity, _> = deserialize_async(&blob).await
             else {
@@ -1011,7 +1011,7 @@ impl JsonRpcInterface {
         }
 
         let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
-        let tree = self.sled_db.open_tree(&tree_name)?;
+        let tree = self.kvdb.open_tree_default(&tree_name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
             return Ok(vec![format!("No such account: \"{account_name}\"")])
         };
@@ -1074,7 +1074,7 @@ impl JsonRpcInterface {
         }
 
         let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
-        let tree = self.sled_db.open_tree(&tree_name)?;
+        let tree = self.kvdb.open_tree_default(&tree_name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
             return Ok(strings_to_json(vec![
                 format!("No such account: \"{account_name}\""),
@@ -1105,7 +1105,7 @@ impl JsonRpcInterface {
         // in-memory identity, which would have stale counter state if it
         // were the previously-active one) because the default tree is
         // meant to mirror an account tree exactly.
-        let db_default = self.sled_db.open_tree(ACCOUNTS_DEFAULT_TREE)?;
+        let db_default = self.kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
         db_default.insert(ACCOUNTS_KEY_RLN_IDENTITY, blob.as_ref())?;
 
         *self.rln_identity.write().await = Some(identity);
@@ -1139,7 +1139,7 @@ impl JsonRpcInterface {
         }
 
         let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
-        let tree = self.sled_db.open_tree(&tree_name)?;
+        let tree = self.kvdb.open_tree_default(&tree_name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
             return Ok(strings_to_json(vec![format!("No such account: \"{account_name}\"")]))
         };
@@ -1155,7 +1155,7 @@ impl JsonRpcInterface {
                          a clean account first, then retry."
                     )]))
                 }
-                self.sled_db.drop_tree(&tree_name)?;
+                self.kvdb.drop_tree(&tree_name)?;
                 return Ok(strings_to_json(vec![format!(
                     "Dropped corrupted account \"{account_name}\"."
                 )]))
@@ -1172,7 +1172,7 @@ impl JsonRpcInterface {
             }
         }
 
-        self.sled_db.drop_tree(&tree_name)?;
+        self.kvdb.drop_tree(&tree_name)?;
 
         Ok(strings_to_json(vec![format!("Successfully deregistered account \"{account_name}\"")]))
     }
@@ -1206,7 +1206,7 @@ impl JsonRpcInterface {
         }
 
         let tree_name = format!("{ACCOUNTS_DB_PREFIX}{account_name}");
-        let tree = self.sled_db.open_tree(&tree_name)?;
+        let tree = self.kvdb.open_tree_default(&tree_name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
             return Ok(strings_to_json(vec![format!("No such account: \"{account_name}\"")]))
         };
@@ -1270,7 +1270,7 @@ impl JsonRpcInterface {
 
         // Drop the local account tree. The on-network slash makes the
         // account unusable anyway.
-        self.sled_db.drop_tree(&tree_name)?;
+        self.kvdb.drop_tree(&tree_name)?;
 
         Ok(strings_to_json(vec![
             format!("SLASHED \"{account_name}\". The identity is permanently retired."),
@@ -1290,7 +1290,7 @@ fn is_account_name_char(byte: u8) -> bool {
     byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')
 }
 
-/// Return true when a local account name is safe as a sled tree suffix.
+/// Return true when a local account name is safe as a kvdb tree suffix.
 fn is_valid_account_name(account_name: &str) -> bool {
     account_name != "default" &&
         !account_name.is_empty() &&

+ 37 - 60
bin/tau/taud/src/main.rs

@@ -35,9 +35,9 @@ use darkfi_serial::{
     SerialDecodable, SerialEncodable,
 };
 use futures::{select, FutureExt};
+use kvdb_overlay::{Batch, Database, Tree};
 use libc::mkfifo;
 use rand::rngs::OsRng;
-use sled_overlay::sled;
 use smol::{fs, stream::StreamExt};
 use structopt_toml::StructOptToml;
 use tinyjson::JsonValue;
@@ -87,27 +87,14 @@ const TAUD_GENESIS_CONTENTS: &[u8] = b"taud-v1";
 
 /// How many rotation periods to keep in the rolling DAG window.
 /// With `hours_rotation = 1` and `max_dags = 24`, this gives a
-/// 24-hour history window. Older events are evicted from sled.
+/// 24-hour history window. Older events are evicted from kvdb.
 const TAUD_MAX_DAGS: usize = 1;
 
-/// Sled cache capacity multiplier.
-const BYTES_PER_MIB: u64 = 1024 * 1024;
-
 /// Per-epoch limit printed by `--gen-rln-identity`.
 fn generated_rln_identity_user_msg_limit() -> u64 {
     darkfi::event_graph::rln::GENESIS_USER_MSG_LIMIT
 }
 
-fn sled_cache_capacity_bytes(name: &str, cache_mb: u64) -> Result<u64> {
-    if cache_mb == 0 {
-        return Err(Error::Custom(format!("{name} must be greater than 0")))
-    }
-
-    cache_mb
-        .checked_mul(BYTES_PER_MIB)
-        .ok_or_else(|| Error::Custom(format!("{name} overflows bytes")))
-}
-
 mod jsonrpc;
 mod settings;
 
@@ -316,27 +303,27 @@ async fn get_workspaces(settings: &Args) -> Result<BTreeMap<String, Workspace>>
 
 /// Atomically mark a message as seen.
 pub async fn mark_seen(
-    sled_db: sled::Db,
-    seen: OnceLock<sled::Tree>,
+    kvdb: Database,
+    seen: OnceLock<Tree>,
     event_id: &blake3::Hash,
 ) -> Result<()> {
-    let db = seen.get_or_init(|| sled_db.open_tree("tau_seen").unwrap());
+    let tree = seen.get_or_init(|| kvdb.open_tree_default("tau_seen").unwrap());
 
     debug!(target: "taud", "Marking event {event_id} as seen");
-    let mut batch = sled::Batch::default();
+    let mut batch = Batch::new();
     batch.insert(event_id.as_bytes(), &[]);
-    Ok(db.apply_batch(batch)?)
+    Ok(kvdb.atomic_write(&[(tree, &batch)])?)
 }
 
 /// Check if a message was already marked seen.
 pub async fn is_seen(
-    sled_db: sled::Db,
-    seen: OnceLock<sled::Tree>,
+    kvdb: Database,
+    seen: OnceLock<Tree>,
     event_id: &blake3::Hash,
 ) -> Result<bool> {
-    let db = seen.get_or_init(|| sled_db.open_tree("tau_seen").unwrap());
+    let tree = seen.get_or_init(|| kvdb.open_tree_default("tau_seen").unwrap());
 
-    Ok(db.contains_key(event_id.as_bytes())?)
+    Ok(tree.contains_key(event_id.as_bytes())?)
 }
 
 #[allow(clippy::too_many_arguments)]
@@ -344,10 +331,10 @@ async fn start_sync_loop(
     event_graph: EventGraphPtr,
     broadcast_rcv: smol::channel::Receiver<TaskInfo>,
     workspaces: Arc<BTreeMap<String, Workspace>>,
-    sled_db: sled::Db,
+    kvdb: Database,
     settings: Args,
     p2p: P2pPtr,
-    seen: OnceLock<sled::Tree>,
+    seen: OnceLock<Tree>,
     rln_identity: Arc<smol::lock::RwLock<Option<RlnIdentity>>>,
 ) -> TaudResult<()> {
     let incoming = event_graph.event_pub.clone().subscribe().await;
@@ -381,7 +368,7 @@ async fn start_sync_loop(
                         let (rln_identity, mid) = {
                             let mut active = rln_identity.write().await;
                             match reserve_rln_message_id_in_store(
-                                &sled_db,
+                                &kvdb,
                                 &mut active,
                                 event.header.timestamp,
                             )
@@ -425,10 +412,10 @@ async fn start_sync_loop(
             // Process message from the network. These should only be EncryptedTask.
             task_event = incoming.receive().fuse() => {
                 let event_id = task_event.header.id();
-                if is_seen(sled_db.clone(), seen.clone(), &event_id).await? {
+                if is_seen(kvdb.clone(), seen.clone(), &event_id).await? {
                     continue
                 }
-                mark_seen(sled_db.clone(), seen.clone(), &event_id).await?;
+                mark_seen(kvdb.clone(), seen.clone(), &event_id).await?;
 
                 // Try to deserialize the `Event`'s content into a `EncryptedTask`
                 let enc_task: EncryptedTask = match deserialize_async_partial(task_event.content()).await {
@@ -706,25 +693,17 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     // let fast_mode = settings.fast_mode;
 
     info!(target: "taud", "Instantiating event DAG");
-    let sled_db = sled::open(datastore)?;
+    let kvdb = Database::open_default(&datastore)?;
 
     let zk_key_db = if let Some(zk_key_datastore) = zk_key_datastore.as_ref() {
-        let zk_key_sled_cache_capacity =
-            sled_cache_capacity_bytes("zk_key_sled_cache_mb", settings.zk_key_sled_cache_mb)?;
-        info!(target: "taud", "Opening RLN key datastore with {} MiB sled cache", settings.zk_key_sled_cache_mb);
-        Some(
-            match sled::Config::new()
-                .path(zk_key_datastore.clone())
-                .cache_capacity(zk_key_sled_cache_capacity)
-                .open()
-            {
-                Ok(v) => v,
-                Err(e) => {
-                    error!(target: "taud", "Failed to open RLN key datastore `{zk_key_datastore:?}`: {e}");
-                    return Err(e.into());
-                }
-            },
-        )
+        info!(target: "taud", "Opening RLN key datastore");
+        Some(match Database::open_default(zk_key_datastore) {
+            Ok(v) => v,
+            Err(e) => {
+                error!(target: "taud", "Failed to open RLN key datastore `{zk_key_datastore:?}`: {e}");
+                return Err(e.into());
+            }
+        })
     } else {
         None
     };
@@ -755,7 +734,7 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     let event_graph = match if let Some(zk_key_db) = zk_key_db.clone() {
         EventGraph::new_with_zk_key_db(
             p2p.clone(),
-            sled_db.clone(),
+            kvdb.clone(),
             zk_key_db,
             replay_datastore.clone(),
             replay_mode,
@@ -766,7 +745,7 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     } else {
         EventGraph::new(
             p2p.clone(),
-            sled_db.clone(),
+            kvdb.clone(),
             replay_datastore.clone(),
             replay_mode,
             eg_config,
@@ -785,7 +764,7 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     // loading account state that cannot affect outbound messages.
     let rln_identity: Arc<smol::lock::RwLock<Option<RlnIdentity>>> =
         Arc::new(smol::lock::RwLock::new(if event_graph.rln_enabled() {
-            let rln_identity = load_default_rln_identity(&sled_db).await?;
+            let rln_identity = load_default_rln_identity(&kvdb).await?;
             if rln_identity.is_some() {
                 info!(target: "taud", "Default RLN account set");
             }
@@ -848,7 +827,7 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     }
 
     let seen = OnceLock::new();
-    seen.set(sled_db.open_tree("tau_seen").unwrap()).unwrap();
+    seen.set(kvdb.open_tree_default("tau_seen").unwrap()).unwrap();
 
     ////////////////////
     // get history
@@ -858,10 +837,10 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     for event in dag_events.iter() {
         let event_id = event.header.id();
         // If it was seen, skip
-        if is_seen(sled_db.clone(), seen.clone(), &event_id).await? {
+        if is_seen(kvdb.clone(), seen.clone(), &event_id).await? {
             continue
         }
-        mark_seen(sled_db.clone(), seen.clone(), &event_id).await?;
+        mark_seen(kvdb.clone(), seen.clone(), &event_id).await?;
 
         // Try to deserialize it. (Here we skip errors)
         let Ok((enc_task, _)) = deserialize_async_partial(event.content()).await else { continue };
@@ -881,7 +860,7 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
             event_graph.clone(),
             broadcast_rcv,
             workspaces.clone(),
-            sled_db.clone(),
+            kvdb.clone(),
             settings.clone(),
             p2p.clone(),
             seen.clone(),
@@ -963,7 +942,7 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
         event_graph.clone(),
         json_sub,
         deg_sub,
-        sled_db.clone(),
+        kvdb.clone(),
         rln_identity.clone(),
     ));
     let rpc_task = StoppableTask::new();
@@ -995,14 +974,12 @@ pub const TAUD_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     dnet_task.stop().await;
     deg_task.stop().await;
 
-    info!(target: "taud", "Flushing sled database...");
-    let flushed_bytes = sled_db.flush_async().await?;
-    info!(target: "taud", "Flushed {flushed_bytes} bytes");
+    info!(target: "taud", "Flushing kvdb database...");
+    kvdb.flush_default_mode_async().await?;
 
     if let Some(zk_key_db) = zk_key_db {
-        info!(target: "taud", "Flushing RLN key sled database...");
-        let flushed_key_bytes = zk_key_db.flush_async().await?;
-        info!(target: "taud", "Flushed {flushed_key_bytes} RLN key bytes");
+        info!(target: "taud", "Flushing RLN key kvdb database...");
+        zk_key_db.flush_default_mode_async().await?;
     }
 
     info!(target: "taud", "Shut down successfully");

+ 25 - 29
bin/tau/taud/src/rln.rs

@@ -31,19 +31,19 @@ use darkfi_sdk::{crypto::poseidon_hash, pasta::pallas};
 use darkfi_serial::{
     async_trait, deserialize_async, serialize_async, SerialDecodable, SerialEncodable,
 };
+use kvdb_overlay::Database;
 use rand::{CryptoRng, RngCore};
-use sled_overlay::sled;
 use tracing::{info, warn};
 
 /// Domain-separation tags for credential generation.
 pub const RLN_TRAPDOOR_DERIVATION_PATH: pallas::Base = pallas::Base::from_raw([4311, 0, 0, 0]);
 pub const RLN_NULLIFIER_DERIVATION_PATH: pallas::Base = pallas::Base::from_raw([4312, 0, 0, 0]);
 
-/// Name of the sled tree that mirrors the currently-active identity.
+/// Name of the kvdb tree that mirrors the currently-active identity.
 /// Read on startup to populate the active RLN identity.
 pub const ACCOUNTS_DEFAULT_TREE: &str = "tau_account_default";
 
-/// Prefix of the sled tree under which each registered account lives.
+/// Prefix of the kvdb tree under which each registered account lives.
 pub const ACCOUNTS_DB_PREFIX: &str = "tau_account_";
 
 /// Key inside each account tree holding the serialized [`RlnIdentity`].
@@ -216,15 +216,12 @@ pub enum RlnMessageReservation {
 }
 
 /// Persist the active RLN counter to the default mirror and matching account tree.
-pub async fn persist_rln_identity_counter(
-    sled_db: &sled::Db,
-    identity: &RlnIdentity,
-) -> Result<()> {
+pub async fn persist_rln_identity_counter(kvdb: &Database, identity: &RlnIdentity) -> Result<()> {
     let encoded = serialize_async(identity).await;
     let active_commitment = identity.commitment();
     let mut updated_account = false;
 
-    for raw in sled_db.tree_names() {
+    for raw in kvdb.tree_names()? {
         let bytes: &[u8] = raw.as_ref();
         let Ok(name) = std::str::from_utf8(bytes) else { continue };
         let Some(account_name) = name.strip_prefix(ACCOUNTS_DB_PREFIX) else { continue };
@@ -232,13 +229,13 @@ pub async fn persist_rln_identity_counter(
             continue
         }
 
-        let tree = sled_db.open_tree(name)?;
+        let tree = kvdb.open_tree_default(name)?;
         let Some(blob) = tree.get(ACCOUNTS_KEY_RLN_IDENTITY)? else { continue };
         let Ok(stored): std::result::Result<RlnIdentity, _> = deserialize_async(&blob).await else {
             continue
         };
         if stored.commitment() == active_commitment {
-            tree.insert(ACCOUNTS_KEY_RLN_IDENTITY, encoded.clone())?;
+            tree.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded)?;
             updated_account = true;
         }
     }
@@ -250,15 +247,15 @@ pub async fn persist_rln_identity_counter(
         );
     }
 
-    let default_db = sled_db.open_tree(ACCOUNTS_DEFAULT_TREE)?;
-    default_db.insert(ACCOUNTS_KEY_RLN_IDENTITY, encoded)?;
-    sled_db.flush_async().await?;
+    let default_db = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
+    default_db.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded)?;
+    kvdb.flush_default_mode_async().await?;
     Ok(())
 }
 
 /// Reserve the next RLN message ID and persist it before proof creation.
 pub async fn reserve_rln_message_id_in_store(
-    sled_db: &sled::Db,
+    kvdb: &Database,
     active: &mut Option<RlnIdentity>,
     now_millis: u64,
 ) -> Result<RlnMessageReservation> {
@@ -269,17 +266,17 @@ pub async fn reserve_rln_message_id_in_store(
         return Ok(RlnMessageReservation::BudgetExhausted)
     };
 
-    persist_rln_identity_counter(sled_db, &updated).await?;
+    persist_rln_identity_counter(kvdb, &updated).await?;
     *current = updated;
 
     Ok(RlnMessageReservation::Reserved { identity: updated, message_id })
 }
 
 /// Load the active (default-mirror) RLN identity, if any.
-pub async fn load_default_rln_identity(sled_db: &sled::Db) -> Result<Option<RlnIdentity>> {
-    let default_db = sled_db.open_tree(ACCOUNTS_DEFAULT_TREE)?;
+pub async fn load_default_rln_identity(kvdb: &Database) -> Result<Option<RlnIdentity>> {
+    let default_db = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE)?;
     let Some(blob) = default_db.get(ACCOUNTS_KEY_RLN_IDENTITY)? else {
-        if default_db.is_empty() {
+        if default_db.is_empty()? {
             return Ok(None)
         }
 
@@ -302,9 +299,9 @@ mod tests {
     #[test]
     fn load_default_rln_identity_returns_none_for_empty_tree() {
         smol::block_on(async {
-            let sled_db = sled::Config::new().temporary(true).open().unwrap();
+            let (kvdb, _kvdb_folder) = Database::open_temp().unwrap();
 
-            let identity = load_default_rln_identity(&sled_db).await.unwrap();
+            let identity = load_default_rln_identity(&kvdb).await.unwrap();
 
             assert!(identity.is_none());
         })
@@ -313,9 +310,9 @@ mod tests {
     #[test]
     fn rln_message_reservation_persists_default_and_account_counters() {
         smol::block_on(async {
-            let sled_db = sled::Config::new().temporary(true).open().unwrap();
-            let account = sled_db.open_tree(format!("{ACCOUNTS_DB_PREFIX}alice")).unwrap();
-            let default = sled_db.open_tree(ACCOUNTS_DEFAULT_TREE).unwrap();
+            let (kvdb, _kvdb_folder) = Database::open_temp().unwrap();
+            let account = kvdb.open_tree_default(&format!("{ACCOUNTS_DB_PREFIX}alice")).unwrap();
+            let default = kvdb.open_tree_default(ACCOUNTS_DEFAULT_TREE).unwrap();
             let identity = RlnIdentity {
                 nullifier: pallas::Base::from(0xabc_u64),
                 trapdoor: pallas::Base::from(0xdef_u64),
@@ -324,13 +321,13 @@ mod tests {
                 last_epoch: 0,
             };
             let encoded = serialize_async(&identity).await;
-            account.insert(ACCOUNTS_KEY_RLN_IDENTITY, encoded.clone()).unwrap();
-            default.insert(ACCOUNTS_KEY_RLN_IDENTITY, encoded).unwrap();
+            account.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded).unwrap();
+            default.insert(ACCOUNTS_KEY_RLN_IDENTITY, &encoded).unwrap();
 
             let now = 1_704_067_800_000;
             let mut active = Some(identity);
             let reservation =
-                reserve_rln_message_id_in_store(&sled_db, &mut active, now).await.unwrap();
+                reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap();
             let RlnMessageReservation::Reserved { identity: reserved, message_id } = reservation
             else {
                 panic!("expected reservation")
@@ -351,14 +348,13 @@ mod tests {
             assert_eq!(stored_account.message_id, 1);
 
             let reservation =
-                reserve_rln_message_id_in_store(&sled_db, &mut active, now).await.unwrap();
+                reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap();
             let RlnMessageReservation::Reserved { message_id, .. } = reservation else {
                 panic!("expected second reservation")
             };
             assert_eq!(message_id, 1);
 
-            let exhausted =
-                reserve_rln_message_id_in_store(&sled_db, &mut active, now).await.unwrap();
+            let exhausted = reserve_rln_message_id_in_store(&kvdb, &mut active, now).await.unwrap();
             assert!(matches!(exhausted, RlnMessageReservation::BudgetExhausted));
         })
     }

+ 1 - 5
bin/tau/taud/src/settings.rs

@@ -49,10 +49,6 @@ pub struct Args {
     /// Datastore path for RLN proving and verifying keys
     pub zk_key_datastore: String,
 
-    #[structopt(long, default_value = "16")]
-    /// Sled cache capacity for the RLN key datastore, in MiB
-    pub zk_key_sled_cache_mb: u64,
-
     #[structopt(long)]
     /// Generate a new RLN identity
     pub gen_rln_identity: bool,
@@ -62,7 +58,7 @@ pub struct Args {
     pub gen_genesis_rln_identities: Option<u64>,
 
     #[structopt(long)]
-    /// Flag to store Sled DB instructions
+    /// Flag to store KVDB instructions
     pub replay_mode: bool,
 
     #[structopt(flatten)]

+ 1 - 4
bin/tau/taud/taud_config.toml

@@ -14,10 +14,7 @@
 ## Datastore path for RLN proving and verifying keys
 #zk_key_datastore = "~/.local/share/darkfi/taud/zk_keys"
 
-## Sled cache capacity for the RLN key datastore, in MiB
-#zk_key_sled_cache_mb = 16
-
-## Run in replay mode to store Sled DB instructions
+## Run in replay mode to store KVDB instructions
 ## (for eventgraph debugging tool)
 #replay_mode = false
 

+ 4 - 4
src/event_graph/event.rs

@@ -19,7 +19,7 @@
 use std::{cmp::Ordering, collections::HashSet};
 
 use darkfi_serial::{async_trait, deserialize_async, Encodable, SerialDecodable, SerialEncodable};
-use sled_overlay::{sled, SledTreeOverlay};
+use kvdb_overlay::{Tree, TreeOverlay};
 
 use super::{
     util::{unix_timestamp_millis, HOUR_MS},
@@ -88,10 +88,10 @@ impl Header {
     /// `dag_genesis` is the timestamp/name of the target rotating DAG slot.
     pub async fn validate(
         &self,
-        header_dag: &sled::Tree,
+        header_dag: &Tree,
         config: &EventGraphConfig,
         dag_genesis: u64,
-        overlay: Option<&SledTreeOverlay>,
+        overlay: Option<&TreeOverlay>,
     ) -> Result<bool> {
         if !self.timestamp_fits_slot(config, dag_genesis) {
             return Ok(false)
@@ -189,7 +189,7 @@ impl Event {
     /// `dag_genesis` is the timestamp/name of the target rotating DAG slot.
     pub async fn dag_validate(
         &self,
-        hdr_dag: &sled::Tree,
+        hdr_dag: &Tree,
         config: &EventGraphConfig,
         dag_genesis: u64,
     ) -> Result<bool> {

+ 93 - 96
src/event_graph/mod.rs

@@ -33,7 +33,7 @@ use std::{
 use darkfi_sdk::{crypto::pasta_prelude::PrimeField, pasta::pallas};
 use darkfi_serial::{deserialize_async, deserialize_async_partial, serialize_async};
 use futures::{stream::FuturesUnordered, StreamExt};
-use sled_overlay::{sled, SledTreeOverlay};
+use kvdb_overlay::{Database, Tree, TreeOverlay};
 use smol::{
     lock::{OnceCell, RwLock},
     Executor,
@@ -129,8 +129,8 @@ pub struct EventGraphConfig {
     ///
     /// * `Some(n)` - keep n rotation periods.
     ///   When the n+1 period is created, the oldest is permanently
-    ///   deleted from sled. This is the normal mode for end-user nodes.
-    /// * `None` - never prune. Every DAG ever created is kept in sled
+    ///   deleted from kvdb. This is the normal mode for end-user nodes.
+    /// * `None` - never prune. Every DAG ever created is kept in kvdb
     ///   and loaded at startup. This is archive mode for nodes that want
     ///   complete history.
     ///
@@ -225,7 +225,7 @@ impl TimeIndex {
         Self::default()
     }
 
-    pub async fn from_header_dag(tree: &sled::Tree) -> Result<Self> {
+    pub async fn from_header_dag(tree: &Tree) -> Result<Self> {
         let mut idx = Self::new();
         for item in tree.iter() {
             let (id, hdr) = item?;
@@ -333,8 +333,8 @@ impl TimeIndex {
 
 /// All per-DAG state: trees, tips, and the timestamp index.
 pub struct DagSlot {
-    pub header_tree: sled::Tree,
-    pub main_tree: sled::Tree,
+    pub header_tree: Tree,
+    pub main_tree: Tree,
     pub tips: LayerUTips,
     pub time_index: TimeIndex,
 }
@@ -342,7 +342,7 @@ pub struct DagSlot {
 /// Full-scan tip computation.
 /// Compute unreferenced tips - events that exist in the DAG but are
 /// not referenced as a parent by any other event - grouped by layer.
-pub(crate) async fn compute_unreferenced_tips(dag: &sled::Tree) -> Result<LayerUTips> {
+pub(crate) async fn compute_unreferenced_tips(dag: &Tree) -> Result<LayerUTips> {
     let mut candidates: HashMap<blake3::Hash, u64> = HashMap::new();
     let mut referenced: HashSet<blake3::Hash> = HashSet::new();
 
@@ -394,7 +394,7 @@ fn select_parents_from_tips(tips: &LayerUTips) -> (u64, [blake3::Hash; N_EVENT_P
 
 /// Storage layer for all rotating DAGs.
 pub struct DagStore {
-    db: sled::Db,
+    db: Database,
     dags: BTreeMap<u64, DagSlot>,
 }
 
@@ -403,46 +403,45 @@ impl DagStore {
     ///
     /// * **Bounded mode** (`max_dags = Some(n)`): create a rolling
     ///   window of the most recent `n` DAGs. Old trees already in
-    ///   sled outside this window are left untouched (they're just
+    ///   kvdb outside this window are left untouched (they're just
     ///   not loaded into memory).
     /// * **Archive mode** (`max_dags = None`): discover *all*
-    ///   existing DAG trees in sled and load them, plus ensure the
+    ///   existing DAG trees in kvdb and load them, plus ensure the
     ///   recent window exists. Nothing is ever dropped.
-    pub async fn new(sled_db: sled::Db, config: &EventGraphConfig) -> Result<Self> {
+    pub async fn new(kvdb: Database, config: &EventGraphConfig) -> Result<Self> {
         config.validate()?;
         let mut dags = BTreeMap::new();
 
         if config.hours_rotation == 0 {
             let genesis = generate_genesis(config)?;
-            dags.insert(genesis.header.timestamp, Self::create_slot(&sled_db, &genesis).await?);
-            return Ok(Self { db: sled_db, dags })
+            dags.insert(genesis.header.timestamp, Self::create_slot(&kvdb, &genesis).await?);
+            return Ok(Self { db: kvdb, dags })
         }
 
         // Determine how many recent DAGs to create/ensure exist.
         let window = config.max_dags.unwrap_or(24);
 
         // In archive mode, first discover and load any existing DAG
-        // trees that are already in sled from previous runs.
+        // trees that are already in kvdb from previous runs.
         //
         // A DAG is stored across two trees: `<timestamp>` for events
         // and `headers_<timestamp>` for headers. We walk every tree
-        // name in sled and pick out the ones whose name is a valid u64
+        // name in kvdb and pick out the ones whose name is a valid u64
         // timestamp.
         if config.max_dags.is_none() {
-            for name in sled_db.tree_names() {
-                let name_str = String::from_utf8_lossy(&name);
-                if let Ok(ts) = name_str.parse::<u64>() {
-                    // Reconstruct the genesis for this timestamp
-                    let hdr = Header {
-                        timestamp: ts,
-                        parents: NULL_PARENTS,
-                        layer: 0,
-                        content_hash: blake3::hash(&config.genesis_contents),
-                    };
-                    let genesis = Event { header: hdr, content: config.genesis_contents.clone() };
-                    let slot = Self::create_slot(&sled_db, &genesis).await?;
-                    dags.insert(ts, slot);
-                }
+            for name in kvdb.tree_names()? {
+                let Ok(ts) = name.parse::<u64>() else { continue };
+
+                // Reconstruct the genesis for this timestamp
+                let hdr = Header {
+                    timestamp: ts,
+                    parents: NULL_PARENTS,
+                    layer: 0,
+                    content_hash: blake3::hash(&config.genesis_contents),
+                };
+                let genesis = Event { header: hdr, content: config.genesis_contents.clone() };
+                let slot = Self::create_slot(&kvdb, &genesis).await?;
+                dags.insert(ts, slot);
             }
         }
 
@@ -451,7 +450,7 @@ impl DagStore {
         for i in 1..=window {
             let ts = next_hour_timestamp((i as i64) - (window as i64))?;
             if dags.contains_key(&ts) {
-                // Already loaded from sled discovery
+                // Already loaded from kvdb discovery
                 continue
             }
             let hdr = Header {
@@ -461,24 +460,24 @@ impl DagStore {
                 content_hash: blake3::hash(&config.genesis_contents),
             };
             let genesis = Event { header: hdr, content: config.genesis_contents.clone() };
-            dags.insert(ts, Self::create_slot(&sled_db, &genesis).await?);
+            dags.insert(ts, Self::create_slot(&kvdb, &genesis).await?);
         }
 
-        Ok(Self { db: sled_db, dags })
+        Ok(Self { db: kvdb, dags })
     }
 
-    async fn create_slot(db: &sled::Db, genesis: &Event) -> Result<DagSlot> {
+    async fn create_slot(kvdb: &Database, genesis: &Event) -> Result<DagSlot> {
         let name = genesis.header.timestamp.to_string();
-        let ht = db.open_tree(format!("headers_{name}"))?;
-        let mt = db.open_tree(&name)?;
+        let ht = kvdb.open_tree_default(&format!("headers_{name}"))?;
+        let mt = kvdb.open_tree_default(&name)?;
         for (tree, data) in
             [(&ht, serialize_async(&genesis.header).await), (&mt, serialize_async(genesis).await)]
         {
-            if tree.is_empty() {
-                let mut ov = SledTreeOverlay::new(tree);
+            if tree.is_empty()? {
+                let mut ov = TreeOverlay::new(tree);
                 ov.insert(genesis.id().as_bytes(), &data)?;
                 if let Some(b) = ov.aggregate() {
-                    tree.apply_batch(b)?;
+                    kvdb.atomic_write(&[(tree, &b)])?;
                 }
             }
         }
@@ -502,8 +501,8 @@ impl DagStore {
                 let Some((_, old)) = self.dags.pop_first() else {
                     return Err(Error::Custom("event graph DAG store is empty".into()))
                 };
-                self.db.drop_tree(old.header_tree.name())?;
-                self.db.drop_tree(old.main_tree.name())?;
+                self.db.drop_tree(&old.header_tree.name()?)?;
+                self.db.drop_tree(&old.main_tree.name()?)?;
             }
         }
         let slot = Self::create_slot(&self.db, genesis).await?;
@@ -519,8 +518,8 @@ impl DagStore {
         self.dags.get_mut(ts)
     }
 
-    pub fn get_header_tree(&self, dag_name: &str) -> Result<sled::Tree> {
-        Ok(self.db.open_tree(format!("headers_{dag_name}"))?)
+    pub fn get_header_tree(&self, dag_name: &str) -> Result<Tree> {
+        Ok(self.db.open_tree_default(&format!("headers_{dag_name}"))?)
     }
 
     pub fn dag_timestamps(&self) -> Vec<u64> {
@@ -687,7 +686,7 @@ pub struct EventGraph {
     /// by `handle_event_req` to forward blobs to syncing peers.
     /// Pruned by `dag_prune` when the corresponding rotating DAG
     /// rolls out of the retention window.
-    pub(crate) dag_blobs: sled::Tree,
+    pub(crate) dag_blobs: Tree,
     /// Verified range-sync bodies waiting for older parent bodies before they
     /// can be durably committed to the rotating DAG body tree.
     lazy_pending: RwLock<HashMap<u64, HashMap<blake3::Hash, PendingLazyEvent>>>,
@@ -705,7 +704,7 @@ pub struct EventGraph {
     /// See [`Self::apply_rln_static_event`] for the canonical-order
     /// rationale and [`Self::is_root_valid_at`] for how this is
     /// consulted during signal verification.
-    pub(crate) rln_historical_roots_ordered: sled::Tree,
+    pub(crate) rln_historical_roots_ordered: Tree,
     /// Reverse index: `(root:32, ordered_key:40) -> []`.
     ///
     /// A root can appear more than once when static events are no-ops
@@ -713,14 +712,14 @@ pub struct EventGraph {
     /// index stores every canonical occurrence rather than a single
     /// root-to-key mapping. [`Self::is_root_valid_at`] scans this
     /// prefix and accepts if any interval for the root matches.
-    pub(crate) rln_historical_roots_by_value: sled::Tree,
-    pub(crate) static_dag: sled::Tree,
+    pub(crate) rln_historical_roots_by_value: Tree,
+    pub(crate) static_dag: Tree,
     /// Side-table mapping `event_id -> original RLN blob` for static
     /// events. Used by [`Self::static_sync`] to re-verify the ZK
     /// proof of historical events at sync time. Every static-DAG
     /// event MUST have a corresponding entry - `static_sync` rejects
     /// events whose blob isn't available rather than falling through.
-    pub(crate) static_dag_blobs: sled::Tree,
+    pub(crate) static_dag_blobs: Tree,
     datastore: PathBuf,
     replay_mode: bool,
     pub(crate) broadcasted_ids: RwLock<HashSet<blake3::Hash>>,
@@ -736,7 +735,7 @@ pub struct EventGraph {
     pub synced: AtomicBool,
     pub deg_enabled: AtomicBool,
     deg_publisher: PublisherPtr<DegEvent>,
-    pub sled_db: sled::Db,
+    pub kvdb: Database,
     pub zk_keys: Option<Arc<ZkKeys>>,
     pub identity_state: Option<RwLock<IdentityState>>,
     pub rln_state: Option<RwLock<RlnState>>,
@@ -764,21 +763,21 @@ impl EventGraph {
     /// Create a new Event Graph.
     pub async fn new(
         p2p: P2pPtr,
-        sled_db: sled::Db,
+        kvdb: Database,
         datastore: PathBuf,
         replay_mode: bool,
         config: EventGraphConfig,
         ex: Arc<Executor<'_>>,
     ) -> Result<EventGraphPtr> {
-        let zk_key_db = sled_db.clone();
-        Self::new_with_zk_key_db(p2p, sled_db, zk_key_db, datastore, replay_mode, config, ex).await
+        let zk_key_db = kvdb.clone();
+        Self::new_with_zk_key_db(p2p, kvdb, zk_key_db, datastore, replay_mode, config, ex).await
     }
 
     /// Create a new Event Graph using a separate DB for RLN key material.
     pub async fn new_with_zk_key_db(
         p2p: P2pPtr,
-        sled_db: sled::Db,
-        zk_key_db: sled::Db,
+        kvdb: Database,
+        zk_key_db: Database,
         datastore: PathBuf,
         replay_mode: bool,
         config: EventGraphConfig,
@@ -793,7 +792,7 @@ impl EventGraph {
             info!(target: "event_graph::new", "[EVENTGRAPH] RLN disabled; skipping key initialization");
             None
         };
-        Self::with_optional_zk_keys(p2p, sled_db, datastore, replay_mode, config, zk_keys, ex).await
+        Self::with_optional_zk_keys(p2p, kvdb, datastore, replay_mode, config, zk_keys, ex).await
     }
 
     /// Same as [`Self::new`] but accepts a pre-built [`ZkKeys`].
@@ -805,20 +804,20 @@ impl EventGraph {
     /// them per-test would blow out RAM and `/dev/shm`.
     pub async fn with_zk_keys(
         p2p: P2pPtr,
-        sled_db: sled::Db,
+        kvdb: Database,
         datastore: PathBuf,
         replay_mode: bool,
         config: EventGraphConfig,
         zk_keys: Arc<ZkKeys>,
         ex: Arc<Executor<'_>>,
     ) -> Result<EventGraphPtr> {
-        Self::with_optional_zk_keys(p2p, sled_db, datastore, replay_mode, config, Some(zk_keys), ex)
+        Self::with_optional_zk_keys(p2p, kvdb, datastore, replay_mode, config, Some(zk_keys), ex)
             .await
     }
 
     async fn with_optional_zk_keys(
         p2p: P2pPtr,
-        sled_db: sled::Db,
+        kvdb: Database,
         datastore: PathBuf,
         replay_mode: bool,
         config: EventGraphConfig,
@@ -833,7 +832,7 @@ impl EventGraph {
             None
         };
         let identity_state = if rln_enabled {
-            let identity_state = IdentityState::new(&sled_db)?;
+            let identity_state = IdentityState::new(&kvdb)?;
             log_memory("after RLN identity state initialization");
             Some(identity_state)
         } else {
@@ -847,10 +846,10 @@ impl EventGraph {
             } else {
                 (Vec::new(), HashSet::new())
             };
-        let dag_store = DagStore::new(sled_db.clone(), &config).await?;
-        let static_dag = Self::static_new(&sled_db, &config).await?;
-        let static_dag_blobs = sled_db.open_tree("static-dag-blobs")?;
-        let dag_blobs = sled_db.open_tree("dag-blobs")?;
+        let dag_store = DagStore::new(kvdb.clone(), &config).await?;
+        let static_dag = Self::static_new(&kvdb, &config).await?;
+        let static_dag_blobs = kvdb.open_tree_default("static-dag-blobs")?;
+        let dag_blobs = kvdb.open_tree_default("dag-blobs")?;
 
         // Historical-roots side-tables. See the design comment on
         // `EventGraph::apply_rln_static_event` for the full rationale.
@@ -860,8 +859,10 @@ impl EventGraph {
         // `ordered` tree gives us canonical replay (and successor
         // lookup for the time-window check), the `by_value` tree
         // gives us O(log n) "is this root historical?" queries.
-        let rln_historical_roots_ordered = sled_db.open_tree("rln-historical-roots-ordered")?;
-        let rln_historical_roots_by_value = sled_db.open_tree("rln-historical-roots-by-value")?;
+        let rln_historical_roots_ordered =
+            kvdb.open_tree_default("rln-historical-roots-ordered")?;
+        let rln_historical_roots_by_value =
+            kvdb.open_tree_default("rln-historical-roots-by-value")?;
 
         // Check whether the current genesis event is already in the
         // store. If not, we need to prune (create a fresh slot).
@@ -873,7 +874,7 @@ impl EventGraph {
 
         let self_ = Arc::new(Self {
             p2p,
-            sled_db: sled_db.clone(),
+            kvdb: kvdb.clone(),
             dag_store: RwLock::new(dag_store),
             static_dag,
             static_dag_blobs,
@@ -1048,8 +1049,8 @@ impl EventGraph {
             Some(root) => actual_root == root,
             None => false,
         };
-        let recorded_count = self.rln_historical_roots_ordered.len();
-        let by_value_count = self.rln_historical_roots_by_value.len();
+        let recorded_count = self.rln_historical_roots_ordered.len()?;
+        let by_value_count = self.rln_historical_roots_by_value.len()?;
         let consistent = historical_roots_consistent &&
             leaves_consistent &&
             slashed_consistent &&
@@ -1105,10 +1106,10 @@ impl EventGraph {
     }
 
     fn historical_roots_index_consistent(&self, expected_count: usize) -> Result<bool> {
-        if self.rln_historical_roots_ordered.len() != expected_count {
+        if self.rln_historical_roots_ordered.len()? != expected_count {
             return Ok(false)
         }
-        if self.rln_historical_roots_by_value.len() != expected_count {
+        if self.rln_historical_roots_by_value.len()? != expected_count {
             return Ok(false)
         }
 
@@ -1124,7 +1125,7 @@ impl EventGraph {
             let mut ordered_key = [0u8; 40];
             ordered_key.copy_from_slice(&ordered_key_bytes);
             let by_value_key = encode_historical_root_by_value_key(&root, &ordered_key);
-            if !self.rln_historical_roots_by_value.contains_key(by_value_key)? {
+            if !self.rln_historical_roots_by_value.contains_key(&by_value_key)? {
                 return Ok(false)
             }
         }
@@ -1239,7 +1240,7 @@ impl EventGraph {
         for item in slot.header_tree.iter() {
             let (hb, val) = item?;
             let hdr: Header = deserialize_async(&val).await?;
-            if hdr.parents != NULL_PARENTS && !slot.main_tree.contains_key(hb)? {
+            if hdr.parents != NULL_PARENTS && !slot.main_tree.contains_key(&hb)? {
                 sorted.push(hdr);
             }
         }
@@ -2138,17 +2139,11 @@ impl EventGraph {
             let next =
                 next_rotation_timestamp(self.config.initial_genesis, self.config.hours_rotation)?;
 
-            loop {
-                match millis_until_next_rotation(next) {
-                    Ok(remaining_ms) => {
-                        if remaining_ms == 0 {
-                            break
-                        }
-                        msleep(remaining_ms.min(10_000)).await;
-                    }
-                    // we've already passed the boundary
-                    Err(_) => break,
+            while let Ok(remaining_ms) = millis_until_next_rotation(next) {
+                if remaining_ms == 0 {
+                    break
                 }
+                msleep(remaining_ms.min(10_000)).await;
             }
 
             let hdr = Header {
@@ -2439,7 +2434,7 @@ impl EventGraph {
 
         let mut ids = Vec::with_capacity(accepted.len());
         let mut committed_indices = Vec::with_capacity(accepted.len());
-        let mut overlay = SledTreeOverlay::new(&slot.main_tree);
+        let mut overlay = TreeOverlay::new(&slot.main_tree);
         let mut staged_body_ids = HashSet::with_capacity(accepted.len());
 
         'commit: for &i in &accepted {
@@ -2495,7 +2490,7 @@ impl EventGraph {
         }
 
         if let Some(b) = overlay.aggregate() {
-            slot.main_tree.apply_batch(b)?;
+            self.kvdb.atomic_write(&[(&slot.main_tree, &b)])?;
         } else {
             return Ok(vec![])
         }
@@ -2582,7 +2577,7 @@ impl EventGraph {
 
         let mut store = self.dag_store.write().await;
         let slot = store.get_slot_mut(&dag_ts).ok_or(Error::DagSyncFailed)?;
-        let mut overlay = SledTreeOverlay::new(&slot.header_tree);
+        let mut overlay = TreeOverlay::new(&slot.header_tree);
         let mut staged_headers = Vec::new();
         let mut hdrs = headers;
         hdrs.sort_by_key(|h| h.layer);
@@ -2622,7 +2617,7 @@ impl EventGraph {
         }
 
         if let Some(b) = overlay.aggregate() {
-            slot.header_tree.apply_batch(b)?;
+            self.kvdb.atomic_write(&[(&slot.header_tree, &b)])?;
             for (timestamp, hid) in staged_headers {
                 slot.time_index.insert(timestamp, hid);
             }
@@ -2741,7 +2736,7 @@ impl EventGraph {
         &self,
         visited: &mut HashSet<blake3::Hash>,
         hdr: Header,
-        tree: &sled::Tree,
+        tree: &Tree,
     ) -> Result<()> {
         let mut stack = VecDeque::new();
         stack.push_back(hdr);
@@ -2759,14 +2754,14 @@ impl EventGraph {
         Ok(())
     }
 
-    async fn static_new(sled_db: &sled::Db, config: &EventGraphConfig) -> Result<sled::Tree> {
-        let tree = sled_db.open_tree("static-dag")?;
+    async fn static_new(kvdb: &Database, config: &EventGraphConfig) -> Result<Tree> {
+        let tree = kvdb.open_tree_default("static-dag")?;
         let genesis = generate_static_genesis(config);
-        let mut ov = SledTreeOverlay::new(&tree);
+        let mut ov = TreeOverlay::new(&tree);
         ov.insert(genesis.id().as_bytes(), &serialize_async(&genesis).await)?;
 
         if let Some(b) = ov.aggregate() {
-            tree.apply_batch(b)?;
+            kvdb.atomic_write(&[(&tree, &b)])?;
         }
 
         Ok(tree)
@@ -2777,11 +2772,11 @@ impl EventGraph {
     }
 
     fn static_persist_serialized(&self, ev_id: &blake3::Hash, ev_bytes: &[u8]) -> Result<()> {
-        let mut ov = SledTreeOverlay::new(&self.static_dag);
+        let mut ov = TreeOverlay::new(&self.static_dag);
         ov.insert(ev_id.as_bytes(), ev_bytes)?;
 
         if let Some(b) = ov.aggregate() {
-            self.static_dag.apply_batch(b)?;
+            self.kvdb.atomic_write(&[(&self.static_dag, &b)])?;
         }
 
         Ok(())
@@ -3059,9 +3054,9 @@ impl EventGraph {
         // every event has exactly one entry, no conditional skips.
         let key = encode_historical_root_key(ev.header.layer, &ev.id());
         let value = encode_historical_root_value(&new_root, ev.header.timestamp);
-        self.rln_historical_roots_ordered.insert(key, value.as_slice())?;
+        self.rln_historical_roots_ordered.insert(&key, value.as_slice())?;
         let by_value_key = encode_historical_root_by_value_key(&new_root, &key);
-        self.rln_historical_roots_by_value.insert(by_value_key, &[])?;
+        self.rln_historical_roots_by_value.insert(&by_value_key, &[])?;
 
         Ok(new_root)
     }
@@ -3102,7 +3097,7 @@ impl EventGraph {
         let lo = signal_timestamp.saturating_sub(drift);
         let hi = signal_timestamp.saturating_add(drift);
 
-        for item in self.rln_historical_roots_by_value.scan_prefix(root.to_repr()) {
+        for item in self.rln_historical_roots_by_value.prefix_iter(&root.to_repr()) {
             let (by_value_key, _) = item?;
             if by_value_key.len() != 72 {
                 continue
@@ -3422,7 +3417,9 @@ impl EventGraph {
                             Ok(state) => state.read().await.root(),
                             Err(_) => return SignalCheck::Rejected,
                         };
-                        let historical_count = self.rln_historical_roots_ordered.len();
+                        let Ok(historical_count) = self.rln_historical_roots_ordered.len() else {
+                            return SignalCheck::Rejected
+                        };
                         warn!(
                             target: "event_graph::rln_verify_signal",
                             "[RLN] Signal rejected: merkle_root not valid at signal \

+ 78 - 73
src/event_graph/rln.rs

@@ -41,9 +41,9 @@ use darkfi_sdk::{
 };
 use darkfi_serial::{async_trait, FutAsyncWriteExt, SerialDecodable, SerialEncodable};
 use halo2_proofs::{arithmetic::Field, circuit::Value};
+use kvdb_overlay::{Database, Tree};
 use num_bigint::BigUint;
 use rand::rngs::OsRng;
-use sled_overlay::sled;
 use tracing::{error, info};
 
 use super::Event;
@@ -269,40 +269,41 @@ const SLASH_PK_KEY: &str = "rlnv2-diff-slash-pk";
 const IDENTITY_LEAVES_TREE: &str = "rln-identity-leaves";
 const SLASHED_IDENTITY_LEAVES_TREE: &str = "rln-slashed-identity-leaves";
 const IDENTITY_SMT_NODES_TREE: &str = "rln-identity-smt-nodes";
+const STORAGE_TREE: &str = "rln-storage";
 
-type SmtSledFp = SparseMerkleTree<
+type SmtKvdbFp = SparseMerkleTree<
     'static,
     SMT_FP_DEPTH,
     { SMT_FP_DEPTH + 1 },
     pallas::Base,
     PoseidonFp,
-    SledStorageFp,
+    KvdbStorageFp,
 >;
 
-/// Sled-backed storage for RLN identity SMT nodes.
+/// Kvdb-backed storage for RLN identity SMT nodes.
 ///
 /// The leaf/tombstone trees remain the durable source of truth. This
 /// tree stores the SMT's internal node cache so membership proofs and
 /// root lookups do not require keeping the full sparse tree in heap.
 #[derive(Clone)]
-struct SledStorageFp {
-    tree: sled::Tree,
+struct KvdbStorageFp {
+    tree: Tree,
 }
 
-impl SledStorageFp {
-    fn new(tree: sled::Tree) -> Self {
+impl KvdbStorageFp {
+    fn new(tree: Tree) -> Self {
         Self { tree }
     }
 }
 
-impl StorageAdapter for SledStorageFp {
+impl StorageAdapter for KvdbStorageFp {
     type Value = pallas::Base;
 
     fn put(&mut self, key: BigUint, value: pallas::Base) -> ContractResult {
-        if let Err(e) = self.tree.insert(key.to_bytes_le(), value.to_repr().as_ref()) {
+        if let Err(e) = self.tree.insert(&key.to_bytes_le(), &value.to_repr()) {
             error!(
                 target: "event_graph::rln",
-                "[RLN] SMT sled put failed: {e}",
+                "[RLN] SMT kvdb put failed: {e}",
             );
             return Err(ContractError::SmtPutFailed)
         }
@@ -311,12 +312,12 @@ impl StorageAdapter for SledStorageFp {
     }
 
     fn get(&self, key: &BigUint) -> Option<pallas::Base> {
-        let value = match self.tree.get(key.to_bytes_le()) {
+        let value = match self.tree.get(&key.to_bytes_le()) {
             Ok(value) => value?,
             Err(e) => {
                 error!(
                     target: "event_graph::rln",
-                    "[RLN] SMT sled get failed: {e}",
+                    "[RLN] SMT kvdb get failed: {e}",
                 );
                 return None
             }
@@ -325,7 +326,7 @@ impl StorageAdapter for SledStorageFp {
         if value.len() != 32 {
             error!(
                 target: "event_graph::rln",
-                "[RLN] SMT sled node must be 32 bytes, got {}",
+                "[RLN] SMT kvdb node must be 32 bytes, got {}",
                 value.len(),
             );
             return None
@@ -337,10 +338,10 @@ impl StorageAdapter for SledStorageFp {
     }
 
     fn del(&mut self, key: &BigUint) -> ContractResult {
-        if let Err(e) = self.tree.remove(key.to_bytes_le()) {
+        if let Err(e) = self.tree.remove(&key.to_bytes_le()) {
             error!(
                 target: "event_graph::rln",
-                "[RLN] SMT sled del failed: {e}",
+                "[RLN] SMT kvdb del failed: {e}",
             );
             return Err(ContractError::SmtDelFailed)
         }
@@ -357,51 +358,53 @@ pub struct ZkKeys {
     pub signal_vk: VerifyingKey,
     /// Verifying key for slash proofs.
     pub slash_vk: VerifyingKey,
-    /// Reference to the sled DB for proving-key access during local proving.
-    sled_db: sled::Db,
+    /// Reference to the kvdb tree for proving-key access during local
+    /// proving.
+    tree: Tree,
 }
 
 impl ZkKeys {
     /// Ensure all required keys exist and load only verifying keys into memory.
-    pub fn build_and_load(sled_db: &sled::Db) -> Result<Self> {
-        Self::ensure_keys(sled_db)?;
-        Self::load_verifying_keys(sled_db)
+    pub fn build_and_load(kvdb: &Database) -> Result<Self> {
+        let tree = kvdb.open_tree_default(STORAGE_TREE)?;
+        Self::ensure_keys(&tree)?;
+        Self::load_verifying_keys(&tree)
     }
 
     /// Build missing proving and verifying keys into persistent storage.
-    fn ensure_keys(sled_db: &sled::Db) -> Result<()> {
-        ensure_key(sled_db, REGISTER_VK_KEY, RLN2_REGISTER_ZKBIN, KeyKind::Vk)?;
-        ensure_key(sled_db, REGISTER_PK_KEY, RLN2_REGISTER_ZKBIN, KeyKind::Pk)?;
-        ensure_key(sled_db, SIGNAL_VK_KEY, RLN2_SIGNAL_ZKBIN, KeyKind::Vk)?;
-        ensure_key(sled_db, SIGNAL_PK_KEY, RLN2_SIGNAL_ZKBIN, KeyKind::Pk)?;
-        ensure_key(sled_db, SLASH_PK_KEY, RLN2_SLASH_ZKBIN, KeyKind::Pk)?;
-        ensure_key(sled_db, SLASH_VK_KEY, RLN2_SLASH_ZKBIN, KeyKind::Vk)?;
+    fn ensure_keys(tree: &Tree) -> Result<()> {
+        ensure_key(tree, REGISTER_VK_KEY, RLN2_REGISTER_ZKBIN, KeyKind::Vk)?;
+        ensure_key(tree, REGISTER_PK_KEY, RLN2_REGISTER_ZKBIN, KeyKind::Pk)?;
+        ensure_key(tree, SIGNAL_VK_KEY, RLN2_SIGNAL_ZKBIN, KeyKind::Vk)?;
+        ensure_key(tree, SIGNAL_PK_KEY, RLN2_SIGNAL_ZKBIN, KeyKind::Pk)?;
+        ensure_key(tree, SLASH_PK_KEY, RLN2_SLASH_ZKBIN, KeyKind::Pk)?;
+        ensure_key(tree, SLASH_VK_KEY, RLN2_SLASH_ZKBIN, KeyKind::Vk)?;
         Ok(())
     }
 
     /// Load the verifying keys needed by validation paths.
-    fn load_verifying_keys(sled_db: &sled::Db) -> Result<Self> {
+    fn load_verifying_keys(tree: &Tree) -> Result<Self> {
         Ok(Self {
-            register_vk: read_vk(sled_db, REGISTER_VK_KEY, RLN2_REGISTER_ZKBIN)?,
-            signal_vk: read_vk(sled_db, SIGNAL_VK_KEY, RLN2_SIGNAL_ZKBIN)?,
-            slash_vk: read_vk(sled_db, SLASH_VK_KEY, RLN2_SLASH_ZKBIN)?,
-            sled_db: sled_db.clone(),
+            register_vk: read_vk(tree, REGISTER_VK_KEY, RLN2_REGISTER_ZKBIN)?,
+            signal_vk: read_vk(tree, SIGNAL_VK_KEY, RLN2_SIGNAL_ZKBIN)?,
+            slash_vk: read_vk(tree, SLASH_VK_KEY, RLN2_SLASH_ZKBIN)?,
+            tree: tree.clone(),
         })
     }
 
-    /// Load the slash proving key from sled.
+    /// Load the slash proving key from kvdb tree.
     pub fn load_slash_pk(&self) -> Result<ProvingKey> {
-        read_pk(&self.sled_db, SLASH_PK_KEY, RLN2_SLASH_ZKBIN)
+        read_pk(&self.tree, SLASH_PK_KEY, RLN2_SLASH_ZKBIN)
     }
 
-    /// Load the register proving key from sled.
+    /// Load the register proving key from kvdb tree.
     pub fn load_register_pk(&self) -> Result<ProvingKey> {
-        read_pk(&self.sled_db, REGISTER_PK_KEY, RLN2_REGISTER_ZKBIN)
+        read_pk(&self.tree, REGISTER_PK_KEY, RLN2_REGISTER_ZKBIN)
     }
 
-    /// Load the signal proving key from sled.
+    /// Load the signal proving key from kvdb tree.
     pub fn load_signal_pk(&self) -> Result<ProvingKey> {
-        read_pk(&self.sled_db, SIGNAL_PK_KEY, RLN2_SIGNAL_ZKBIN)
+        read_pk(&self.tree, SIGNAL_PK_KEY, RLN2_SIGNAL_ZKBIN)
     }
 }
 
@@ -562,22 +565,22 @@ pub enum StaticEventCheck {
 /// Merkle Tree (SMT).
 ///
 /// Persistence model: leaf commitments and slash tombstones are stored in
-/// dedicated sled trees. SMT internal nodes are also sled-backed, but are
+/// dedicated kvdb trees. SMT internal nodes are also kvdb-backed, but are
 /// treated as rebuildable derived state from the canonical static DAG and the
 /// leaf/tombstone side tables.
 pub struct IdentityState {
-    smt: SmtSledFp,
-    leaves: sled::Tree,
-    slashed: sled::Tree,
-    smt_nodes: sled::Tree,
+    smt: SmtKvdbFp,
+    leaves: Tree,
+    slashed: Tree,
+    smt_nodes: Tree,
     recent_roots: VecDeque<pallas::Base>,
 }
 
 impl IdentityState {
-    pub fn new(sled_db: &sled::Db) -> Result<Self> {
-        let leaves = sled_db.open_tree(IDENTITY_LEAVES_TREE)?;
-        let slashed = sled_db.open_tree(SLASHED_IDENTITY_LEAVES_TREE)?;
-        let smt_nodes = sled_db.open_tree(IDENTITY_SMT_NODES_TREE)?;
+    pub fn new(kvdb: &Database) -> Result<Self> {
+        let leaves = kvdb.open_tree_default(IDENTITY_LEAVES_TREE)?;
+        let slashed = kvdb.open_tree_default(SLASHED_IDENTITY_LEAVES_TREE)?;
+        let smt_nodes = kvdb.open_tree_default(IDENTITY_SMT_NODES_TREE)?;
         let smt = Self::new_smt(smt_nodes.clone());
 
         let mut state = Self {
@@ -588,7 +591,7 @@ impl IdentityState {
             recent_roots: VecDeque::with_capacity(ROOT_HISTORY_SIZE),
         };
 
-        if state.smt_nodes.is_empty() && !state.leaves.is_empty() {
+        if state.smt_nodes.is_empty()? && !state.leaves.is_empty()? {
             state.restore_smt_nodes_from_leaves()?;
         }
 
@@ -596,10 +599,10 @@ impl IdentityState {
         Ok(state)
     }
 
-    fn new_smt(smt_nodes: sled::Tree) -> SmtSledFp {
+    fn new_smt(smt_nodes: Tree) -> SmtKvdbFp {
         let hasher = PoseidonFp::new();
-        let store = SledStorageFp::new(smt_nodes);
-        SmtSledFp::new(store, hasher, &EMPTY_NODES_FP)
+        let store = KvdbStorageFp::new(smt_nodes);
+        SmtKvdbFp::new(store, hasher, &EMPTY_NODES_FP)
     }
 
     /// Return the root of an empty RLN identity SMT.
@@ -607,7 +610,7 @@ impl IdentityState {
         EMPTY_NODES_FP[0]
     }
 
-    /// Recreate the sled-backed SMT node tree from persisted leaves.
+    /// Recreate the kvdb-backed SMT node tree from persisted leaves.
     ///
     /// This is a migration and repair path for databases that have the old
     /// leaf side table but no `rln-identity-smt-nodes` tree yet. It restores
@@ -617,13 +620,13 @@ impl IdentityState {
 
         for item in self.leaves.iter() {
             let (key, val) = item?;
-            if key.len() != 32 || val.len() != 32 || key.as_ref() != val.as_ref() {
+            if key.len() != 32 || val.len() != 32 || key != val {
                 continue
             }
 
             let mut repr = [0u8; 32];
             repr.copy_from_slice(&val);
-            if self.slashed.contains_key(repr)? {
+            if self.slashed.contains_key(&repr)? {
                 continue
             }
 
@@ -636,7 +639,7 @@ impl IdentityState {
         if restored > 0 {
             info!(
                 target: "event_graph::rln",
-                "[RLN] Restored {} sled-backed identity SMT nodes from leaves",
+                "[RLN] Restored {} kvdb-backed identity SMT nodes from leaves",
                 restored,
             );
         }
@@ -646,12 +649,12 @@ impl IdentityState {
 
     /// Returns true if the commitment is already a leaf in the tree.
     pub fn contains(&self, commitment: &pallas::Base) -> bool {
-        self.leaves.contains_key(commitment.to_repr()).unwrap_or(false)
+        self.leaves.contains_key(&commitment.to_repr()).unwrap_or(false)
     }
 
     /// Returns true if the commitment has been permanently slashed.
     pub fn is_slashed(&self, commitment: &pallas::Base) -> bool {
-        self.slashed.contains_key(commitment.to_repr()).unwrap_or(false)
+        self.slashed.contains_key(&commitment.to_repr()).unwrap_or(false)
     }
 
     /// Register a new identity.
@@ -667,7 +670,7 @@ impl IdentityState {
         if self.contains(&commitment) {
             return Err(Error::Custom("RLN: duplicate identity commitment".into()))
         }
-        self.leaves.insert(commitment.to_repr(), commitment.to_repr().as_ref())?;
+        self.leaves.insert(&commitment.to_repr(), &commitment.to_repr())?;
         self.smt.insert_batch(vec![(commitment, commitment)])?;
         self.push_root();
         Ok(())
@@ -679,11 +682,11 @@ impl IdentityState {
     /// still recorded as a tombstone, but the SMT root is unchanged.
     pub fn slash(&mut self, commitment: pallas::Base) -> Result<()> {
         let repr = commitment.to_repr();
-        self.slashed.insert(repr, repr.as_ref())?;
+        self.slashed.insert(&repr, &repr)?;
         if !self.contains(&commitment) {
             return Ok(())
         }
-        self.leaves.remove(repr)?;
+        self.leaves.remove(&repr)?;
         self.smt.remove_leaves(vec![(commitment, commitment)])?;
         self.push_root();
         Ok(())
@@ -710,7 +713,7 @@ impl IdentityState {
                     val.len()
                 )))
             }
-            if key.as_ref() != val.as_ref() {
+            if key != val {
                 return Err(Error::Custom("RLN identity leaf key/value mismatch".into()))
             }
             let mut repr = [0u8; 32];
@@ -732,7 +735,7 @@ impl IdentityState {
                     val.len()
                 )))
             }
-            if key.as_ref() != val.as_ref() {
+            if key != val {
                 return Err(Error::Custom("RLN slashed identity key/value mismatch".into()))
             }
             let mut repr = [0u8; 32];
@@ -768,11 +771,11 @@ impl IdentityState {
         self.recent_roots.push_back(root);
     }
 
-    /// Reset the sled-backed SMT and the persistent leaves tree to an
+    /// Reset the kvdb-backed SMT and the persistent leaves tree to an
     /// empty state, in preparation for replaying the canonical
     /// static-DAG history.
     pub fn clear_for_rebuild(&mut self) -> Result<()> {
-        // Drop every derived identity state from sled.
+        // Drop every derived identity state from kvdb.
         self.leaves.clear()?;
         self.slashed.clear()?;
         self.smt_nodes.clear()?;
@@ -1005,9 +1008,9 @@ enum KeyKind {
     Vk,
 }
 
-/// Build a key into sled if it doesn't already exist.
-fn ensure_key(sled_db: &sled::Db, key: &str, zkbin_bytes: &[u8], kind: KeyKind) -> Result<()> {
-    if sled_db.contains_key(key)? {
+/// Build a key into kvdb tree if it doesn't already exist.
+fn ensure_key(tree: &Tree, key: &str, zkbin_bytes: &[u8], kind: KeyKind) -> Result<()> {
+    if tree.contains_key(key.as_bytes())? {
         return Ok(())
     }
 
@@ -1026,19 +1029,21 @@ fn ensure_key(sled_db: &sled::Db, key: &str, zkbin_bytes: &[u8], kind: KeyKind)
             vk.write(&mut buf)?;
         }
     }
-    sled_db.insert(key, buf)?;
+    tree.insert(key.as_bytes(), &buf)?;
     Ok(())
 }
 
-fn read_vk(sled_db: &sled::Db, key: &str, zkbin_bytes: &[u8]) -> Result<VerifyingKey> {
-    let bytes = sled_db.get(key)?.ok_or_else(|| Error::Custom(format!("{key} not found")))?;
+fn read_vk(tree: &Tree, key: &str, zkbin_bytes: &[u8]) -> Result<VerifyingKey> {
+    let bytes =
+        tree.get(key.as_bytes())?.ok_or_else(|| Error::Custom(format!("{key} not found")))?;
     let zkbin = ZkBinary::decode(zkbin_bytes, false)?;
     let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
     Ok(VerifyingKey::read(&mut Cursor::new(bytes), circuit)?)
 }
 
-fn read_pk(sled_db: &sled::Db, key: &str, zkbin_bytes: &[u8]) -> Result<ProvingKey> {
-    let bytes = sled_db.get(key)?.ok_or_else(|| Error::Custom(format!("{key} not found")))?;
+fn read_pk(tree: &Tree, key: &str, zkbin_bytes: &[u8]) -> Result<ProvingKey> {
+    let bytes =
+        tree.get(key.as_bytes())?.ok_or_else(|| Error::Custom(format!("{key} not found")))?;
     let zkbin = ZkBinary::decode(zkbin_bytes, false)?;
     let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
     Ok(ProvingKey::read(&mut Cursor::new(bytes), circuit)?)

+ 21 - 24
src/event_graph/test_helpers.rs

@@ -25,7 +25,7 @@ use std::{
 };
 
 use darkfi_sdk::{crypto::pasta_prelude::PrimeField, pasta::pallas};
-use sled_overlay::sled;
+use kvdb_overlay::{Database, TempDir};
 use smol::{channel, future, Executor};
 use url::Url;
 
@@ -62,7 +62,7 @@ pub fn bounded_dag_store_config() -> EventGraphConfig {
 }
 
 /// Archive-mode config: never evicts old DAGs and discovers
-/// existing trees from sled on construction. Like
+/// existing trees from kvdb on construction. Like
 /// [`bounded_dag_store_config`] this is for `DagStore`-direct
 /// tests only.
 pub fn archive_config() -> EventGraphConfig {
@@ -101,43 +101,40 @@ fn shared_zk_keys() -> Arc<crate::event_graph::rln::ZkKeys> {
             let cache_dir =
                 std::env::temp_dir().join(format!("darkfi-test-zk-cache-{}", &zkbin_hash[..16]));
 
-            let db = sled::Config::new().path(&cache_dir).open().unwrap_or_else(|e| {
+            let kvdb = Database::open_default(&cache_dir).unwrap_or_else(|e| {
                 panic!(
-                    "failed to open shared ZK key sled DB at {}: {e}\n\
+                    "failed to open shared ZK key KVDB at {}: {e}\n\
                          (if the cache is corrupted, run `rm -rf {}`)",
                     cache_dir.display(),
                     cache_dir.display(),
                 )
             });
-            let keys = ZkKeys::build_and_load(&db).expect("failed to build shared ZK keys");
+            let keys = ZkKeys::build_and_load(&kvdb).expect("failed to build shared ZK keys");
             Arc::new(keys)
         })
         .clone()
 }
 
-pub async fn make_eg() -> EventGraphPtr {
+pub async fn make_eg() -> (EventGraphPtr, TempDir) {
     make_eg_with_config(test_config()).await
 }
 
 /// Construct an [`EventGraph`] with a caller-provided test config.
-pub async fn make_eg_with_config(config: EventGraphConfig) -> EventGraphPtr {
-    let sled_db = sled::Config::new().temporary(true).open().unwrap();
-    make_eg_with_config_and_db(config, sled_db).await
+pub async fn make_eg_with_config(config: EventGraphConfig) -> (EventGraphPtr, TempDir) {
+    let (kvdb, kvdb_folder) = Database::open_temp().unwrap();
+    (make_eg_with_config_and_db(config, kvdb).await, kvdb_folder)
 }
 
-/// Construct an [`EventGraph`] with a caller-provided config and sled DB.
-pub async fn make_eg_with_config_and_db(
-    config: EventGraphConfig,
-    sled_db: sled::Db,
-) -> EventGraphPtr {
+/// Construct an [`EventGraph`] with a caller-provided config and KVDB.
+pub async fn make_eg_with_config_and_db(config: EventGraphConfig, kvdb: Database) -> EventGraphPtr {
     let ex = Arc::new(Executor::new());
     let p2p = P2p::new(Settings::default(), ex.clone()).await.unwrap();
     if config.rln_enabled {
-        EventGraph::with_zk_keys(p2p, sled_db, "/tmp".into(), false, config, shared_zk_keys(), ex)
+        EventGraph::with_zk_keys(p2p, kvdb, "/tmp".into(), false, config, shared_zk_keys(), ex)
             .await
             .unwrap()
     } else {
-        EventGraph::new(p2p, sled_db, "/tmp".into(), false, config, ex).await.unwrap()
+        EventGraph::new(p2p, kvdb, "/tmp".into(), false, config, ex).await.unwrap()
     }
 }
 
@@ -162,7 +159,7 @@ async fn spawn_node(
     port_offset: usize,
     peer_offsets: Vec<usize>,
     ex: Arc<Executor<'static>>,
-) -> EventGraphPtr {
+) -> (EventGraphPtr, TempDir) {
     let mut profiles = HashMap::new();
     profiles.insert(
         "tcp".to_string(),
@@ -187,10 +184,10 @@ async fn spawn_node(
     };
 
     let p2p = P2p::new(settings, ex.clone()).await.unwrap();
-    let sled_db = sled::Config::new().temporary(true).open().unwrap();
+    let (kvdb, kvdb_folder) = Database::open_temp().unwrap();
     let eg = EventGraph::with_zk_keys(
         p2p.clone(),
-        sled_db,
+        kvdb,
         "/tmp".into(),
         false,
         test_config(),
@@ -215,7 +212,7 @@ async fn spawn_node(
         })
         .await;
 
-    eg
+    (eg, kvdb_folder)
 }
 
 /// Bootstrap an N-node ring, start the P2P stacks, and wait 5
@@ -223,7 +220,7 @@ async fn spawn_node(
 ///
 /// Each call gets a fresh non-overlapping port range, so multiple
 /// `make_network` invocations can run in parallel.
-pub async fn make_network(ex: Arc<Executor<'static>>) -> Vec<EventGraphPtr> {
+pub async fn make_network(ex: Arc<Executor<'static>>) -> Vec<(EventGraphPtr, TempDir)> {
     use rand::{prelude::SliceRandom, rngs::ThreadRng};
 
     let port_base = alloc_port_base();
@@ -236,7 +233,7 @@ pub async fn make_network(ex: Arc<Executor<'static>>) -> Vec<EventGraphPtr> {
         let conns: Vec<usize> = others.choose_multiple(&mut rng, N_CONNS).copied().collect();
         nodes.push(spawn_node(port_base, i, conns, ex.clone()).await);
     }
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         eg.p2p.clone().start().await.unwrap();
     }
     crate::system::sleep(5).await;
@@ -244,8 +241,8 @@ pub async fn make_network(ex: Arc<Executor<'static>>) -> Vec<EventGraphPtr> {
 }
 
 /// Stop every node's P2P stack. Call at end of multi-node tests.
-pub async fn shutdown_network(nodes: &[EventGraphPtr]) {
-    for eg in nodes {
+pub async fn shutdown_network(nodes: &[(EventGraphPtr, TempDir)]) {
+    for (eg, _) in nodes {
         eg.p2p.clone().stop().await;
     }
 }

+ 144 - 144
src/event_graph/tests.rs

@@ -24,7 +24,6 @@ use std::{
 };
 
 use darkfi_serial::{deserialize_async, serialize_async};
-use sled_overlay::sled;
 use smol::Executor;
 
 use crate::{
@@ -259,14 +258,14 @@ fn evgr_config_rejects_invalid_rotation_settings() {
 #[test]
 fn evgr_invalid_config_does_not_open_dag_trees() {
     smol::block_on(async {
-        let sled_db = sled::Config::new().temporary(true).open().unwrap();
-        let before = sled_db.tree_names();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+        let before = kvdb.tree_names().unwrap();
         let config = EventGraphConfig { hours_rotation: 1, max_dags: Some(0), ..test_config() };
 
-        let result = DagStore::new(sled_db.clone(), &config).await;
+        let result = DagStore::new(kvdb.clone(), &config).await;
 
         assert!(matches!(result, Err(crate::Error::Custom(_))));
-        assert_eq!(sled_db.tree_names(), before);
+        assert_eq!(kvdb.tree_names().unwrap(), before);
     })
 }
 
@@ -312,11 +311,6 @@ fn evgr_time_index_queries_and_saturating_cursor() {
     assert_eq!(idx2.after(u64::MAX, 10).len(), 0);
 }
 
-async fn make_dag_store() -> Result<DagStore> {
-    let sled_db = sled::Config::new().temporary(true).open().unwrap();
-    DagStore::new(sled_db, &bounded_dag_store_config()).await
-}
-
 #[test]
 fn evgr_dag_store_eviction_policy() {
     // Bounded vs archive mode in one test:
@@ -324,7 +318,8 @@ fn evgr_dag_store_eviction_policy() {
     //   (b) archive: adding 30 DAGs leaves all 30 plus the originals.
     smol::block_on(async {
         // (a) bounded
-        let mut store = make_dag_store().await.unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+        let mut store = DagStore::new(kvdb, &bounded_dag_store_config()).await.unwrap();
         let oldest_ts = store.dag_timestamps()[0];
         let new_ts = next_hour_timestamp(1).unwrap();
         let hdr = Header {
@@ -340,8 +335,8 @@ fn evgr_dag_store_eviction_policy() {
         assert!(store.get_slot(&oldest_ts).is_none());
 
         // (b) archive
-        let sled_db = sled::Config::new().temporary(true).open().unwrap();
-        let mut archive = DagStore::new(sled_db, &archive_config()).await.unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+        let mut archive = DagStore::new(kvdb, &archive_config()).await.unwrap();
         let initial = archive.dag_timestamps().len();
         for i in 1..=30i64 {
             let ts = next_hour_timestamp(i).unwrap();
@@ -364,8 +359,8 @@ fn evgr_dag_store_window_matches_generated_config_limits() {
         for limit in 1..=6_usize {
             let config =
                 EventGraphConfig { hours_rotation: 1, max_dags: Some(limit), ..test_config() };
-            let sled_db = sled::Config::new().temporary(true).open().unwrap();
-            let mut store = DagStore::new(sled_db, &config).await.unwrap();
+            let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+            let mut store = DagStore::new(kvdb, &config).await.unwrap();
             assert_eq!(store.dag_timestamps().len(), limit);
 
             let mut expected = store.dag_timestamps();
@@ -393,10 +388,10 @@ fn evgr_dag_store_window_matches_generated_config_limits() {
 #[test]
 fn evgr_dag_store_archive_mode_discovers_existing_trees() {
     smol::block_on(async {
-        let sled_db = sled::Config::new().temporary(true).open().unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
         let historical_ts = next_hour_timestamp(-100).unwrap();
         {
-            let mut store = DagStore::new(sled_db.clone(), &archive_config()).await.unwrap();
+            let mut store = DagStore::new(kvdb.clone(), &archive_config()).await.unwrap();
             let hdr = Header {
                 timestamp: historical_ts,
                 parents: NULL_PARENTS,
@@ -408,7 +403,7 @@ fn evgr_dag_store_archive_mode_discovers_existing_trees() {
             drop(store);
         }
 
-        let store = DagStore::new(sled_db, &archive_config()).await.unwrap();
+        let store = DagStore::new(kvdb, &archive_config()).await.unwrap();
         assert!(
             store.get_slot(&historical_ts).is_some(),
             "Archive mode should discover historical DAGs on restart"
@@ -419,17 +414,17 @@ fn evgr_dag_store_archive_mode_discovers_existing_trees() {
 #[test]
 fn evgr_dag_store_rejects_corrupt_header_index_on_open() {
     smol::block_on(async {
-        let sled_db = sled::Config::new().temporary(true).open().unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
         let config = bounded_dag_store_config();
-        let store = DagStore::new(sled_db.clone(), &config).await.unwrap();
+        let store = DagStore::new(kvdb.clone(), &config).await.unwrap();
         let ts = *store.dag_timestamps().last().unwrap();
         drop(store);
 
         let bad_id = [0u8; 32];
-        let headers = sled_db.open_tree(format!("headers_{ts}")).unwrap();
+        let headers = kvdb.open_tree_default(&format!("headers_{ts}")).unwrap();
         headers.insert(bad_id.as_slice(), b"not-a-header".as_slice()).unwrap();
 
-        let result = DagStore::new(sled_db, &config).await;
+        let result = DagStore::new(kvdb, &config).await;
         assert!(result.is_err(), "corrupt header bytes should fail DAG startup");
     })
 }
@@ -437,17 +432,17 @@ fn evgr_dag_store_rejects_corrupt_header_index_on_open() {
 #[test]
 fn evgr_dag_store_rejects_corrupt_event_tree_on_open() {
     smol::block_on(async {
-        let sled_db = sled::Config::new().temporary(true).open().unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
         let config = bounded_dag_store_config();
-        let store = DagStore::new(sled_db.clone(), &config).await.unwrap();
+        let store = DagStore::new(kvdb.clone(), &config).await.unwrap();
         let ts = *store.dag_timestamps().last().unwrap();
         drop(store);
 
         let bad_id = [0u8; 32];
-        let events = sled_db.open_tree(ts.to_string()).unwrap();
+        let events = kvdb.open_tree_default(&ts.to_string()).unwrap();
         events.insert(bad_id.as_slice(), b"not-an-event".as_slice()).unwrap();
 
-        let result = DagStore::new(sled_db, &config).await;
+        let result = DagStore::new(kvdb, &config).await;
         assert!(result.is_err(), "corrupt event bytes should fail DAG startup");
     })
 }
@@ -455,7 +450,7 @@ fn evgr_dag_store_rejects_corrupt_event_tree_on_open() {
 #[test]
 fn evgr_rotating_event_creation_rejects_missing_current_dag_slot() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_ts = eg.current_genesis.read().await.header.timestamp;
         eg.dag_store.write().await.dags.remove(&dag_ts);
 
@@ -467,7 +462,7 @@ fn evgr_rotating_event_creation_rejects_missing_current_dag_slot() {
 #[test]
 fn evgr_static_event_creation_rejects_corrupt_static_dag() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let bad_id = [0u8; 32];
         eg.static_dag.insert(bad_id.as_slice(), b"not-an-event".as_slice()).unwrap();
 
@@ -479,7 +474,8 @@ fn evgr_static_event_creation_rejects_corrupt_static_dag() {
 #[test]
 fn evgr_compute_unreferenced_tips_single_pass() {
     smol::block_on(async {
-        let store = make_dag_store().await.unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+        let store = DagStore::new(kvdb, &bounded_dag_store_config()).await.unwrap();
         let ts = *store.dag_timestamps().last().unwrap();
         let slot = store.get_slot(&ts).unwrap();
         let genesis_hash = *slot.tips.get(&0).unwrap().iter().next().unwrap();
@@ -497,7 +493,7 @@ fn evgr_compute_unreferenced_tips_single_pass() {
             },
             content: b"e2".to_vec(),
         };
-        slot.main_tree.insert(e2.id().as_bytes(), serialize_async(&e2).await).unwrap();
+        slot.main_tree.insert(e2.id().as_bytes(), &serialize_async(&e2).await).unwrap();
 
         let mut p = [NULL_ID; N_EVENT_PARENTS];
         p[0] = e2.id();
@@ -510,7 +506,7 @@ fn evgr_compute_unreferenced_tips_single_pass() {
             },
             content: b"e3".to_vec(),
         };
-        slot.main_tree.insert(e3.id().as_bytes(), serialize_async(&e3).await).unwrap();
+        slot.main_tree.insert(e3.id().as_bytes(), &serialize_async(&e3).await).unwrap();
 
         let mut p = [NULL_ID; N_EVENT_PARENTS];
         p[0] = genesis_hash;
@@ -523,7 +519,7 @@ fn evgr_compute_unreferenced_tips_single_pass() {
             },
             content: b"e4".to_vec(),
         };
-        slot.main_tree.insert(e4.id().as_bytes(), serialize_async(&e4).await).unwrap();
+        slot.main_tree.insert(e4.id().as_bytes(), &serialize_async(&e4).await).unwrap();
 
         assert_ne!(e2.id(), e4.id(), "e2 and e4 must have distinct IDs");
 
@@ -694,7 +690,7 @@ async fn assert_rotating_dag_storage_invariants(eg: &EventGraphPtr, dag_ts: u64,
 fn evgr_generated_dags_preserve_storage_invariants() {
     smol::block_on(async {
         for (seed, event_count) in [(1_u64, 18_usize), (7, 27), (19, 36)] {
-            let eg = make_eg().await;
+            let (eg, _kvdb_folder) = make_eg().await;
             let (dag_ts, _) = insert_generated_dag_shape(&eg, seed, event_count).await;
             assert_rotating_dag_storage_invariants(&eg, dag_ts, seed).await;
         }
@@ -707,7 +703,7 @@ fn evgr_dag_insert_valid_and_duplicate() {
     // subscriber. Second (duplicate) insert: returns an empty list,
     // doesn't re-fire.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_ts = eg.current_genesis.read().await.header.timestamp;
         let dag_name = dag_ts.to_string();
         let sub = eg.event_pub.clone().subscribe().await;
@@ -735,7 +731,7 @@ fn evgr_dag_insert_valid_and_duplicate() {
 #[test]
 fn evgr_duplicate_header_insert_does_not_duplicate_time_index() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_ts = eg.current_genesis.read().await.header.timestamp;
         let dag_name = dag_ts.to_string();
         let event = Event::new(b"time-index-dedup".to_vec(), &eg).await.unwrap();
@@ -765,7 +761,7 @@ fn evgr_duplicate_header_insert_does_not_duplicate_time_index() {
 #[test]
 fn evgr_dag_insert_without_header_skipped() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
         let event = Event::new(b"orphan".to_vec(), &eg).await.unwrap();
         let ids = eg.dag_insert(slice::from_ref(&event), &dag_name).await.unwrap();
@@ -776,7 +772,7 @@ fn evgr_dag_insert_without_header_skipped() {
 #[test]
 fn evgr_header_insert_rejects_layer_jump() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let genesis = eg.current_genesis.read().await.clone();
         let dag_name = genesis.header.timestamp.to_string();
         let mut parents = [NULL_ID; N_EVENT_PARENTS];
@@ -800,7 +796,7 @@ fn evgr_header_insert_rejects_layer_jump() {
 #[test]
 fn evgr_header_insert_rejects_duplicate_parents() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let genesis = eg.current_genesis.read().await.clone();
         let dag_name = genesis.header.timestamp.to_string();
         let mut parents = [NULL_ID; N_EVENT_PARENTS];
@@ -825,8 +821,8 @@ fn evgr_header_insert_rejects_duplicate_parents() {
 #[test]
 fn evgr_header_validate_rejects_layer_overflow_parent() {
     smol::block_on(async {
-        let db = sled::Config::new().temporary(true).open().unwrap();
-        let tree = db.open_tree("headers").unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+        let tree = kvdb.open_tree_default("headers").unwrap();
         let timestamp = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
         let parent = Header {
             timestamp,
@@ -834,7 +830,7 @@ fn evgr_header_validate_rejects_layer_overflow_parent() {
             layer: u64::MAX,
             content_hash: blake3::hash(b"overflow-parent"),
         };
-        tree.insert(parent.id().as_bytes(), serialize_async(&parent).await).unwrap();
+        tree.insert(parent.id().as_bytes(), &serialize_async(&parent).await).unwrap();
 
         let mut parents = [NULL_ID; N_EVENT_PARENTS];
         parents[0] = parent.id();
@@ -854,8 +850,8 @@ fn evgr_header_validate_uses_target_slot_bounds() {
     smol::block_on(async {
         const HOUR_MS: u64 = 3_600_000;
 
-        let db = sled::Config::new().temporary(true).open().unwrap();
-        let tree = db.open_tree("headers").unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+        let tree = kvdb.open_tree_default("headers").unwrap();
         let dag_ts = 1_704_067_200_000;
         let drift = crate::event_graph::EVENT_TIME_DRIFT;
         let config = EventGraphConfig { hours_rotation: 6, ..test_config() };
@@ -865,7 +861,7 @@ fn evgr_header_validate_uses_target_slot_bounds() {
             layer: 0,
             content_hash: blake3::hash(&config.genesis_contents),
         };
-        tree.insert(genesis.id().as_bytes(), serialize_async(&genesis).await).unwrap();
+        tree.insert(genesis.id().as_bytes(), &serialize_async(&genesis).await).unwrap();
 
         let mut parents = [NULL_ID; N_EVENT_PARENTS];
         parents[0] = genesis.id();
@@ -895,8 +891,8 @@ fn evgr_header_validate_ignores_future_initial_genesis() {
     smol::block_on(async {
         const HOUR_MS: u64 = 3_600_000;
 
-        let db = sled::Config::new().temporary(true).open().unwrap();
-        let tree = db.open_tree("headers").unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+        let tree = kvdb.open_tree_default("headers").unwrap();
         let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
         let dag_ts = now.saturating_sub(HOUR_MS);
         let config =
@@ -907,7 +903,7 @@ fn evgr_header_validate_ignores_future_initial_genesis() {
             layer: 0,
             content_hash: blake3::hash(&config.genesis_contents),
         };
-        tree.insert(genesis.id().as_bytes(), serialize_async(&genesis).await).unwrap();
+        tree.insert(genesis.id().as_bytes(), &serialize_async(&genesis).await).unwrap();
 
         let mut parents = [NULL_ID; N_EVENT_PARENTS];
         parents[0] = genesis.id();
@@ -925,8 +921,8 @@ fn evgr_header_validate_ignores_future_initial_genesis() {
 #[test]
 fn evgr_header_validate_no_rotation_rejects_far_future() {
     smol::block_on(async {
-        let db = sled::Config::new().temporary(true).open().unwrap();
-        let tree = db.open_tree("headers").unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+        let tree = kvdb.open_tree_default("headers").unwrap();
         let config = test_config();
         let dag_ts = config.initial_genesis;
         let genesis = Header {
@@ -935,7 +931,7 @@ fn evgr_header_validate_no_rotation_rejects_far_future() {
             layer: 0,
             content_hash: blake3::hash(&config.genesis_contents),
         };
-        tree.insert(genesis.id().as_bytes(), serialize_async(&genesis).await).unwrap();
+        tree.insert(genesis.id().as_bytes(), &serialize_async(&genesis).await).unwrap();
 
         let mut parents = [NULL_ID; N_EVENT_PARENTS];
         parents[0] = genesis.id();
@@ -963,7 +959,7 @@ fn evgr_header_validate_no_rotation_rejects_far_future() {
 fn evgr_header_insert_rejects_unloaded_dag_slot() {
     smol::block_on(async {
         let config = EventGraphConfig { hours_rotation: 1, max_dags: Some(2), ..test_config() };
-        let eg = make_eg_with_config(config).await;
+        let (eg, _kvdb_folder) = make_eg_with_config(config).await;
         let dag_ts = next_hour_timestamp(-100).unwrap();
         let dag_name = dag_ts.to_string();
         let genesis = Header {
@@ -989,7 +985,7 @@ fn evgr_header_insert_rejects_unloaded_dag_slot() {
 #[test]
 fn evgr_fetch_headers_with_tips_is_bounded_and_layer_ordered() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_ts = eg.current_genesis.read().await.header.timestamp;
         let dag_name = dag_ts.to_string();
         let base = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
@@ -1026,7 +1022,7 @@ fn evgr_fetch_headers_with_tips_is_bounded_and_layer_ordered() {
 #[test]
 fn evgr_fetch_page_both_directions() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
         let base = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
         for i in 0..(MAX_RANGE_PAGE_SIZE as u64 + 10) {
@@ -1058,7 +1054,7 @@ fn evgr_fetch_page_both_directions() {
 #[test]
 fn evgr_fetch_page_with_blobs_is_dag_scoped_and_blob_aligned() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_ts = eg.current_genesis.read().await.header.timestamp;
         let dag_name = dag_ts.to_string();
         let base = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
@@ -1113,7 +1109,7 @@ fn evgr_fetch_page_with_blobs_is_dag_scoped_and_blob_aligned() {
 #[test]
 fn evgr_fetch_page_with_blobs_keeps_same_timestamp_cursor_position() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
         let ts = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
         let mut events = vec![];
@@ -1156,11 +1152,11 @@ async fn range_req_returns_blob_aligned_backward_page(ex: Arc<Executor<'static>>
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
     let base = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
     let mut events = vec![];
@@ -1170,15 +1166,15 @@ async fn range_req_returns_blob_aligned_backward_page(ex: Arc<Executor<'static>>
         let event = Event::with_timestamp(
             base + u64::from(i),
             vec![b'r', b'a', b'n', b'g', b'e', i],
-            &nodes[0],
+            &nodes[0].0,
         )
         .await
         .unwrap();
         let message_id = alice.next_message_id(event.header.timestamp).expect("budget");
-        let blob_struct = alice.create_signal(&event, message_id, &nodes[0]).await.unwrap();
+        let blob_struct = alice.create_signal(&event, message_id, &nodes[0].0).await.unwrap();
         let blob = serialize_async(&blob_struct).await;
 
-        for eg in nodes.iter().take(4) {
+        for (eg, _) in nodes.iter().take(4) {
             eg.insert_signal_with_blob(&event, &blob, &dag_name).await.unwrap();
         }
 
@@ -1187,11 +1183,12 @@ async fn range_req_returns_blob_aligned_backward_page(ex: Arc<Executor<'static>>
     }
 
     nodes[4]
+        .0
         .header_dag_insert(events.iter().map(|event| event.header.clone()).collect(), &dag_name)
         .await
         .unwrap();
 
-    let peer = nodes[4].p2p.hosts().peers().into_iter().next().expect("connected peer");
+    let peer = nodes[4].0.p2p.hosts().peers().into_iter().next().expect("connected peer");
     let sub = peer.subscribe_msg::<RangeRep>().await.unwrap();
     peer.send(&RangeReq {
         dag_name: dag_name.clone(),
@@ -1217,7 +1214,7 @@ async fn range_req_returns_blob_aligned_backward_page(ex: Arc<Executor<'static>>
     assert!(!rep.3);
     for (event, blob) in rep.0.iter().zip(rep.1.iter()) {
         assert!(matches!(
-            nodes[4].rln_verify_signal(event, blob).await,
+            nodes[4].0.rln_verify_signal(event, blob).await,
             crate::event_graph::rln::SignalCheck::Accepted
         ));
     }
@@ -1234,11 +1231,11 @@ async fn dag_sync_range_drains_pending_backward_page(ex: Arc<Executor<'static>>)
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
     let base = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
     let mut events = vec![];
@@ -1247,15 +1244,15 @@ async fn dag_sync_range_drains_pending_backward_page(ex: Arc<Executor<'static>>)
         let event = Event::with_timestamp(
             base + u64::from(i),
             vec![b'l', b'a', b'z', b'y', b'-', i],
-            &nodes[0],
+            &nodes[0].0,
         )
         .await
         .unwrap();
         let message_id = alice.next_message_id(event.header.timestamp).expect("budget");
-        let blob_struct = alice.create_signal(&event, message_id, &nodes[0]).await.unwrap();
+        let blob_struct = alice.create_signal(&event, message_id, &nodes[0].0).await.unwrap();
         let blob = serialize_async(&blob_struct).await;
 
-        for eg in nodes.iter().take(4) {
+        for (eg, _) in nodes.iter().take(4) {
             eg.insert_signal_with_blob(&event, &blob, &dag_name).await.unwrap();
         }
 
@@ -1263,6 +1260,7 @@ async fn dag_sync_range_drains_pending_backward_page(ex: Arc<Executor<'static>>)
     }
 
     let first = nodes[4]
+        .0
         .dag_sync_range(dag_ts, RangeCursor::newest(), SyncDirection::Backward, 2)
         .await
         .unwrap();
@@ -1274,7 +1272,7 @@ async fn dag_sync_range_drains_pending_backward_page(ex: Arc<Executor<'static>>)
     assert!(!first.exhausted);
 
     {
-        let store = nodes[4].dag_store.read().await;
+        let store = nodes[4].0.dag_store.read().await;
         let slot = store.get_slot(&dag_ts).unwrap();
         for event in &events {
             assert!(!slot.main_tree.contains_key(event.id().as_bytes()).unwrap());
@@ -1282,6 +1280,7 @@ async fn dag_sync_range_drains_pending_backward_page(ex: Arc<Executor<'static>>)
     }
 
     let second = nodes[4]
+        .0
         .dag_sync_range(dag_ts, first.next_cursor, SyncDirection::Backward, 2)
         .await
         .unwrap();
@@ -1291,11 +1290,11 @@ async fn dag_sync_range_drains_pending_backward_page(ex: Arc<Executor<'static>>)
         assert!(second.committed.contains(&event.id()));
     }
 
-    let store = nodes[4].dag_store.read().await;
+    let store = nodes[4].0.dag_store.read().await;
     let slot = store.get_slot(&dag_ts).unwrap();
     for event in &events {
         assert!(slot.main_tree.contains_key(event.id().as_bytes()).unwrap());
-        assert!(nodes[4].dag_blob_fetch(&event.id()).unwrap().is_some());
+        assert!(nodes[4].0.dag_blob_fetch(&event.id()).unwrap().is_some());
     }
     drop(store);
 
@@ -1305,7 +1304,7 @@ async fn dag_sync_range_drains_pending_backward_page(ex: Arc<Executor<'static>>)
 #[test]
 fn evgr_order_events_rejects_corrupt_event_record() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_ts = eg.current_genesis.read().await.header.timestamp;
         let bad_id = [0u8; 32];
         {
@@ -1320,7 +1319,7 @@ fn evgr_order_events_rejects_corrupt_event_record() {
 }
 
 async fn build_graph() -> Result<(EventGraphPtr, std::collections::HashMap<&'static str, Event>)> {
-    let eg = make_eg().await;
+    let (eg, _kvdb_folder) = make_eg().await;
     let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
     let genesis_hash = eg.current_genesis.read().await.id();
     let base = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
@@ -1406,27 +1405,27 @@ async fn propagation_with_real_blob(ex: Arc<Executor<'static>>) {
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.expect("register alice");
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
-    let event = Event::new(b"hello-via-rln".to_vec(), &nodes[0]).await.unwrap();
+    let event = Event::new(b"hello-via-rln".to_vec(), &nodes[0].0).await.unwrap();
 
     let message_id =
         alice.next_message_id(event.header.timestamp).expect("budget available on first signal");
-    let blob_struct = alice.create_signal(&event, message_id, &nodes[0]).await.unwrap();
+    let blob_struct = alice.create_signal(&event, message_id, &nodes[0].0).await.unwrap();
     let blob = serialize_async(&blob_struct).await;
 
-    nodes[0].header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
-    nodes[0].dag_insert(slice::from_ref(&event), &dag_name).await.unwrap();
-    nodes[0].dag_blob_store(&event.id(), &blob).unwrap();
-    nodes[0].p2p.broadcast(&EventPut(event.clone(), blob.clone())).await.unwrap();
+    nodes[0].0.header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
+    nodes[0].0.dag_insert(slice::from_ref(&event), &dag_name).await.unwrap();
+    nodes[0].0.dag_blob_store(&event.id(), &blob).unwrap();
+    nodes[0].0.p2p.broadcast(&EventPut(event.clone(), blob.clone())).await.unwrap();
 
     sleep(5).await;
 
-    for (i, eg) in nodes.iter().enumerate() {
+    for (i, (eg, _)) in nodes.iter().enumerate() {
         let store = eg.dag_store.read().await;
         let slot = store.get_slot(&dag_ts).unwrap();
         assert!(
@@ -1452,37 +1451,37 @@ async fn event_put_ingests_body_after_header_only_sync(ex: Arc<Executor<'static>
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
-    let event = Event::new(b"header-only-then-live-body".to_vec(), &nodes[0]).await.unwrap();
+    let event = Event::new(b"header-only-then-live-body".to_vec(), &nodes[0].0).await.unwrap();
     let message_id = alice.next_message_id(event.header.timestamp).expect("budget");
-    let blob_struct = alice.create_signal(&event, message_id, &nodes[0]).await.unwrap();
+    let blob_struct = alice.create_signal(&event, message_id, &nodes[0].0).await.unwrap();
     let blob = serialize_async(&blob_struct).await;
 
-    nodes[4].header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
+    nodes[4].0.header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
     {
-        let store = nodes[4].dag_store.read().await;
+        let store = nodes[4].0.dag_store.read().await;
         let slot = store.get_slot(&dag_ts).unwrap();
         assert!(slot.header_tree.contains_key(event.id().as_bytes()).unwrap());
         assert!(!slot.main_tree.contains_key(event.id().as_bytes()).unwrap());
     }
-    assert!(nodes[4].dag_blob_fetch(&event.id()).unwrap().is_none());
+    assert!(nodes[4].0.dag_blob_fetch(&event.id()).unwrap().is_none());
 
-    nodes[0].p2p.broadcast(&EventPut(event.clone(), blob.clone())).await.unwrap();
+    nodes[0].0.p2p.broadcast(&EventPut(event.clone(), blob.clone())).await.unwrap();
     sleep(5).await;
 
-    let store = nodes[4].dag_store.read().await;
+    let store = nodes[4].0.dag_store.read().await;
     let slot = store.get_slot(&dag_ts).unwrap();
     assert!(
         slot.main_tree.contains_key(event.id().as_bytes()).unwrap(),
         "EventPut should fill a body when only the header was known",
     );
     drop(store);
-    assert_eq!(nodes[4].dag_blob_fetch(&event.id()).unwrap().unwrap(), blob);
+    assert_eq!(nodes[4].0.dag_blob_fetch(&event.id()).unwrap().unwrap(), blob);
 
     shutdown_network(&nodes).await;
 }
@@ -1497,13 +1496,13 @@ async fn empty_blob_rejected(ex: Arc<Executor<'static>>) {
     // Every recipient must strike the sender and refuse to insert.
     let nodes = make_network(ex).await;
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
-    let event = Event::new(b"unauthenticated".to_vec(), &nodes[0]).await.unwrap();
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
+    let event = Event::new(b"unauthenticated".to_vec(), &nodes[0].0).await.unwrap();
 
-    nodes[0].p2p.broadcast(&EventPut(event.clone(), vec![])).await.unwrap();
+    nodes[0].0.p2p.broadcast(&EventPut(event.clone(), vec![])).await.unwrap();
     sleep(5).await;
 
-    for (i, eg) in nodes.iter().enumerate().skip(1) {
+    for (i, (eg, _)) in nodes.iter().enumerate().skip(1) {
         let store = eg.dag_store.read().await;
         let slot = store.get_slot(&dag_ts).unwrap();
         assert!(
@@ -1524,14 +1523,14 @@ async fn malformed_event_rejected_before_rln(ex: Arc<Executor<'static>>) {
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
-    let event = Event::new(b"preflight-live".to_vec(), &nodes[0]).await.unwrap();
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
+    let event = Event::new(b"preflight-live".to_vec(), &nodes[0].0).await.unwrap();
     let message_id = alice.next_message_id(event.header.timestamp).expect("budget");
-    let blob = alice.create_signal(&event, message_id, &nodes[0]).await.unwrap();
+    let blob = alice.create_signal(&event, message_id, &nodes[0].0).await.unwrap();
     let internal_nullifier = blob.internal_nullifier;
     let blob = serialize_async(&blob).await;
 
@@ -1539,11 +1538,11 @@ async fn malformed_event_rejected_before_rln(ex: Arc<Executor<'static>>) {
     malformed.content.extend_from_slice(b"-tampered");
     assert!(!malformed.content_matches_header());
 
-    nodes[0].p2p.broadcast(&EventPut(malformed.clone(), blob)).await.unwrap();
+    nodes[0].0.p2p.broadcast(&EventPut(malformed.clone(), blob)).await.unwrap();
     sleep(5).await;
 
     let epoch = epoch_of(malformed.header.timestamp);
-    for (i, eg) in nodes.iter().enumerate().skip(1) {
+    for (i, (eg, _)) in nodes.iter().enumerate().skip(1) {
         let store = eg.dag_store.read().await;
         let slot = store.get_slot(&dag_ts).unwrap();
         assert!(
@@ -1573,7 +1572,7 @@ async fn genesis_with_blob_rejected(ex: Arc<Executor<'static>>) {
     // deterministic and don't carry signals.
     let nodes = make_network(ex).await;
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
 
     let header = Header {
         timestamp: dag_ts,
@@ -1584,10 +1583,10 @@ async fn genesis_with_blob_rejected(ex: Arc<Executor<'static>>) {
     let event = Event { header, content: b"forged-genesis".to_vec() };
     let fake_blob = b"this-should-not-be-here".to_vec();
 
-    nodes[0].p2p.broadcast(&EventPut(event.clone(), fake_blob)).await.unwrap();
+    nodes[0].0.p2p.broadcast(&EventPut(event.clone(), fake_blob)).await.unwrap();
     sleep(5).await;
 
-    for (i, eg) in nodes.iter().enumerate().skip(1) {
+    for (i, (eg, _)) in nodes.iter().enumerate().skip(1) {
         let store = eg.dag_store.read().await;
         let slot = store.get_slot(&dag_ts).unwrap();
         assert!(
@@ -1602,7 +1601,7 @@ async fn genesis_with_blob_rejected(ex: Arc<Executor<'static>>) {
 #[test]
 fn evgr_fetch_missing_events_rejects_corrupt_header_record() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_ts = eg.current_genesis.read().await.header.timestamp;
         let dag_name = dag_ts.to_string();
         let bad_id = [0u8; 32];
@@ -1653,8 +1652,8 @@ async fn seed_rotating_event_unchecked(
 #[test]
 fn evgr_dag_insert_rejects_child_when_parent_body_rejected() {
     smol::block_on(async {
-        let source = make_eg().await;
-        let recipient = make_eg().await;
+        let (source, _source_kvdb_folder) = make_eg().await;
+        let (recipient, _recipient_kvdb_folder) = make_eg().await;
         let mut alice = TestIdentity::new();
         alice.register_directly(&source).await.unwrap();
         alice.register_directly(&recipient).await.unwrap();
@@ -1707,34 +1706,34 @@ async fn dag_sync_with_blob(ex: Arc<Executor<'static>>) {
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
 
-    let event = Event::new(b"synced-message".to_vec(), &nodes[0]).await.unwrap();
+    let event = Event::new(b"synced-message".to_vec(), &nodes[0].0).await.unwrap();
     let message_id = alice.next_message_id(event.header.timestamp).expect("budget");
-    let blob_struct = alice.create_signal(&event, message_id, &nodes[0]).await.unwrap();
+    let blob_struct = alice.create_signal(&event, message_id, &nodes[0].0).await.unwrap();
     let blob = serialize_async(&blob_struct).await;
 
-    for eg in nodes.iter().take(4) {
+    for (eg, _) in nodes.iter().take(4) {
         eg.header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
         eg.dag_insert(slice::from_ref(&event), &dag_name).await.unwrap();
         eg.dag_blob_store(&event.id(), &blob).unwrap();
     }
 
     {
-        let store = nodes[4].dag_store.read().await;
+        let store = nodes[4].0.dag_store.read().await;
         let slot = store.get_slot(&dag_ts).unwrap();
         assert!(!slot.main_tree.contains_key(event.id().as_bytes()).unwrap());
     }
 
-    nodes[4].dag_sync(dag_ts).await.expect("dag_sync should succeed");
+    nodes[4].0.dag_sync(dag_ts).await.expect("dag_sync should succeed");
     sleep(2).await;
 
-    let store = nodes[4].dag_store.read().await;
+    let store = nodes[4].0.dag_store.read().await;
     let slot = store.get_slot(&dag_ts).unwrap();
     assert!(
         slot.main_tree.contains_key(event.id().as_bytes()).unwrap(),
@@ -1742,7 +1741,7 @@ async fn dag_sync_with_blob(ex: Arc<Executor<'static>>) {
     );
     drop(store);
     assert!(
-        nodes[4].dag_blob_fetch(&event.id()).unwrap().is_some(),
+        nodes[4].0.dag_blob_fetch(&event.id()).unwrap().is_some(),
         "sync should bring the blob over too - without it, node 4 can't serve future late-joiners",
     );
 
@@ -1758,14 +1757,14 @@ async fn dag_sync_rejects_child_when_parent_body_rejected(ex: Arc<Executor<'stat
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
     let (parent, child, child_blob) = make_parent_child_with_child_blob(
-        &nodes[0],
+        &nodes[0].0,
         &mut alice,
         b"parent-body-closure-sync",
         b"child-body-closure-sync",
@@ -1773,19 +1772,19 @@ async fn dag_sync_rejects_child_when_parent_body_rejected(ex: Arc<Executor<'stat
     .await;
     let bad_parent_blob = b"bad-parent-rln-blob-sync".to_vec();
 
-    for eg in nodes.iter().take(4) {
+    for (eg, _) in nodes.iter().take(4) {
         seed_rotating_event_unchecked(eg, &parent, &bad_parent_blob, &dag_name).await;
         seed_rotating_event_unchecked(eg, &child, &child_blob, &dag_name).await;
     }
 
-    let result = nodes[4].dag_sync(dag_ts).await;
+    let result = nodes[4].0.dag_sync(dag_ts).await;
     assert!(
         matches!(result, Err(crate::Error::DagSyncFailed)),
         "dag_sync should fail when a required parent body is rejected, got {result:?}",
     );
     sleep(2).await;
 
-    let store = nodes[4].dag_store.read().await;
+    let store = nodes[4].0.dag_store.read().await;
     let slot = store.get_slot(&dag_ts).unwrap();
     assert!(
         !slot.main_tree.contains_key(parent.id().as_bytes()).unwrap(),
@@ -1812,33 +1811,33 @@ async fn dag_sync_rejects_bad_blob(ex: Arc<Executor<'static>>) {
     let nodes = make_network(ex).await;
 
     let alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
-    let event = Event::new(b"crafted-injection".to_vec(), &nodes[0]).await.unwrap();
+    let event = Event::new(b"crafted-injection".to_vec(), &nodes[0].0).await.unwrap();
 
     // Garbage bytes - won't deserialize as a real RLN signal, won't
     // verify. We don't need a real (failing) proof to exercise the
     // rejection path.
     let bad_blob = b"definitely-not-a-real-rln-blob".to_vec();
 
-    for eg in nodes.iter().take(4) {
+    for (eg, _) in nodes.iter().take(4) {
         eg.header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
         eg.dag_insert(slice::from_ref(&event), &dag_name).await.unwrap();
         eg.dag_blob_store(&event.id(), &bad_blob).unwrap();
     }
 
-    let result = nodes[4].dag_sync(dag_ts).await;
+    let result = nodes[4].0.dag_sync(dag_ts).await;
     assert!(
         matches!(result, Err(crate::Error::DagSyncFailed)),
         "dag_sync should fail when a requested body is rejected, got {result:?}",
     );
     sleep(2).await;
 
-    let store = nodes[4].dag_store.read().await;
+    let store = nodes[4].0.dag_store.read().await;
     let slot = store.get_slot(&dag_ts).unwrap();
     assert!(
         !slot.main_tree.contains_key(event.id().as_bytes()).unwrap(),
@@ -1857,14 +1856,14 @@ async fn fetch_parents_rejects_child_when_parent_body_rejected(ex: Arc<Executor<
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
     let (parent, child, child_blob) = make_parent_child_with_child_blob(
-        &nodes[0],
+        &nodes[0].0,
         &mut alice,
         b"parent-body-closure-fetch",
         b"child-body-closure-fetch",
@@ -1872,14 +1871,14 @@ async fn fetch_parents_rejects_child_when_parent_body_rejected(ex: Arc<Executor<
     .await;
     let bad_parent_blob = b"bad-parent-rln-blob-fetch".to_vec();
 
-    for eg in nodes.iter().take(4) {
+    for (eg, _) in nodes.iter().take(4) {
         seed_rotating_event_unchecked(eg, &parent, &bad_parent_blob, &dag_name).await;
     }
 
-    nodes[0].p2p.broadcast(&EventPut(child.clone(), child_blob)).await.unwrap();
+    nodes[0].0.p2p.broadcast(&EventPut(child.clone(), child_blob)).await.unwrap();
     sleep(5).await;
 
-    let store = nodes[4].dag_store.read().await;
+    let store = nodes[4].0.dag_store.read().await;
     let slot = store.get_slot(&dag_ts).unwrap();
     assert!(
         !slot.main_tree.contains_key(parent.id().as_bytes()).unwrap(),
@@ -1914,33 +1913,34 @@ async fn dormant_user_can_post_after_long_silence(ex: Arc<Executor<'static>>) {
     let nodes = make_network(ex).await;
 
     let mut alice = TestIdentity::new();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         alice.register_directly(eg).await.unwrap();
     }
 
     // Push the recent_roots window past Alice's registration.
     for seed in 100..117_u64 {
         let other = TestIdentity::with_seed(seed);
-        for eg in &nodes {
+        for (eg, _) in &nodes {
             other.register_directly(eg).await.unwrap();
         }
     }
 
-    let event = Event::new(b"long-silent-but-still-registered".to_vec(), &nodes[0]).await.unwrap();
+    let event =
+        Event::new(b"long-silent-but-still-registered".to_vec(), &nodes[0].0).await.unwrap();
     let message_id = alice.next_message_id(event.header.timestamp).expect("budget");
-    let blob_struct = alice.create_signal(&event, message_id, &nodes[0]).await.unwrap();
+    let blob_struct = alice.create_signal(&event, message_id, &nodes[0].0).await.unwrap();
     let blob = serialize_async(&blob_struct).await;
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
-    nodes[0].header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
-    nodes[0].dag_insert(slice::from_ref(&event), &dag_name).await.unwrap();
-    nodes[0].dag_blob_store(&event.id(), &blob).unwrap();
-    nodes[0].p2p.broadcast(&EventPut(event.clone(), blob)).await.unwrap();
+    nodes[0].0.header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
+    nodes[0].0.dag_insert(slice::from_ref(&event), &dag_name).await.unwrap();
+    nodes[0].0.dag_blob_store(&event.id(), &blob).unwrap();
+    nodes[0].0.p2p.broadcast(&EventPut(event.clone(), blob)).await.unwrap();
 
     sleep(5).await;
 
-    for (i, eg) in nodes.iter().enumerate() {
+    for (i, (eg, _)) in nodes.iter().enumerate() {
         let store = eg.dag_store.read().await;
         let slot = store.get_slot(&dag_ts).unwrap();
         assert!(

+ 150 - 138
src/event_graph/tests_rln.rs

@@ -26,7 +26,6 @@ use darkfi_sdk::{
     pasta::pallas,
 };
 use darkfi_serial::{deserialize_async, serialize_async};
-use sled_overlay::sled;
 use smol::Executor;
 
 use crate::{
@@ -161,8 +160,8 @@ fn rln_message_metadata_prune_old() {
 
 #[test]
 fn rln_identity_state_register_then_slash() {
-    let db = sled::Config::new().temporary(true).open().unwrap();
-    let mut s = IdentityState::new(&db).unwrap();
+    let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+    let mut s = IdentityState::new(&kvdb).unwrap();
 
     let c = pallas::Base::from(0xabcd_1234u64);
     assert!(!s.contains(&c));
@@ -177,8 +176,8 @@ fn rln_identity_state_register_then_slash() {
 
 #[test]
 fn rln_identity_state_register_rejects_duplicate() {
-    let db = sled::Config::new().temporary(true).open().unwrap();
-    let mut s = IdentityState::new(&db).unwrap();
+    let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+    let mut s = IdentityState::new(&kvdb).unwrap();
 
     let c = pallas::Base::from(99u64);
     s.register(c).unwrap();
@@ -188,8 +187,8 @@ fn rln_identity_state_register_rejects_duplicate() {
 
 #[test]
 fn rln_identity_state_slash_idempotent_for_unknown() {
-    let db = sled::Config::new().temporary(true).open().unwrap();
-    let mut s = IdentityState::new(&db).unwrap();
+    let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+    let mut s = IdentityState::new(&kvdb).unwrap();
     // Slashing something that was never registered is not an error. This
     // matters for P2P propagation: a slash event may legitimately arrive twice
     // via different paths. The commitment is still tombstoned permanently.
@@ -201,60 +200,60 @@ fn rln_identity_state_slash_idempotent_for_unknown() {
 
 #[test]
 fn rln_identity_state_persists_across_reopen() {
-    let db = sled::Config::new().temporary(true).open().unwrap();
+    let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
     let c = pallas::Base::from(0xfeedu64);
     let slashed = pallas::Base::from(0xdead_u64);
 
     {
-        let mut s = IdentityState::new(&db).unwrap();
+        let mut s = IdentityState::new(&kvdb).unwrap();
         s.register(c).unwrap();
         s.slash(slashed).unwrap();
-    } // drop closes the in-memory SMT but the derived state is in sled
+    } // drop closes the in-memory SMT but the derived state is in kvdb
 
-    let mut s2 = IdentityState::new(&db).unwrap();
+    let mut s2 = IdentityState::new(&kvdb).unwrap();
     assert!(s2.contains(&c), "leaf should survive close-and-reopen");
     assert!(s2.is_slashed(&slashed), "tombstone should survive close-and-reopen");
     assert!(s2.register(slashed).is_err());
 }
 
 #[test]
-fn rln_identity_state_restores_sled_smt_nodes_from_leaves() {
-    let db = sled::Config::new().temporary(true).open().unwrap();
+fn rln_identity_state_restores_kvdb_smt_nodes_from_leaves() {
+    let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
     let c = pallas::Base::from(0xbeefu64);
 
     let original_root = {
-        let mut s = IdentityState::new(&db).unwrap();
+        let mut s = IdentityState::new(&kvdb).unwrap();
         s.register(c).unwrap();
         s.root()
     };
 
-    db.open_tree("rln-identity-smt-nodes").unwrap().clear().unwrap();
+    kvdb.open_tree_default("rln-identity-smt-nodes").unwrap().clear().unwrap();
 
-    let s2 = IdentityState::new(&db).unwrap();
+    let s2 = IdentityState::new(&kvdb).unwrap();
     assert!(s2.contains(&c));
     assert_eq!(s2.root(), original_root);
     assert!(s2.prove_membership(&c).verify(&original_root, &c, &c));
 }
 
 #[test]
-fn rln_rebuild_detects_stale_sled_smt_nodes() {
+fn rln_rebuild_detects_stale_kvdb_smt_nodes() {
     smol::block_on(async {
         let config = EventGraphConfig { hours_rotation: 1, ..test_config() };
-        let eg = make_eg_with_config(config).await;
+        let (eg, _kvdb_folder) = make_eg_with_config(config).await;
         let commitment = genesis_commitment_at(&eg, 0);
         let original_root = eg.identity_state.as_ref().unwrap().read().await.root();
 
-        eg.sled_db.open_tree("rln-identity-smt-nodes").unwrap().clear().unwrap();
+        eg.kvdb.open_tree_default("rln-identity-smt-nodes").unwrap().clear().unwrap();
 
         assert!(eg.rln_contains(&commitment).await);
         assert_ne!(eg.identity_state.as_ref().unwrap().read().await.root(), original_root);
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 1);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 1);
 
         eg.rebuild_historical_roots_if_needed().await.unwrap();
 
         assert!(eg.rln_contains(&commitment).await);
         assert_eq!(eg.identity_state.as_ref().unwrap().read().await.root(), original_root);
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 1);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 1);
     })
 }
 
@@ -415,7 +414,7 @@ fn rln_bootstrapped_identities_parent_static_genesis() {
             hours_rotation: 1,
             ..crate::event_graph::test_helpers::test_config()
         };
-        let eg = make_eg_with_config(config).await;
+        let (eg, _kvdb_folder) = make_eg_with_config(config).await;
         let static_genesis =
             generate_genesis(&EventGraphConfig { hours_rotation: 0, ..eg.config.clone() }).unwrap();
         let static_genesis_id = static_genesis.id();
@@ -453,15 +452,15 @@ fn rln_startup_rebuild_before_bootstrap_restores_configured_identity() {
         let commitment = pallas::Base::from_repr(config.pregenerated_identity_commitments[0])
             .into_option()
             .unwrap();
-        let db = sled::Config::new().temporary(true).open().unwrap();
+        let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
 
         // Simulate a crash after the identity leaf was written but before the
         // corresponding pregenerated static event reached the static DAG.
-        let leaves = db.open_tree("rln-identity-leaves").unwrap();
-        leaves.insert(commitment.to_repr(), commitment.to_repr().as_ref()).unwrap();
+        let leaves = kvdb.open_tree_default("rln-identity-leaves").unwrap();
+        leaves.insert(&commitment.to_repr(), &commitment.to_repr()).unwrap();
         leaves.insert(b"bad-leaf", b"bad-value").unwrap();
 
-        let eg = make_eg_with_config_and_db(config, db).await;
+        let eg = make_eg_with_config_and_db(config, kvdb).await;
         assert!(eg.rln_contains(&commitment).await);
 
         let mut matching_static_events = 0usize;
@@ -485,7 +484,7 @@ fn rln_startup_rebuild_before_bootstrap_restores_configured_identity() {
 fn rln_rebuild_detects_stale_leaf_with_same_count() {
     smol::block_on(async {
         let config = EventGraphConfig { hours_rotation: 1, ..test_config() };
-        let eg = make_eg_with_config(config).await;
+        let (eg, _kvdb_folder) = make_eg_with_config(config).await;
         let commitment = genesis_commitment_at(&eg, 0);
         let stale = pallas::Base::from(0x51a1e_u64);
 
@@ -497,14 +496,14 @@ fn rln_rebuild_detects_stale_leaf_with_same_count() {
 
         assert!(!eg.rln_contains(&commitment).await);
         assert!(eg.rln_contains(&stale).await);
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 1);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 1);
 
         eg.rebuild_historical_roots_if_needed().await.unwrap();
 
         assert!(eg.rln_contains(&commitment).await);
         assert!(!eg.rln_contains(&stale).await);
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 1);
-        assert_eq!(eg.rln_historical_roots_by_value.len(), 1);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 1);
+        assert_eq!(eg.rln_historical_roots_by_value.len().unwrap(), 1);
     })
 }
 
@@ -522,7 +521,7 @@ fn rln_verify_signal_rejects_malformed_blobs() {
     // garbage bytes, or a truncated valid serialization. All must
     // be `Rejected`, never crash the verifier or mutate metadata.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let ev = make_static_event(b"static-event-1", &eg).await;
 
         // Empty.
@@ -569,7 +568,7 @@ fn rln_verify_signal_rejects_out_of_range_msg_limit() {
     // through the bound check (and would only fail because we don't
     // have a real proof - that's the purpose of the e2e tests).
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let ev = make_static_event(b"static-event-2", &eg).await;
         let root = eg.identity_state.as_ref().unwrap().read().await.root();
         let mk = |limit: u64| Blob {
@@ -592,7 +591,7 @@ fn rln_verify_signal_rejects_out_of_range_msg_limit() {
 #[test]
 fn rln_verify_signal_no_metadata_mutation_on_reject() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let ev = make_static_event(b"static-event-3", &eg).await;
 
         // Use the real current root to bypass the root check, but
@@ -660,7 +659,7 @@ fn genesis_commitment_at(eg: &EventGraphPtr, index: usize) -> pallas::Base {
 #[test]
 fn rln_static_event_pregenerated_guard_accepted() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let commitment = genesis_commitment_at(&eg, 0);
         let node = RLNNode::Registration(commitment);
 
@@ -672,7 +671,7 @@ fn rln_static_event_pregenerated_guard_accepted() {
 #[test]
 fn rln_static_event_guard_with_unknown_commitment_is_malicious() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let commitment = pallas::Base::from(0xdead_beefu64);
         assert!(!eg.config.pregenerated_identity_commitments.contains(&commitment.to_repr()));
 
@@ -688,7 +687,7 @@ fn rln_static_event_non_guard_registration_blobs_rejected() {
     // registration blobs are rejected before proof parsing until staked,
     // contract-backed admission exists.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let node = RLNNode::Registration(pallas::Base::from(1u64));
         let cases: &[(u64, &str)] = &[
             (0, "zero limit is structurally invalid"),
@@ -715,7 +714,7 @@ fn rln_static_event_registration_duplicate_commitment_soft_reject() {
     // peers may legitimately be relaying the same registration
     // event concurrently.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let commitment = pallas::Base::from(0xc0ffeeu64);
         eg.identity_state.as_ref().unwrap().write().await.register(commitment).unwrap();
 
@@ -746,7 +745,7 @@ fn rln_static_event_slash_invalid_blobs_rejected() {
     //   (b) Unknown root - the blob's merkle_root has never been a
     //       tree state.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
 
         // (a) Mismatched commitment.
         let real_root = eg.identity_state.as_ref().unwrap().read().await.root();
@@ -774,8 +773,8 @@ fn rln_identity_state_re_register_after_slash_requires_new_commitment() {
     // A slashed identity can re-register with new credentials
     // (different commitment). The ban is on the commitment, not
     // on the underlying network identity.
-    let db = sled::Config::new().temporary(true).open().unwrap();
-    let mut s = IdentityState::new(&db).unwrap();
+    let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+    let mut s = IdentityState::new(&kvdb).unwrap();
 
     let c1 = pallas::Base::from(1u64);
     let c2 = pallas::Base::from(2u64);
@@ -798,8 +797,8 @@ fn rln_identity_state_re_register_after_slash_requires_new_commitment() {
 fn rln_identity_state_root_history_window() {
     // ROOT_HISTORY_SIZE is 16. After 17 registrations the original
     // empty root should have been displaced.
-    let db = sled::Config::new().temporary(true).open().unwrap();
-    let mut s = IdentityState::new(&db).unwrap();
+    let (kvdb, _kvdb_folder) = kvdb_overlay::Database::open_temp().unwrap();
+    let mut s = IdentityState::new(&kvdb).unwrap();
     let original_empty_root = s.root();
     assert!(s.is_known_root(&original_empty_root));
 
@@ -818,14 +817,15 @@ fn rln_identity_state_root_history_window() {
 
 /// Build a fresh EG and an Alice identity. Convenience for the
 /// most common e2e setup.
-async fn fresh_identity_and_eg() -> (EventGraphPtr, TestIdentity) {
-    (make_eg().await, TestIdentity::new())
+async fn fresh_identity_and_eg() -> (EventGraphPtr, kvdb_overlay::TempDir, TestIdentity) {
+    let (eg, kvdb_folder) = make_eg().await;
+    (eg, kvdb_folder, TestIdentity::new())
 }
 
 #[test]
 fn rln_e2e_signals_up_to_user_limit() {
     smol::block_on(async {
-        let (eg, mut id) = fresh_identity_and_eg().await;
+        let (eg, _kvdb_folder, mut id) = fresh_identity_and_eg().await;
         // Use the smallest meaningful limit so the test is fast.
         id.user_message_limit = 3;
         id.register_directly(&eg).await.unwrap();
@@ -849,7 +849,7 @@ fn rln_e2e_signals_up_to_user_limit() {
 #[test]
 fn rln_e2e_duplicate_signal_dropped_not_slashed() {
     smol::block_on(async {
-        let (eg, mut id) = fresh_identity_and_eg().await;
+        let (eg, _kvdb_folder, mut id) = fresh_identity_and_eg().await;
         id.register_directly(&eg).await.unwrap();
 
         let event = make_static_event(b"static-event-6", &eg).await;
@@ -874,7 +874,7 @@ fn rln_verify_signal_rejects_recent_pre_slash_root_after_drift() {
     smol::block_on(async {
         use crate::event_graph::event::Header;
 
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let mut id = TestIdentity::new();
         let commitment = id.commitment();
         let drift = crate::event_graph::EVENT_TIME_DRIFT;
@@ -927,7 +927,7 @@ fn rln_verify_signal_rejects_recent_pre_slash_root_after_drift() {
 #[test]
 fn rln_e2e_slot_reuse_is_slashable() {
     smol::block_on(async {
-        let (eg, mut id) = fresh_identity_and_eg().await;
+        let (eg, _kvdb_folder, mut id) = fresh_identity_and_eg().await;
         id.register_directly(&eg).await.unwrap();
 
         // First signal at message_id=0
@@ -965,7 +965,7 @@ fn rln_e2e_slot_reuse_is_slashable() {
 fn rln_e2e_slash_proof_round_trip() {
     // Recover identity_secret_hash, build a slash proof, verify it.
     smol::block_on(async {
-        let (eg, id) = fresh_identity_and_eg().await;
+        let (eg, _kvdb_folder, id) = fresh_identity_and_eg().await;
         id.register_directly(&eg).await.unwrap();
 
         // Drive a slash by forging two shares for the same
@@ -1040,7 +1040,7 @@ async fn concurrent_slashes(ex: Arc<Executor<'static>>) {
 
     let id = TestIdentity::new();
     let commitment = id.commitment();
-    for eg in &nodes {
+    for (eg, _) in &nodes {
         eg.identity_state.as_ref().unwrap().write().await.register(commitment).expect("reg");
     }
 
@@ -1064,30 +1064,32 @@ async fn concurrent_slashes(ex: Arc<Executor<'static>>) {
     }
 
     let ish = id.identity_secret_hash();
-    let (ev0, bytes0) = build_slash(&nodes[0], ish, commitment).await;
-    let (ev1, bytes1) = build_slash(&nodes[1], ish, commitment).await;
+    let (ev0, bytes0) = build_slash(&nodes[0].0, ish, commitment).await;
+    let (ev1, bytes1) = build_slash(&nodes[1].0, ish, commitment).await;
 
     // Commit locally to each origin through the verified static-event path.
     let slash_node = RLNNode::Slashing(commitment);
     nodes[0]
+        .0
         .commit_verified_static_event(&ev0, &bytes0, &slash_node)
         .await
         .expect("commit slash 0");
     nodes[1]
+        .0
         .commit_verified_static_event(&ev1, &bytes1, &slash_node)
         .await
         .expect("commit slash 1");
 
     // Broadcast concurrently.
-    let f0 = nodes[0].static_broadcast(ev0, bytes0);
-    let f1 = nodes[1].static_broadcast(ev1, bytes1);
+    let f0 = nodes[0].0.static_broadcast(ev0, bytes0);
+    let f1 = nodes[1].0.static_broadcast(ev1, bytes1);
     let (_, _) = futures::future::join(f0, f1).await;
 
     sleep(5).await;
 
     // Every node must have removed the commitment, regardless
     // of which slash event it processed first.
-    for (i, eg) in nodes.iter().enumerate() {
+    for (i, (eg, _)) in nodes.iter().enumerate() {
         assert!(!eg.rln_contains(&commitment).await, "node {i} still has the slashed identity",);
     }
 
@@ -1103,7 +1105,7 @@ fn rln_static_slashes_persist_and_tombstone_commitment() {
             pregenerated_identity_commitments: vec![commitment.to_repr()],
             ..test_config()
         };
-        let eg = make_eg_with_config(config).await;
+        let (eg, _kvdb_folder) = make_eg_with_config(config).await;
 
         let reg_node = RLNNode::Registration(commitment);
         let reg_event = synth_static_event(1, 499_000, &reg_node).await;
@@ -1139,8 +1141,8 @@ fn rln_static_slashes_persist_and_tombstone_commitment() {
         eg.commit_verified_static_event(&replayed_slash, &blob, &slash_node).await.unwrap();
         assert!(eg.static_fetch(&replayed_slash.id()).await.unwrap().is_some());
         assert_eq!(eg.static_blob_fetch(&replayed_slash.id()).unwrap().unwrap(), blob);
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 3);
-        assert_eq!(eg.rln_historical_roots_by_value.len(), 3);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 3);
+        assert_eq!(eg.rln_historical_roots_by_value.len().unwrap(), 3);
 
         let re_registration = synth_static_event(4, 500_002, &reg_node).await;
         let outcome = eg
@@ -1157,7 +1159,7 @@ fn rln_static_slashes_persist_and_tombstone_commitment() {
             .is_err());
         assert!(eg.static_fetch(&re_registration.id()).await.unwrap().is_none());
         assert!(eg.static_blob_fetch(&re_registration.id()).unwrap().is_none());
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 3);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 3);
 
         eg.rln_historical_roots_ordered.clear().unwrap();
         eg.rln_historical_roots_by_value.clear().unwrap();
@@ -1165,7 +1167,7 @@ fn rln_static_slashes_persist_and_tombstone_commitment() {
         eg.rebuild_historical_roots_if_needed().await.unwrap();
         assert!(!eg.rln_contains(&commitment).await);
         assert!(eg.identity_state.as_ref().unwrap().read().await.is_slashed(&commitment));
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 3);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 3);
     })
 }
 
@@ -1173,7 +1175,7 @@ fn rln_static_slashes_persist_and_tombstone_commitment() {
 fn rln_static_blob_audit_repairs_pregenerated_guard() {
     smol::block_on(async {
         let config = EventGraphConfig { hours_rotation: 1, ..test_config() };
-        let eg = make_eg_with_config(config).await;
+        let (eg, _kvdb_folder) = make_eg_with_config(config).await;
         let commitment = genesis_commitment_at(&eg, 0);
 
         let mut registration_event = None;
@@ -1203,7 +1205,7 @@ fn rln_static_blob_audit_repairs_pregenerated_guard() {
 #[test]
 fn rln_static_blob_audit_does_not_fabricate_slash_blob() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let commitment = pallas::Base::from(0x0051_5a5b_u64);
         let node = RLNNode::Slashing(commitment);
         let ev = synth_static_event(1, 400_000, &node).await;
@@ -1224,19 +1226,20 @@ fn rln_multi_node_old_static_event_propagates() {
 async fn old_static_event_propagates(ex: Arc<Executor<'static>>) {
     let nodes = make_network(ex).await;
 
-    let commitment = pallas::Base::from_repr(nodes[0].config.pregenerated_identity_commitments[0])
-        .into_option()
-        .unwrap();
+    let commitment =
+        pallas::Base::from_repr(nodes[0].0.config.pregenerated_identity_commitments[0])
+            .into_option()
+            .unwrap();
     let rln_node = RLNNode::Registration(commitment);
     let content = serialize_async(&rln_node).await;
 
     let static_genesis =
-        generate_genesis(&EventGraphConfig { hours_rotation: 0, ..nodes[0].config.clone() })
+        generate_genesis(&EventGraphConfig { hours_rotation: 0, ..nodes[0].0.config.clone() })
             .unwrap();
     let mut parents = NULL_PARENTS;
     parents[0] = static_genesis.id();
     let header = crate::event_graph::event::Header {
-        timestamp: nodes[0].config.initial_genesis + 1,
+        timestamp: nodes[0].0.config.initial_genesis + 1,
         parents,
         layer: 1,
         content_hash: blake3::hash(&content),
@@ -1247,12 +1250,12 @@ async fn old_static_event_propagates(ex: Arc<Executor<'static>>) {
     assert!(!event.validate_new(), "precondition: event is outside live drift window");
     assert!(event.validate_new_static(), "precondition: static structural validation passes");
 
-    nodes[0].commit_verified_static_event(&event, &blob, &rln_node).await.unwrap();
-    nodes[0].static_broadcast(event.clone(), blob).await.unwrap();
+    nodes[0].0.commit_verified_static_event(&event, &blob, &rln_node).await.unwrap();
+    nodes[0].0.static_broadcast(event.clone(), blob).await.unwrap();
 
     sleep(5).await;
 
-    for (i, eg) in nodes.iter().enumerate() {
+    for (i, (eg, _)) in nodes.iter().enumerate() {
         assert!(eg.rln_contains(&commitment).await, "node {i} did not accept old static event");
         assert!(eg.static_fetch(&event.id()).await.unwrap().is_some());
     }
@@ -1284,40 +1287,41 @@ async fn static_sync_registration(ex: Arc<Executor<'static>>) {
     // disabled, so this test uses an app-configured pregenerated identity.
     let nodes = make_network(ex).await;
 
-    let commitment = pallas::Base::from_repr(nodes[0].config.pregenerated_identity_commitments[0])
-        .into_option()
-        .unwrap();
+    let commitment =
+        pallas::Base::from_repr(nodes[0].0.config.pregenerated_identity_commitments[0])
+            .into_option()
+            .unwrap();
     let blob_bytes = GENESIS_BLOB_GUARD.to_vec();
 
     let rln_node = RLNNode::Registration(commitment);
     let content = serialize_async(&rln_node).await;
-    let event = Event::new_static(content, &nodes[0]).await.unwrap();
+    let event = Event::new_static(content, &nodes[0].0).await.unwrap();
 
     // Seed nodes 0..=3 the same way a real verified static event is
     // committed: blob and event become durable before RLN side tables,
     // and subscribers are notified after the RLN apply step.
-    for eg in nodes.iter().take(4) {
+    for (eg, _) in nodes.iter().take(4) {
         eg.commit_verified_static_event(&event, &blob_bytes, &rln_node).await.unwrap();
     }
 
     // Node 4 knows nothing. Verify the precondition.
     assert!(
-        !nodes[4].rln_contains(&commitment).await,
+        !nodes[4].0.rln_contains(&commitment).await,
         "precondition: node 4 should not yet have the commitment",
     );
 
     // Sync.
-    nodes[4].static_sync().await.expect("static_sync should succeed");
+    nodes[4].0.static_sync().await.expect("static_sync should succeed");
 
     // Node 4 now has it.
     assert!(
-        nodes[4].rln_contains(&commitment).await,
+        nodes[4].0.rln_contains(&commitment).await,
         "node 4 should have the commitment after static_sync",
     );
 
     // The event itself is in node 4's static DAG.
     assert!(
-        nodes[4].static_fetch(&event.id()).await.unwrap().is_some(),
+        nodes[4].0.static_fetch(&event.id()).await.unwrap().is_some(),
         "node 4 should have the event body after static_sync",
     );
 
@@ -1332,43 +1336,45 @@ async fn static_sync_rejects_child_when_parent_rejected(ex: Arc<Executor<'static
     let nodes = make_network(ex).await;
 
     let bad_node = RLNNode::Registration(pallas::Base::from(0x0051_a71c_u64));
-    let bad_parent = Event::new_static(serialize_async(&bad_node).await, &nodes[0]).await.unwrap();
+    let bad_parent =
+        Event::new_static(serialize_async(&bad_node).await, &nodes[0].0).await.unwrap();
     let bad_blob = b"not-a-valid-static-rln-blob".to_vec();
-    for eg in nodes.iter().take(4) {
+    for (eg, _) in nodes.iter().take(4) {
         eg.static_insert(&bad_parent).await.unwrap();
         eg.static_blob_store(&bad_parent.id(), &bad_blob).unwrap();
     }
 
-    let commitment = pallas::Base::from_repr(nodes[0].config.pregenerated_identity_commitments[0])
-        .into_option()
-        .unwrap();
+    let commitment =
+        pallas::Base::from_repr(nodes[0].0.config.pregenerated_identity_commitments[0])
+            .into_option()
+            .unwrap();
     let child_node = RLNNode::Registration(commitment);
-    let child = Event::new_static(serialize_async(&child_node).await, &nodes[0]).await.unwrap();
+    let child = Event::new_static(serialize_async(&child_node).await, &nodes[0].0).await.unwrap();
     assert!(child.header.parents.contains(&bad_parent.id()));
 
     let child_blob = GENESIS_BLOB_GUARD.to_vec();
-    for eg in nodes.iter().take(4) {
+    for (eg, _) in nodes.iter().take(4) {
         eg.commit_verified_static_event(&child, &child_blob, &child_node).await.unwrap();
     }
 
-    assert!(nodes[4].static_fetch(&bad_parent.id()).await.unwrap().is_none());
-    assert!(nodes[4].static_fetch(&child.id()).await.unwrap().is_none());
-    assert!(!nodes[4].rln_contains(&commitment).await);
+    assert!(nodes[4].0.static_fetch(&bad_parent.id()).await.unwrap().is_none());
+    assert!(nodes[4].0.static_fetch(&child.id()).await.unwrap().is_none());
+    assert!(!nodes[4].0.rln_contains(&commitment).await);
 
-    let result = nodes[4].static_sync().await;
+    let result = nodes[4].0.static_sync().await;
     assert!(
         matches!(result, Err(crate::Error::DagSyncFailed)),
         "static_sync should fail clearly when a fetched static parent is rejected, got {result:?}",
     );
     assert!(
-        nodes[4].static_fetch(&bad_parent.id()).await.unwrap().is_none(),
+        nodes[4].0.static_fetch(&bad_parent.id()).await.unwrap().is_none(),
         "bad static parent should not be committed",
     );
     assert!(
-        nodes[4].static_fetch(&child.id()).await.unwrap().is_none(),
+        nodes[4].0.static_fetch(&child.id()).await.unwrap().is_none(),
         "child static event should not be committed without its parent",
     );
-    assert!(!nodes[4].rln_contains(&commitment).await);
+    assert!(!nodes[4].0.rln_contains(&commitment).await);
 
     shutdown_network(&nodes).await;
 }
@@ -1382,7 +1388,7 @@ fn rln_multi_node_static_sync_no_peers_is_ok() {
     // critical security bug (a fresh node could just refuse all
     // peers and claim to be consistent).
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let r = eg.static_sync().await;
         assert!(
             matches!(r, Err(crate::Error::DagSyncFailed)),
@@ -1413,13 +1419,13 @@ async fn static_sync_blob_propagation(ex: Arc<Executor<'static>>) {
     let commitment = id.commitment();
 
     let content = serialize_async(&RLNNode::Registration(commitment)).await;
-    let event = Event::new_static(content.clone(), &nodes[0]).await.unwrap();
+    let event = Event::new_static(content.clone(), &nodes[0].0).await.unwrap();
     // Synthetic blob - content doesn't matter for propagation
     // testing, only that it's non-empty so static_sync's
     // verification path takes the "blob present" branch.
     let synthetic_blob = b"synthetic-test-blob-bytes".to_vec();
 
-    for eg in nodes.iter().take(4) {
+    for (eg, _) in nodes.iter().take(4) {
         eg.identity_state.as_ref().unwrap().write().await.register(commitment).unwrap();
         eg.static_insert(&event).await.unwrap();
         // Synthetic blob will fail rln_verify_static_event (no
@@ -1432,13 +1438,13 @@ async fn static_sync_blob_propagation(ex: Arc<Executor<'static>>) {
     }
 
     // Node 4 starts empty.
-    assert!(node_does_not_have_blob(&nodes[4], &event.id()));
+    assert!(node_does_not_have_blob(&nodes[4].0, &event.id()));
 
     // Sync. Verification will fail on node 4 (synthetic blob
     // doesn't carry a real proof), so the EVENT won't end up
     // in node 4's static_dag - but the BLOB request travelled,
     // which is what we're testing here.
-    let _ = nodes[4].static_sync().await;
+    let _ = nodes[4].0.static_sync().await;
 
     // We can't assert the event got applied (verification
     // failed by design). What we CAN assert: nothing crashed,
@@ -1483,35 +1489,36 @@ async fn dag_injection_rejected(ex: Arc<Executor<'static>>) {
     // exercises the same code path without real keys.
     let nodes = make_network(ex).await;
 
-    let dag_ts = nodes[0].current_genesis.read().await.header.timestamp;
+    let dag_ts = nodes[0].0.current_genesis.read().await.header.timestamp;
     let dag_name = dag_ts.to_string();
 
     // Craft an event that LOOKS valid (proper parents from
     // node 0's tip set) but has a garbage blob. We pre-insert
     // its header so the structural validation passes on the
     // recipient.
-    let injected = Event::new(b"injected by malicious peer".to_vec(), &nodes[0]).await.unwrap();
+    let injected = Event::new(b"injected by malicious peer".to_vec(), &nodes[0].0).await.unwrap();
     let bad_blob = b"not-a-real-rln-blob".to_vec();
 
     // Node 0 records the bad event in its own DAG and stashes
     // the bad blob.
-    nodes[0].header_dag_insert(vec![injected.header.clone()], &dag_name).await.unwrap();
+    nodes[0].0.header_dag_insert(vec![injected.header.clone()], &dag_name).await.unwrap();
     // Bypass the verifier path - directly write to the trees
     // to simulate a malicious peer. We don't have a clean API
     // for that since we deliberately don't expose one in
     // production; reach into the internals here for the test.
-    nodes[0].dag_blobs.insert(injected.id().as_bytes(), bad_blob.as_slice()).unwrap();
+    nodes[0].0.dag_blobs.insert(injected.id().as_bytes(), bad_blob.as_slice()).unwrap();
     // Insert via the lenient `dag_insert` path (no blob check) to
     // simulate a malicious peer that has bypassed verification.
     // Production never calls `dag_insert` for received events -
     // only for events the node has already verified itself, or
     // for already-known events. A real attacker would write
-    // directly to sled; this is observationally equivalent.
-    nodes[0].dag_insert(std::slice::from_ref(&injected), &dag_name).await.unwrap();
+    // directly to kvdb; this is observationally equivalent.
+    nodes[0].0.dag_insert(std::slice::from_ref(&injected), &dag_name).await.unwrap();
 
     // Sanity: node 0 has the event.
     assert!(
         nodes[0]
+            .0
             .dag_store
             .read()
             .await
@@ -1527,10 +1534,11 @@ async fn dag_injection_rejected(ex: Arc<Executor<'static>>) {
     // fetch_missing_events which calls dag_insert_with_blobs
     // with the blob from node 0 - that's the verification
     // gate.
-    let _ = nodes[1].dag_sync(dag_ts).await;
+    let _ = nodes[1].0.dag_sync(dag_ts).await;
 
     // Node 1 must NOT have the injected event.
     let recipient_has = nodes[1]
+        .0
         .dag_store
         .read()
         .await
@@ -1547,11 +1555,11 @@ async fn dag_injection_rejected(ex: Arc<Executor<'static>>) {
 
 #[test]
 fn rln_blob_side_tables_round_trip() {
-    // Both `static_dag_blobs` and `dag_blobs` use the same sled
+    // Both `static_dag_blobs` and `dag_blobs` use the same kvdb
     // mechanics. One test exercises both, including the idempotent
     // re-store and last-writer-wins overwrite.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let eid_s = blake3::hash(b"fake-static-event-id");
         let eid_d = blake3::hash(b"fake-rotating-event-id");
         let blob_a = b"first-bytes".to_vec();
@@ -1581,7 +1589,7 @@ fn rln_static_blob_fetch_missing_is_none_not_error() {
     // because static_sync uses Option<Vec<u8>>; an Err leak would
     // wedge sync.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let unknown = blake3::hash(b"never-stored");
         let result = eg.static_blob_fetch(&unknown).unwrap();
         assert!(result.is_none());
@@ -1603,7 +1611,7 @@ fn rln_dag_insert_with_blobs_already_known_skips_verification() {
     // least not error) because the already-known check fires before
     // the verifier.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
 
         // Build a real event so it passes structural validation.
@@ -1648,7 +1656,7 @@ fn rln_dag_insert_with_blobs_rejects_missing_blob_on_non_genesis() {
     // This is the regression coverage for the policy tightening
     // that closed Vector 2 sync-time injection.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
         let event = Event::new(b"missing-blob".to_vec(), &eg).await.unwrap();
         eg.header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
@@ -1679,7 +1687,7 @@ fn rln_disabled_skips_state_and_accepts_empty_signal_blob() {
             pregenerated_identity_commitments: Vec::new(),
             ..test_config()
         };
-        let eg = make_eg_with_config(config).await;
+        let (eg, _kvdb_folder) = make_eg_with_config(config).await;
 
         assert!(!eg.rln_enabled());
         assert!(eg.zk_keys.is_none());
@@ -1720,7 +1728,7 @@ fn rln_dag_insert_with_blobs_rejects_bad_content_before_verification() {
     // proof for that header would verify. This must be rejected before RLN
     // verification so the share is never recorded.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let mut alice = TestIdentity::new();
         alice.register_directly(&eg).await.unwrap();
 
@@ -1762,7 +1770,7 @@ fn rln_insert_signal_with_blob_rejects_missing_blob_on_non_genesis() {
     // blob is rejected before any header, body, or blob side-table
     // state is persisted.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let dag_ts = eg.current_genesis.read().await.header.timestamp;
         let dag_name = dag_ts.to_string();
         let event = Event::new(b"public-missing-blob".to_vec(), &eg).await.unwrap();
@@ -1788,7 +1796,7 @@ fn rln_dag_insert_with_blobs_genesis_skips_verification() {
     // verifier path. This is what allows dag_prune to seed a fresh
     // DAG.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
 
         // The current_genesis IS such an event - already inserted
         // by the constructor. Re-inserting it via dag_insert_with_blobs
@@ -1827,7 +1835,7 @@ fn rln_dag_blobs_pruned_with_dag_rotation() {
     // This test is gated on max_dags being Some - under archival
     // mode (None), no eviction happens and the test would loop.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         if eg.config.max_dags.is_none() {
             // Archival mode - eviction never happens. Skip.
             return
@@ -1899,7 +1907,7 @@ fn rln_is_root_valid_at_respects_drift_window() {
     // direction), and stays valid for as long as it remains the
     // live root (until the next event).
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let drift = crate::event_graph::EVENT_TIME_DRIFT;
         let t_r: u64 = 1_000_000;
         let commitment = pallas::Base::from(0xaaaa_u64);
@@ -1960,7 +1968,7 @@ fn rln_slashed_identity_signal_rejection_lifecycle() {
     // Exercising the proof-verification side of (b) requires real
     // ZK keys and is left to the multi-node integration tests.
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let drift = crate::event_graph::EVENT_TIME_DRIFT;
 
         // The slashed identity's commitment.
@@ -2055,7 +2063,7 @@ fn rln_slashed_identity_signal_rejection_lifecycle() {
 #[test]
 fn rln_repeated_historical_root_keeps_original_interval() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let drift = crate::event_graph::EVENT_TIME_DRIFT;
 
         let commitment_a = pallas::Base::from(0xaaaa_0001_u64);
@@ -2078,8 +2086,8 @@ fn rln_repeated_historical_root_keeps_original_interval() {
 
         assert_eq!(duplicate_root, root_a);
         assert_ne!(root_b, root_a);
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 3);
-        assert_eq!(eg.rln_historical_roots_by_value.len(), 3);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 3);
+        assert_eq!(eg.rln_historical_roots_by_value.len().unwrap(), 3);
 
         assert!(eg.is_root_valid_at(&root_a, t0).unwrap());
         assert!(eg.is_root_valid_at(&root_a, t_duplicate).unwrap());
@@ -2090,7 +2098,7 @@ fn rln_repeated_historical_root_keeps_original_interval() {
 #[test]
 fn rln_commit_verified_static_event_notifies_after_rln_apply() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
         let sub = eg.static_subscribe().await;
 
         let commitment = pallas::Base::from(0xc0de_0001_u64);
@@ -2114,7 +2122,7 @@ fn rln_commit_verified_static_event_notifies_after_rln_apply() {
 #[test]
 fn rln_rebuild_restores_static_event_committed_before_rln_apply() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
 
         let commitment = pallas::Base::from(0xc0de_0002_u64);
         let node = RLNNode::Registration(commitment);
@@ -2127,14 +2135,14 @@ fn rln_rebuild_restores_static_event_committed_before_rln_apply() {
         eg.static_blob_store(&ev.id(), &blob).unwrap();
         eg.static_insert(&ev).await.unwrap();
         assert!(!eg.rln_contains(&commitment).await);
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 0);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 0);
 
         eg.rebuild_historical_roots_if_needed().await.unwrap();
 
         assert!(eg.rln_contains(&commitment).await);
         assert_eq!(eg.static_blob_fetch(&ev.id()).unwrap().unwrap(), blob);
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 1);
-        assert_eq!(eg.rln_historical_roots_by_value.len(), 1);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 1);
+        assert_eq!(eg.rln_historical_roots_by_value.len().unwrap(), 1);
         let state = eg.identity_state.as_ref().unwrap().read().await;
         assert!(state.is_known_root(&state.root()));
     })
@@ -2148,8 +2156,8 @@ fn rln_canonical_order_produces_same_roots_regardless_of_apply_order() {
     // ensures all nodes produce the same SEQUENCE of intermediate
     // roots when replaying the same set of events.
     smol::block_on(async {
-        let eg_a = make_eg().await;
-        let eg_b = make_eg().await;
+        let (eg_a, _kvdb_folder_a) = make_eg().await;
+        let (eg_b, _kvdb_folder_b) = make_eg().await;
 
         let c1 = pallas::Base::from(0x1111_u64);
         let c2 = pallas::Base::from(0x2222_u64);
@@ -2211,7 +2219,7 @@ fn rln_canonical_order_produces_same_roots_regardless_of_apply_order() {
 #[test]
 fn rln_rebuild_historical_roots() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
 
         let c1 = pallas::Base::from(0x3333_u64);
         let c2 = pallas::Base::from(0x4444_u64);
@@ -2226,9 +2234,9 @@ fn rln_rebuild_historical_roots() {
         eg.static_insert(&ev2).await.unwrap();
 
         // (b) Run rebuild on a consistent state - should be a no-op.
-        let before = eg.rln_historical_roots_ordered.len();
+        let before = eg.rln_historical_roots_ordered.len().unwrap();
         eg.rebuild_historical_roots_if_needed().await.unwrap();
-        assert_eq!(eg.rln_historical_roots_ordered.len(), before, "no-op when consistent");
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), before, "no-op when consistent");
 
         // (a) Wipe tables and rebuild.
         eg.rln_historical_roots_ordered.clear().unwrap();
@@ -2239,14 +2247,18 @@ fn rln_rebuild_historical_roots() {
 
         assert!(eg.is_root_valid_at(&r1, 100_000).unwrap(), "rebuild restored r1");
         assert!(eg.is_root_valid_at(&r2, 100_001).unwrap(), "rebuild restored r2");
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 2, "exactly one entry per static event",);
+        assert_eq!(
+            eg.rln_historical_roots_ordered.len().unwrap(),
+            2,
+            "exactly one entry per static event",
+        );
     })
 }
 
 #[test]
 fn rln_rebuild_repairs_mismatched_historical_root_by_value_index() {
     smol::block_on(async {
-        let eg = make_eg().await;
+        let (eg, _kvdb_folder) = make_eg().await;
 
         let c1 = pallas::Base::from(0x5555_u64);
         let c2 = pallas::Base::from(0x6666_u64);
@@ -2266,15 +2278,15 @@ fn rln_rebuild_repairs_mismatched_historical_root_by_value_index() {
         let bogus_a = [0u8; 72];
         let mut bogus_b = [0u8; 72];
         bogus_b[71] = 1;
-        eg.rln_historical_roots_by_value.insert(bogus_a, &[]).unwrap();
-        eg.rln_historical_roots_by_value.insert(bogus_b, &[]).unwrap();
-        assert_eq!(eg.rln_historical_roots_by_value.len(), 2);
+        eg.rln_historical_roots_by_value.insert(&bogus_a, &[]).unwrap();
+        eg.rln_historical_roots_by_value.insert(&bogus_b, &[]).unwrap();
+        assert_eq!(eg.rln_historical_roots_by_value.len().unwrap(), 2);
         assert!(!eg.is_root_valid_at(&r2, 200_001).unwrap());
 
         eg.rebuild_historical_roots_if_needed().await.unwrap();
 
-        assert_eq!(eg.rln_historical_roots_ordered.len(), 2);
-        assert_eq!(eg.rln_historical_roots_by_value.len(), 2);
+        assert_eq!(eg.rln_historical_roots_ordered.len().unwrap(), 2);
+        assert_eq!(eg.rln_historical_roots_by_value.len().unwrap(), 2);
         assert!(eg.is_root_valid_at(&r2, 200_001).unwrap());
     })
 }
@@ -2283,7 +2295,7 @@ fn rln_rebuild_repairs_mismatched_historical_root_by_value_index() {
 fn rln_perf_signal_verify() {
     use std::time::Instant;
     smol::block_on(async {
-        let (eg, mut id) = fresh_identity_and_eg().await;
+        let (eg, _kvdb_folder, mut id) = fresh_identity_and_eg().await;
         id.user_message_limit = 50; // enough headroom
         id.register_directly(&eg).await.unwrap();
 

+ 3 - 4
src/event_graph/util.rs

@@ -27,7 +27,6 @@ use std::{
 };
 
 use darkfi_serial::{deserialize, deserialize_async, serialize};
-use sled_overlay::sled;
 use tinyjson::JsonValue;
 
 use super::{
@@ -164,11 +163,11 @@ pub async fn recreate_from_replayer_log(datastore: &Path) -> JsonResult {
         Ok(reader) => reader,
         Err(e) => return replay_error(e.to_string()),
     };
-    let sled_db = match sled::open(datastore.join("replayed_db")) {
+    let kvdb = match kvdb_overlay::Database::open_default(&datastore.join("replayed_db")) {
         Ok(db) => db,
         Err(e) => return replay_error(e.to_string()),
     };
-    let dag = match sled_db.open_tree("replayer") {
+    let dag = match kvdb.open_tree_default("replayer") {
         Ok(tree) => tree,
         Err(e) => return replay_error(e.to_string()),
     };
@@ -185,7 +184,7 @@ pub async fn recreate_from_replayer_log(datastore: &Path) -> JsonResult {
                 Ok(event) => event,
                 Err(e) => return replay_error(e.to_string()),
             };
-            if let Err(e) = dag.insert(v.header.id().as_bytes(), serialize(&v)) {
+            if let Err(e) = dag.insert(v.header.id().as_bytes(), &serialize(&v)) {
                 return replay_error(e.to_string())
             }
         }