Browse Source

darkirc: Keep registrations pregenerated-only

x 1 month ago
parent
commit
ae6e8e22f5
4 changed files with 97 additions and 107 deletions
  1. 31 88
      bin/darkirc/src/irc/services/nickserv.rs
  2. 14 8
      src/event_graph/mod.rs
  3. 12 2
      src/event_graph/rln.rs
  4. 40 9
      src/event_graph/tests_rln.rs

+ 31 - 88
bin/darkirc/src/irc/services/nickserv.rs

@@ -459,17 +459,6 @@ impl NickServ {
             return Ok(vec![notice(nick, "Invalid user_msg_limit: must be at least 1.")])
         }
 
-        // Open the per-account sled tree
-        let db =
-            self.server.darkirc.sled.open_tree(format!("{ACCOUNTS_DB_PREFIX}{account_name}"))?;
-
-        if !db.is_empty() {
-            return Ok(vec![notice(nick, "This account name is already registered.")])
-        }
-
-        // Open the default-mirror sled tree
-        let db_default = self.server.darkirc.sled.open_tree(ACCOUNTS_DEFAULT_TREE)?;
-
         // Parse the secrets. The original code used `.unwrap()` on
         // the `try_into` for the byte-length check, which would
         // panic on any input that wasn't exactly 32 bytes. Convert
@@ -494,7 +483,33 @@ impl NickServ {
             last_epoch: 0,
         };
 
-        // Store account
+        let is_genesis =
+            GENESIS_COMMITMENTS_REPR.contains(&new_rln_identity.commitment().to_repr());
+        if !is_genesis {
+            return Ok(vec![notice(
+                nick,
+                "Registration is currently limited to pregenerated identities.",
+            )])
+        }
+
+        if user_msg_limit != GENESIS_USER_MSG_LIMIT {
+            return Ok(vec![notice(
+                nick,
+                format!("Genesis account must use user_msg_limit={}", GENESIS_USER_MSG_LIMIT),
+            )])
+        }
+
+        // Open the per-account sled tree only after the identity has
+        // passed the pregenerated-admission checks. Rejected identities
+        // must not leave account state behind or become active locally.
+        let db =
+            self.server.darkirc.sled.open_tree(format!("{ACCOUNTS_DB_PREFIX}{account_name}"))?;
+
+        if !db.is_empty() {
+            return Ok(vec![notice(nick, "This account name is already registered.")])
+        }
+
+        // Store account.
         db.insert(ACCOUNTS_KEY_RLN_IDENTITY, serialize_async(&new_rln_identity).await)?;
 
         // First-ever registration also becomes the active one. We
@@ -502,77 +517,12 @@ impl NickServ {
         // tree) because that's the source of truth at runtime.
         let became_active = self.server.rln_identity.read().await.is_none();
         if became_active {
+            let db_default = self.server.darkirc.sled.open_tree(ACCOUNTS_DEFAULT_TREE)?;
             db_default
                 .insert(ACCOUNTS_KEY_RLN_IDENTITY, serialize_async(&new_rln_identity).await)?;
             *self.server.rln_identity.write().await = Some(new_rln_identity);
         }
 
-        let is_genesis =
-            GENESIS_COMMITMENTS_REPR.contains(&new_rln_identity.commitment().to_repr());
-
-        if is_genesis {
-            if user_msg_limit != GENESIS_USER_MSG_LIMIT {
-                let mut replies = vec![notice(
-                    nick,
-                    format!("Genesis account must use user_msg_limit={}", GENESIS_USER_MSG_LIMIT),
-                )];
-                replies.push(notice(
-                    nick,
-                    format!("Use `DEREGISTER {account_name}` to remove this account."),
-                ));
-            }
-            let mut replies =
-                vec![notice(nick, format!("Successfully registered account \"{account_name}\""))];
-            if became_active {
-                replies
-                    .push(notice(nick, format!("\"{account_name}\" is now the active identity.")));
-            } else {
-                replies.push(notice(
-                    nick,
-                    format!("Use `SET {account_name}` to make this the active identity."),
-                ));
-            }
-            Ok(replies)
-        } else {
-            let replies =
-                vec![notice(nick, format!("Failed to register account \"{account_name}\""))];
-            Ok(replies)
-        }
-        // 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.
-        //    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 - but ONLY if
-        //    the local DAG is synced. A pre-sync broadcast just
-        //    vanishes (peers gate `handle_static_put` on their own
-        //    is_synced state, and we can't serve our tips for
-        //    pulls because `handle_tip_req` gates on is_synced
-        //    too). When unsynced, we defer the broadcast to a
-        //    watcher task that drains the pending queue once sync
-        //    completes.
-        /*
-        evgr.apply_rln_static_event(&event, &rln_node).await?;
-        evgr.static_blob_store(&event.id(), &blob_bytes)?;
-        evgr.static_insert(&event).await?;
-
-        let broadcast_status = if evgr.is_synced() {
-            evgr.static_broadcast(event, blob_bytes).await?;
-            BroadcastStatus::Sent
-        } else {
-            self.server.pending_static_broadcasts.lock().await.push((event, blob_bytes));
-            BroadcastStatus::Deferred
-        };
-
         let mut replies =
             vec![notice(nick, format!("Successfully registered account \"{account_name}\""))];
         if became_active {
@@ -583,17 +533,10 @@ impl NickServ {
                 format!("Use `SET {account_name}` to make this the active identity."),
             ));
         }
-
-        if broadcast_status == BroadcastStatus::Deferred {
-            replies.push(notice(
-                nick,
-                "Note: local DAG is not yet synced; the registration is stored \
-                 locally and will be broadcast to peers once sync completes.",
-            ));
-        }
-
+        // Pregenerated identities are already bootstrapped into
+        // the static DAG. Future staked registration will need to
+        // add a contract-backed network broadcast path here.
         Ok(replies)
-        */
     }
 
     /// Handle the DEREGISTER command.

