Skip to content

Commit 1da50e3

Browse files
feat(examples): simple schnorr and or composition (#24)
1 parent 40c130c commit 1da50e3

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ the macro expansion happens), they need an extra step to be enabled.
5959
`lib.rs` or `main.rs`, to enable Rust's nightly-only benchmark
6060
feature.
6161

62+
## More information
63+
64+
We include runnable examples to demonstrate how to use the `sigma-rs` toolkit in [examples/](https://github.com/mmaker/sigma-rs/tree/main/examples).
6265

6366
## Funding
6467

examples/schnorr.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! Example: Schnorr proof of knowledge.
2+
//!
3+
//! This example demonstrates how to prove knowledge of a discrete logarithm using `sigma-rs`.
4+
//! A common use case is authentication: proving you know a secret key without revealing it.
5+
//!
6+
//! The prover convinces a verifier that it knows a secret $x$ such that: $$P = x \cdot G$$
7+
//!
8+
//! where $G$ is a generator of a prime-order group $\mathbb{G}$ and $P$ is a public group element.
9+
//!
10+
//! ---
11+
//!
12+
//! In `sigma-rs`, this is achieved using three core abstractions:
13+
//!
14+
//! 1. [`LinearRelation`] — describes the *morphism* (algebraic relation) between secret scalars and group elements.
15+
//! This forms the mathematical statement of the protocol. In our case, $P = x \cdot G$.
16+
//!
17+
//! 2. [`SchnorrProtocol`] — defines the interactive Sigma protocol for the given morphism.
18+
//! This handles the commit-challenge-response structure of the protocol, following the standard Sigma protocol flow:
19+
//! - P → V: commit $K = r·G$
20+
//! - V → P: challenge $c$
21+
//! - P → V: response $s = r + c·x$
22+
//!
23+
//! 3. [`NISigmaProtocol`] — wraps the interactive protocol using the Fiat-Shamir transformation,
24+
//! converting it to a *non-interactive zero-knowledge proof* (NIZK) by deriving the challenge $c$
25+
//! from a transcript hash using a [`Codec`] (here, [`ShakeCodec`]).
26+
//!
27+
//! The codec ensures domain separation and deterministic Fiat-Shamir challenges,
28+
//! yielding secure, standalone proofs.
29+
//!
30+
//! ---
31+
//!
32+
//! This example uses the Ristretto group from `curve25519-dalek`, which provides a prime-order group
33+
//! suitable for secure zero-knowledge protocols.
34+
use curve25519_dalek::RistrettoPoint;
35+
use curve25519_dalek::scalar::Scalar;
36+
use group::{Group, GroupEncoding};
37+
use rand::rngs::OsRng;
38+
39+
use sigma_rs::codec::ShakeCodec;
40+
use sigma_rs::fiat_shamir::NISigmaProtocol;
41+
use sigma_rs::linear_relation::LinearRelation;
42+
use sigma_rs::schnorr_protocol::SchnorrProtocol;
43+
44+
/// Construct the relation `P = x·G` and return it along with the witness `x`:
45+
/// - `x` is an element from a group of prime order $p$, typically $\mathbb{Z}_p$,
46+
/// - `P`, `G` are elements over a group $\mathbb{G}$ of order $p$.
47+
#[allow(non_snake_case)]
48+
pub fn discrete_logarithm<G: Group + GroupEncoding>(
49+
x: G::Scalar,
50+
) -> (LinearRelation<G>, Vec<G::Scalar>) {
51+
let mut morphism: LinearRelation<G> = LinearRelation::new();
52+
53+
// First, we allocate symbolic variables for our relation.
54+
// These represent the structure of our proof, not concrete values yet.
55+
56+
// Allocate a variable for the scalar witness `x`
57+
let var_x = morphism.allocate_scalar();
58+
59+
// Allocate a variable for the group element `G` (the generator)
60+
let var_G = morphism.allocate_element();
61+
62+
// Now that all variables are defined, we describe the relation we want to prove.
63+
// The variable `P` is allocated now, as it is the result of the computation to be proven.
64+
65+
// Create the constraint `P = x * G`
66+
let var_P = morphism.allocate_eq(var_x * var_G);
67+
68+
// We now assign real values to the variables we have defined for our relation above.
69+
// Since the witness is provided to the function, we only need to assign the group points.
70+
71+
// Assign the group generator to the corresponding variable `G`
72+
morphism.assign_element(var_G, G::generator());
73+
74+
// Assign the value of the image to the variable `P` (i.e., evaluate the group equation for `x`)
75+
morphism.compute_image(&[x]).unwrap();
76+
77+
// The relation has been defined and is ready to be used in a protocol.
78+
// Sanity check: ensure `P = x * G`
79+
let P = morphism.morphism.group_elements.get(var_P).unwrap();
80+
assert_eq!(P, G::generator() * x);
81+
82+
// Output the relation and the witness for the upcoming proof
83+
(morphism, vec![x])
84+
}
85+
86+
#[allow(non_snake_case)]
87+
fn main() {
88+
// Choose a secret scalar `x` (the witness).
89+
let x = Scalar::random(&mut OsRng);
90+
91+
// Construct a relation `P = x * G` and retrieve:
92+
// - The constraint system defined in the helper function above.
93+
// - The witness in vector format (useful for proofs with multiple witnesses).
94+
let (relation, witness) = discrete_logarithm(x);
95+
96+
// Build the Sigma protocol instance from the relation.
97+
let schnorr = SchnorrProtocol::<RistrettoPoint>::from(relation);
98+
99+
// Convert the Sigma protocol instance to a non-interactive protocol via Fiat-Shamir.
100+
// A domain separator is given as a byte-sequence to identify the current instance being proven.
101+
let nizk = NISigmaProtocol::<_, ShakeCodec<RistrettoPoint>>::new(b"schnorr-example", schnorr);
102+
103+
// Generate a non-interactive proof with the witness.
104+
// The non-interactive proof contains the 3 elements of a sigma protocol transcript: a commitment, a challenge, and a response.
105+
// This transcript can be transmitted by the prover to any verifier, without revealing any secret information about the prover.
106+
let (commitment, challenge, response) = nizk.prove(&witness, &mut OsRng).unwrap();
107+
108+
// Verification requires checking the proof against the transcript and constraint system.
109+
// The verifier can be convinced that the prover indeed knows `x` so that `P = x*G`. This doesn't require any interaction with the prover other than receiving the transcript.
110+
let verified = nizk.verify(&commitment, &challenge, &response).is_ok();
111+
println!("Schnorr NIZK proof verified: {verified}");
112+
}

examples/simple_composition.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//! Example: OR-proofs.
2+
//!
3+
//! This example demonstrates disjunctive proofs: proving you satisfy ONE of multiple conditions
4+
//! without revealing which. For instance, proving you own either address A or address B,
5+
//! without revealing which address is yours.
6+
//!
7+
//! The two statements are expressed over a group \mathbb{G} of prime order $p$, where the discrete logarithm problem is hard.
8+
//! In this group, the prover wishes to convince a verifier that it knows a secret scalar $x \in \mathbb{Z}_p$
9+
//! such that *at least one* of the following statements holds:
10+
//!
11+
//! 1. **Discrete Logarithm (DLog):**
12+
//!
13+
//! $$X_1 = x_1 \cdot G$$
14+
//!
15+
//! 2. **Discrete Log Equality (DLEQ):**
16+
//!
17+
//! $$X_2 = x_2 \cdot G \quad \text{and} \quad Y_2 = x_2 \cdot H$$
18+
//!
19+
//! For these statements, the elements $G, H, X_1, X_2, Y_2$ are all publicly available to any verifier.
20+
//!
21+
//! In our specific case, we will consider that the prover only knows a witness for the second statement. However,
22+
//! he will prove *the disjunction* of the two statements, using an OR-composition of Sigma protocols.
23+
//! This proof reveals nothing about *which* statement and witness has been used by the prover.
24+
//!
25+
//! ---
26+
//!
27+
//! In `sigma-rs`, this is implemented using three core abstractions:
28+
//!
29+
//! 1. [`LinearRelation`] — describes the *morphism* (algebraic relation) between secret scalars and group elements.
30+
//! This forms the mathematical statement of the protocol. In our case, $X_1 = x \cdot G \cup X_2 = x_2 \cdot G \quad \text{and} \quad Y_2 = x_2 \cdot H$
31+
//!
32+
//! 2. [`Protocol`] — defines the interactive Sigma protocol for the given morphism.
33+
//! This handles the commit-challenge-response structure of the protocol, following the standard Sigma protocol flow:
34+
//! - P → V: commit
35+
//! - V → P: challenge
36+
//! - P → V: response
37+
//!
38+
//! 3. [`NISigmaProtocol`] — wraps the interactive protocol using the Fiat-Shamir transformation,
39+
//! converting it to a *non-interactive zero-knowledge proof* (NIZK) by deriving the challenge
40+
//! from a transcript hash using a [`Codec`] (here, [`ShakeCodec`]).
41+
//!
42+
//! The resulting proof is non-interactive, zero-knowledge, and secure in the random oracle model.
43+
//!
44+
//! ---
45+
//!
46+
//! This example uses the Ristretto group from `curve25519-dalek`, a prime-order group designed for security and
47+
//! compatibility with zero-knowledge protocols.
48+
use curve25519_dalek::ristretto::RistrettoPoint;
49+
use curve25519_dalek::scalar::Scalar;
50+
use group::{Group, GroupEncoding};
51+
use rand::rngs::OsRng;
52+
use sigma_rs::{
53+
LinearRelation,
54+
codec::ShakeCodec,
55+
composition::{Protocol, ProtocolWitness},
56+
fiat_shamir::NISigmaProtocol,
57+
};
58+
59+
type G = RistrettoPoint;
60+
61+
/// Construct the relation `X = x·G` and return it along with the witness `x`.
62+
/// - `x` is a secret scalar in $\mathbb{Z}_p$.
63+
/// - `G`, `X` are elements in a prime-order group $\mathbb{G}$.
64+
#[allow(non_snake_case)]
65+
pub fn discrete_logarithm<G: Group + GroupEncoding>(
66+
x: G::Scalar,
67+
) -> (LinearRelation<G>, Vec<G::Scalar>) {
68+
let mut morphism = LinearRelation::<G>::new();
69+
70+
// Allocate symbolic variables for our relation
71+
let var_x = morphism.allocate_scalar();
72+
let var_G = morphism.allocate_element();
73+
74+
// Define the constraint: `X = x * G`
75+
let var_X = morphism.allocate_eq(var_x * var_G);
76+
77+
// Assign concrete values
78+
morphism.assign_element(var_G, G::generator());
79+
morphism.compute_image(&[x]).unwrap();
80+
81+
// Verify: X = x * G
82+
let X = morphism.morphism.group_elements.get(var_X).unwrap();
83+
assert_eq!(X, G::generator() * x);
84+
85+
(morphism, vec![x])
86+
}
87+
88+
/// Construct the relation `(X = x·G, Y = x·H)` and return it along with the witness `x`.
89+
/// This represents a DLEQ (discrete log equality) statement between two basepoints.
90+
/// - `x` is a secret scalar in $\mathbb{Z}_p$.
91+
/// - `G`, `H`, `X`, `Y` are elements in a prime-order group $\mathbb{G}$. `G` in particular is a fixed generator.
92+
#[allow(non_snake_case)]
93+
pub fn dleq<G: Group + GroupEncoding>(x: G::Scalar, H: G) -> (LinearRelation<G>, Vec<G::Scalar>) {
94+
let mut morphism = LinearRelation::<G>::new();
95+
96+
// Allocate symbolic variables
97+
let var_x = morphism.allocate_scalar();
98+
let [var_G, var_H] = morphism.allocate_elements();
99+
100+
// Define the constraints: X = x * G, Y = x * H
101+
let _var_X = morphism.allocate_eq(var_x * var_G);
102+
let _var_Y = morphism.allocate_eq(var_x * var_H);
103+
104+
// Assign concrete values
105+
morphism.assign_elements([(var_G, G::generator()), (var_H, H)]);
106+
morphism.compute_image(&[x]).unwrap();
107+
108+
// Verify: X = x * G and Y = x * H
109+
let X = morphism.morphism.group_elements.get(_var_X).unwrap();
110+
let Y = morphism.morphism.group_elements.get(_var_Y).unwrap();
111+
assert_eq!(X, G::generator() * x);
112+
assert_eq!(Y, H * x);
113+
114+
(morphism, vec![x])
115+
}
116+
117+
#[allow(non_snake_case)]
118+
fn main() {
119+
let mut rng = OsRng;
120+
121+
// Setup: Create two relations
122+
// Relation 1: DLog (we don't know a witness for this)
123+
let _x1 = Scalar::random(&mut rng);
124+
let (rel1, _) = discrete_logarithm::<G>(_x1);
125+
126+
// Relation 2: DLEQ (we DO know the witness)
127+
let x2 = Scalar::random(&mut rng);
128+
let H = G::random(&mut rng);
129+
let (rel2, witness2) = dleq::<G>(x2, H);
130+
131+
// Compose into OR protocol
132+
133+
// Wrap each relation into a Sigma protocol
134+
let proto1 = Protocol::from(rel1);
135+
let proto2 = Protocol::from(rel2);
136+
137+
// Compose both protocols using logical OR
138+
let composed = Protocol::Or(vec![proto1, proto2]);
139+
140+
// Declare the known witness for the second protocol (index = 1)
141+
let witness = ProtocolWitness::Or(1, vec![ProtocolWitness::Simple(witness2)]);
142+
143+
// Generate and verify proof
144+
// Make it non-interactive via Fiat-Shamir
145+
let nizk = NISigmaProtocol::<_, ShakeCodec<G>>::new(b"or_proof_example", composed);
146+
147+
// Generate proof (proving we know witness for statement 2, but not revealing which)
148+
let proof = nizk
149+
.prove_batchable(&witness, &mut rng)
150+
.expect("Proof generation should succeed");
151+
152+
// Verify the proof
153+
let verified = nizk.verify_batchable(&proof).is_ok();
154+
println!("OR-proof verified: {verified}");
155+
}

0 commit comments

Comments
 (0)