Pārlūkot izejas kodu

event_graph: Optional RLN

x 1 mēnesi atpakaļ
vecāks
revīzija
63c7c5cd84

+ 4 - 0
bin/darkirc/darkirc_config.toml

@@ -26,6 +26,10 @@
 ## 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
+
 ## Datastore path for RLN proving and verifying keys
 #zk_key_datastore = "~/.local/share/darkfi/darkirc_zk_keys"
 

+ 2 - 2
bin/darkirc/src/crypto/rln.rs

@@ -195,7 +195,7 @@ impl RlnIdentity {
         let y = a_0 + x * a_1;
 
         // Canonical membership path via the EG.
-        let (root, path) = eg.rln_membership_path(&self.commitment()).await;
+        let (root, path) = eg.rln_membership_path(&self.commitment()).await?;
 
         let request = SignalProvingRequest {
             nullifier: self.nullifier,
@@ -218,7 +218,7 @@ impl RlnIdentity {
             "[RLN] Creating signal proof for event {}",
             event.id(),
         );
-        let proof = eg.zk_keys.prove_signal(request).await?.proof;
+        let proof = eg.rln_zk_keys()?.prove_signal(request).await?.proof;
         log_memory("after local signal proving");
 
         Ok(Blob {

+ 6 - 15
bin/darkirc/src/irc/client.rs

@@ -245,21 +245,10 @@ impl Client {
                                 let dag_name = current_genesis.header.timestamp.to_string();
                                 drop(current_genesis);
 
-                                // Build the RLN signal blob FIRST,
-                                // BEFORE touching the local DAG. The
-                                // contract for any rotating-DAG event
-                                // is that it carries a valid signal
-                                // proof. If we can't produce one
-                                // (no identity, exhausted budget,
-                                // proof-construction failure) we
-                                // refuse to admit the event into the
-                                // local DAG at all. Letting it land
-                                // locally with an empty blob would
-                                // leave a no-blob event in our
-                                // dag_main_tree that we can't serve
-                                // honestly to syncing peers and that
-                                // peers would strict-reject anyway.
-                                let blob = {
+                                // Build the RLN signal blob before touching the local
+                                // DAG when RLN is enabled. With RLN disabled, outbound
+                                // events deliberately carry no proof blob.
+                                let blob = if self.server.darkirc.event_graph.rln_enabled() {
                                     let (rln_identity, mid) = match self
                                         .server
                                         .reserve_rln_message_id(event.header.timestamp)
@@ -303,6 +292,8 @@ impl Client {
                                             return Err(e)
                                         }
                                     }
+                                } else {
+                                    Vec::new()
                                 };
 
                                 // Commit our outbound signal through

+ 12 - 5
bin/darkirc/src/irc/server.rs

@@ -275,11 +275,18 @@ impl IrcServer {
             _ => None,
         };
 
-        // Set the default RLN account if any
-        let rln_identity = load_default_rln_identity(&darkirc.sled).await?;
-        if rln_identity.is_some() {
-            info!("Default RLN account set");
-        }
+        // 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?;
+            if rln_identity.is_some() {
+                info!("Default RLN account set");
+            }
+            rln_identity
+        } else {
+            info!("RLN disabled; skipping default RLN account load");
+            None
+        };
 
         let self_ = Arc::new(Self {
             darkirc,

+ 14 - 2
bin/darkirc/src/irc/services/nickserv.rs

@@ -413,6 +413,10 @@ impl NickServ {
         nick: &str,
         tokens: &mut SplitAsciiWhitespace<'_>,
     ) -> Result<Vec<ReplyType>> {
+        if !self.server.darkirc.event_graph.rln_enabled() {
+            return Ok(vec![notice(nick, "RLN is disabled; registration is not required.")])
+        }
+
         // Gather the tokens
         let (
             Some(account_name),
@@ -611,6 +615,10 @@ impl NickServ {
         nick: &str,
         tokens: &mut SplitAsciiWhitespace<'_>,
     ) -> Result<Vec<ReplyType>> {
+        if !self.server.darkirc.event_graph.rln_enabled() {
+            return Ok(vec![notice(nick, "RLN is disabled; SET has no effect.")])
+        }
+
         let Some(account_name) = tokens.next() else {
             return Ok(vec![notice(nick, "Invalid syntax. Use `SET <account_name>`.")])
         };
@@ -706,6 +714,10 @@ impl NickServ {
         nick: &str,
         tokens: &mut SplitAsciiWhitespace<'_>,
     ) -> Result<Vec<ReplyType>> {
+        if !self.server.darkirc.event_graph.rln_enabled() {
+            return Ok(vec![notice(nick, "RLN is disabled; SLASH is unavailable.")])
+        }
+
         let Some(account_name) = tokens.next() else {
             return Ok(notices(
                 nick,
@@ -795,13 +807,13 @@ impl NickServ {
         // future trusted remote prover can implement the same API.
         let identity_secret_hash = identity.identity_secret_hash();
         let request = {
-            let id_state = evgr.identity_state.read().await;
+            let id_state = evgr.rln_identity_state()?.read().await;
             prepare_slash_proof_request(identity_secret_hash, &id_state)
         };
         let root = request.merkle_root;
 
         log_memory("before slash proving");
-        let proof = evgr.zk_keys.prove_slash(request).await?.proof;
+        let proof = evgr.rln_zk_keys()?.prove_slash(request).await?.proof;
         log_memory("after slash proving");
 
         let slash_blob = SlashBlob { proof, identity_secret_hash, merkle_root: root };

+ 72 - 37
bin/darkirc/src/main.rs

@@ -181,6 +181,10 @@ struct Args {
     /// 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>,
+
     #[structopt(short, long, default_value = "~/.local/share/darkfi/replayed_darkirc_db")]
     /// Replay logs (DB) path
     replay_datastore: String,
@@ -443,6 +447,7 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     }
 
     info!("Initializing DarkIRC node");
+    let rln_enabled = args.rln_enabled.unwrap_or(false);
 
     // Create datastore path if not there already.
     let datastore = match expand_path(&args.datastore) {
@@ -457,17 +462,23 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
         return Err(e.into());
     }
 
-    let zk_key_datastore = match expand_path(&args.zk_key_datastore) {
-        Ok(v) => v,
-        Err(e) => {
-            error!("Bad RLN key datastore path `{}`: {e}", args.zk_key_datastore);
-            return Err(e);
+    let zk_key_datastore = if rln_enabled {
+        let zk_key_datastore = match expand_path(&args.zk_key_datastore) {
+            Ok(v) => v,
+            Err(e) => {
+                error!("Bad RLN key datastore path `{}`: {e}", args.zk_key_datastore);
+                return Err(e);
+            }
+        };
+        if let Err(e) = fs::create_dir_all(&zk_key_datastore).await {
+            error!("Failed to create RLN key datastore path `{zk_key_datastore:?}`: {e}");
+            return Err(e.into());
         }
+        Some(zk_key_datastore)
+    } else {
+        info!("RLN disabled; skipping RLN key datastore setup");
+        None
     };
-    if let Err(e) = fs::create_dir_all(&zk_key_datastore).await {
-        error!("Failed to create RLN key datastore path `{zk_key_datastore:?}`: {e}");
-        return Err(e.into());
-    }
 
     let replay_datastore = match expand_path(&args.replay_datastore) {
         Ok(v) => v,
@@ -499,19 +510,25 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     };
     log_memory("after sled open");
 
-    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);
-    let zk_key_db = 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());
-        }
+    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());
+                }
+            },
+        )
+    } else {
+        None
     };
 
     let p2p_settings: darkfi::net::Settings =
@@ -528,20 +545,36 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
         initial_genesis: DARKIRC_INITIAL_GENESIS,
         hours_rotation: DARKIRC_HOURS_ROTATION,
         genesis_contents: DARKIRC_GENESIS_CONTENTS.to_vec(),
-        pregenerated_identity_commitments: genesis_commits::pregenerated_identity_commitments(),
+        rln_enabled,
+        pregenerated_identity_commitments: if rln_enabled {
+            genesis_commits::pregenerated_identity_commitments()
+        } else {
+            Vec::new()
+        },
         max_dags: Some(args.history_retention_dags),
     };
-    let event_graph = match EventGraph::new_with_zk_key_db(
-        p2p.clone(),
-        sled_db.clone(),
-        zk_key_db.clone(),
-        replay_datastore.clone(),
-        replay_mode,
-        eg_config,
-        ex.clone(),
-    )
-    .await
-    {
+    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(),
+            zk_key_db,
+            replay_datastore.clone(),
+            replay_mode,
+            eg_config,
+            ex.clone(),
+        )
+        .await
+    } else {
+        EventGraph::new(
+            p2p.clone(),
+            sled_db.clone(),
+            replay_datastore.clone(),
+            replay_mode,
+            eg_config,
+            ex.clone(),
+        )
+        .await
+    } {
         Ok(v) => v,
         Err(e) => {
             error!("Event graph failed to start: {e}");
@@ -803,9 +836,11 @@ pub const DARKIRC_GENESIS_COMMITMENTS_REPR: &[[u8; 32]] = &[
     let flushed_bytes = sled_db.flush_async().await?;
     info!("Flushed {flushed_bytes} bytes");
 
-    info!("Flushing RLN key sled database...");
-    let flushed_key_bytes = zk_key_db.flush_async().await?;
-    info!("Flushed {flushed_key_bytes} RLN key bytes");
+    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!("Shut down successfully");
     Ok(())

+ 15 - 15
bin/tau/taud/src/main.rs

@@ -22,7 +22,6 @@ use std::{
     ffi::CString,
     fs::{create_dir_all, remove_dir_all},
     io::{stdin, Write},
-    slice,
     str::FromStr,
     sync::{atomic::Ordering, Arc, OnceLock},
 };
@@ -339,23 +338,23 @@ async fn start_sync_loop(
                     let encrypted_task = encrypt_sign_task(&tk, ws)?;
                     info!(target: "taud", "Send the task: ref: {}", tk.ref_id);
                     // Build a DAG event and return it.
-                    let event = Event::new(
-                        serialize_async(&encrypted_task).await,
-                        &event_graph,
-                    )
-                    .await;
-
-                    // If it fails for some reason, for now, we just note it
-                    // and pass.
+                    let event = match Event::new(serialize_async(&encrypted_task).await, &event_graph).await {
+                        Ok(event) => event,
+                        Err(e) => {
+                            error!(target: "taud", "Failed creating new DAG event: {e}");
+                            continue
+                        }
+                    };
+
                     let current_genesis = event_graph.current_genesis.read().await;
                     let dag_name = current_genesis.header.timestamp.to_string();
-                    if let Err(e) = event_graph.header_dag_insert(vec![event.header.clone()], &dag_name).await {
-                        error!(target: "taud", "Failed inserting new header to DAG: {}", e);
-                    }
-                    if let Err(e) = event_graph.dag_insert(slice::from_ref(&event), &dag_name).await {
+                    drop(current_genesis);
+
+                    if let Err(e) = event_graph.insert_signal_with_blob(&event, &[], &dag_name).await {
                         error!(target: "taud", "Failed inserting new event to DAG: {e}");
                     } else {
-                        // Otherwise, broadcast it
+                        // Otherwise, broadcast it. Taud runs EventGraph with RLN disabled,
+                        // so the blob is intentionally empty.
                         p2p.broadcast(&EventPut(event, vec![])).await;
                     }
                 }
@@ -567,6 +566,7 @@ async fn realmain(settings: Args, executor: Arc<smol::Executor<'static>>) -> Res
         initial_genesis: TAUD_INITIAL_GENESIS,
         hours_rotation: TAUD_HOURS_ROTATION,
         genesis_contents: TAUD_GENESIS_CONTENTS.to_vec(),
+        rln_enabled: false,
         pregenerated_identity_commitments: Vec::new(),
         max_dags: Some(TAUD_MAX_DAGS),
     };
@@ -633,7 +633,7 @@ async fn realmain(settings: Args, executor: Arc<smol::Executor<'static>>) -> Res
     ////////////////////
     // get history
     ////////////////////
-    let dag_events = event_graph.order_events().await;
+    let dag_events = event_graph.order_events().await?;
 
     for event in dag_events.iter() {
         let event_id = event.header.id();

+ 209 - 57
src/event_graph/mod.rs

@@ -111,6 +111,13 @@ pub struct EventGraphConfig {
     /// Unique payload embedded in genesis events.
     /// Different protocols must use different values.
     pub genesis_contents: Vec<u8>,
+    /// Enable RLN proof generation and verification.
+    ///
+    /// When false, EventGraph skips RLN key loading, identity SMT
+    /// initialization, static identity bootstrap/rebuild, and proof
+    /// verification. Rotating events are accepted structurally with empty
+    /// blobs, and outbound events are sent without proofs.
+    pub rln_enabled: bool,
     /// App-provided pregenerated RLN identity commitments.
     ///
     /// EventGraph treats these as the only proof-less registration
@@ -729,9 +736,9 @@ pub struct EventGraph {
     pub deg_enabled: AtomicBool,
     deg_publisher: PublisherPtr<DegEvent>,
     pub sled_db: sled::Db,
-    pub zk_keys: Arc<ZkKeys>,
-    pub identity_state: RwLock<IdentityState>,
-    pub rln_state: RwLock<RlnState>,
+    pub zk_keys: Option<Arc<ZkKeys>>,
+    pub identity_state: Option<RwLock<IdentityState>>,
+    pub rln_state: Option<RwLock<RlnState>>,
     /// App identifier mixed into the RLN external nullifier. Derived
     /// from `config.genesis_contents` so two deployments using the
     /// same circuit cannot collide on internal_nullifiers.
@@ -777,9 +784,15 @@ impl EventGraph {
         ex: Arc<Executor<'_>>,
     ) -> Result<EventGraphPtr> {
         config.validate()?;
-        let zk_keys = Arc::new(ZkKeys::build_and_load(&zk_key_db)?);
-        log_memory("after RLN key initialization");
-        Self::with_zk_keys(p2p, sled_db, datastore, replay_mode, config, zk_keys, ex).await
+        let zk_keys = if config.rln_enabled {
+            let zk_keys = Arc::new(ZkKeys::build_and_load(&zk_key_db)?);
+            log_memory("after RLN key initialization");
+            Some(zk_keys)
+        } else {
+            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
     }
 
     /// Same as [`Self::new`] but accepts a pre-built [`ZkKeys`].
@@ -797,14 +810,42 @@ impl EventGraph {
         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)
+            .await
+    }
+
+    async fn with_optional_zk_keys(
+        p2p: P2pPtr,
+        sled_db: sled::Db,
+        datastore: PathBuf,
+        replay_mode: bool,
+        config: EventGraphConfig,
+        zk_keys: Option<Arc<ZkKeys>>,
+        ex: Arc<Executor<'_>>,
     ) -> Result<EventGraphPtr> {
         config.validate()?;
-        let identity_state = IdentityState::new(&sled_db)?;
-        log_memory("after RLN identity state initialization");
+        let rln_enabled = config.rln_enabled;
+        let zk_keys = if rln_enabled {
+            Some(zk_keys.ok_or_else(|| Error::Custom("RLN enabled without ZK keys".into()))?)
+        } else {
+            None
+        };
+        let identity_state = if rln_enabled {
+            let identity_state = IdentityState::new(&sled_db)?;
+            log_memory("after RLN identity state initialization");
+            Some(identity_state)
+        } else {
+            None
+        };
         let rln_app_id = rln::RlnAppId::from_genesis(&config.genesis_contents);
         let current_genesis = generate_genesis(&config)?;
         let (pregenerated_identity_commitments, pregenerated_identity_commitment_reprs) =
-            validate_pregenerated_identity_commitments(&config)?;
+            if rln_enabled {
+                validate_pregenerated_identity_commitments(&config)?
+            } 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")?;
@@ -853,8 +894,8 @@ impl EventGraph {
             deg_enabled: AtomicBool::new(false),
             deg_publisher: Publisher::new(),
             zk_keys,
-            identity_state: RwLock::new(identity_state),
-            rln_state: RwLock::new(RlnState::new()),
+            identity_state: identity_state.map(RwLock::new),
+            rln_state: rln_enabled.then(|| RwLock::new(RlnState::new())),
             rln_app_id,
         });
 
@@ -866,20 +907,22 @@ impl EventGraph {
             self_.dag_prune(current_genesis).await?;
         }
 
-        // Reconcile persisted RLN state before bootstrapping. If an
-        // earlier process crashed after writing identity leaves but before
-        // inserting the corresponding static event, bootstrapping must see
-        // the corrected leaf set rather than skip the configured identity.
-        self_.rebuild_historical_roots_if_needed().await?;
+        if rln_enabled {
+            // Reconcile persisted RLN state before bootstrapping. If an
+            // earlier process crashed after writing identity leaves but before
+            // inserting the corresponding static event, bootstrapping must see
+            // the corrected leaf set rather than skip the configured identity.
+            self_.rebuild_historical_roots_if_needed().await?;
 
-        // Init genesis registration events after recovery has made the
-        // static DAG authoritative for the current identity tree.
-        if config.hours_rotation > 0 {
-            self_.bootstrap_genesis_identities().await?;
-            log_memory("after genesis identity bootstrap");
-        }
+            // Init genesis registration events after recovery has made the
+            // static DAG authoritative for the current identity tree.
+            if config.hours_rotation > 0 {
+                self_.bootstrap_genesis_identities().await?;
+                log_memory("after genesis identity bootstrap");
+            }
 
-        self_.audit_static_blobs().await?;
+            self_.audit_static_blobs().await?;
+        }
 
         if config.hours_rotation > 0 {
             let task = StoppableTask::new();
@@ -913,6 +956,10 @@ impl EventGraph {
     /// indexes are derived from it so crashes between the old split write
     /// steps cannot leave stale leaves or unusable root indexes behind.
     async fn rebuild_historical_roots_if_needed(self: &Arc<Self>) -> Result<()> {
+        if !self.rln_enabled() {
+            return Ok(())
+        }
+
         let mut events: Vec<(Event, rln::RLNNode)> = vec![];
 
         for item in self.static_dag.iter() {
@@ -957,7 +1004,8 @@ impl EventGraph {
         let expected_leaves = expected_commitments.len();
         let expected_slashed_count = expected_slashed.len();
         let (actual_commitments, actual_slashed, actual_root) = {
-            let state = self.identity_state.read().await;
+            let state_lock = self.rln_identity_state()?;
+            let state = state_lock.read().await;
             (state.commitment_reprs(), state.slashed_commitment_reprs(), state.root())
         };
         let (actual_leaves, leaves_consistent) = match actual_commitments {
@@ -1037,7 +1085,7 @@ impl EventGraph {
         self.rln_historical_roots_by_value.clear()?;
 
         {
-            let mut state = self.identity_state.write().await;
+            let mut state = self.rln_identity_state()?.write().await;
             state.clear_for_rebuild()?;
         }
         log_memory("after RLN state clear for rebuild");
@@ -1663,6 +1711,13 @@ impl EventGraph {
                 continue
             }
 
+            if !self.rln_enabled() {
+                self.commit_static_event_unverified(&ev, &blob).await?;
+                committed.insert(eid);
+                applied += 1;
+                continue
+            }
+
             let rln_node: rln::RLNNode = match deserialize_async_partial(ev.content()).await {
                 Ok((v, _)) => v,
                 Err(_) => {
@@ -1836,15 +1891,19 @@ impl EventGraph {
                 continue
             }
 
-            let blob = match self.dag_blob_fetch(&id)? {
-                Some(blob) if !blob.is_empty() => blob,
-                _ => {
-                    warn!(
-                        target: "event_graph::range",
-                        "[EVENTGRAPH] refusing to serve range event {id} without blob",
-                    );
-                    continue
+            let blob = if self.rln_enabled() {
+                match self.dag_blob_fetch(&id)? {
+                    Some(blob) if !blob.is_empty() => blob,
+                    _ => {
+                        warn!(
+                            target: "event_graph::range",
+                            "[EVENTGRAPH] refusing to serve range event {id} without blob",
+                        );
+                        continue
+                    }
                 }
+            } else {
+                Vec::new()
             };
 
             events.push(event);
@@ -1867,6 +1926,7 @@ impl EventGraph {
         peer_next_cursor: RangeCursor,
         exhausted: bool,
     ) -> Result<RangeSyncPage> {
+        let blobs = if !self.rln_enabled() { vec![Vec::new(); events.len()] } else { blobs };
         if events.len() != blobs.len() || events.len() > limit || events.len() > MAX_RANGE_PAGE_SIZE
         {
             return Err(Error::DagSyncFailed)
@@ -1908,7 +1968,7 @@ impl EventGraph {
                 if !event.dag_validate(&slot.header_tree, &self.config, dag_ts).await? {
                     return Err(Error::DagSyncFailed)
                 }
-                if !already_have && !already_pending && blob.is_empty() {
+                if self.rln_enabled() && !already_have && !already_pending && blob.is_empty() {
                     return Err(Error::DagSyncFailed)
                 }
 
@@ -1924,6 +1984,12 @@ impl EventGraph {
                 continue
             }
 
+            if !self.rln_enabled() {
+                newly_pending.push(PendingLazyEvent { event: event.clone(), blob });
+                accepted_events.push(event);
+                continue
+            }
+
             match self.rln_verify_signal(&event, &blob).await {
                 rln::SignalCheck::Accepted => {
                     newly_pending.push(PendingLazyEvent { event: event.clone(), blob });
@@ -2094,6 +2160,10 @@ impl EventGraph {
         blob: &[u8],
         dag_name: &str,
     ) -> Result<Vec<blake3::Hash>> {
+        if !self.rln_enabled() {
+            self.header_dag_insert(vec![event.header.clone()], dag_name).await?;
+            return self.dag_insert(std::slice::from_ref(event), dag_name).await
+        }
         if event.header.parents != NULL_PARENTS && blob.is_empty() {
             return Err(Error::Custom("rotating-DAG signal event blob must not be empty".into()))
         }
@@ -2153,12 +2223,12 @@ impl EventGraph {
         blob: &[u8],
         dag_name: &str,
     ) -> Result<Vec<blake3::Hash>> {
-        if event.header.parents != NULL_PARENTS && blob.is_empty() {
+        if self.rln_enabled() && event.header.parents != NULL_PARENTS && blob.is_empty() {
             return Err(Error::Custom("verified signal event blob must not be empty".into()))
         }
 
         self.header_dag_insert(vec![event.header.clone()], dag_name).await?;
-        if event.header.parents != NULL_PARENTS {
+        if self.rln_enabled() && event.header.parents != NULL_PARENTS {
             self.dag_blob_store(&event.id(), blob)?;
         }
         self.dag_insert(std::slice::from_ref(event), dag_name).await
@@ -2292,6 +2362,12 @@ impl EventGraph {
                 continue
             }
 
+            if !self.rln_enabled() {
+                accepted.push(i);
+                accepted_body_ids.insert(eid);
+                continue
+            }
+
             let blob = blobs.get(i).cloned().unwrap_or_default();
             if blob.is_empty() {
                 if require_blobs {
@@ -2343,7 +2419,6 @@ impl EventGraph {
         let mut store = self.dag_store.write().await;
         let slot = store.get_slot_mut(&dag_ts).ok_or(Error::DagSyncFailed)?;
 
-        let mut accepted = accepted;
         sort_event_indices(events, &mut accepted);
 
         let mut ids = Vec::with_capacity(accepted.len());
@@ -2387,12 +2462,14 @@ impl EventGraph {
 
             // Persist the blob alongside the event for future
             // sync-time re-verification by other late-joiners.
-            if let Some(blob) = blobs.get(i) {
-                if !blob.is_empty() {
-                    if require_blobs {
-                        self.dag_blob_store(&eid, blob)?;
-                    } else {
-                        let _ = self.dag_blob_store(&eid, blob);
+            if self.rln_enabled() {
+                if let Some(blob) = blobs.get(i) {
+                    if !blob.is_empty() {
+                        if require_blobs {
+                            self.dag_blob_store(&eid, blob)?;
+                        } else {
+                            let _ = self.dag_blob_store(&eid, blob);
+                        }
                     }
                 }
             }
@@ -2703,6 +2780,19 @@ impl EventGraph {
         Ok(())
     }
 
+    /// Commit a static-DAG event without RLN verification or identity-state
+    /// mutation.
+    ///
+    /// Used when RLN is disabled. The static DAG remains available for generic
+    /// long-lived graph state, but the event content is not interpreted as an
+    /// RLN registration or slash.
+    pub async fn commit_static_event_unverified(&self, ev: &Event, _blob: &[u8]) -> Result<()> {
+        let ev_bytes = serialize_async(ev).await;
+        self.static_persist_serialized(&ev.id(), &ev_bytes)?;
+        self.static_pub.notify(ev.clone()).await;
+        Ok(())
+    }
+
     /// Durably commit a verified static RLN event.
     ///
     /// The write order is intentional: blob first, static DAG second, RLN
@@ -2717,13 +2807,17 @@ impl EventGraph {
         blob: &[u8],
         rln_node: &rln::RLNNode,
     ) -> Result<pallas::Base> {
+        if !self.rln_enabled() {
+            self.commit_static_event_unverified(ev, blob).await?;
+            return Ok(IdentityState::empty_root())
+        }
         if blob.is_empty() {
             return Err(Error::Custom("static RLN event blob must not be empty".into()))
         }
 
         let ev_id = ev.id();
         let ev_bytes = serialize_async(ev).await;
-        let mut state = self.identity_state.write().await;
+        let mut state = self.rln_identity_state()?.write().await;
         Self::ensure_rln_static_event_transition(&state, rln_node)?;
 
         self.static_blob_store(&ev_id, blob)?;
@@ -2754,6 +2848,10 @@ impl EventGraph {
     /// blobs and future staked registration proofs are not reconstructible and
     /// are logged for operator intervention.
     async fn audit_static_blobs(&self) -> Result<()> {
+        if !self.rln_enabled() {
+            return Ok(())
+        }
+
         let mut repaired = 0usize;
         let mut unrecoverable = 0usize;
         let mut malformed = 0usize;
@@ -2894,7 +2992,10 @@ impl EventGraph {
         ev: &Event,
         node: &rln::RLNNode,
     ) -> Result<pallas::Base> {
-        let mut state = self.identity_state.write().await;
+        if !self.rln_enabled() {
+            return Ok(IdentityState::empty_root())
+        }
+        let mut state = self.rln_identity_state()?.write().await;
         self.apply_rln_static_event_locked(ev, node, &mut state)
     }
 
@@ -3146,6 +3247,26 @@ impl EventGraph {
         self.synced.load(Ordering::Acquire)
     }
 
+    /// True when RLN proof generation and verification are enabled.
+    pub fn rln_enabled(&self) -> bool {
+        self.config.rln_enabled
+    }
+
+    /// Return the RLN identity state when RLN is enabled.
+    pub fn rln_identity_state(&self) -> Result<&RwLock<IdentityState>> {
+        self.identity_state.as_ref().ok_or_else(|| Error::Custom("RLN is disabled".into()))
+    }
+
+    /// Return the RLN share metadata state when RLN is enabled.
+    pub fn rln_share_state(&self) -> Result<&RwLock<RlnState>> {
+        self.rln_state.as_ref().ok_or_else(|| Error::Custom("RLN is disabled".into()))
+    }
+
+    /// Return the RLN ZK keys when RLN is enabled.
+    pub fn rln_zk_keys(&self) -> Result<&ZkKeys> {
+        self.zk_keys.as_deref().ok_or_else(|| Error::Custom("RLN is disabled".into()))
+    }
+
     pub async fn deg_subscribe(&self) -> Subscription<DegEvent> {
         self.deg_publisher.clone().subscribe().await
     }
@@ -3195,14 +3316,15 @@ impl EventGraph {
     pub async fn rln_membership_path(
         &self,
         commitment: &darkfi_sdk::pasta::pallas::Base,
-    ) -> (darkfi_sdk::pasta::pallas::Base, darkfi_sdk::crypto::smt::PathFp) {
-        let s = self.identity_state.read().await;
-        (s.root(), s.prove_membership(commitment))
+    ) -> Result<(darkfi_sdk::pasta::pallas::Base, darkfi_sdk::crypto::smt::PathFp)> {
+        let s = self.rln_identity_state()?.read().await;
+        Ok((s.root(), s.prove_membership(commitment)))
     }
 
     /// True if the given identity commitment is registered.
     pub async fn rln_contains(&self, commitment: &darkfi_sdk::pasta::pallas::Base) -> bool {
-        self.identity_state.read().await.contains(commitment)
+        let Ok(state) = self.rln_identity_state() else { return false };
+        state.read().await.contains(commitment)
     }
 
     /// Verify an RLN signal blob against this event graph's state.
@@ -3224,6 +3346,10 @@ impl EventGraph {
         use darkfi_sdk::{crypto::poseidon_hash, pasta::pallas};
         use rln::{epoch_of, hash_event, Blob, SignalCheck, MAX_MSG_LIMIT};
 
+        if !self.rln_enabled() {
+            return SignalCheck::Accepted
+        }
+
         let rcvd: Blob = match deserialize_async_partial(blob).await {
             Ok((v, _)) => v,
             Err(_) => return SignalCheck::Rejected,
@@ -3255,7 +3381,8 @@ impl EventGraph {
         //    the timestamp-window check below so old pre-slash roots
         //    cannot stay valid indefinitely.
         {
-            let id_state = self.identity_state.read().await;
+            let Ok(id_state_lock) = self.rln_identity_state() else { return SignalCheck::Rejected };
+            let id_state = id_state_lock.read().await;
             if !id_state.is_current_root(&rcvd.merkle_root) {
                 drop(id_state);
                 match self.is_root_valid_at(&rcvd.merkle_root, event.header.timestamp) {
@@ -3276,7 +3403,10 @@ impl EventGraph {
                         // misbehavior, useful for operators
                         // debugging "why are my messages being
                         // rejected" or investigating peer abuse.
-                        let local_root = self.identity_state.read().await.root();
+                        let local_root = match self.rln_identity_state() {
+                            Ok(state) => state.read().await.root(),
+                            Err(_) => return SignalCheck::Rejected,
+                        };
                         let historical_count = self.rln_historical_roots_ordered.len();
                         warn!(
                             target: "event_graph::rln_verify_signal",
@@ -3317,13 +3447,15 @@ impl EventGraph {
             rcvd.y,
             rcvd.internal_nullifier,
         ];
-        if rcvd.proof.verify(&self.zk_keys.signal_vk, &pi).is_err() {
+        let Ok(zk_keys) = self.rln_zk_keys() else { return SignalCheck::Rejected };
+        if rcvd.proof.verify(&zk_keys.signal_vk, &pi).is_err() {
             return SignalCheck::Rejected
         }
 
         // 3) Now consult the metadata table. Any share we look at
         //    here is guaranteed to be from a valid proof.
-        let mut state = self.rln_state.write().await;
+        let Ok(rln_state) = self.rln_share_state() else { return SignalCheck::Rejected };
+        let mut state = rln_state.write().await;
 
         // Prune metadata relative to THIS signal's epoch, not
         // wall-clock. The retention window is conceptually
@@ -3390,6 +3522,15 @@ impl EventGraph {
         use darkfi_sdk::crypto::poseidon_hash;
         use rln::{RLNNode, SlashBlob, StaticEventCheck};
 
+        if !self.rln_enabled() {
+            return match rln_node {
+                RLNNode::Registration(commitment) => {
+                    StaticEventCheck::AcceptedRegistration(*commitment)
+                }
+                RLNNode::Slashing(commitment) => StaticEventCheck::AcceptedSlash(*commitment),
+            }
+        }
+
         match rln_node {
             RLNNode::Registration(commitment) => {
                 // Current admission policy is pregenerated identities only.
@@ -3399,7 +3540,10 @@ impl EventGraph {
                 if blob == rln::GENESIS_BLOB_GUARD {
                     let repr = commitment.to_repr();
                     if self.pregenerated_identity_commitment_reprs.contains(&repr) {
-                        let state = self.identity_state.read().await;
+                        let Ok(state_lock) = self.rln_identity_state() else {
+                            return StaticEventCheck::Rejected
+                        };
+                        let state = state_lock.read().await;
                         if state.contains(commitment) || state.is_slashed(commitment) {
                             return StaticEventCheck::Rejected
                         }
@@ -3465,7 +3609,8 @@ impl EventGraph {
                 };
 
                 let pi = vec![sl.identity_secret_hash, sl.merkle_root];
-                if sl.proof.verify(&self.zk_keys.slash_vk, &pi).is_err() {
+                let Ok(zk_keys) = self.rln_zk_keys() else { return StaticEventCheck::Rejected };
+                if sl.proof.verify(&zk_keys.slash_vk, &pi).is_err() {
                     return StaticEventCheck::Rejected
                 }
 
@@ -3483,7 +3628,10 @@ impl EventGraph {
                 // accepts any root that was live within DRIFT of the
                 // slash timestamp.
                 {
-                    let id_state = self.identity_state.read().await;
+                    let Ok(id_state_lock) = self.rln_identity_state() else {
+                        return StaticEventCheck::Rejected
+                    };
+                    let id_state = id_state_lock.read().await;
                     if !id_state.is_current_root(&sl.merkle_root) {
                         drop(id_state);
                         match self.is_root_valid_at(&sl.merkle_root, event_timestamp) {
@@ -3503,6 +3651,10 @@ impl EventGraph {
     /// event itself is inserted. Idempotent - skips any commitment
     /// already present in the identity tree.
     pub async fn bootstrap_genesis_identities(&self) -> Result<()> {
+        if !self.rln_enabled() {
+            return Ok(())
+        }
+
         // Deterministic for configured pregenerated identities.
         let genesis_event = generate_static_genesis(&self.config);
         let genesis_id = genesis_event.id();
@@ -3518,7 +3670,7 @@ impl EventGraph {
 
         for commitment in self.pregenerated_identity_commitments.iter() {
             {
-                let state = self.identity_state.read().await;
+                let state = self.rln_identity_state()?.read().await;
                 if state.contains(commitment) {
                     skipped_active += 1;
                     continue

+ 48 - 15
src/event_graph/proto.rs

@@ -463,10 +463,13 @@ impl ProtocolEventGraph {
         let mut bantimes = MovingWindow::new(WINDOW_EXPIRY_TIME);
 
         loop {
-            let (event, blob) = match self.ev_put_sub.receive().await {
+            let (event, mut blob) = match self.ev_put_sub.receive().await {
                 Ok(v) => (v.0.clone(), v.1.clone()),
                 Err(_) => continue,
             };
+            if !self.event_graph.rln_enabled() {
+                blob.clear();
+            }
 
             if !self.event_graph.is_synced() {
                 continue
@@ -511,17 +514,19 @@ impl ProtocolEventGraph {
                 continue
             }
 
-            // RLN: every non-genesis event MUST carry a signal proof. The only
-            // exception is genesis-shaped events (parents == NULL_PARENTS),
-            // which are deterministic consensus inputs and never carry blobs.
-            if event.header.parents != NULL_PARENTS {
-                if blob.is_empty() {
+            // RLN: every non-genesis event MUST carry a signal proof when RLN
+            // is enabled. With RLN disabled, blobs are ignored and events are
+            // admitted based on DAG structure only.
+            if self.event_graph.rln_enabled() {
+                if event.header.parents != NULL_PARENTS {
+                    if blob.is_empty() {
+                        self.clone().strike().await?;
+                        continue
+                    }
+                } else if !blob.is_empty() {
                     self.clone().strike().await?;
                     continue
                 }
-            } else if !blob.is_empty() {
-                self.clone().strike().await?;
-                continue
             }
 
             // Fetch missing parents (depth-bounded)
@@ -572,7 +577,10 @@ impl ProtocolEventGraph {
                 continue
             }
 
-            if event.header.parents != NULL_PARENTS && self.verify_rln_signal(&event, &blob).await {
+            if self.event_graph.rln_enabled() &&
+                event.header.parents != NULL_PARENTS &&
+                self.verify_rln_signal(&event, &blob).await
+            {
                 continue
             }
 
@@ -708,6 +716,10 @@ impl ProtocolEventGraph {
     /// wrapper that translates the [`rln::SignalCheck`] outcome
     /// into "accept or reject" plus the slash side effect.
     async fn verify_rln_signal(&self, event: &Event, blob: &[u8]) -> bool {
+        if !self.event_graph.rln_enabled() {
+            return false
+        }
+
         match self.event_graph.rln_verify_signal(event, blob).await {
             rln::SignalCheck::Accepted => false,
             rln::SignalCheck::Rejected => true,
@@ -729,6 +741,10 @@ impl ProtocolEventGraph {
     /// baked into `identity_secret_hash`, so the verifier doesn't
     /// need to know it explicitly.
     async fn slash(&self, shares: Vec<(pallas::Base, pallas::Base)>) {
+        if !self.event_graph.rln_enabled() {
+            return
+        }
+
         let identity_secret_hash = match sss_recover(&shares) {
             Ok(s) => s,
             Err(e) => {
@@ -744,7 +760,8 @@ impl ProtocolEventGraph {
         // were against a stale root we no longer have, or we have a
         // bug). Either way, do not broadcast a bogus slash.
         {
-            let id_state = self.event_graph.identity_state.read().await;
+            let Ok(id_state) = self.event_graph.rln_identity_state() else { return };
+            let id_state = id_state.read().await;
             if !id_state.contains(&commitment) {
                 warn!(
                     target: "event_graph::protocol",
@@ -755,11 +772,13 @@ impl ProtocolEventGraph {
         }
 
         let request = {
-            let id = self.event_graph.identity_state.read().await;
+            let Ok(id) = self.event_graph.rln_identity_state() else { return };
+            let id = id.read().await;
             prepare_slash_proof_request(identity_secret_hash, &id)
         };
         let root = request.merkle_root;
-        let proof = match self.event_graph.zk_keys.prove_slash(request).await {
+        let Ok(zk_keys) = self.event_graph.rln_zk_keys() else { return };
+        let proof = match zk_keys.prove_slash(request).await {
             Ok(response) => response.proof,
             Err(e) => {
                 error!(target: "event_graph::protocol", "[RLN] Slash proof creation failed: {e}");
@@ -825,6 +844,18 @@ impl ProtocolEventGraph {
                 continue
             }
 
+            if !self.event_graph.rln_enabled() {
+                if let Err(e) = self.event_graph.commit_static_event_unverified(&event, &[]).await {
+                    warn!(
+                        target: "event_graph::protocol",
+                        "[EVENTGRAPH] unverified static commit failed: {e}",
+                    );
+                    continue
+                }
+                self.event_graph.static_broadcast(event, Vec::new()).await?;
+                continue
+            }
+
             let rln_node: RLNNode = match deserialize_async_partial(event.content()).await {
                 Ok((v, _)) => v,
                 Err(_) => continue,
@@ -908,12 +939,14 @@ impl ProtocolEventGraph {
                     continue
                 }
                 if let Some(ev) = self.event_graph.fetch_event_from_dags(id).await? {
-                    let blob = if in_static {
+                    let blob = if !self.event_graph.rln_enabled() {
+                        Vec::new()
+                    } else if in_static {
                         self.event_graph.static_blob_fetch(id).unwrap_or(None).unwrap_or_default()
                     } else {
                         self.event_graph.dag_blob_fetch(id).unwrap_or(None).unwrap_or_default()
                     };
-                    if !in_static && blob.is_empty() {
+                    if self.event_graph.rln_enabled() && !in_static && blob.is_empty() {
                         // Rotating-DAG event without a blob. Don't ship
                         // it, just log loudly.
                         warn!(

+ 10 - 5
src/event_graph/test_helpers.rs

@@ -44,6 +44,7 @@ pub fn test_config() -> EventGraphConfig {
         initial_genesis: 1_704_067_200_000, // 2024-01-01 UTC
         hours_rotation: 0,
         genesis_contents: b"darkfi-test-graph".to_vec(),
+        rln_enabled: true,
         pregenerated_identity_commitments: test_pregenerated_identity_commitments(),
         max_dags: Some(24),
     }
@@ -131,9 +132,13 @@ pub async fn make_eg_with_config_and_db(
 ) -> EventGraphPtr {
     let ex = Arc::new(Executor::new());
     let p2p = P2p::new(Settings::default(), ex.clone()).await.unwrap();
-    EventGraph::with_zk_keys(p2p, sled_db, "/tmp".into(), false, config, shared_zk_keys(), ex)
-        .await
-        .unwrap()
+    if config.rln_enabled {
+        EventGraph::with_zk_keys(p2p, sled_db, "/tmp".into(), false, config, shared_zk_keys(), ex)
+            .await
+            .unwrap()
+    } else {
+        EventGraph::new(p2p, sled_db, "/tmp".into(), false, config, ex).await.unwrap()
+    }
 }
 
 /// Number of nodes a `make_network` call brings up.
@@ -353,7 +358,7 @@ mod test_identity {
             eg: &EventGraphPtr,
         ) -> Result<Blob> {
             let commitment = self.commitment();
-            let (root, path) = eg.rln_membership_path(&commitment).await;
+            let (root, path) = eg.rln_membership_path(&commitment).await?;
 
             let app_id = eg.rln_app_id().as_field();
             let epoch = epoch_of(event.header.timestamp);
@@ -380,7 +385,7 @@ mod test_identity {
                 y,
                 internal_nullifier,
             };
-            let proof = eg.zk_keys.prove_signal(request).await?.proof;
+            let proof = eg.rln_zk_keys()?.prove_signal(request).await?.proof;
 
             Ok(Blob {
                 proof,

+ 1 - 1
src/event_graph/tests.rs

@@ -1552,7 +1552,7 @@ async fn malformed_event_rejected_before_rln(ex: Arc<Executor<'static>>) {
         );
         drop(store);
 
-        let state = eg.rln_state.read().await;
+        let state = eg.rln_state.as_ref().unwrap().read().await;
         assert!(
             !state.metadata.is_reused(epoch, &internal_nullifier),
             "node {i} ran RLN verification before structural rejection",

+ 77 - 28
src/event_graph/tests_rln.rs

@@ -242,18 +242,18 @@ fn rln_rebuild_detects_stale_sled_smt_nodes() {
         let config = EventGraphConfig { hours_rotation: 1, ..test_config() };
         let eg = make_eg_with_config(config).await;
         let commitment = genesis_commitment_at(&eg, 0);
-        let original_root = eg.identity_state.read().await.root();
+        let original_root = eg.identity_state.as_ref().unwrap().read().await.root();
 
         eg.sled_db.open_tree("rln-identity-smt-nodes").unwrap().clear().unwrap();
 
         assert!(eg.rln_contains(&commitment).await);
-        assert_ne!(eg.identity_state.read().await.root(), original_root);
+        assert_ne!(eg.identity_state.as_ref().unwrap().read().await.root(), original_root);
         assert_eq!(eg.rln_historical_roots_ordered.len(), 1);
 
         eg.rebuild_historical_roots_if_needed().await.unwrap();
 
         assert!(eg.rln_contains(&commitment).await);
-        assert_eq!(eg.identity_state.read().await.root(), original_root);
+        assert_eq!(eg.identity_state.as_ref().unwrap().read().await.root(), original_root);
         assert_eq!(eg.rln_historical_roots_ordered.len(), 1);
     })
 }
@@ -490,7 +490,7 @@ fn rln_rebuild_detects_stale_leaf_with_same_count() {
         let stale = pallas::Base::from(0x51a1e_u64);
 
         {
-            let mut state = eg.identity_state.write().await;
+            let mut state = eg.identity_state.as_ref().unwrap().write().await;
             state.clear_for_rebuild().unwrap();
             state.register(stale).unwrap();
         }
@@ -540,7 +540,7 @@ fn rln_verify_signal_rejects_malformed_blobs() {
             y: pallas::Base::zero(),
             internal_nullifier: pallas::Base::from(1u64),
             user_msg_limit: 5,
-            merkle_root: eg.identity_state.read().await.root(),
+            merkle_root: eg.identity_state.as_ref().unwrap().read().await.root(),
         };
         let bytes = serialize_async(&blob).await;
         let truncated = &bytes[..bytes.len() / 2];
@@ -548,7 +548,14 @@ fn rln_verify_signal_rejects_malformed_blobs() {
 
         // None of these touched metadata.
         assert_eq!(
-            eg.rln_state.read().await.metadata.get_shares(0, &pallas::Base::zero()).len(),
+            eg.rln_state
+                .as_ref()
+                .unwrap()
+                .read()
+                .await
+                .metadata
+                .get_shares(0, &pallas::Base::zero())
+                .len(),
             0
         );
     })
@@ -564,7 +571,7 @@ fn rln_verify_signal_rejects_out_of_range_msg_limit() {
     smol::block_on(async {
         let eg = make_eg().await;
         let ev = make_static_event(b"static-event-2", &eg).await;
-        let root = eg.identity_state.read().await.root();
+        let root = eg.identity_state.as_ref().unwrap().read().await.root();
         let mk = |limit: u64| Blob {
             proof: synthesize_placeholder_proof(),
             y: pallas::Base::zero(),
@@ -591,7 +598,7 @@ fn rln_verify_signal_no_metadata_mutation_on_reject() {
         // Use the real current root to bypass the root check, but
         // the proof itself will fail. The test asserts that even
         // though we got past the root check, no share is recorded.
-        let real_root = eg.identity_state.read().await.root();
+        let real_root = eg.identity_state.as_ref().unwrap().read().await.root();
         let nullifier = pallas::Base::from(0xfeedu64);
         let blob = Blob {
             proof: synthesize_placeholder_proof(),
@@ -610,7 +617,7 @@ fn rln_verify_signal_no_metadata_mutation_on_reject() {
         // verified. Anchor to the SIGNAL's epoch rather than
         // wall-clock so the test is deterministic regardless of
         // when it runs.
-        let state = eg.rln_state.read().await;
+        let state = eg.rln_state.as_ref().unwrap().read().await;
         let event_epoch = epoch_of(ev.header.timestamp);
         for e in (event_epoch.saturating_sub(2))..=event_epoch.saturating_add(1) {
             assert!(
@@ -710,7 +717,7 @@ fn rln_static_event_registration_duplicate_commitment_soft_reject() {
     smol::block_on(async {
         let eg = make_eg().await;
         let commitment = pallas::Base::from(0xc0ffeeu64);
-        eg.identity_state.write().await.register(commitment).unwrap();
+        eg.identity_state.as_ref().unwrap().write().await.register(commitment).unwrap();
 
         let blob =
             placeholder_registration_blob(5, MAX_MSG_LIMIT, RegistrationAttestation::SPECIAL);
@@ -742,7 +749,7 @@ fn rln_static_event_slash_invalid_blobs_rejected() {
         let eg = make_eg().await;
 
         // (a) Mismatched commitment.
-        let real_root = eg.identity_state.read().await.root();
+        let real_root = eg.identity_state.as_ref().unwrap().read().await.root();
         let blob_a = placeholder_slash_blob(pallas::Base::from(0xaaaau64), real_root);
         let bytes_a = serialize_async(&blob_a).await;
         let mismatched_commitment = pallas::Base::from(0xbbbb_bbbbu64);
@@ -904,7 +911,7 @@ fn rln_verify_signal_rejects_recent_pre_slash_root_after_drift() {
         assert!(!eg.rln_contains(&commitment).await);
 
         assert!(
-            eg.identity_state.read().await.is_known_root(&pre_slash_root),
+            eg.identity_state.as_ref().unwrap().read().await.is_known_root(&pre_slash_root),
             "pre-slash root should still be in the recent-root cache",
         );
         assert!(
@@ -979,15 +986,15 @@ fn rln_e2e_slash_proof_round_trip() {
         assert_eq!(recovered, a_0);
 
         let request = {
-            let id_state = eg.identity_state.read().await;
+            let id_state = eg.identity_state.as_ref().unwrap().read().await;
             prepare_slash_proof_request(recovered, &id_state)
         };
         let root = request.merkle_root;
-        let proof = eg.zk_keys.prove_slash(request).await.unwrap().proof;
+        let proof = eg.zk_keys.as_ref().unwrap().prove_slash(request).await.unwrap().proof;
 
         // The recovered commitment must verify against the slash VK.
         let pi = vec![recovered, root];
-        proof.verify(&eg.zk_keys.slash_vk, &pi).expect("slash proof must verify");
+        proof.verify(&eg.zk_keys.as_ref().unwrap().slash_vk, &pi).expect("slash proof must verify");
     })
 }
 
@@ -1034,7 +1041,7 @@ async fn concurrent_slashes(ex: Arc<Executor<'static>>) {
     let id = TestIdentity::new();
     let commitment = id.commitment();
     for eg in &nodes {
-        eg.identity_state.write().await.register(commitment).expect("reg");
+        eg.identity_state.as_ref().unwrap().write().await.register(commitment).expect("reg");
     }
 
     // Helper to build a slash blob on a given node.
@@ -1044,11 +1051,11 @@ async fn concurrent_slashes(ex: Arc<Executor<'static>>) {
         commitment: pallas::Base,
     ) -> (Event, Vec<u8>) {
         let request = {
-            let id_state = eg.identity_state.read().await;
+            let id_state = eg.identity_state.as_ref().unwrap().read().await;
             prepare_slash_proof_request(ish, &id_state)
         };
         let root = request.merkle_root;
-        let proof = eg.zk_keys.prove_slash(request).await.expect("proof").proof;
+        let proof = eg.zk_keys.as_ref().unwrap().prove_slash(request).await.expect("proof").proof;
         let blob = SlashBlob { proof, identity_secret_hash: ish, merkle_root: root };
         let event = Event::new_static(serialize_async(&RLNNode::Slashing(commitment)).await, eg)
             .await
@@ -1105,11 +1112,11 @@ fn rln_static_slashes_persist_and_tombstone_commitment() {
         assert!(eg.rln_contains(&commitment).await);
 
         let request = {
-            let id_state = eg.identity_state.read().await;
+            let id_state = eg.identity_state.as_ref().unwrap().read().await;
             prepare_slash_proof_request(id.identity_secret_hash(), &id_state)
         };
         let root = request.merkle_root;
-        let proof = eg.zk_keys.prove_slash(request).await.unwrap().proof;
+        let proof = eg.zk_keys.as_ref().unwrap().prove_slash(request).await.unwrap().proof;
         let slash_blob =
             SlashBlob { proof, identity_secret_hash: id.identity_secret_hash(), merkle_root: root };
         let blob = serialize_async(&slash_blob).await;
@@ -1121,7 +1128,7 @@ fn rln_static_slashes_persist_and_tombstone_commitment() {
         assert!(matches!(outcome, StaticEventCheck::AcceptedSlash(c) if c == commitment));
         eg.commit_verified_static_event(&first_slash, &blob, &slash_node).await.unwrap();
         assert!(!eg.rln_contains(&commitment).await);
-        assert!(eg.identity_state.read().await.is_slashed(&commitment));
+        assert!(eg.identity_state.as_ref().unwrap().read().await.is_slashed(&commitment));
         assert!(eg.static_fetch(&first_slash.id()).await.unwrap().is_some());
 
         let replayed_slash = synth_static_event(3, 500_001, &slash_node).await;
@@ -1154,10 +1161,10 @@ fn rln_static_slashes_persist_and_tombstone_commitment() {
 
         eg.rln_historical_roots_ordered.clear().unwrap();
         eg.rln_historical_roots_by_value.clear().unwrap();
-        eg.identity_state.write().await.clear_for_rebuild().unwrap();
+        eg.identity_state.as_ref().unwrap().write().await.clear_for_rebuild().unwrap();
         eg.rebuild_historical_roots_if_needed().await.unwrap();
         assert!(!eg.rln_contains(&commitment).await);
-        assert!(eg.identity_state.read().await.is_slashed(&commitment));
+        assert!(eg.identity_state.as_ref().unwrap().read().await.is_slashed(&commitment));
         assert_eq!(eg.rln_historical_roots_ordered.len(), 3);
     })
 }
@@ -1197,7 +1204,7 @@ fn rln_static_blob_audit_repairs_pregenerated_guard() {
 fn rln_static_blob_audit_does_not_fabricate_slash_blob() {
     smol::block_on(async {
         let eg = make_eg().await;
-        let commitment = pallas::Base::from(0x515a_5b_u64);
+        let commitment = pallas::Base::from(0x0051_5a5b_u64);
         let node = RLNNode::Slashing(commitment);
         let ev = synth_static_event(1, 400_000, &node).await;
 
@@ -1324,7 +1331,7 @@ fn rln_multi_node_static_sync_rejects_child_when_parent_rejected() {
 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(0x51a7_1c_u64));
+    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_blob = b"not-a-valid-static-rln-blob".to_vec();
     for eg in nodes.iter().take(4) {
@@ -1413,7 +1420,7 @@ async fn static_sync_blob_propagation(ex: Arc<Executor<'static>>) {
     let synthetic_blob = b"synthetic-test-blob-bytes".to_vec();
 
     for eg in nodes.iter().take(4) {
-        eg.identity_state.write().await.register(commitment).unwrap();
+        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
         // real proof). For this test that's fine - we WANT to
@@ -1664,6 +1671,48 @@ fn rln_dag_insert_with_blobs_rejects_missing_blob_on_non_genesis() {
     })
 }
 
+#[test]
+fn rln_disabled_skips_state_and_accepts_empty_signal_blob() {
+    smol::block_on(async {
+        let config = EventGraphConfig {
+            rln_enabled: false,
+            pregenerated_identity_commitments: Vec::new(),
+            ..test_config()
+        };
+        let eg = make_eg_with_config(config).await;
+
+        assert!(!eg.rln_enabled());
+        assert!(eg.zk_keys.is_none());
+        assert!(eg.identity_state.is_none());
+        assert!(eg.rln_state.is_none());
+        assert!(eg.rln_zk_keys().is_err());
+        assert!(eg.rln_identity_state().is_err());
+        assert!(eg.rln_share_state().is_err());
+
+        let dag_ts = eg.current_genesis.read().await.header.timestamp;
+        let dag_name = dag_ts.to_string();
+        let event = Event::new(b"rln-disabled".to_vec(), &eg).await.unwrap();
+        assert_ne!(event.header.parents, NULL_PARENTS);
+        assert!(matches!(eg.rln_verify_signal(&event, &[]).await, SignalCheck::Accepted));
+
+        let accepted = eg.insert_signal_with_blob(&event, &[], &dag_name).await.unwrap();
+        assert_eq!(accepted, vec![event.id()]);
+
+        let store = eg.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());
+        drop(store);
+        assert!(eg.dag_blob_fetch(&event.id()).unwrap().is_none());
+
+        let static_node = RLNNode::Registration(pallas::Base::from(0x5151u64));
+        let static_event =
+            Event::new_static(serialize_async(&static_node).await, &eg).await.unwrap();
+        eg.commit_static_event_unverified(&static_event, b"ignored-proof").await.unwrap();
+        assert!(eg.static_blob_fetch(&static_event.id()).unwrap().is_none());
+    })
+}
+
 #[test]
 fn rln_dag_insert_with_blobs_rejects_bad_content_before_verification() {
     // RLN proofs bind to the event header ID. If the body no longer matches
@@ -1698,7 +1747,7 @@ fn rln_dag_insert_with_blobs_rejects_bad_content_before_verification() {
             .unwrap();
         assert!(result.is_empty(), "malformed event must not be inserted");
 
-        let state = eg.rln_state.read().await;
+        let state = eg.rln_state.as_ref().unwrap().read().await;
         assert!(
             !state.metadata.is_reused(epoch_of(malformed.header.timestamp), &internal_nullifier),
             "structural rejection must happen before RLN metadata is recorded",
@@ -2086,7 +2135,7 @@ fn rln_rebuild_restores_static_event_committed_before_rln_apply() {
         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);
-        let state = eg.identity_state.read().await;
+        let state = eg.identity_state.as_ref().unwrap().read().await;
         assert!(state.is_known_root(&state.root()));
     })
 }