+ 14 - 8
src/event_graph/mod.rs

@@ -124,6 +124,11 @@ pub type EventGraphPtr = Arc<EventGraph>;
 /// Unreferenced tips grouped by layer.
 pub type LayerUTips = BTreeMap<u64, HashSet<blake3::Hash>>;
 
+/// Generate the deterministic genesis event for the static DAG.
+fn generate_static_genesis(config: &EventGraphConfig) -> Event {
+    generate_genesis(&EventGraphConfig { hours_rotation: 0, ..config.clone() })
+}
+
 /// Bidirectional timestamp -> event-ID index.
 #[derive(Clone, Debug, Default)]
 pub struct TimeIndex {
@@ -2273,9 +2278,10 @@ impl EventGraph {
 
         match rln_node {
             RLNNode::Registration(commitment) => {
-                // No ZK proofs, only commitments in the hardcoded genesis set
-                // are accepted this way, anything else falls through to
-                // normal proof verification.
+                // Current admission policy is pregenerated identities only.
+                // The guard blob is valid exclusively for commitments built
+                // into GENESIS_COMMITMENTS_REPR; pairing it with any other
+                // commitment is an unambiguous forgery attempt.
                 if blob == rln::GENESIS_BLOB_GUARD {
                     let repr = commitment.to_repr();
                     if GENESIS_COMMITMENTS_REPR.contains(&repr) {
@@ -2284,13 +2290,14 @@ impl EventGraph {
                         }
                         return StaticEventCheck::AcceptedRegistration(*commitment)
                     } else {
-                        // Guard blob with unknown commitment = malicious
                         return StaticEventCheck::Malicious
                     }
                 }
 
-                // Accepting only genesis registeration, reject every
-                // other account.
+                // Free non-genesis registration is intentionally disabled:
+                // it is a sybil attack surface. Keep the proof scaffolding
+                // below for the future staked tier, where acceptance must be
+                // backed by a DarkFi smart-contract attestation.
                 StaticEventCheck::Rejected
                 /*
                 #[allow(unreachable_code)]
@@ -2380,8 +2387,7 @@ impl EventGraph {
     /// already present in the identity tree.
     pub async fn bootstrap_genesis_identities(&self) -> Result<()> {
         // Deterministic for premade identities.
-        let genesis_event =
-            generate_genesis(&EventGraphConfig { hours_rotation: 0, ..self.config.clone() });
+        let genesis_event = generate_static_genesis(&self.config);
         let genesis_id = genesis_event.id();
         if !self.static_dag.contains_key(genesis_id.as_bytes())? {
             return Err(Error::Custom("static DAG genesis missing during bootstrap".into()))

+ 12 - 2
src/event_graph/rln.rs

@@ -121,7 +121,12 @@ impl RlnAppId {
     }
 }
 
-/// Versioned attestation accompanying a registration
+/// Versioned attestation accompanying a registration.
+///
+/// Runtime admission currently accepts only pregenerated genesis
+/// identities. This enum is retained for the future staked tier,
+/// where a DarkFi smart-contract attestation must back new identity
+/// registration.
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub enum RegistrationAttestation {
     SPECIAL,
@@ -147,7 +152,12 @@ impl RegistrationAttestation {
 /// The complete blob attached to a registration `EventPut` /
 /// `StaticPut`. The proof's public inputs commit to the
 /// `(commitment, user_message_limit, max_message_limit)` tuple,
-/// and `attestation` carries the (eventual) staking proof.
+/// and `attestation` carries the staking proof.
+///
+/// Non-genesis registration is disabled until contract-backed
+/// staked admission is implemented; current production admission
+/// accepts only pregenerated commitments paired with
+/// [`GENESIS_BLOB_GUARD`].
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct RegistrationBlob {
     pub proof: Proof,

+ 40 - 9
src/event_graph/tests_rln.rs

@@ -18,17 +18,21 @@
 
 use std::{sync::Arc, time::UNIX_EPOCH};
 
-use darkfi_sdk::{crypto::poseidon_hash, pasta::pallas};
+use darkfi_sdk::{
+    crypto::{pasta_prelude::PrimeField, poseidon_hash},
+    pasta::pallas,
+};
 use darkfi_serial::{deserialize_async, serialize_async};
 use sled_overlay::sled;
 use smol::Executor;
 
 use crate::{
     event_graph::{
+        genesis_commits::GENESIS_COMMITMENTS_REPR,
         rln::{
             epoch_of, epoch_start_millis, sss_recover, Blob, IdentityState, MessageMetadata,
             RLNNode, RegistrationAttestation, RegistrationBlob, RlnAppId, SignalCheck, SlashBlob,
-            MAX_MSG_LIMIT, RLN_EPOCH_LEN, RLN_GENESIS,
+            GENESIS_BLOB_GUARD, MAX_MSG_LIMIT, RLN_EPOCH_LEN, RLN_GENESIS,
         },
         test_helpers::{
             make_eg, make_eg_with_config, make_network, run_multi_node_test, shutdown_network,
@@ -525,13 +529,40 @@ fn placeholder_slash_blob(ish: pallas::Base, root: pallas::Base) -> SlashBlob {
     }
 }
 
+fn genesis_commitment_at(index: usize) -> pallas::Base {
+    pallas::Base::from_repr(GENESIS_COMMITMENTS_REPR[index]).into_option().unwrap()
+}
+
+#[test]
+fn rln_static_event_pregenerated_guard_accepted() {
+    smol::block_on(async {
+        let eg = make_eg().await;
+        let commitment = genesis_commitment_at(0);
+        let node = RLNNode::Registration(commitment);
+
+        let outcome = eg.rln_verify_static_event(&node, GENESIS_BLOB_GUARD, 0).await;
+        assert!(matches!(outcome, StaticEventCheck::AcceptedRegistration(c) if c == commitment));
+    })
+}
+
+#[test]
+fn rln_static_event_guard_with_unknown_commitment_is_malicious() {
+    smol::block_on(async {
+        let eg = make_eg().await;
+        let commitment = pallas::Base::from(0xdead_beefu64);
+        assert!(!GENESIS_COMMITMENTS_REPR.contains(&commitment.to_repr()));
+
+        let node = RLNNode::Registration(commitment);
+        let outcome = eg.rln_verify_static_event(&node, GENESIS_BLOB_GUARD, 0).await;
+        assert!(matches!(outcome, StaticEventCheck::Malicious));
+    })
+}
+
 #[test]
-fn rln_static_event_registration_invalid_limits_are_malicious() {
-    // A registration with structurally invalid `user_message_limit`
-    // (zero, above MAX_MSG_LIMIT, or above the attestation's
-    // permitted ceiling) is `Malicious`. Each case is one strike-
-    // worthy violation - peers that relay any of these are
-    // misbehaving.
+fn rln_static_event_free_registration_blobs_rejected() {
+    // Free registration is intentionally disabled: non-guard
+    // registration blobs are rejected before proof parsing until
+    // staked, contract-backed admission exists.
     smol::block_on(async {
         let eg = make_eg().await;
         let node = RLNNode::Registration(pallas::Base::from(1u64));
@@ -548,7 +579,7 @@ fn rln_static_event_registration_invalid_limits_are_malicious() {
             );
             let bytes = serialize_async(&blob).await;
             let outcome = eg.rln_verify_static_event(&node, &bytes, 0).await;
-            assert!(matches!(outcome, StaticEventCheck::Malicious), "{why}");
+            assert!(matches!(outcome, StaticEventCheck::Rejected), "{why}");
         }
     })
 }