Skip to content

Commit f9f671e

Browse files
committed
Add Semaphore
1 parent 12f9576 commit f9f671e

File tree

14 files changed

+1393
-5
lines changed

14 files changed

+1393
-5
lines changed

crates/examples/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ base64.workspace = true
2424
blake2.workspace = true
2525
jwt-simple.workspace = true
2626
sha2.workspace = true
27+
hex = "0.4"
28+
k256 = { workspace = true, features = ["arithmetic"] }
2729

2830
[dev-dependencies]
2931
criterion.workspace = true
@@ -52,3 +54,7 @@ path = "examples/prover.rs"
5254
[[example]]
5355
name = "verifier"
5456
path = "examples/verifier.rs"
57+
58+
[[example]]
59+
name = "padding_validation_test"
60+
path = "examples/padding_validation_test.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_ecdsa::SemaphoreExample};
3+
4+
fn main() -> Result<()> {
5+
let _tracing_guard = tracing_profile::init_tracing()?;
6+
7+
Cli::<SemaphoreExample>::new("semaphore_ecdsa")
8+
.about("Anonymous group membership proofs with nullifiers using ECDSA key derivation")
9+
.run()
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
semaphore_ecdsa circuit
2+
--
3+
Number of gates: 475686
4+
Number of evaluation instructions: 563535
5+
Number of AND constraints: 609289
6+
Number of MUL constraints: 48536
7+
Length of value vec: 1048576
8+
Constants: 72
9+
Inout: 11
10+
Witness: 97
11+
Internal: 887101
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod blake2s;
22
pub mod ethsign;
33
pub mod keccak;
4+
pub mod semaphore_ecdsa;
45
pub mod sha256;
56
pub mod sha512;
67
pub mod zklogin;
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use anyhow::{Result, ensure};
2+
use binius_frontend::{
3+
circuits::semaphore_ecdsa::{
4+
SemaphoreProofECDSA,
5+
circuit::IdentityECDSA,
6+
MerkleTree,
7+
},
8+
compiler::{CircuitBuilder, circuit::WitnessFiller},
9+
};
10+
use clap::Args;
11+
12+
use crate::ExampleCircuit;
13+
14+
/// Semaphore anonymous group membership proof with ECDSA key derivation
15+
pub struct SemaphoreExample {
16+
circuit: SemaphoreProofECDSA,
17+
tree_height: usize,
18+
message_len_bytes: usize,
19+
scope_len_bytes: usize,
20+
}
21+
22+
#[derive(Args, Debug, Clone)]
23+
pub struct Params {
24+
/// Height of the Merkle tree (determines max group size = 2^height)
25+
#[arg(long, default_value_t = 2)]
26+
pub tree_height: usize,
27+
28+
/// Maximum message length in bytes
29+
#[arg(long, default_value_t = 32)]
30+
pub message_len_bytes: usize,
31+
32+
/// Maximum scope length in bytes
33+
#[arg(long, default_value_t = 24)]
34+
pub scope_len_bytes: usize,
35+
}
36+
37+
#[derive(Args, Debug, Clone)]
38+
pub struct Instance {
39+
/// Number of group members to create
40+
#[arg(long, default_value_t = 4)]
41+
pub group_size: usize,
42+
43+
/// Index of the member generating the proof (0-based)
44+
#[arg(long, default_value_t = 1)]
45+
pub prover_index: usize,
46+
47+
/// Message to include in the proof
48+
#[arg(long, default_value = "I vote YES on proposal #42")]
49+
pub message: String,
50+
51+
/// Scope for this signal (prevents double-signaling within scope)
52+
#[arg(long, default_value = "dao_vote_2024_q1")]
53+
pub scope: String,
54+
}
55+
56+
impl ExampleCircuit for SemaphoreExample {
57+
type Params = Params;
58+
type Instance = Instance;
59+
60+
fn build(params: Params, builder: &mut CircuitBuilder) -> Result<Self> {
61+
ensure!(params.tree_height > 0, "Tree height must be > 0");
62+
ensure!(params.message_len_bytes > 0, "Message length must be > 0");
63+
ensure!(params.scope_len_bytes > 0, "Scope length must be > 0");
64+
65+
let circuit = SemaphoreProofECDSA::new(
66+
builder,
67+
params.tree_height,
68+
params.message_len_bytes,
69+
params.scope_len_bytes,
70+
);
71+
72+
Ok(Self {
73+
circuit,
74+
tree_height: params.tree_height,
75+
message_len_bytes: params.message_len_bytes,
76+
scope_len_bytes: params.scope_len_bytes,
77+
})
78+
}
79+
80+
fn populate_witness(&self, instance: Instance, witness: &mut WitnessFiller) -> Result<()> {
81+
// Validate inputs
82+
ensure!(instance.group_size > 0, "Group size must be > 0");
83+
ensure!(instance.prover_index < instance.group_size, "Prover index must be < group size");
84+
ensure!(instance.group_size <= (1 << self.tree_height), "Group size exceeds tree capacity");
85+
ensure!(instance.message.len() <= self.message_len_bytes, "Message too long");
86+
ensure!(instance.scope.len() <= self.scope_len_bytes, "Scope too long");
87+
88+
// Create ECDSA identities
89+
let mut identities = Vec::new();
90+
for i in 0..instance.group_size {
91+
let secret_scalar = [((i + 42) as u8); 32];
92+
identities.push(IdentityECDSA::new(secret_scalar));
93+
}
94+
95+
// Build Merkle tree
96+
let mut tree = MerkleTree::new(self.tree_height);
97+
for identity in &identities {
98+
tree.add_leaf(identity.commitment());
99+
}
100+
101+
// Get proof for the prover
102+
let prover_identity = &identities[instance.prover_index];
103+
let merkle_proof = tree.proof(instance.prover_index);
104+
105+
// Populate witness
106+
self.circuit.populate_witness(
107+
witness,
108+
prover_identity,
109+
&merkle_proof,
110+
instance.message.as_bytes(),
111+
instance.scope.as_bytes(),
112+
);
113+
114+
Ok(())
115+
}
116+
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
use binius_frontend::circuits::semaphore_ecdsa::circuit::IdentityECDSA;
122+
123+
#[test]
124+
fn test_examples_crate_identity_commitment() {
125+
// Test the same computation from the examples crate
126+
let secret_scalar = [0x2b; 32];
127+
println!("Examples crate - Testing secret_scalar: {:02x?}", secret_scalar);
128+
129+
let identity = IdentityECDSA::new(secret_scalar);
130+
let commitment = identity.commitment();
131+
println!("Examples crate - Direct commitment result: {:02x?}", commitment);
132+
133+
// Let's see if this matches what the frontend crate produces
134+
}
135+
}

crates/frontend/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ serde_json.workspace = true
1919
sha2.workspace = true
2020
sha3.workspace = true
2121
smallvec.workspace = true
22+
k256 = { workspace = true, features = ["arithmetic"] }
2223

2324
[dev-dependencies]
2425
base64 = { workspace = true }

crates/frontend/src/circuits/ecdsa/scalar_mul.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ mod tests {
345345
use binius_core::word::Word;
346346
use k256::{
347347
ProjectivePoint, Scalar, U256,
348-
elliptic_curve::{ops::MulByGenerator, scalar::FromUintUnchecked, sec1::ToEncodedPoint},
348+
elliptic_curve::{ops::MulByGenerator, scalar::FromUintUnchecked, sec1::ToEncodedPoint, PrimeField},
349349
};
350350
use rand::prelude::*;
351351

@@ -403,6 +403,48 @@ mod tests {
403403
assert_eq!(w[result.is_point_at_infinity], Word::ZERO);
404404
}
405405

406+
#[test]
407+
fn test_scalar_mul_naive_256bit() {
408+
// Test with full 256-bit scalar to ensure production compatibility
409+
let builder = CircuitBuilder::new();
410+
let curve = Secp256k1::new(&builder);
411+
412+
// Use a 256-bit test scalar
413+
let scalar_bytes = [0x2b; 32];
414+
let scalar_value = num_bigint::BigUint::from_bytes_le(&scalar_bytes);
415+
416+
// Use k256 to compute expected result
417+
let mut scalar_be = scalar_bytes;
418+
scalar_be.reverse();
419+
let k256_scalar = Scalar::from_repr(scalar_be.into()).unwrap();
420+
let k256_point = ProjectivePoint::mul_by_generator(&k256_scalar).to_affine();
421+
422+
// Extract expected coordinates
423+
let point_bytes = k256_point.to_encoded_point(false).to_bytes();
424+
let x_coord = num_bigint::BigUint::from_bytes_be(&point_bytes[1..33]);
425+
let y_coord = num_bigint::BigUint::from_bytes_be(&point_bytes[33..65]);
426+
427+
// Create circuit scalar
428+
let scalar = BigUint::new_constant(&builder, &scalar_value);
429+
let expected_x = BigUint::new_constant(&builder, &x_coord);
430+
let expected_y = BigUint::new_constant(&builder, &y_coord);
431+
432+
let generator = Secp256k1Affine::generator(&builder);
433+
434+
// Test with full 256 bits
435+
let result = scalar_mul_naive(&builder, &curve, 256, &scalar, generator);
436+
437+
// Verify result matches k256
438+
assert_eq(&builder, "result_x_256bit", &result.x, &expected_x);
439+
assert_eq(&builder, "result_y_256bit", &result.y, &expected_y);
440+
441+
// Build and verify circuit
442+
let cs = builder.build();
443+
let mut w = cs.new_witness_filler();
444+
assert!(cs.populate_wire_witness(&mut w).is_ok());
445+
assert_eq!(w[result.is_point_at_infinity], Word::ZERO);
446+
}
447+
406448
#[test]
407449
fn test_scalar_mul_with_endomorphism() {
408450
let builder = CircuitBuilder::new();

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod popcount;
1717
pub mod ripemd;
1818
pub mod rs256;
1919
pub mod secp256k1;
20+
pub mod semaphore_ecdsa;
2021
pub mod sha256;
2122
pub mod sha512;
2223
pub mod skein512;

0 commit comments

Comments
 (0)