Sfoglia il codice sorgente

event_graph: Commit static events before RLN state

x 1 mese fa
parent
commit
3cc14e38b0

+ 5 - 8
bin/darkirc/src/irc/client.rs

@@ -420,14 +420,11 @@ impl Client {
                         }
                     }
 
-                    // Static-event arrival path. Under the new
-                    // EventGraph, by the time `static_pub` notifies us
-                    // here the SMT mutation has ALREADY been applied
-                    // (see `handle_static_put` in event_graph::proto:
-                    // it calls `apply_rln_static_event` BEFORE
-                    // `static_insert`, and `static_insert` is what
-                    // pushes onto `static_pub`). So all we need to do
-                    // is bookkeeping for this client's seen-set.
+                    // Static-event arrival path. EventGraph notifies
+                    // `static_pub` only after `commit_verified_static_event`
+                    // has durably stored the event/blob and applied the RLN
+                    // state change. So all we need to do is bookkeeping for
+                    // this client's seen-set.
 
                     // Mark the message as seen for this USER
                     if let Err(e) = self.mark_seen(&event_id).await {

+ 4 - 7
bin/darkirc/src/irc/services/nickserv.rs

@@ -823,13 +823,10 @@ impl NickServ {
         let rln_node = RLNNode::Slashing(identity.commitment());
         let event = Event::new_static(serialize_async(&rln_node).await, evgr).await;
 
-        // Apply the slash through the canonical pipeline. The order
-        // (apply -> blob_store -> insert -> broadcast) matches the
-        // REGISTER path and the receive-side path in
-        // proto.rs::handle_static_put.
-        evgr.apply_rln_static_event(&event, &rln_node).await?;
-        evgr.static_blob_store(&event.id(), &blob_bytes)?;
-        evgr.static_insert(&event).await?;
+        // Commit through the verified static-event pipeline so durable event
+        // storage stays ahead of RLN side tables, while subscribers still see
+        // the event only after the local RLN state has been updated.
+        evgr.commit_verified_static_event(&event, &blob_bytes, &rln_node).await?;
         evgr.static_broadcast(event, blob_bytes).await?;
 
         // Drop the local account tree. The on-network slash makes

+ 33 - 11
src/event_graph/mod.rs

@@ -1403,13 +1403,7 @@ impl EventGraph {
             match outcome {
                 rln::StaticEventCheck::AcceptedRegistration(_) |
                 rln::StaticEventCheck::AcceptedSlash(_) => {
-                    // apply_rln_static_event handles both Registration
-                    // and Slashing branches and also records the
-                    // post-mutation root in the historical-roots
-                    // side-tables.
-                    let _ = self.apply_rln_static_event(&ev, &rln_node).await;
-                    self.static_blob_store(&ev.id(), &blob)?;
-                    self.static_insert(&ev).await?;
+                    self.commit_verified_static_event(&ev, &blob, &rln_node).await?;
                     applied += 1;
                 }
                 rln::StaticEventCheck::Rejected | rln::StaticEventCheck::Malicious => {
@@ -1950,7 +1944,7 @@ impl EventGraph {
         Ok(())
     }
 
-    pub async fn static_insert(&self, ev: &Event) -> Result<()> {
+    async fn static_persist(&self, ev: &Event) -> Result<()> {
         let mut ov = SledTreeOverlay::new(&self.static_dag);
         ov.insert(ev.id().as_bytes(), &serialize_async(ev).await).unwrap();
 
@@ -1958,10 +1952,40 @@ impl EventGraph {
             self.static_dag.apply_batch(b).unwrap();
         }
 
+        Ok(())
+    }
+
+    pub async fn static_insert(&self, ev: &Event) -> Result<()> {
+        self.static_persist(ev).await?;
         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
+    /// state last. If a process crashes after the static event becomes
+    /// durable but before the identity tree or historical-root indexes are
+    /// updated, startup recovery can rebuild those RLN side tables from the
+    /// static DAG. Subscribers are notified only after the RLN apply step, so
+    /// applications observe the same post-state semantics as the receive path.
+    pub async fn commit_verified_static_event(
+        &self,
+        ev: &Event,
+        blob: &[u8],
+        rln_node: &rln::RLNNode,
+    ) -> Result<pallas::Base> {
+        if blob.is_empty() {
+            return Err(Error::Custom("static RLN event blob must not be empty".into()))
+        }
+
+        self.static_blob_store(&ev.id(), blob)?;
+        self.static_persist(ev).await?;
+        let root = self.apply_rln_static_event(ev, rln_node).await?;
+        self.static_pub.notify(ev.clone()).await;
+        Ok(root)
+    }
+
     pub async fn static_fetch(&self, eid: &blake3::Hash) -> Result<Option<Event>> {
         Ok(match self.static_dag.get(eid.as_bytes())? {
             Some(b) => Some(deserialize_async(&b).await?),
@@ -2674,9 +2698,7 @@ impl EventGraph {
             }
 
             let blob = rln::GENESIS_BLOB_GUARD.to_vec();
-            self.static_blob_store(&event.id(), &blob)?;
-            self.apply_rln_static_event(&event, &rln_node).await?;
-            self.static_insert(&event).await?;
+            self.commit_verified_static_event(&event, &blob, &rln_node).await?;
         }
 
         Ok(())

+ 13 - 26
src/event_graph/proto.rs

@@ -701,14 +701,10 @@ impl ProtocolEventGraph {
         let blob = serialize_async(&slash_blob).await;
         let node = RLNNode::Slashing(commitment);
         let ev = Event::new_static(serialize_async(&node).await, &self.event_graph).await;
-        let _ = self.event_graph.static_insert(&ev).await;
-        // Apply the slash to our own SMT and historical-roots
-        // tables. Like the nickserv originator path, the slasher
-        // doesn't receive its own broadcast back, so we'd never
-        // see the SMT update otherwise. apply_rln_static_event
-        // canonicalizes the SMT mutation and the root recording.
-        let _ = self.event_graph.apply_rln_static_event(&ev, &node).await;
-        let _ = self.event_graph.static_blob_store(&ev.id(), &blob);
+        if let Err(e) = self.event_graph.commit_verified_static_event(&ev, &blob, &node).await {
+            error!(target: "event_graph::protocol", "[RLN] Slash static commit failed: {e}");
+            return
+        }
         let _ = self.event_graph.static_broadcast(ev, blob).await;
     }
 
@@ -759,16 +755,10 @@ impl ProtocolEventGraph {
             }
 
             // Decision is made by EventGraph::rln_verify_static_event,
-            // a pure verification function (no state mutation). We
-            // translate the outcome to: SMT mutation via apply_rln_static_event
-            // (which also records the post-mutation root), or strike,
-            // or drop silently.
-            //
-            // The unified `apply_rln_static_event` is essential to
-            // keep the SMT and historical-roots tables in lockstep.
-            // Bypassing it (e.g. calling .register() directly) would
-            // break sync-time signal verification because the
-            // historical-roots table wouldn't get the new entry.
+            // a pure verification function (no state mutation). Accepted events
+            // are committed through one helper so the static DAG is durable
+            // before RLN state, while subscribers are notified only after the
+            // RLN apply step.
             match self
                 .event_graph
                 .rln_verify_static_event(&rln_node, &blob, event.header.timestamp)
@@ -776,11 +766,14 @@ impl ProtocolEventGraph {
             {
                 rln::StaticEventCheck::AcceptedRegistration(_) |
                 rln::StaticEventCheck::AcceptedSlash(_) => {
-                    if let Err(e) = self.event_graph.apply_rln_static_event(&event, &rln_node).await
+                    if let Err(e) = self
+                        .event_graph
+                        .commit_verified_static_event(&event, &blob, &rln_node)
+                        .await
                     {
                         warn!(
                             target: "event_graph::protocol",
-                            "[RLN] apply_rln_static_event failed: {e}",
+                            "[RLN] commit_verified_static_event failed: {e}",
                         );
                         continue
                     }
@@ -792,12 +785,6 @@ impl ProtocolEventGraph {
                 }
             }
 
-            // Persist the original RLN blob (proof + public inputs +
-            // attestation) alongside the event so `static_sync` on
-            // a future late-joiner can re-verify the proof.
-            self.event_graph.static_blob_store(&event.id(), &blob)?;
-
-            self.event_graph.static_insert(&event).await?;
             self.event_graph.static_broadcast(event, blob).await?;
         }
     }

+ 72 - 14
src/event_graph/tests_rln.rs

@@ -16,7 +16,10 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{sync::Arc, time::UNIX_EPOCH};
+use std::{
+    sync::Arc,
+    time::{Duration, UNIX_EPOCH},
+};
 
 use darkfi_sdk::{
     crypto::{pasta_prelude::PrimeField, poseidon_hash},
@@ -40,7 +43,7 @@ use crate::{
         util::generate_genesis,
         Event, EventGraphConfig, EventGraphPtr, NULL_ID, NULL_PARENTS,
     },
-    system::sleep,
+    system::{sleep, timeout::timeout},
     zk::Proof,
 };
 
@@ -952,11 +955,16 @@ async fn concurrent_slashes(ex: Arc<Executor<'static>>) {
     let (ev0, bytes0) = build_slash(&nodes[0], ish, commitment).await;
     let (ev1, bytes1) = build_slash(&nodes[1], ish, commitment).await;
 
-    // Apply locally to each origin.
-    nodes[0].identity_state.write().await.slash(commitment).expect("s0");
-    nodes[1].identity_state.write().await.slash(commitment).expect("s1");
-    nodes[0].static_insert(&ev0).await.expect("ins0");
-    nodes[1].static_insert(&ev1).await.expect("ins1");
+    // Commit locally to each origin through the verified static-event path.
+    let slash_node = RLNNode::Slashing(commitment);
+    nodes[0]
+        .commit_verified_static_event(&ev0, &bytes0, &slash_node)
+        .await
+        .expect("commit slash 0");
+    nodes[1]
+        .commit_verified_static_event(&ev1, &bytes1, &slash_node)
+        .await
+        .expect("commit slash 1");
 
     // Broadcast concurrently.
     let f0 = nodes[0].static_broadcast(ev0, bytes0);
@@ -1007,14 +1015,11 @@ async fn static_sync_registration(ex: Arc<Executor<'static>>) {
     let content = serialize_async(&rln_node).await;
     let event = Event::new_static(content, &nodes[0]).await;
 
-    // Seed nodes 0..=3 the same way a real broadcast pipeline
-    // would: persist the blob, insert the static event, then
-    // apply the RLN node so identity_state, the SMT root, and
-    // historical-roots all stay consistent.
+    // 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) {
-        eg.static_blob_store(&event.id(), &blob_bytes).unwrap();
-        eg.static_insert(&event).await.unwrap();
-        eg.apply_rln_static_event(&event, &rln_node).await.unwrap();
+        eg.commit_verified_static_event(&event, &blob_bytes, &rln_node).await.unwrap();
     }
 
     // Node 4 knows nothing. Verify the precondition.
@@ -1646,6 +1651,59 @@ 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 sub = eg.static_subscribe().await;
+
+        let commitment = pallas::Base::from(0xc0de_0001_u64);
+        let node = RLNNode::Registration(commitment);
+        let ev = synth_static_event(1, 300_000, &node).await;
+        let blob = b"verified-static-blob".to_vec();
+
+        let root = eg.commit_verified_static_event(&ev, &blob, &node).await.unwrap();
+
+        let Ok(notified) = timeout(Duration::from_secs(1), sub.receive()).await else {
+            panic!("static event notification not received")
+        };
+        assert_eq!(notified.id(), ev.id());
+        assert!(eg.rln_contains(&commitment).await);
+        assert!(eg.is_root_valid_at(&root, ev.header.timestamp).unwrap());
+        assert!(eg.static_fetch(&ev.id()).await.unwrap().is_some());
+        assert_eq!(eg.static_blob_fetch(&ev.id()).unwrap().unwrap(), blob);
+    })
+}
+
+#[test]
+fn rln_rebuild_restores_static_event_committed_before_rln_apply() {
+    smol::block_on(async {
+        let eg = make_eg().await;
+
+        let commitment = pallas::Base::from(0xc0de_0002_u64);
+        let node = RLNNode::Registration(commitment);
+        let ev = synth_static_event(1, 300_001, &node).await;
+        let blob = b"crash-before-rln-apply".to_vec();
+
+        // Simulate a process crash after the blob and static event were made
+        // durable, but before `apply_rln_static_event` updated identity leaves
+        // and historical-root indexes.
+        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);
+
+        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);
+        let state = eg.identity_state.read().await;
+        assert!(state.is_known_root(&state.root()));
+    })
+}
+
 #[test]
 fn rln_canonical_order_produces_same_roots_regardless_of_apply_order() {
     // SMT roots are determined by the SET of leaves, not the