Sfoglia il codice sorgente

darkirc: Apply API changes made in event_graph

x 3 mesi fa
parent
commit
8cba5efd5e

+ 169 - 76
bin/darkirc/src/crypto/rln.rs

@@ -16,51 +16,64 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::UNIX_EPOCH;
-
 use darkfi::{
     event_graph::{
-        rln::{closest_epoch, hash_event},
-        Event,
+        rln::{
+            epoch_of, hash_event, Blob, RegistrationAttestation, RegistrationBlob, MAX_MSG_LIMIT,
+            RLN2_REGISTER_ZKBIN, RLN2_SIGNAL_ZKBIN,
+        },
+        Event, EventGraphPtr,
     },
     zk::{
         halo2::{Field, Value},
-        Proof, ProvingKey, Witness, ZkCircuit,
+        Proof, Witness, ZkCircuit,
     },
     zkas::ZkBinary,
     Result,
 };
-use darkfi_sdk::{
-    crypto::{poseidon_hash, smt::SmtMemoryFp},
-    pasta::pallas,
-};
+use darkfi_sdk::{crypto::poseidon_hash, pasta::pallas};
 use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
 use rand::{rngs::OsRng, CryptoRng, RngCore};
 use tracing::info;
 
+/// Domain-separation tags for credential generation.
 pub const RLN_TRAPDOOR_DERIVATION_PATH: pallas::Base = pallas::Base::from_raw([4211, 0, 0, 0]);
 pub const RLN_NULLIFIER_DERIVATION_PATH: pallas::Base = pallas::Base::from_raw([4212, 0, 0, 0]);
 
