Skip to content

Commit abebab3

Browse files
committed
Add Semaphore
1 parent 43c8daf commit abebab3

File tree

15 files changed

+2261
-4
lines changed

15 files changed

+2261
-4
lines changed

.claude/settings.local.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Read(/Users/jad_irred/Downloads/**)",
5+
"Bash(cargo build:*)",
6+
"Bash(cargo test:*)",
7+
"Bash(mkdir:*)",
8+
"Bash(grep:*)",
9+
"Bash(RUST_BACKTRACE=1 cargo test -p binius-frontend test_semaphore_keccak_single_member --release)",
10+
"Bash(cargo run:*)"
11+
],
12+
"deny": [],
13+
"ask": []
14+
}
15+
}

crates/examples/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ rand.workspace = true
2323
base64.workspace = true
2424
jwt-simple.workspace = true
2525
sha2.workspace = true
26+
hex = "0.4"
2627

2728
[dev-dependencies]
2829
criterion.workspace = true
@@ -47,3 +48,7 @@ path = "examples/prover.rs"
4748
[[example]]
4849
name = "verifier"
4950
path = "examples/verifier.rs"
51+
52+
[[example]]
53+
name = "semaphore"
54+
path = "examples/semaphore.rs"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use anyhow::Result;
2+
use binius_examples::{Cli, circuits::semaphore::SemaphoreExample};
3+
4+
fn main() -> Result<()> {
5+
let _tracing_guard = tracing_profile::init_tracing()?;
6+
7+
Cli::<SemaphoreExample>::new("semaphore")
8+
.about("Anonymous group membership proofs with nullifiers - supports both Keccak and ECDSA variants")
9+
.run()
10+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod ethsign;
22
pub mod keccak;
3+
pub mod semaphore;
34
pub mod sha256;
45
pub mod sha512;
56
pub mod zklogin;
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
use anyhow::{Result, ensure};
2+
use binius_frontend::{
3+
circuits::{
4+
semaphore::{Identity, MerkleTree, SemaphoreProofKeccak},
5+
semaphore_ecdsa::{SemaphoreProofECDSA, circuit::IdentityECDSA},
6+
},
7+
compiler::{CircuitBuilder, circuit::WitnessFiller},
8+
};
9+
use clap::Args;
10+
11+
use crate::ExampleCircuit;
12+
13+
/// Semaphore anonymous group membership proof example
14+
pub struct SemaphoreExample {
15+
circuit_type: CircuitType,
16+
tree_height: usize,
17+
message_len_bytes: usize,
18+
scope_len_bytes: usize,
19+
}
20+
21+
enum CircuitType {
22+
Keccak(SemaphoreProofKeccak),
23+
ECDSA(SemaphoreProofECDSA),
24+
}
25+
26+
#[derive(Args, Debug, Clone)]
27+
pub struct Params {
28+
/// Use ECDSA key derivation (Ethereum-compatible) instead of basic Keccak
29+
#[arg(long)]
30+
pub use_ecdsa: bool,
31+
32+
/// Height of the Merkle tree (determines max group size = 2^height)
33+
#[arg(long, default_value_t = 2)]
34+
pub tree_height: usize,
35+
36+
/// Maximum message length in bytes
37+
#[arg(long, default_value_t = 32)]
38+
pub message_len_bytes: usize,
39+
40+
/// Maximum scope length in bytes
41+
#[arg(long, default_value_t = 24)]
42+
pub scope_len_bytes: usize,
43+
}
44+
45+
#[derive(Args, Debug, Clone)]
46+
pub struct Instance {
47+
/// Number of group members to create
48+
#[arg(long, default_value_t = 4)]
49+
pub group_size: usize,
50+
51+
/// Index of the member generating the proof (0-based)
52+
#[arg(long, default_value_t = 1)]
53+
pub prover_index: usize,
54+
55+
/// Message to include in the proof
56+
#[arg(long, default_value = "I vote YES on proposal #42")]
57+
pub message: String,
58+
59+
/// Scope for this signal (prevents double-signaling within scope)
60+
#[arg(long, default_value = "dao_vote_2024_q1")]
61+
pub scope: String,
62+
63+
}
64+
65+
impl ExampleCircuit for SemaphoreExample {
66+
type Params = Params;
67+
type Instance = Instance;
68+
69+
fn build(params: Params, builder: &mut CircuitBuilder) -> Result<Self> {
70+
ensure!(params.tree_height > 0, "Tree height must be > 0");
71+
ensure!(params.message_len_bytes > 0, "Message length must be > 0");
72+
ensure!(params.scope_len_bytes > 0, "Scope length must be > 0");
73+
74+
let circuit_type = if params.use_ecdsa {
75+
CircuitType::ECDSA(SemaphoreProofECDSA::new(
76+
builder,
77+
params.tree_height,
78+
params.message_len_bytes,
79+
params.scope_len_bytes,
80+
))
81+
} else {
82+
CircuitType::Keccak(SemaphoreProofKeccak::new(
83+
builder,
84+
params.tree_height,
85+
params.message_len_bytes,
86+
params.scope_len_bytes,
87+
))
88+
};
89+
90+
Ok(Self {
91+
circuit_type,
92+
tree_height: params.tree_height,
93+
message_len_bytes: params.message_len_bytes,
94+
scope_len_bytes: params.scope_len_bytes,
95+
})
96+
}
97+
98+
fn populate_witness(&self, instance: Instance, witness: &mut WitnessFiller) -> Result<()> {
99+
100+
// Validate inputs
101+
ensure!(instance.group_size > 0, "Group size must be > 0");
102+
ensure!(instance.prover_index < instance.group_size, "Prover index must be < group size");
103+
ensure!(instance.group_size <= (1 << self.tree_height), "Group size exceeds tree capacity");
104+
ensure!(instance.message.len() <= self.message_len_bytes, "Message too long");
105+
ensure!(instance.scope.len() <= self.scope_len_bytes, "Scope too long");
106+
107+
// Let the standard tracing system handle all output like other examples
108+
109+
match &self.circuit_type {
110+
CircuitType::Keccak(circuit) => {
111+
self.populate_keccak_witness(circuit, &instance, witness)
112+
}
113+
CircuitType::ECDSA(circuit) => {
114+
self.populate_ecdsa_witness(circuit, &instance, witness)
115+
}
116+
}?;
117+
118+
// Tracing system handles timing automatically
119+
120+
Ok(())
121+
}
122+
}
123+
124+
impl SemaphoreExample {
125+
fn populate_keccak_witness(
126+
&self,
127+
circuit: &SemaphoreProofKeccak,
128+
instance: &Instance,
129+
witness: &mut WitnessFiller
130+
) -> Result<()> {
131+
// Create group members
132+
let mut identities = Vec::new();
133+
for i in 0..instance.group_size {
134+
let trapdoor = [((i + 1) as u8); 32];
135+
let nullifier = [((i + 100) as u8); 32];
136+
identities.push(Identity::new(trapdoor, nullifier));
137+
}
138+
139+
// Build Merkle tree
140+
let mut tree = MerkleTree::new(self.tree_height);
141+
for identity in &identities {
142+
tree.add_leaf(identity.commitment());
143+
}
144+
145+
// Get proof for the prover
146+
let prover_identity = &identities[instance.prover_index];
147+
let merkle_proof = tree.proof(instance.prover_index);
148+
149+
// Generate nullifier (computed but not displayed like other examples)
150+
151+
// Populate witness
152+
circuit.populate_witness(
153+
witness,
154+
prover_identity,
155+
&merkle_proof,
156+
instance.message.as_bytes(),
157+
instance.scope.as_bytes(),
158+
);
159+
160+
Ok(())
161+
}
162+
163+
fn populate_ecdsa_witness(
164+
&self,
165+
circuit: &SemaphoreProofECDSA,
166+
instance: &Instance,
167+
witness: &mut WitnessFiller
168+
) -> Result<()> {
169+
// Create ECDSA identities
170+
let mut identities = Vec::new();
171+
for i in 0..instance.group_size {
172+
let secret_scalar = [((i + 42) as u8); 32];
173+
identities.push(IdentityECDSA::new(secret_scalar));
174+
}
175+
176+
// Build Merkle tree
177+
let mut tree = MerkleTree::new(self.tree_height);
178+
for identity in &identities {
179+
tree.add_leaf(identity.commitment());
180+
}
181+
182+
// Get proof for the prover
183+
let prover_identity = &identities[instance.prover_index];
184+
let merkle_proof = tree.proof(instance.prover_index);
185+
186+
// Generate nullifier (computed but not displayed like other examples)
187+
188+
// Populate witness
189+
circuit.populate_witness(
190+
witness,
191+
prover_identity,
192+
&merkle_proof,
193+
instance.message.as_bytes(),
194+
instance.scope.as_bytes(),
195+
);
196+
197+
Ok(())
198+
}
199+
}
200+
201+
/// Helper function to show comparison between variants (used by CLI)
202+
pub fn show_comparison() -> Result<()> {
203+
println!("\n╔══════════════════════════════════════════════╗");
204+
println!("║ Variant Comparison ║");
205+
println!("╚══════════════════════════════════════════════╝\n");
206+
207+
// Build both circuits to get constraint counts
208+
let keccak_builder = CircuitBuilder::new();
209+
let _keccak_circuit = SemaphoreProofKeccak::new(&keccak_builder, 2, 32, 24);
210+
let keccak_compiled = keccak_builder.build();
211+
let keccak_cs = keccak_compiled.constraint_system();
212+
let keccak_total = keccak_cs.and_constraints.len() + keccak_cs.mul_constraints.len();
213+
214+
let ecdsa_builder = CircuitBuilder::new();
215+
let _ecdsa_circuit = SemaphoreProofECDSA::new(&ecdsa_builder, 2, 32, 24);
216+
let ecdsa_compiled = ecdsa_builder.build();
217+
let ecdsa_cs = ecdsa_compiled.constraint_system();
218+
let ecdsa_total = ecdsa_cs.and_constraints.len() + ecdsa_cs.mul_constraints.len();
219+
220+
println!("┌─────────────────────┬────────────┬────────────┬─────────────────────┐");
221+
println!("│ Version │ Constraints│ AND │ MUL │");
222+
println!("├─────────────────────┼────────────┼────────────┼─────────────────────┤");
223+
println!("│ Keccak-only │ {:>10} │ {:>10} │ {:>19} │",
224+
keccak_total, keccak_cs.and_constraints.len(), keccak_cs.mul_constraints.len());
225+
println!("│ ECDSA + Keccak │ {:>10} │ {:>10} │ {:>19} │",
226+
ecdsa_total, ecdsa_cs.and_constraints.len(), ecdsa_cs.mul_constraints.len());
227+
println!("│ Future Poseidon │ ~5000 │ ~4999 │ 1 │");
228+
println!("└─────────────────────┴────────────┴────────────┴─────────────────────┘");
229+
230+
println!("\nUse Cases:");
231+
println!("• Keccak-only: Efficient anonymous voting, private group membership");
232+
println!("• ECDSA+Keccak: Ethereum wallet integration, existing key infrastructure");
233+
println!("• Poseidon (future): On-chain verification, minimal proof sizes");
234+
235+
println!("\nKey Features:");
236+
println!("✓ Anonymous group membership proofs");
237+
println!("✓ Nullifiers prevent double-signaling within scope");
238+
println!("✓ Different scopes allow separate voting contexts");
239+
println!("✓ Zero-knowledge: proves membership without revealing identity");
240+
241+
Ok(())
242+
}

crates/frontend/src/circuits/keccak/mod.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pub const N_WORDS_PER_STATE: usize = 25;
1414
pub const RATE_BYTES: usize = 136;
1515
pub const N_WORDS_PER_BLOCK: usize = RATE_BYTES / 8;
1616

17+
// Semantic constants for hash output sizes
18+
pub const KECCAK256_OUTPUT_BYTES: usize = N_WORDS_PER_DIGEST * 8;
19+
1720
/// Keccak-256 circuit that can handle variable-length inputs up to a specified maximum length.
1821
///
1922
/// # Arguments
@@ -49,6 +52,10 @@ impl Keccak {
4952
message: Vec<Wire>,
5053
) -> Self {
5154
let max_len_bytes = message.len() << 3;
55+
56+
assert!(!message.is_empty(), "Keccak message wires cannot be empty");
57+
assert!(max_len_bytes > 0, "Keccak max message length must be > 0, got {} bytes from {} wires", max_len_bytes, message.len());
58+
5259
// number of blocks needed for the maximum sized message
5360
let n_blocks = (max_len_bytes + 1).div_ceil(RATE_BYTES);
5461

@@ -236,11 +243,11 @@ impl Keccak {
236243
/// * w - The witness filler to populate
237244
/// * message_bytes - The input message as a byte slice
238245
pub fn populate_message(&self, w: &mut WitnessFiller<'_>, message_bytes: &[u8]) {
246+
let max_capacity = self.max_len_bytes();
239247
assert!(
240-
message_bytes.len() <= self.max_len_bytes(),
241-
"Message length {} exceeds maximum {}",
242-
message_bytes.len(),
243-
self.max_len_bytes()
248+
message_bytes.len() <= max_capacity,
249+
"Message length {} exceeds maximum capacity {} (allocated {} wires × 8 = {} bytes)",
250+
message_bytes.len(), max_capacity, self.message.len(), self.message.len() * 8
244251
);
245252

246253
// populate message words from input bytes

crates/frontend/src/circuits/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub mod multiplexer;
1515
pub mod popcount;
1616
pub mod rs256;
1717
pub mod secp256k1;
18+
pub mod semaphore;
19+
pub mod semaphore_ecdsa;
1820
pub mod sha256;
1921
pub mod sha512;
2022
pub mod skein512;

0 commit comments

Comments
 (0)