Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ version = "0.1.0"
edition = "2024"
rust-version = "1.87"

[workspace]
members = ["crates/leansig-wasm"]
default-members = ["."]

[lints.clippy]
# all lints that are on by default (correctness, suspicious, style, complexity, perf)
all = { level = "warn", priority = -1 }
Expand Down Expand Up @@ -33,7 +37,7 @@ upper_case_acronyms = "allow"
rand = "0.10"
sha3 = "0.10.8"
num-bigint = "0.4.6"
rayon = "1.10.0"
rayon = { version = "1.10.0", optional = true }
num-traits = "0.2.19"
dashmap = "6.1.0"
serde = { version = "1.0", features = ["derive", "alloc"] }
Expand All @@ -46,12 +50,17 @@ p3-baby-bear = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b4dcde46
p3-koala-bear = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b4dcde46" }
p3-symmetric = { git = "https://github.com/Plonky3/Plonky3.git", rev = "b4dcde46" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.4.2", features = ["wasm_js"] }

[dev-dependencies]
criterion = "0.7"
proptest = "1.7"
bincode = { version = "2.0.1", features = ["serde"] }

[features]
default = ["parallel-rayon"]
parallel-rayon = ["dep:rayon"]
slow-tests = []
with-gen-benches-sha = []
with-gen-benches-poseidon = []
Expand All @@ -60,6 +69,10 @@ with-gen-benches-poseidon = []
name = "benchmark"
harness = false

[[bench]]
name = "demo_perf"
harness = false

[profile.profiling]
inherits = "release"
debug = true
debug = true
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,44 @@ Confidence intervals can also be shown via
python3 benchmark-mean.py target --intervals
```

For a focused comparison of the portable demo scheme with and without `rayon`, use

```bash
./scripts/collect_demo_perf.sh
```

## Web And Extension PoCs

The repo now includes:

- a minimal MV3 Chrome extension proof of concept in `examples/chrome-extension-poc/`
- a `wasm-bindgen` wrapper crate in `crates/leansig-wasm/`

The WASM wrapper exposes a small wallet-like API for:

- key generation
- secret/public key serialization
- signing a 32-byte message for a chosen epoch
- detached verification from only public key bytes, epoch, message bytes, and signature bytes
- prepared-interval inspection and manual preparation advance

The Chrome extension can be built in two modes:

```bash
# Fast interactive demo parameters: lifetime 2^10, w=2.
./examples/chrome-extension-poc/build.sh

# Larger benchmark-oriented parameters: lifetime 2^18, w=2.
./examples/chrome-extension-poc/build.sh production
```

After building, open `chrome://extensions`, enable Developer mode, click `Load unpacked`,
and select `examples/chrome-extension-poc/`. The extension page has buttons to generate a
keypair, sign, verify, advance preparation, and run an in-Chrome performance sample.

The extension packaging flow is documented in
[examples/chrome-extension-poc/README.md](examples/chrome-extension-poc/README.md).

## License

Apache Version 2.0.
76 changes: 76 additions & 0 deletions benches/benchmark_poseidon.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::hint::black_box;
use std::time::Instant;

use criterion::{Criterion, SamplingMode};
use rand::RngExt;
Expand All @@ -8,6 +9,9 @@ use leansig::{
signature::{
SignatureScheme, SignatureSchemeSecretKey,
generalized_xmss::instantiations_poseidon::{
lifetime_2_to_the_10::target_sum::{
SIGTargetSumLifetime10W2NoOff, SIGTargetSumLifetime10W2Off10,
},
lifetime_2_to_the_18::target_sum::{
SIGTargetSumLifetime18W1NoOff, SIGTargetSumLifetime18W1Off10,
SIGTargetSumLifetime18W2NoOff, SIGTargetSumLifetime18W2Off10,
Expand Down Expand Up @@ -96,6 +100,75 @@ pub fn benchmark_signature_scheme<S: SignatureScheme>(c: &mut Criterion, descrip
group.finish();
}

/// Benchmark the cost of shifting the secret key's prepared interval forward.
///
/// This benchmark intentionally excludes key generation from the timed region by
/// pre-generating as many keys as Criterion asks us to consume.
pub fn benchmark_preparation_scheme<S: SignatureScheme>(c: &mut Criterion, description: &str) {
let mut group = c.benchmark_group(format!("Poseidon preparation: {description}"));
group.sampling_mode(SamplingMode::Flat);
group.sample_size(30);

let log_lifetime = S::LIFETIME.ilog2() as usize;
let max_advances_per_key = (1usize << (log_lifetime / 2)).saturating_sub(2);
assert!(
max_advances_per_key > 0,
"advance_preparation benchmark requires at least one possible advance"
);

group.bench_function("- advance_preparation", |b| {
b.iter_custom(|iters| {
let mut rng = rand::rng();
let keys_needed = (iters as usize).div_ceil(max_advances_per_key);
let mut secret_keys: Vec<S::SecretKey> = (0..keys_needed)
.map(|_| S::key_gen(&mut rng, 0, S::LIFETIME as usize).1)
.collect();

let start = Instant::now();
let mut remaining = iters as usize;

for secret_key in &mut secret_keys {
if remaining == 0 {
break;
}

let advances_for_key = remaining.min(max_advances_per_key);
for _ in 0..advances_for_key {
secret_key.advance_preparation();
}

black_box(secret_key.get_prepared_interval());
remaining -= advances_for_key;
}

start.elapsed()
});
});

group.finish();
}

/// Benchmarking demo-friendly Lifetime 2^10 for Target Sum Encoding
fn bench_lifetime10_target_sum(c: &mut Criterion) {
benchmark_signature_scheme::<SIGTargetSumLifetime10W2NoOff>(
c,
"Target Sum, Lifetime 2^10, w = 2, no offset",
);
benchmark_signature_scheme::<SIGTargetSumLifetime10W2Off10>(
c,
"Target Sum, Lifetime 2^10, w = 2, 10% offset",
);

benchmark_preparation_scheme::<SIGTargetSumLifetime10W2NoOff>(
c,
"Target Sum, Lifetime 2^10, w = 2, no offset",
);
benchmark_preparation_scheme::<SIGTargetSumLifetime10W2Off10>(
c,
"Target Sum, Lifetime 2^10, w = 2, 10% offset",
);
}

/// Benchmarking Lifetime 2^18 for Target Sum Encoding
fn bench_lifetime18_target_sum(c: &mut Criterion) {
benchmark_signature_scheme::<SIGTargetSumLifetime18W1NoOff>(
Expand Down Expand Up @@ -175,6 +248,9 @@ fn bench_lifetime20_target_sum(c: &mut Criterion) {
}

pub fn bench_function_poseidon(c: &mut Criterion) {
// benchmarking demo lifetime 2^10 - Target Sum
bench_lifetime10_target_sum(c);

// benchmarking lifetime 2^18 - Target Sum
bench_lifetime18_target_sum(c);

Expand Down
123 changes: 123 additions & 0 deletions benches/demo_perf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use std::hint::black_box;
use std::time::Instant;

use criterion::{BenchmarkId, Criterion, SamplingMode, criterion_group, criterion_main};
use rand::RngExt;

use leansig::{
MESSAGE_LENGTH,
signature::{
SignatureScheme, SignatureSchemeSecretKey,
generalized_xmss::instantiations_poseidon::lifetime_2_to_the_10::target_sum::SIGTargetSumLifetime10W2NoOff,
},
};

type DemoScheme = SIGTargetSumLifetime10W2NoOff;

fn bench_demo_scheme(c: &mut Criterion) {
let mut group = c.benchmark_group("demo/perf");
group.sampling_mode(SamplingMode::Flat);

let mut rng = rand::rng();

group.sample_size(10);
group.bench_function(BenchmarkId::new("keygen", "lifetime_2^10_w2"), |b| {
b.iter(|| {
let _ = DemoScheme::key_gen(black_box(&mut rng), 0, DemoScheme::LIFETIME as usize);
});
});

let (public_key, secret_key) = DemoScheme::key_gen(&mut rng, 0, DemoScheme::LIFETIME as usize);
let prepared_interval = secret_key.get_prepared_interval();
let message: [u8; MESSAGE_LENGTH] = rng.random();

group.sample_size(60);
group.bench_function(BenchmarkId::new("sign", "lifetime_2^10_w2"), |b| {
let mut next_epoch = prepared_interval.start as u32;
b.iter(|| {
let epoch = next_epoch;
next_epoch += 1;
if next_epoch >= prepared_interval.end as u32 {
next_epoch = prepared_interval.start as u32;
}

let _ = DemoScheme::sign(
black_box(&secret_key),
black_box(epoch),
black_box(&message),
)
.expect("signing should succeed for prepared demo epochs");
});
});

let signatures: Vec<(
u32,
[u8; MESSAGE_LENGTH],
<DemoScheme as SignatureScheme>::Signature,
)> = (0..64)
.map(|offset| {
let epoch = prepared_interval.start as u32 + offset;
let signature =
DemoScheme::sign(&secret_key, epoch, &message).expect("precomputing signature");
(epoch, message, signature)
})
.collect();

group.bench_function(BenchmarkId::new("verify", "lifetime_2^10_w2"), |b| {
let mut index = 0usize;
b.iter(|| {
let (epoch, benchmark_message, signature) = &signatures[index];
index = (index + 1) % signatures.len();

let _ = DemoScheme::verify(
black_box(&public_key),
*epoch,
black_box(benchmark_message),
black_box(signature),
);
});
});

group.sample_size(30);
let log_lifetime = DemoScheme::LIFETIME.ilog2() as usize;
let max_advances_per_key = (1usize << (log_lifetime / 2)).saturating_sub(2);

group.bench_function(
BenchmarkId::new("advance_preparation", "lifetime_2^10_w2"),
|b| {
b.iter_custom(|iters| {
let keys_needed = (iters as usize).div_ceil(max_advances_per_key);
let mut benchmark_rng = rand::rng();
let mut secret_keys: Vec<_> = (0..keys_needed)
.map(|_| {
DemoScheme::key_gen(&mut benchmark_rng, 0, DemoScheme::LIFETIME as usize).1
})
.collect();

let start = Instant::now();
let mut remaining = iters as usize;

for secret_key in &mut secret_keys {
if remaining == 0 {
break;
}

let advances_for_key = remaining.min(max_advances_per_key);
for _ in 0..advances_for_key {
secret_key.advance_preparation();
}

black_box(secret_key.get_prepared_interval());
remaining -= advances_for_key;
}

start.elapsed()
});
},
);

group.finish();
}

criterion_group!(demo_perf, bench_demo_scheme);
criterion_main!(demo_perf);
20 changes: 20 additions & 0 deletions crates/leansig-wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "leansig-wasm"
version = "0.1.0"
edition = "2024"
rust-version = "1.87"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
leansig = { path = "../..", default-features = false }
rand = "0.10"
wasm-bindgen = "0.2"

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.4.2", features = ["wasm_js"] }

[features]
default = []
production-lifetime-18-w2 = []
Loading