-pub const RLN2_REGISTER_ZKBIN: &[u8] =
-    include_bytes!("../../../../src/event_graph/proof/rlnv2-diff-register.zk.bin");
-pub const RLN2_SIGNAL_ZKBIN: &[u8] =
-    include_bytes!("../../../../src/event_graph/proof/rlnv2-diff-signal.zk.bin");
-
-/// TODO: this is arbitrary it should be based on stake
-pub const MAX_MSG_LIMIT: u64 = 100;
-
+/// A user-side RLN identity: long-lived secrets plus an in-memory
+/// per-epoch send counter.
+///
+/// The struct is `Copy` so it can be cheaply duplicated when handed
+/// off to client tasks; the per-epoch counter (`message_id`,
+/// `last_epoch`) is therefore expected to be tracked by whichever
+/// task owns the canonical mutable copy. In the typical DarkIRC
+/// configuration that's the `IrcServer::rln_identity` field
+/// (`RwLock<Option<RlnIdentity>>`).
+///
+/// `message_id` and `last_epoch` are not persisted to disk on
+/// shutdown. An RLN epoch is `RLN_EPOCH_LEN` (10 minutes); the
+/// worst case of a node restart is that the counter resets to 0
+/// for the current epoch, which only risks a slash if the user
+/// actually sent distinct messages with the same message_id in the
+/// same epoch - which generally requires hot-restarting at sub-
+/// second cadence. If/when that becomes an operational concern,
+/// persist the counter through a `next_message_id_persisted`-style
+/// helper at the call site.
 #[derive(Copy, Clone, SerialEncodable, SerialDecodable)]
 pub struct RlnIdentity {
     pub nullifier: pallas::Base,
     pub trapdoor: pallas::Base,
     pub user_message_limit: u64,
-    /// This should increment during a single epoch and reset on new epochs
+    /// Monotonic counter within the current epoch. Reset whenever
+    /// `last_epoch` advances.
     pub message_id: u64,
-    /// Last known epoch
+    /// Last epoch we observed. Bookkeeping for the counter reset
+    /// above; not used cryptographically.
     pub last_epoch: u64,
 }
 
 impl RlnIdentity {
+    /// Generate a fresh identity.
     pub fn new(mut rng: impl CryptoRng + RngCore) -> Self {
         Self {
             nullifier: poseidon_hash([
@@ -68,92 +81,172 @@ impl RlnIdentity {
                 pallas::Base::random(&mut rng),
             ]),
             trapdoor: poseidon_hash([RLN_TRAPDOOR_DERIVATION_PATH, pallas::Base::random(&mut rng)]),
-            user_message_limit: MAX_MSG_LIMIT,
+            // Default to the free-tier cap. The operator can request
+            // a higher limit at registration time (subject to the
+            // attestation gating in `RegistrationAttestation::permits`,
+            // which currently only honours the free-tier cap until
+            // staking lands).
+            user_message_limit: RegistrationAttestation::FREE_TIER_LIMIT,
             message_id: 0,
-            last_epoch: closest_epoch(UNIX_EPOCH.elapsed().unwrap().as_millis() as u64),
+            last_epoch: 0,
         }
     }
 
+    /// `identity_secret = poseidon(nullifier, trapdoor)`. Internal
+    /// to the RLN-V2 algebra.
+    pub fn identity_secret(&self) -> pallas::Base {
+        poseidon_hash([self.nullifier, self.trapdoor])
+    }
+
+    /// `identity_secret_hash = poseidon(identity_secret, user_message_limit)`.
+    /// This is the value recovered by SSS during a slash, NOT the
+    /// raw secret tuple.
+    pub fn identity_secret_hash(&self) -> pallas::Base {
+        poseidon_hash([self.identity_secret(), pallas::Base::from(self.user_message_limit)])
+    }
+
+    /// `commitment = poseidon(identity_secret_hash)`. The leaf in
+    /// the SMT.
     pub fn commitment(&self) -> pallas::Base {
-        let identity_secret = poseidon_hash([self.nullifier, self.trapdoor]);
-        let identity_secret_hash = poseidon_hash([identity_secret, self.user_message_limit.into()]);
+        poseidon_hash([self.identity_secret_hash()])
+    }
 
-        poseidon_hash([identity_secret_hash])
+    /// Advance the per-epoch counter for a signal at the given
+    /// timestamp. Returns `None` if the user has already burnt
+    /// their `user_message_limit` for this epoch (in which case the
+    /// caller should drop the message rather than emit a signal
+    /// that would slash the identity).
+    ///
+    /// On epoch rollover the counter resets and a fresh slot 0 is
+    /// returned.
+    pub fn next_message_id(&mut self, now_millis: u64) -> Option<u64> {
+        let epoch = epoch_of(now_millis);
+        if epoch != self.last_epoch {
+            self.last_epoch = epoch;
+            self.message_id = 0;
+        }
+        if self.message_id >= self.user_message_limit {
+            return None
+        }
+        let m = self.message_id;
+        self.message_id += 1;
+        Some(m)
     }
 
-    pub fn create_register_proof(
-        &self,
-        event: &Event,
-        identities_tree: &mut SmtMemoryFp,
-        register_pk: &ProvingKey,
-    ) -> Result<Proof> {
+    /// Build a [`RegistrationBlob`] suitable for broadcast as a
+    /// `StaticPut`. The proving key comes from the EventGraph's
+    /// shared `ZkKeys` cache.
+    pub fn create_registration(&self, eg: &EventGraphPtr) -> Result<RegistrationBlob> {
+        let zkbin = ZkBinary::decode(RLN2_REGISTER_ZKBIN, false)?;
+
+        // Witness order MUST match the rlnv2-diff-register.zk circuit.
         let witnesses = vec![
             Witness::Base(Value::known(self.nullifier)),
             Witness::Base(Value::known(self.trapdoor)),
             Witness::Base(Value::known(pallas::Base::from(self.user_message_limit))),
             Witness::Base(Value::known(pallas::Base::from(MAX_MSG_LIMIT))),
         ];
-
-        let commitment = self.commitment();
-        let public_inputs = vec![commitment, pallas::Base::from(self.user_message_limit)];
-
-        info!(target: "crypto::rln::create_register_proof", "[RLN] Creating register proof for account {}", event.header.id());
-        let register_zkbin = ZkBinary::decode(RLN2_REGISTER_ZKBIN, false)?;
-        let register_circuit = ZkCircuit::new(witnesses, &register_zkbin);
-
-        let proof = Proof::create(register_pk, &[register_circuit], &public_inputs, &mut OsRng)?;
-
-        let leaf = vec![commitment];
-        let leaf: Vec<_> = leaf.into_iter().map(|l| (l, l)).collect();
-        // TODO: Recipients should verify that identity doesn't exist already before insert.
-        identities_tree.insert_batch(leaf.clone()).unwrap(); // leaf == pos
-        Ok(proof)
+        // Public-input order MUST match `constrain_instance` in the
+        // same .zk file.
+        let pi = vec![
+            self.commitment(),
+            pallas::Base::from(self.user_message_limit),
+            pallas::Base::from(MAX_MSG_LIMIT),
+        ];
+        let circuit = ZkCircuit::new(witnesses, &zkbin);
+        let pk = eg.zk_keys.load_register_pk()?;
+
+        info!(
+            target: "darkirc::crypto::rln",
+            "[RLN] Creating registration proof for commitment {:?}",
+            self.commitment(),
+        );
+        let proof = Proof::create(&pk, &[circuit], &pi, &mut OsRng)?;
+
+        Ok(RegistrationBlob {
+            proof,
+            user_message_limit: self.user_message_limit,
+            max_message_limit: MAX_MSG_LIMIT,
+            attestation: RegistrationAttestation::Free,
+        })
     }
 
-    pub fn create_signal_proof(
+    /// Build a signal [`Blob`] for the given event using `message_id`.
+    ///
+    /// The merkle root and inclusion path come from the
+    /// EventGraph's canonical [`IdentityState`] via
+    /// [`EventGraph::rln_membership_path`] - the verifier and the
+    /// prover therefore agree on the root by construction, with no
+    /// risk of the client and the EG drifting out of sync.
+    ///
+    /// [`IdentityState`]: darkfi::event_graph::rln::IdentityState
+    /// [`EventGraph::rln_membership_path`]: darkfi::event_graph::EventGraph::rln_membership_path
+    pub async fn create_signal(
         &self,
         event: &Event,
-        identity_tree: &SmtMemoryFp,
-        signal_pk: &ProvingKey,
-    ) -> Result<(Proof, pallas::Base, pallas::Base, u64)> {
-        // 1. Construct share
-        let rln_app_identifier = pallas::Base::from(1000);
-        let epoch = pallas::Base::from(closest_epoch(event.header.timestamp));
-        let message_id = pallas::Base::from(self.message_id);
-        let external_nullifier = poseidon_hash([epoch, rln_app_identifier]);
-        let a_0 = poseidon_hash([self.nullifier, self.trapdoor]);
-        let a_1 = poseidon_hash([a_0, external_nullifier, message_id]);
+        message_id: u64,
+        eg: &EventGraphPtr,
+    ) -> Result<Blob> {
+        // RLN external nullifier: ties the message to (epoch, app).
+        // Cross-app isolation comes from `app_id` differing per
+        // EventGraph deployment (derived from
+        // config.genesis_contents).
+        let app_id = eg.rln_app_id().as_field();
+        let epoch = pallas::Base::from(epoch_of(event.header.timestamp));
+        let mid = pallas::Base::from(message_id);
+        let ext_null = poseidon_hash([epoch, app_id]);
+
+        // Rate-limit polynomial: y = a_0 + x * a_1.
+        // a_0 is identity_secret_hash; a_1 is bound to (a_0,
+        // ext_null, message_id). Two distinct (x, y) for the same
+        // internal nullifier let SSS recover a_0, which is what
+        // enables slashing.
+        let a_0 = self.identity_secret_hash();
+        let a_1 = poseidon_hash([a_0, ext_null, mid]);
+        let internal_nullifier = poseidon_hash([a_1]);
         let x = hash_event(event);
         let y = a_0 + x * a_1;
 
-        let internal_nullifier = poseidon_hash([a_1]);
+        // Canonical membership path via the EG.
+        let (root, path) = eg.rln_membership_path(&self.commitment()).await;
 
-        // 2. Inclusion proof
-        let commitment = self.commitment();
-        let identity_root = identity_tree.root();
-        let identity_path = identity_tree.prove_membership(&commitment);
-        // TODO: Delete me later
-        assert!(identity_path.verify(&identity_root, &commitment, &commitment));
+        let zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false)?;
 
-        // 3. Create ZK proof
+        // Witness order MUST match rlnv2-diff-signal.zk.
         let witnesses = vec![
             Witness::Base(Value::known(self.nullifier)),
             Witness::Base(Value::known(self.trapdoor)),
             Witness::Base(Value::known(pallas::Base::from(self.user_message_limit))),
-            Witness::SparseMerklePath(Value::known(identity_path.path)),
+            Witness::SparseMerklePath(Value::known(path.path)),
             Witness::Base(Value::known(x)),
-            Witness::Base(Value::known(message_id)),
+            Witness::Base(Value::known(mid)),
             Witness::Base(Value::known(epoch)),
         ];
-
-        let public_inputs = vec![identity_root, external_nullifier, x, y, internal_nullifier];
-
-        info!(target: "crypto::rln::create_signal_proof", "[RLN] Creating signal proof for event {}", event.header.id());
-        let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false)?;
-        let signal_circuit = ZkCircuit::new(witnesses, &signal_zkbin);
-
-        let proof = Proof::create(signal_pk, &[signal_circuit], &public_inputs, &mut OsRng)?;
-        Ok((proof, y, internal_nullifier, self.user_message_limit))
-        // Ok((proof, public_inputs))
+        // PI order MUST match `constrain_instance` in the .zk file.
+        let pi = vec![
+            root,
+            ext_null,
+            pallas::Base::from(self.user_message_limit),
+            x,
+            y,
+            internal_nullifier,
+        ];
+        let circuit = ZkCircuit::new(witnesses, &zkbin);
+        let pk = eg.zk_keys.load_signal_pk()?;
+
+        info!(
+            target: "darkirc::crypto::rln",
+            "[RLN] Creating signal proof for event {}",
+            event.id(),
+        );
+        let proof = Proof::create(&pk, &[circuit], &pi, &mut OsRng)?;
+
+        Ok(Blob {
+            proof,
+            y,
+            internal_nullifier,
+            user_msg_limit: self.user_message_limit,
+            merkle_root: root,
+        })
     }
 }

+ 53 - 69
bin/darkirc/src/irc/client.rs

@@ -18,7 +18,6 @@
 
 use std::{
     collections::{HashMap, HashSet, VecDeque},
-    io::Cursor,
     slice,
     sync::{
         atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
@@ -27,17 +26,10 @@ use std::{
 };
 
 use darkfi::{
-    event_graph::{
-        proto::EventPut,
-        rln::{closest_epoch, process_commitment, Blob, RLNNode},
-        Event, NULL_ID,
-    },
+    event_graph::{proto::EventPut, Event, NULL_ID},
     system::Subscription,
-    zk::{empty_witnesses, Proof, ProvingKey, ZkCircuit},
-    zkas::ZkBinary,
     Error, Result,
 };
-use darkfi_sdk::pasta::pallas;
 use darkfi_serial::{deserialize_async_partial, serialize_async};
 use futures::FutureExt;
 use sled_overlay::sled;
@@ -220,27 +212,50 @@ impl Client {
                                     }
 
                                     let blob = {
-                                        // If we have a RLN identity, now we'll build a ZK proof.
-                                        // Also I really want GOTO in Rust... Fags.
-                                        if let Some(ref mut rln_identity) = *self.server.rln_identity.write().await {
-                                            if rln_identity.last_epoch != closest_epoch(event.header.timestamp) {
-                                                rln_identity.last_epoch = closest_epoch(event.header.timestamp);
-                                                rln_identity.message_id = 0;
-                                            }
-
-                                            rln_identity.message_id += 1;
-
-                                            let (proof, y, internal_nullifier, user_msg_limit) = match self.create_rln_signal_proof(rln_identity, &event).await {
-                                                Ok(v) => v,
-                                                Err(e) => {
-                                                    // TODO: Send a message to the IRC client telling that sending went wrong
-                                                    error!("[IRC CLIENT] Failed creating RLN signal proof: {e}");
-                                                    return Err(e)
+                                        // If we have a configured RLN identity,
+                                        // produce a signal blob through the
+                                        // EventGraph's canonical RLN path. The
+                                        // EG owns the SMT root, the proving key,
+                                        // and the slot bookkeeping; we just
+                                        // hand it the (event, message_id) pair
+                                        // and get back a fully-formed `Blob`.
+                                        //
+                                        // If the per-epoch budget is exhausted,
+                                        // `next_message_id` returns None and we
+                                        // drop the message rather than emit a
+                                        // signal that would cause a slash.
+                                        if let Some(ref mut rln_identity) =
+                                            *self.server.rln_identity.write().await
+                                        {
+                                            match rln_identity
+                                                .next_message_id(event.header.timestamp)
+                                            {
+                                                Some(mid) => match rln_identity
+                                                    .create_signal(
+                                                        &event,
+                                                        mid,
+                                                        &self.server.darkirc.event_graph,
+                                                    )
+                                                    .await
+                                                {
+                                                    Ok(blob) => serialize_async(&blob).await,
+                                                    Err(e) => {
+                                                        error!(
+                                                            "[IRC CLIENT] Failed creating RLN \
+                                                             signal proof: {e}"
+                                                        );
+                                                        return Err(e)
+                                                    }
+                                                },
+                                                None => {
+                                                    warn!(
+                                                        "[IRC CLIENT] RLN message budget \
+                                                         exhausted for this epoch; dropping \
+                                                         message to avoid slash"
+                                                    );
+                                                    continue
                                                 }
-                                            };
-
-                                            let blob = Blob{ proof, y, internal_nullifier, user_msg_limit };
-                                            serialize_async(&blob).await
+                                            }
                                         } else {
                                             vec![]
                                         }
@@ -369,19 +384,14 @@ impl Client {
                         }
                     }
 
-                    // Update SMT
-                    let fetched_rln_commitment: RLNNode = match deserialize_async_partial(r.content()).await
-                    {
-                        Ok((v, _)) => v,
-                        Err(e) => {
-                            error!(target: "irc::client", "[RLN] Failed deserializing incoming RLN Identity events: {}", e);
-                            continue
-                        }
-                    };
-
-                    let mut identities_tree = self.server.darkirc.event_graph.rln_identity_tree.write().await;
-                    process_commitment(fetched_rln_commitment, &mut identities_tree)?;
-                    drop(identities_tree);
+                    // 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.
 
                     // Mark the message as seen for this USER
                     if let Err(e) = self.mark_seen(&event_id).await {
@@ -511,7 +521,7 @@ impl Client {
         if cmd.as_str() == "PRIVMSG" && replies.is_empty() {
             // If the DAG is not synced yet, queue client lines
             // Once synced, send queued lines and continue as normal
-            if !*self.server.darkirc.event_graph.synced.read().await {
+            if !self.server.darkirc.event_graph.is_synced() {
                 debug!("DAG is still syncing, queuing and skipping...");
                 let privmsg = self.args_to_privmsg(args).await;
                 args_queue.push_back(privmsg);
@@ -587,30 +597,4 @@ impl Client {
 
         Ok(db.contains_key(event_id.as_bytes())?)
     }
-
-    /// Abstraction for RLN signal proof creation
-    async fn create_rln_signal_proof(
-        &self,
-        rln_identity: &RlnIdentity,
-        event: &Event,
-    ) -> Result<(Proof, pallas::Base, pallas::Base, u64)> {
-        let identity_tree = self.server.darkirc.event_graph.rln_identity_tree.read().await;
-
-        // Retrieve the ZK proving key from the db
-        let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false)?;
-        let signal_circuit = ZkCircuit::new(empty_witnesses(&signal_zkbin)?, &signal_zkbin);
-        let signal_pk = {
-            let Some(proving_key) = self.server.server_store.get("rlnv2-diff-signal-pk")? else {
-                return Err(Error::DatabaseError(
-                    "RLN signal proving key not found in server store".to_string(),
-                ))
-            };
-            let mut reader = Cursor::new(&*proving_key);
-            let pk = ProvingKey::read(&mut reader, signal_circuit)?;
-            drop(proving_key);
-            pk
-        };
-
-        rln_identity.create_signal_proof(event, &identity_tree, &signal_pk)
-    }
 }

+ 3 - 3
bin/darkirc/src/irc/rpl.rs

@@ -79,7 +79,7 @@ pub const RPL_LISTSTART: u16 = 321;
 /// Sent as a reply to the LIST command, this numeric sends information
 /// about a channel to the client. `<channel>` is the name of the channel.
 /// `<client count>` is an integer indicating how many clients are joined
-/// to that channel. `<topic>` is the channels topic.
+/// to that channel. `<topic>` is the channel's topic.
 pub const RPL_LIST: u16 = 322;
 
 /// `<client> :End of /LIST`
@@ -172,13 +172,13 @@ pub const ERR_NOORIGIN: u16 = 409;
 
 /// `<client> :No recipient given (<command>)`
 ///
-/// Returned by the PRIVMSG command to indicate the message wasnt
+/// Returned by the PRIVMSG command to indicate the message wasn't
 /// delivered because there was no recipient given.
 pub const ERR_NORECIPIENT: u16 = 411;
 
 /// `<client> :No text to send`
 ///
-/// Returned by the PRIVMSG command to indicate the message wasnt
+/// Returned by the PRIVMSG command to indicate the message wasn't
 /// delivered because there was no text to send.
 pub const ERR_NOTEXTTOSEND: u16 = 412;
 

+ 3 - 59
bin/darkirc/src/irc/server.rs

@@ -19,17 +19,12 @@
 use std::{collections::HashMap, fs::File, io::BufReader, path::PathBuf, sync::Arc};
 
 use darkfi::{
-    event_graph::{
-        rln::{process_commitment, RLNNode},
-        Event,
-    },
+    event_graph::Event,
     system::{StoppableTask, StoppableTaskPtr, Subscription},
     util::path::expand_path,
-    zk::{empty_witnesses, ProvingKey, ZkCircuit},
-    zkas::ZkBinary,
     Error, Result,
 };
-use darkfi_serial::{deserialize_async, deserialize_async_partial};
+use darkfi_serial::deserialize_async;
 use futures_rustls::{
     rustls::{self, pki_types::PrivateKeyDer},
     TlsAcceptor,
@@ -51,10 +46,7 @@ use super::{
     IrcChannel, IrcContact,
 };
 use crate::{
-    crypto::{
-        rln::{RlnIdentity, RLN2_REGISTER_ZKBIN, RLN2_SIGNAL_ZKBIN},
-        saltbox,
-    },
+    crypto::{rln::RlnIdentity, saltbox},
     pad,
     settings::{
         parse_autojoin_channels, parse_configured_channels, parse_configured_contacts,
@@ -150,54 +142,6 @@ impl IrcServer {
         // Open persistent dbs
         let server_store = darkirc.sled.open_tree("server_store")?;
 
-        // Generate RLN proving and verifying keys, if needed
-        let rln_register_zkbin = ZkBinary::decode(RLN2_REGISTER_ZKBIN, false)?;
-        let rln_register_circuit =
-            ZkCircuit::new(empty_witnesses(&rln_register_zkbin)?, &rln_register_zkbin);
-
-        if server_store.get("rlnv2-diff-register-pk")?.is_none() {
-            info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Register ProvingKey");
-            let provingkey = ProvingKey::build(rln_register_zkbin.k, &rln_register_circuit);
-            let mut buf = vec![];
-            provingkey.write(&mut buf)?;
-            server_store.insert("rlnv2-diff-register-pk", buf)?;
-        }
-
-        // Generate RLN proving and verifying keys, if needed
-        let rln_signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false)?;
-        let rln_signal_circuit =
-            ZkCircuit::new(empty_witnesses(&rln_signal_zkbin)?, &rln_signal_zkbin);
-
-        if server_store.get("rlnv2-diff-signal-pk")?.is_none() {
-            info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Signal ProvingKey");
-            let provingkey = ProvingKey::build(rln_signal_zkbin.k, &rln_signal_circuit);
-            let mut buf = vec![];
-            provingkey.write(&mut buf)?;
-            server_store.insert("rlnv2-diff-signal-pk", buf)?;
-        }
-
-        // Construct SMT from static DAG
-        let mut identity_tree = darkirc.event_graph.rln_identity_tree.write().await;
-        let mut events = darkirc.event_graph.static_fetch_all().await?;
-        events.sort_by_key(|a| a.header.timestamp);
-
-        for event in events.iter() {
-            // info!("event: {}", event.id());
-            let fetched_rln_commitment: RLNNode = match deserialize_async_partial(event.content())
-                .await
-            {
-                Ok((v, _)) => v,
-                Err(e) => {
-                    error!(target: "irc::server", "[RLN] Failed deserializing incoming RLN Identity events: {}", e);
-                    continue
-                }
-            };
-
-            process_commitment(fetched_rln_commitment, &mut identity_tree)?;
-        }
-
-        drop(identity_tree);
-
         // Set the default RLN account if any
         let default_db = darkirc.sled.open_tree(format!("{}default", ACCOUNTS_DB_PREFIX))?;
         let rln_identity = if !default_db.is_empty() {

+ 38 - 39
bin/darkirc/src/irc/services/nickserv.rs

@@ -16,26 +16,18 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{io::Cursor, str::SplitAsciiWhitespace, sync::Arc, time::UNIX_EPOCH};
+use std::{str::SplitAsciiWhitespace, sync::Arc};
 
 use darkfi::{
-    event_graph::{
-        rln::{closest_epoch, RLNNode},
-        Event,
-    },
-    zk::{empty_witnesses, ProvingKey, ZkCircuit},
-    zkas::ZkBinary,
-    Error, Result,
+    event_graph::{rln::RLNNode, Event},
+    Result,
 };
 use darkfi_sdk::{crypto::pasta_prelude::PrimeField, pasta::pallas};
 use darkfi_serial::serialize_async;
 use smol::lock::RwLock;
 
 use super::super::{client::ReplyType, rpl::*};
-use crate::{
-    crypto::rln::{RlnIdentity, RLN2_REGISTER_ZKBIN},
-    IrcServer,
-};
+use crate::{crypto::rln::RlnIdentity, IrcServer};
 
 pub const ACCOUNTS_DB_PREFIX: &str = "darkirc_account_";
 pub const ACCOUNTS_KEY_RLN_IDENTITY: &[u8] = b"rln_identity";
@@ -193,13 +185,16 @@ impl NickServ {
                 }
             };
 
-        // Create a new RLN identity and insert it into the db tree
+        // Create a new RLN identity and insert it into the db tree.
+        // `last_epoch` is initialised to 0 deterministically - the
+        // first call to `next_message_id` will detect the rollover
+        // to the current wall-clock epoch.
         let new_rln_identity = RlnIdentity {
             nullifier: identity_nullifier,
             trapdoor: identity_trapdoor,
             user_message_limit: user_msg_limit,
-            message_id: 0, // TODO
-            last_epoch: closest_epoch(UNIX_EPOCH.elapsed().unwrap().as_millis() as u64),
+            message_id: 0,
+            last_epoch: 0,
         };
 
         // Store account
@@ -212,32 +207,36 @@ impl NickServ {
 
         *self.server.rln_identity.write().await = Some(new_rln_identity);
 
-        // Update SMT, DAG and broadcast
-        let rln_commitment = new_rln_identity.commitment();
-        let rln_commitment = RLNNode::Registration(rln_commitment);
+        // Build the static-DAG event and the registration blob.
+        // The blob format is now `RegistrationBlob` (proof +
+        // user_message_limit + max_message_limit + attestation),
+        // verified by the EG via `rln_verify_static_event`.
         let evgr = &self.server.darkirc.event_graph;
-        let event = Event::new_static(serialize_async(&rln_commitment).await, evgr).await;
-
-        // Retrieve the register ZK proving key from the db
-        let register_zkbin = ZkBinary::decode(RLN2_REGISTER_ZKBIN, false)?;
-        let register_circuit = ZkCircuit::new(empty_witnesses(&register_zkbin)?, &register_zkbin);
-        let Some(proving_key) = self.server.server_store.get("rlnv2-diff-register-pk")? else {
-            return Err(Error::DatabaseError(
-                "RLN register proving key not found in server store".to_string(),
-            ))
-        };
-        let mut reader = Cursor::new(proving_key);
-        let proving_key = ProvingKey::read(&mut reader, register_circuit)?;
-        let mut identity_tree = self.server.darkirc.event_graph.rln_identity_tree.write().await;
-
-        let proof =
-            new_rln_identity.create_register_proof(&event, &mut identity_tree, &proving_key)?;
-
-        drop(identity_tree);
-
-        let blob = serialize_async(&(proof, user_msg_limit)).await;
+        let rln_node = RLNNode::Registration(new_rln_identity.commitment());
+        let event = Event::new_static(serialize_async(&rln_node).await, evgr).await;
+
+        let registration_blob = new_rln_identity.create_registration(evgr)?;
+        let blob_bytes = serialize_async(&registration_blob).await;
+
+        // Apply the registration through the canonical pipeline:
+        //
+        // 1. `apply_rln_static_event` mutates the SMT and records
+        //    the post-mutation root in the historical-roots table.
+        //    This is the SAME entry point that
+        //    `proto.rs::handle_static_put` calls for events arriving
+        //    over the wire, so locally-originated and remote
+        //    registrations end up in the same canonical state.
+        // 2. `static_blob_store` persists the blob alongside the
+        //    event so a future late-joiner can re-verify the proof
+        //    during `static_sync`.
+        // 3. `static_insert` writes the event to the static DAG
+        //    and notifies `static_pub` (which the IRC client
+        //    subscription picks up for its own bookkeeping).
+        // 4. `static_broadcast` re-emits to peers.
+        evgr.apply_rln_static_event(&event, &rln_node).await?;
+        evgr.static_blob_store(&event.id(), &blob_bytes)?;
         evgr.static_insert(&event).await?;
-        evgr.static_broadcast(event, blob).await?;
+        evgr.static_broadcast(event, blob_bytes).await?;
 
         Ok(vec![ReplyType::Notice((
             "NickServ".to_string(),

+ 4 - 0
bin/darkirc/src/lib.rs

@@ -63,6 +63,8 @@ pub struct DarkIrc {
     dnet_sub: JsonSubscriber,
     /// deg JSON-RPC subscriber
     deg_sub: JsonSubscriber,
+    /// Gource visualization JSON-RPC subscriber
+    gource_sub: JsonSubscriber,
     /// Replay logs (DB) path
     replay_datastore: PathBuf,
 }
@@ -74,6 +76,7 @@ impl DarkIrc {
         event_graph: EventGraphPtr,
         dnet_sub: JsonSubscriber,
         deg_sub: JsonSubscriber,
+        gource_sub: JsonSubscriber,
         replay_datastore: PathBuf,
     ) -> Self {
         Self {
@@ -83,6 +86,7 @@ impl DarkIrc {
             rpc_connections: Mutex::new(HashSet::new()),
             dnet_sub,
             deg_sub,
+            gource_sub,
             replay_datastore,
         }
     }

+ 109 - 10
bin/darkirc/src/main.rs

@@ -16,16 +16,20 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{io::Write, sync::Arc};
+use std::{
+    io::Write,
+    sync::{atomic::Ordering, Arc},
+};
 
 use darkfi::{
     async_daemonize, cli_desc,
-    event_graph::{proto::ProtocolEventGraph, EventGraph, EventGraphPtr},
+    event_graph::{proto::ProtocolEventGraph, EventGraph, EventGraphConfig, EventGraphPtr},
     net::{session::SESSION_DEFAULT, settings::SettingsOpt, P2p, P2pPtr},
     rpc::{
         jsonrpc::JsonSubscriber,
         server::{listen_and_serve, RequestHandler},
         settings::{RpcSettings, RpcSettingsOpt},
+        util::JsonValue,
     },
     system::{sleep, StoppableTask, Subscription},
     util::path::{expand_path, get_config_path},
@@ -36,6 +40,7 @@ use darkfi_sdk::crypto::pasta_prelude::PrimeField;
 use irc2::{
     crypto::{bcrypt::bcrypt_hash_password, rln::RlnIdentity},
     irc::server::IrcServer,
+    rpc,
     settings::list_configured_contacts,
     DarkIrc,
 };
@@ -50,6 +55,31 @@ use url::Url;
 const CONFIG_FILE: &str = "darkirc_config.toml";
 const CONFIG_FILE_CONTENTS: &str = include_str!("../darkirc_config.toml");
 
+// =====================================================================
+// DarkIRC consensus parameters.
+//
+// These define the EventGraph configuration that EVERY DarkIRC node
+// in the network must agree on. Changing any of them is a hard fork.
+// They are passed verbatim to `EventGraph::new` at startup.
+// =====================================================================
+
+/// Epoch origin for DAG rotation (UTC midnight, 1 March 2025).
+/// Rotation boundaries are computed as offsets from this point.
+const DARKIRC_INITIAL_GENESIS: u64 = 1_740_787_200_000;
+
+/// DAG rotation period, in hours.
+const DARKIRC_HOURS_ROTATION: u64 = 1;
+
+/// Genesis payload. Two protocols MUST use distinct values; this
+/// also feeds into `RlnAppId::from_genesis` so RLN signals from one
+/// deployment never appear valid on another.
+const DARKIRC_GENESIS_CONTENTS: &[u8] = b"darkirc-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.
+const DARKIRC_MAX_DAGS: usize = 24;
+
 fn panic_hook(panic_info: &std::panic::PanicHookInfo) {
     error!("panic occurred: {panic_info}");
     error!("{}", std::backtrace::Backtrace::force_capture());
@@ -302,7 +332,6 @@ async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
         }
     };
     let replay_mode = args.replay_mode;
-    let fast_mode = args.fast_mode;
 
     info!("Instantiating event DAG");
     let sled_db = match sled::open(datastore.clone()) {
@@ -321,13 +350,19 @@ async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
             return Err(e);
         }
     };
+    // Consensus config. Every node must use exactly these values.
+    let eg_config = EventGraphConfig {
+        initial_genesis: DARKIRC_INITIAL_GENESIS,
+        hours_rotation: DARKIRC_HOURS_ROTATION,
+        genesis_contents: DARKIRC_GENESIS_CONTENTS.to_vec(),
+        max_dags: Some(DARKIRC_MAX_DAGS),
+    };
     let event_graph = match EventGraph::new(
         p2p.clone(),
         sled_db.clone(),
         replay_datastore.clone(),
         replay_mode,
-        fast_mode,
-        1,
+        eg_config,
         ex.clone(),
     )
     .await
@@ -339,6 +374,8 @@ async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
         }
     };
 
+    // The prune task is only spawned when `hours_rotation > 0`. We
+    // require rotation here, so the unwrap is safe.
     let prune_task = event_graph.prune_task.get().unwrap();
 
     info!("Registering EventGraph P2P protocol");
@@ -386,7 +423,33 @@ async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
             loop {
                 let event = deg_sub.receive().await;
                 debug!("Got deg event: {event:?}");
-                deg_sub_.notify(vec![event.into()].into()).await;
+                let json = deg_event_to_json(&event);
+                deg_sub_.notify(vec![json].into()).await;
+            }
+        },
+        |res| async {
+            match res {
+                Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
+                Err(e) => panic!("{e}"),
+            }
+        },
+        Error::DetachedTaskStopped,
+        ex.clone(),
+    );
+
+    info!("Starting Gource subs task");
+    let gource_sub = JsonSubscriber::new("gource.subscribe_events");
+    let gource_sub_ = gource_sub.clone();
+    let event_graph_gource = event_graph.clone();
+    let gource_task = StoppableTask::new();
+    gource_task.clone().start(
+        async move {
+            let event_pub = event_graph_gource.event_pub.clone().subscribe().await;
+            loop {
+                let ev = event_pub.receive().await;
+                if let Some(json) = rpc::privmsg_event_to_gource(&ev).await {
+                    gource_sub_.notify(vec![json].into()).await;
+                }
             }
         },
         |res| async {
@@ -407,6 +470,7 @@ async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
         event_graph.clone(),
         dnet_sub,
         deg_sub,
+        gource_sub,
         replay_datastore.clone(),
     ));
     let darkirc_ = Arc::clone(&darkirc);
@@ -509,6 +573,7 @@ async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
     rpc_task.stop().await;
     dnet_task.stop().await;
     deg_task.stop().await;
+    gource_task.stop().await;
 
     info!("Stopping IRC server");
     irc_task.stop().await;
@@ -550,11 +615,20 @@ async fn sync_task(
                     Err(e) => {
                         error!("Failed syncing static graph: {e}");
                         p2p.stop().await;
-                        return Err(Error::StaticDagSyncFailed)
+                        return Err(Error::DagSyncFailed)
                     }
                 }
                 info!("Syncing event DAG");
-                match event_graph.sync_selected(dags_count, fast_mode).await {
+                // Sync mode is per-call: full sync replays
+                // every event (heavy, used by archival nodes), fast
+                // sync only fetches headers (light, used by clients
+                // that don't need to re-verify history).
+                let sync_result = if fast_mode {
+                    event_graph.sync_selected_headers(dags_count).await
+                } else {
+                    event_graph.sync_selected(dags_count).await
+                };
+                match sync_result {
                     Ok(()) => break,
                     Err(e) => {
                         // TODO: Maybe at this point we should prune or something?
@@ -564,7 +638,7 @@ async fn sync_task(
                     }
                 }
             } else {
-                *event_graph.synced.write().await = true;
+                event_graph.synced.store(true, Ordering::Release);
                 break;
             }
         } else {
@@ -594,10 +668,35 @@ async fn sync_and_monitor(
             Err(Error::NetworkNotConnected) => {
                 // Sync node again
                 info!("Network disconnection detected, resyncing...");
-                *event_graph.synced.write().await = false;
+                event_graph.synced.store(false, Ordering::Release);
                 sync_task(&p2p, &event_graph, skip_dag_sync, fast_mode, dags_count).await?;
             }
             Err(e) => return Err(e),
         }
     }
 }
+
+fn deg_event_to_json(ev: &darkfi::event_graph::deg::DegEvent) -> JsonValue {
+    use darkfi::{
+        event_graph::deg::{DegEvent, MessageInfo},
+        rpc::util::json_map,
+    };
+
+    fn info_to_json(direction: &str, info: &MessageInfo) -> JsonValue {
+        let info_arr: Vec<JsonValue> = info.info.iter().cloned().map(JsonValue::String).collect();
+        json_map([
+            ("direction", JsonValue::String(direction.into())),
+            ("cmd", JsonValue::String(info.cmd.clone())),
+            // NanoTimestamp's Display is the human-readable form;
+            // emit it as a string to avoid losing precision through
+            // the JSON number type (f64 can't hold nanos cleanly).
+            ("time", JsonValue::String(format!("{}", info.time))),
+            ("info", JsonValue::Array(info_arr)),
+        ])
+    }
+
+    match ev {
+        DegEvent::SendMessage(info) => info_to_json("send", info),
+        DegEvent::RecvMessage(info) => info_to_json("recv", info),
+    }
+}

+ 71 - 4
bin/darkirc/src/rpc.rs

@@ -19,20 +19,22 @@ use std::collections::HashSet;
 
 use async_trait::async_trait;
 use darkfi::{
-    event_graph::util::recreate_from_replayer_log,
+    event_graph::{util::recreate_from_replayer_log, Event},
     net::P2pPtr,
     rpc::{
         jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
         p2p_method::HandlerP2p,
         server::RequestHandler,
-        util::JsonValue,
+        util::{json_map, JsonValue},
     },
     system::StoppableTaskPtr,
 };
+use darkfi_serial::deserialize_async_partial;
 use smol::lock::MutexGuard;
 use tracing::debug;
 
 use super::DarkIrc;
+use crate::irc::Privmsg;
 
 #[async_trait]
 impl RequestHandler<()> for DarkIrc {
@@ -50,6 +52,8 @@ impl RequestHandler<()> for DarkIrc {
             "eventgraph.get_info" => self.eg_get_info(req.id, req.params).await,
             "eventgraph.replay" => self.eg_rep_info(req.id, req.params).await,
 
+            "gource.subscribe_events" => self.gource_subscribe_events(req.id, req.params).await,
+
             _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
         }
     }
@@ -122,6 +126,33 @@ impl DarkIrc {
         self.deg_sub.clone().into()
     }
 
+    // RPCAPI:
+    // Initializes a subscription to the Gource visualization feed.
+    // Once a subscription is established, every rotating-DAG event
+    // that successfully decodes as a Privmsg is projected to a
+    // Gource-shaped record and forwarded to the subscriber.
+    //
+    // To feed Gource directly, reformat to the pipe-delimited custom
+    // log format and pipe it in:
+    // ```
+    //   ... | jq -r '.params[0]
+    //              | "\(.timestamp)|\(.user)|\(.action)|\(.path)"' \
+    //       | gource --log-format custom -
+    // ```
+    //
+    // --> {"jsonrpc": "2.0", "method": "gource.subscribe_events", "params": [], "id": 1}
+    // <-- {"jsonrpc": "2.0", "method": "gource.subscribe_events", "params": [`event`]}
+    pub async fn gource_subscribe_events(&self, id: i64, params: JsonValue) -> JsonResult {
+        let Some(params) = params.get::<Vec<JsonValue>>() else {
+            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        };
+        if !params.is_empty() {
+            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        }
+
+        self.gource_sub.clone().into()
+    }
+
     // RPCAPI:
     // Activate or deactivate deg in the EVENTGRAPH.
     // By sending `true`, deg will be activated, and by sending `false` deg
@@ -140,9 +171,9 @@ impl DarkIrc {
         let switch = params[0].get::<bool>().unwrap();
 
         if *switch {
-            self.event_graph.deg_enable().await;
+            self.event_graph.deg_enable();
         } else {
-            self.event_graph.deg_disable().await;
+            self.event_graph.deg_disable();
         }
 
         JsonResponse::new(JsonValue::Boolean(true), id).into()
@@ -186,3 +217,39 @@ impl HandlerP2p for DarkIrc {
         self.p2p.clone()
     }
 }
+
+/// Project a single rotating-DAG event to a Gource-shaped record.
+///
+/// Returns `None` if the event content isn't a [`Privmsg`] or the
+/// privmsg's channel field is empty (in which case there's nothing
+/// useful to visualize).
+pub async fn privmsg_event_to_gource(event: &Event) -> Option<JsonValue> {
+    let privmsg: Privmsg = match deserialize_async_partial(event.content()).await {
+        Ok((v, _)) => v,
+        Err(_) => return None,
+    };
+
+    if privmsg.channel.is_empty() {
+        return None
+    }
+
+    let path = if let Some(name) = privmsg.channel.strip_prefix('#') {
+        format!("channels/{name}")
+    } else {
+        format!("dms/{}", privmsg.channel)
+    };
+
+    // Gource's custom log expects Unix seconds, not millis.
+    let unix_secs = event.header.timestamp / 1_000;
+
+    Some(json_map([
+        ("timestamp", JsonValue::String(unix_secs.to_string())),
+        ("user", JsonValue::String(privmsg.nick.clone())),
+        // "M" = modify. We always emit "M" because tracking
+        // first-touch (which would justify "A") would need
+        // cross-event state and gource creates the file on first
+        // reference automatically anyway.
+        ("action", JsonValue::String("M".into())),
+        ("path", JsonValue::String(path)),
+    ]))
+}

+ 6 - 5
bin/darkirc/src/settings.rs

@@ -19,11 +19,10 @@
 use std::{
     collections::{HashMap, HashSet},
     sync::Arc,
-    time::UNIX_EPOCH,
 };
 
 use crypto_box::PublicKey;
-use darkfi::{event_graph::rln::closest_epoch, Error::ParseFailed, Result};
+use darkfi::{Error::ParseFailed, Result};
 use darkfi_sdk::{crypto::pasta_prelude::PrimeField, pasta::pallas};
 use tracing::info;
 
@@ -236,10 +235,12 @@ pub fn parse_rln_identity(data: &toml::Value) -> Result<Option<RlnIdentity>> {
         nullifier: identity_nullifier,
         trapdoor: identity_trapdoor,
         user_message_limit,
-        // TODO: FIXME: We should probably keep track of these rather than
-        // resetting here
+        // Per-epoch counters start fresh on load. The first call to
+        // `next_message_id` will detect the epoch transition (from
+        // 0 to whatever the current wall-clock epoch is) and reset
+        // `message_id` accordingly.
         message_id: 0,
-        last_epoch: closest_epoch(UNIX_EPOCH.elapsed().unwrap().as_secs()),
+        last_epoch: 0,
     }))
 }