diff --git a/rln-cli/src/examples/multi_message_id.rs b/rln-cli/src/examples/multi_message_id.rs index 2a2d217e..fde38ae2 100644 --- a/rln-cli/src/examples/multi_message_id.rs +++ b/rln-cli/src/examples/multi_message_id.rs @@ -114,7 +114,7 @@ impl RLNSystem { println!("Registered users:"); for (index, identity) in &self.local_identities { - println!("User Index: {index}"); + println!("User: {index}"); println!("+ Identity secret: {}", *identity.identity_secret); println!("+ Identity commitment: {}", identity.id_commitment); println!(); @@ -129,7 +129,7 @@ impl RLNSystem { poseidon_hash(&[identity.id_commitment, Fr::from(MESSAGE_LIMIT)]).unwrap(); match self.rln.set_next_leaf(rate_commitment) { Ok(_) => { - println!("Registered User Index: {index}"); + println!("Registered user: {index}"); println!("+ Identity secret: {}", *identity.identity_secret); println!("+ Identity commitment: {}", identity.id_commitment); self.local_identities.insert(index, identity); @@ -191,7 +191,7 @@ impl RLNSystem { ) -> Result { let identity = match self.local_identities.get(&user_index) { Some(identity) => identity, - None => return Err(format!("user index {user_index} not found").into()), + None => return Err(format!("User {user_index} not found").into()), }; let (path_elements, identity_path_index) = self.rln.get_merkle_proof(user_index)?; @@ -209,10 +209,9 @@ impl RLNSystem { )?; let (proof, proof_values) = self.rln.generate_rln_proof(&witness)?; - let active_count = selector_used.iter().filter(|&&s| s).count(); println!("Proof generated successfully:"); - println!("+ User Index: {user_index}"); + println!("+ User: {user_index}"); println!( "+ Active message slots: {active_count}/{}", self.rln.max_out() @@ -276,7 +275,7 @@ impl RLNSystem { ); self.local_identities.remove(&user_index); self.rln.delete_leaf(user_index)?; - println!("User index {user_index} has been SLASHED"); + println!("User {user_index} has been SLASHED"); Ok(()) } } else { @@ -375,7 +374,9 @@ fn show_commands() { println!( " list - List registered users" ); - println!(" register - Register a new user index"); + println!( + " register - Register a new user" + ); println!(" send -u -m --selector -s - Send a message with proof"); println!(" (example: send -u 0 -m 0,1,2,3 --selector 1,1,0,0 -s \"hello\")"); println!( diff --git a/rln-cli/src/examples/partial.rs b/rln-cli/src/examples/partial.rs index 2915e2d9..0f565884 100644 --- a/rln-cli/src/examples/partial.rs +++ b/rln-cli/src/examples/partial.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, VecDeque}, fs::File, io::{stdin, stdout, Read, Write}, path::{Path, PathBuf}, @@ -16,6 +16,10 @@ const MESSAGE_LIMIT: u32 = 1; const TREE_DEPTH: usize = 20; +const ROOT_HISTORY_LIMIT: usize = 3; + +const PARTIAL_REFRESH_INTERVAL: usize = ROOT_HISTORY_LIMIT; + type Result = std::result::Result>; #[derive(Parser)] @@ -28,6 +32,7 @@ struct Cli { #[derive(Subcommand)] enum Commands { List, + Roots, Register, Send { #[arg(short, long)] @@ -47,6 +52,14 @@ struct Identity { id_commitment: Fr, } +#[derive(Clone)] +struct CachedPartialProof { + root: Fr, + proof: PartialProof, + path_elements: Vec, + path_index: Vec, +} + impl Identity { fn new() -> Self { let (identity_secret, id_commitment) = keygen().unwrap(); @@ -61,8 +74,10 @@ struct RLNSystem { rln: RLN, used_nullifiers: HashMap, local_identities: HashMap, - partial_proofs: HashMap, + partial_proofs: HashMap, external_nullifier: Fr, + latest_roots: VecDeque, + pending_registrations: usize, } impl RLNSystem { @@ -92,6 +107,8 @@ impl RLNSystem { resources[1].clone(), tree_config, )?; + let mut latest_roots = VecDeque::new(); + latest_roots.push_front(rln.get_root()); println!("RLN instance initialized successfully"); Ok(RLNSystem { rln, @@ -99,6 +116,8 @@ impl RLNSystem { local_identities: HashMap::new(), partial_proofs: HashMap::new(), external_nullifier, + latest_roots, + pending_registrations: 0, }) } @@ -110,13 +129,40 @@ impl RLNSystem { println!("Registered users:"); for (index, identity) in &self.local_identities { - println!("User Index: {index}"); + println!("User: {index}"); println!("+ Identity secret: {}", *identity.identity_secret); println!("+ Identity commitment: {}", identity.id_commitment); println!(); } } + fn list_roots(&self) { + if self.latest_roots.is_empty() { + println!("No roots recorded yet."); + return; + } + + println!("Latest roots (newest first, max {ROOT_HISTORY_LIMIT}):"); + for (i, root) in self.latest_roots.iter().enumerate() { + println!("#{i}: {root}"); + } + } + + fn record_root(&mut self) { + let current_root = self.rln.get_root(); + if self.latest_roots.front() == Some(¤t_root) { + return; + } + self.latest_roots.push_front(current_root); + while self.latest_roots.len() > ROOT_HISTORY_LIMIT { + self.latest_roots.pop_back(); + } + } + + fn root_is_recent(&self, root: &Fr) -> bool { + self.latest_roots.iter().any(|r| r == root) + } + fn register_user(&mut self) -> Result { let index = self.rln.leaves_set(); let identity = Identity::new(); @@ -125,11 +171,24 @@ impl RLNSystem { poseidon_hash(&[identity.id_commitment, Fr::from(MESSAGE_LIMIT)]).unwrap(); match self.rln.set_next_leaf(rate_commitment) { Ok(_) => { - println!("Registered User Index: {index}"); + println!("Registered user: {index}"); println!("+ Identity secret: {}", *identity.identity_secret); println!("+ Identity commitment: {}", identity.id_commitment); self.local_identities.insert(index, identity); - self.rebuild_partial_proofs()?; + self.record_root(); + self.pending_registrations += 1; + if self.pending_registrations >= PARTIAL_REFRESH_INTERVAL { + self.rebuild_partial_proofs()?; + self.pending_registrations = 0; + println!( + "Refreshed partial proofs after {PARTIAL_REFRESH_INTERVAL} registrations" + ); + } else { + let remaining = PARTIAL_REFRESH_INTERVAL - self.pending_registrations; + println!( + "Skipping partial proof refresh: {remaining} more registration(s) before next refresh" + ); + } } Err(_) => { println!("Maximum user limit reached: 2^{TREE_DEPTH}"); @@ -141,6 +200,8 @@ impl RLNSystem { fn rebuild_partial_proofs(&mut self) -> Result<()> { let indices: Vec = self.local_identities.keys().copied().collect(); + let current_root = self.rln.get_root(); + self.partial_proofs.clear(); for user_index in indices { let identity = self.local_identities[&user_index].clone(); let (path_elements, identity_path_index) = self.rln.get_merkle_proof(user_index)?; @@ -148,15 +209,23 @@ impl RLNSystem { identity.identity_secret.clone(), Fr::from(MESSAGE_LIMIT), Fr::from(0u32), - path_elements, - identity_path_index, + path_elements.clone(), + identity_path_index.clone(), Fr::from(0u64), self.external_nullifier, )?; let partial_witness = RLNPartialWitnessInput::from(&witness); let partial_proof = self.rln.generate_partial_zk_proof(&partial_witness)?; - self.partial_proofs.insert(user_index, partial_proof); - println!("Pre-generated partial proof for User Index: {user_index}"); + self.partial_proofs.insert( + user_index, + CachedPartialProof { + root: current_root, + proof: partial_proof, + path_elements, + path_index: identity_path_index, + }, + ); + println!("Pre-generated partial proof for user: {user_index}"); } Ok(()) } @@ -170,41 +239,68 @@ impl RLNSystem { ) -> Result { let identity = match self.local_identities.get(&user_index) { Some(identity) => identity, - None => return Err(format!("user index {user_index} not found").into()), + None => return Err(format!("User {user_index} not found").into()), }; - let (path_elements, identity_path_index) = self.rln.get_merkle_proof(user_index)?; let x = hash_to_field_le(signal.as_bytes())?; + let current_root = self.rln.get_root(); - let witness = RLNWitnessInput::new( - identity.identity_secret.clone(), - Fr::from(MESSAGE_LIMIT), - Fr::from(message_id), - path_elements, - identity_path_index, - x, - external_nullifier, - )?; - - let partial_proof = match self.partial_proofs.get(&user_index) { - Some(cached) => { - println!("Using cached partial proof for User Index: {user_index}"); - cached.clone() + let (witness, partial_proof) = match self.partial_proofs.get(&user_index) { + Some(cached) if self.root_is_recent(&cached.root) => { + println!( + "Using cached partial proof for user {user_index} (root {})", + cached.root + ); + let witness = RLNWitnessInput::new( + identity.identity_secret.clone(), + Fr::from(MESSAGE_LIMIT), + Fr::from(message_id), + cached.path_elements.clone(), + cached.path_index.clone(), + x, + external_nullifier, + )?; + (witness, cached.proof.clone()) } - None => { + _ => { + println!( + "Cached partial proof missing or stale for user {user_index}; generating fresh proof" + ); + let (path_elements, identity_path_index) = self.rln.get_merkle_proof(user_index)?; + let witness = RLNWitnessInput::new( + identity.identity_secret.clone(), + Fr::from(MESSAGE_LIMIT), + Fr::from(message_id), + path_elements.clone(), + identity_path_index.clone(), + x, + external_nullifier, + )?; let partial_witness = RLNPartialWitnessInput::from(&witness); - self.rln.generate_partial_zk_proof(&partial_witness)? + let generated = self.rln.generate_partial_zk_proof(&partial_witness)?; + self.partial_proofs.insert( + user_index, + CachedPartialProof { + root: current_root, + proof: generated.clone(), + path_elements, + path_index: identity_path_index, + }, + ); + (witness, generated) } }; let (proof, proof_values) = self.rln.finish_rln_proof(&partial_proof, &witness)?; - println!("Proof generated successfully:"); - println!("+ User Index: {user_index}"); + println!("+ User: {user_index}"); println!("+ Message ID: {message_id}"); println!("+ Signal: {signal}"); - let verified = self.rln.verify_rln_proof(&proof, &proof_values, &x)?; + let latest_roots: Vec = self.latest_roots.iter().copied().collect(); + let verified = self + .rln + .verify_with_roots(&proof, &proof_values, &x, &latest_roots)?; if verified { println!("Proof verified successfully"); } @@ -254,7 +350,8 @@ impl RLNSystem { self.local_identities.remove(&user_index); self.partial_proofs.remove(&user_index); self.rln.delete_leaf(user_index)?; - println!("User index {user_index} has been SLASHED"); + self.record_root(); + println!("User {user_index} has been SLASHED"); Ok(()) } } else { @@ -291,6 +388,9 @@ fn main() -> Result<()> { Commands::List => { rln_system.list_users(); } + Commands::Roots => { + rln_system.list_roots(); + } Commands::Register => { rln_system.register_user()?; } @@ -334,7 +434,8 @@ fn main() -> Result<()> { fn show_commands() { println!("Available commands:"); println!(" list - List registered users"); - println!(" register - Register a new user index"); + println!(" roots - Show latest 3 recorded roots"); + println!(" register - Register a new user"); println!(" send -u -m -s - Send a message with partial proof"); println!(" (example: send -u 0 -m 0 -s \"hello\")"); println!(" clear - Clear the screen"); diff --git a/rln-cli/src/examples/relay.rs b/rln-cli/src/examples/relay.rs index 6082d348..149d391f 100644 --- a/rln-cli/src/examples/relay.rs +++ b/rln-cli/src/examples/relay.rs @@ -106,7 +106,7 @@ impl RLNSystem { println!("Registered users:"); for (index, identity) in &self.local_identities { - println!("User Index: {index}"); + println!("User: {index}"); println!("+ Identity secret: {}", *identity.identity_secret); println!("+ Identity commitment: {}", identity.id_commitment); println!(); @@ -121,7 +121,7 @@ impl RLNSystem { poseidon_hash(&[identity.id_commitment, Fr::from(MESSAGE_LIMIT)]).unwrap(); match self.rln.set_next_leaf(rate_commitment) { Ok(_) => { - println!("Registered User Index: {index}"); + println!("Registered user: {index}"); println!("+ Identity secret: {}", *identity.identity_secret); println!("+ Identity commitment: {}", identity.id_commitment); self.local_identities.insert(index, identity); @@ -143,7 +143,7 @@ impl RLNSystem { ) -> Result { let identity = match self.local_identities.get(&user_index) { Some(identity) => identity, - None => return Err(format!("user index {user_index} not found").into()), + None => return Err(format!("User {user_index} not found").into()), }; let (path_elements, identity_path_index) = self.rln.get_merkle_proof(user_index)?; @@ -160,9 +160,8 @@ impl RLNSystem { )?; let (proof, proof_values) = self.rln.generate_rln_proof(&witness)?; - println!("Proof generated successfully:"); - println!("+ User Index: {user_index}"); + println!("+ User: {user_index}"); println!("+ Message ID: {message_id}"); println!("+ Signal: {signal}"); @@ -215,7 +214,7 @@ impl RLNSystem { ); self.local_identities.remove(&user_index); self.rln.delete_leaf(user_index)?; - println!("User index {user_index} has been SLASHED"); + println!("User {user_index} has been SLASHED"); Ok(()) } } else { @@ -295,7 +294,7 @@ fn main() -> Result<()> { fn show_commands() { println!("Available commands:"); println!(" list - List registered users"); - println!(" register - Register a new user index"); + println!(" register - Register a new user"); println!(" send -u -m -s - Send a message with proof"); println!(" (example: send -u 0 -m 0 -s \"hello\")"); println!(" clear - Clear the screen"); diff --git a/rln-cli/src/examples/stateless.rs b/rln-cli/src/examples/stateless.rs index 09ff88f1..909cf810 100644 --- a/rln-cli/src/examples/stateless.rs +++ b/rln-cli/src/examples/stateless.rs @@ -90,7 +90,7 @@ impl RLNSystem { println!("Registered users:"); for (index, identity) in &self.local_identities { - println!("User Index: {index}"); + println!("User: {index}"); println!("+ Identity secret: {}", *identity.identity_secret); println!("+ Identity commitment: {}", identity.id_commitment); println!(); @@ -105,7 +105,7 @@ impl RLNSystem { poseidon_hash(&[identity.id_commitment, Fr::from(MESSAGE_LIMIT)]).unwrap(); self.tree.update_next(rate_commitment)?; - println!("Registered User Index: {index}"); + println!("Registered user: {index}"); println!("+ Identity secret: {}", *identity.identity_secret); println!("+ Identity commitment: {}", identity.id_commitment); @@ -122,7 +122,7 @@ impl RLNSystem { ) -> Result { let identity = match self.local_identities.get(&user_index) { Some(identity) => identity, - None => return Err(format!("user index {user_index} not found").into()), + None => return Err(format!("User {user_index} not found").into()), }; let merkle_proof = self.tree.proof(user_index)?; @@ -139,9 +139,8 @@ impl RLNSystem { )?; let (proof, proof_values) = self.rln.generate_rln_proof(&witness)?; - println!("Proof generated successfully:"); - println!("+ User Index: {user_index}"); + println!("+ User: {user_index}"); println!("+ Message ID: {message_id}"); println!("+ Signal: {signal}"); @@ -204,7 +203,7 @@ impl RLNSystem { *leaked_identity_secret ); self.local_identities.remove(&user_index); - println!("User index {user_index} has been SLASHED"); + println!("User {user_index} has been SLASHED"); Ok(()) } } else { @@ -285,7 +284,7 @@ fn main() -> Result<()> { fn show_commands() { println!("Available commands:"); println!(" list - List registered users"); - println!(" register - Register a new user index"); + println!(" register - Register a new user"); println!(" send -u -m -s - Send a message with proof"); println!(" (example: send -u 0 -m 0 -s \"hello\")"); println!(" clear - Clear the screen"); diff --git a/rln/README.md b/rln/README.md index 3ce1329e..75702665 100644 --- a/rln/README.md +++ b/rln/README.md @@ -331,7 +331,9 @@ Partial proof generation is an optimization technique that allows us to split th **Observed speedup:** Finishing a partial proof is roughly **2.5–3× faster** than generating a full proof from scratch, since the expensive Merkle proof contribution is pre-computed and reused across multiple messages. See the [preliminary benchmarks](https://github.com/logos-storage/rln-fast#benchmarks) in the [rln-fast](https://github.com/logos-storage/rln-fast) repository for details. -**When this optimization is less effective:** In environments where the membership set changes very frequently (e.g., a highly dynamic system with many joins/leaves), the cached data is invalidated often and the overhead of pre-computation may outweigh the benefit. +**Using cached partials across recent roots**. To reuse partial proofs while the tree changes, cache the Merkle path alongside the root used to build the partial proof and verify against a bounded set of recent roots (for example, the last few roots) via APIs like [`verify_with_roots`](./src/public.rs#L743). This keeps cached partials usable for short-lived historical roots while limiting replay risk; when a root falls out of the allowed window or a member is removed/slashed, rebuild the partial proof with the latest root and path so revoked members cannot keep proving with stale roots. + +**When this optimization is less effective:** In environments where the membership set changes very frequently, the cached data is invalidated often and the overhead of pre-computation may outweigh the benefit. ## Detailed Protocol Flow