mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-29 18:39:30 +00:00
Sequencer aggregation
This commit is contained in:
parent
455e0a925e
commit
32813f606b
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4979,6 +4979,7 @@ dependencies = [
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"bridge_core",
|
||||
"bytemuck",
|
||||
"clock_core",
|
||||
"env_logger",
|
||||
"faucet_core",
|
||||
|
||||
BIN
artifacts/program_methods/aggregator_circuit.bin
Normal file
BIN
artifacts/program_methods/aggregator_circuit.bin
Normal file
Binary file not shown.
BIN
artifacts/program_methods/aggregator_circuit_strict.bin
Normal file
BIN
artifacts/program_methods/aggregator_circuit_strict.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/ppe_aggregation.bin
Normal file
BIN
artifacts/test_program_methods/ppe_aggregation.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
74
bench_aggregator_cuda.sh
Executable file
74
bench_aggregator_cuda.sh
Executable file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# Benchmarks the aggregator circuit (core and strict variants) with CUDA acceleration.
|
||||
#
|
||||
# Fixtures must be generated first:
|
||||
# cargo run --release -p ppe_test_data_gen -- --output ppe_fixtures.bin
|
||||
#
|
||||
# Usage:
|
||||
# ./bench_aggregator_cuda.sh
|
||||
#
|
||||
# Environment:
|
||||
# PPE_FIXTURES — path to fixture file (default: ppe_fixtures.bin)
|
||||
# COUNTS — space-separated list of transaction counts (default: "1 3 5")
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Point the build at CUDA 13.0 (required for Blackwell / compute_120).
|
||||
export NVCC=/usr/local/cuda-13.0/bin/nvcc
|
||||
export CUDA_HOME=/usr/local/cuda-13.0
|
||||
export PATH="/usr/local/cuda-13.0/bin:$PATH"
|
||||
|
||||
FIXTURES="$(realpath "${PPE_FIXTURES:-ppe_fixtures.bin}")"
|
||||
COUNTS="${COUNTS:-2 3 4 5 6 7 8 10 12 14 16}"
|
||||
SEGMENT_LIMIT_PO2="${PPE_SEGMENT_LIMIT_PO2-19}"
|
||||
|
||||
if [ ! -f "$FIXTURES" ]; then
|
||||
echo "ERROR: fixture file '$FIXTURES' not found."
|
||||
echo "Generate it first:"
|
||||
echo " cargo run --release -p ppe_test_data_gen -- --output $FIXTURES"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "\n%-6s %-8s %14s %20s\n" "n" "variant" "proving_ms" "proof_size_bytes"
|
||||
printf "%-6s %-8s %14s %20s\n" "------" "--------" "--------------" "--------------------"
|
||||
|
||||
run_bench() {
|
||||
local count=$1
|
||||
local strict=$2
|
||||
local variant
|
||||
variant=$([ "$strict" = "1" ] && echo "strict" || echo "core")
|
||||
|
||||
local segment_limit_env=()
|
||||
if [ -n "$SEGMENT_LIMIT_PO2" ]; then
|
||||
segment_limit_env=(PPE_SEGMENT_LIMIT_PO2="$SEGMENT_LIMIT_PO2")
|
||||
fi
|
||||
|
||||
local line
|
||||
line=$(
|
||||
env \
|
||||
PPE_FIXTURES="$FIXTURES" \
|
||||
AGGREGATOR_COUNT="$count" \
|
||||
AGGREGATOR_STRICT="$strict" \
|
||||
"${segment_limit_env[@]}" \
|
||||
cargo test -p lee --features cuda,prove bench_aggregator -- --nocapture 2>&1 \
|
||||
| grep "\[lee::analytics\] aggregator" || true
|
||||
)
|
||||
|
||||
if [ -z "$line" ]; then
|
||||
printf "%-6s %-8s %14s %20s\n" "$count" "$variant" "failed" "-"
|
||||
return
|
||||
fi
|
||||
|
||||
local proving_ms proof_size
|
||||
proving_ms=$(echo "$line" | grep -o 'proving_ms=[0-9]*' | cut -d= -f2)
|
||||
proof_size=$(echo "$line" | grep -o 'proof_size_bytes=[0-9]*' | cut -d= -f2)
|
||||
|
||||
printf "%-6s %-8s %14s %20s\n" "$count" "$variant" "$proving_ms" "$proof_size"
|
||||
}
|
||||
|
||||
for count in $COUNTS; do
|
||||
run_bench "$count" "0"
|
||||
run_bench "$count" "1"
|
||||
done
|
||||
|
||||
printf "\n"
|
||||
@ -32,6 +32,7 @@ risc0-binfmt = "3.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
lee_core = { workspace = true, features = ["test_utils"] }
|
||||
bytemuck.workspace = true
|
||||
token_core.workspace = true
|
||||
authenticated_transfer_core.workspace = true
|
||||
test_program_methods.workspace = true
|
||||
|
||||
35
lee/state_machine/core/src/aggregator_circuit_io.rs
Normal file
35
lee/state_machine/core/src/aggregator_circuit_io.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{BlockId, PrivacyPreservingCircuitOutput, Timestamp, program::ProgramId};
|
||||
|
||||
/// Input to the aggregator circuit.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AggregatorCircuitInput {
|
||||
/// Image ID of the privacy-preserving circuit. Passed as a runtime value so the
|
||||
/// guest does not need a compile-time dependency on the image ID.
|
||||
pub privacy_preserving_circuit_id: ProgramId,
|
||||
pub block_id: BlockId,
|
||||
pub timestamp: Timestamp,
|
||||
pub circuit_outputs: Vec<PrivacyPreservingCircuitOutput>,
|
||||
}
|
||||
|
||||
/// Output committed to the journal by the aggregator circuit.
|
||||
///
|
||||
/// Preserves the full `PrivacyPreservingCircuitOutput` for each transaction so observers
|
||||
/// can perform state-dependent checks (nonces, commitment freshness, nullifier uniqueness)
|
||||
/// independently. Only the individual proofs are dropped.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
pub struct AggregatorCircuitOutput {
|
||||
pub block_id: BlockId,
|
||||
pub timestamp: Timestamp,
|
||||
pub circuit_outputs: Vec<PrivacyPreservingCircuitOutput>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl AggregatorCircuitOutput {
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
bytemuck::cast_slice(&risc0_zkvm::serde::to_vec(self).unwrap()).to_vec()
|
||||
}
|
||||
}
|
||||
@ -29,10 +29,9 @@ pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [
|
||||
129, 241, 118, 39, 41, 253, 141, 171, 184, 71, 8, 41,
|
||||
];
|
||||
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)
|
||||
#[derive(
|
||||
Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, BorshSerialize,
|
||||
BorshDeserialize,
|
||||
)]
|
||||
pub struct Commitment(pub(super) [u8; 32]);
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
reason = "We prefer to group methods by functionality rather than by type for encoding"
|
||||
)]
|
||||
|
||||
pub use aggregator_circuit_io::{AggregatorCircuitInput, AggregatorCircuitOutput};
|
||||
pub use circuit_io::{
|
||||
InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
|
||||
};
|
||||
@ -15,6 +16,7 @@ pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKe
|
||||
pub use program::PrivateAccountKind;
|
||||
|
||||
pub mod account;
|
||||
mod aggregator_circuit_io;
|
||||
mod circuit_io;
|
||||
mod commitment;
|
||||
mod encoding;
|
||||
|
||||
@ -65,10 +65,9 @@ impl From<&NullifierSecretKey> for NullifierPublicKey {
|
||||
|
||||
pub type NullifierSecretKey = [u8; 32];
|
||||
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)
|
||||
#[derive(
|
||||
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, BorshSerialize,
|
||||
BorshDeserialize,
|
||||
)]
|
||||
pub struct Nullifier(pub(super) [u8; 32]);
|
||||
|
||||
|
||||
205
lee/state_machine/src/aggregator_circuit.rs
Normal file
205
lee/state_machine/src/aggregator_circuit.rs
Normal file
@ -0,0 +1,205 @@
|
||||
//! Host-side aggregator circuit: batches multiple privacy-preserving circuit proofs into
|
||||
//! a single aggregated proof.
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
AggregatorCircuitInput, AggregatorCircuitOutput, BlockId, PrivacyPreservingCircuitOutput,
|
||||
Timestamp,
|
||||
};
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
use crate::{
|
||||
error::LeeError, privacy_preserving_transaction::circuit::Proof,
|
||||
program_methods::PRIVACY_PRESERVING_CIRCUIT_ID,
|
||||
};
|
||||
|
||||
/// Proof produced by the aggregator circuit.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct AggregatorProof(Vec<u8>);
|
||||
|
||||
impl AggregatorProof {
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn from_inner(inner: Vec<u8>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_valid_for(&self, output: &AggregatorCircuitOutput, circuit_id: [u32; 8]) -> bool {
|
||||
verify_proof(&self.0, output, circuit_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_proof(
|
||||
proof_bytes: &[u8],
|
||||
output: &AggregatorCircuitOutput,
|
||||
circuit_id: [u32; 8],
|
||||
) -> bool {
|
||||
let Ok(inner) = borsh::from_slice::<InnerReceipt>(proof_bytes) else {
|
||||
return false;
|
||||
};
|
||||
let receipt = Receipt::new(inner, output.to_bytes());
|
||||
receipt.verify(circuit_id).is_ok()
|
||||
}
|
||||
|
||||
/// Aggregates N privacy-preserving circuit proofs into a single proof.
|
||||
///
|
||||
/// `elf` is the compiled aggregator circuit binary. Use
|
||||
/// `lee::program_methods::AGGREGATOR_CIRCUIT_ELF` for the core circuit or
|
||||
/// `AGGREGATOR_CIRCUIT_STRICT_ELF` for the strict variant.
|
||||
pub fn aggregate(
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)>,
|
||||
elf: &[u8],
|
||||
segment_limit_po2: Option<u32>,
|
||||
) -> Result<(AggregatorCircuitOutput, AggregatorProof), LeeError> {
|
||||
run_aggregator(block_id, timestamp, proofs, elf, segment_limit_po2)
|
||||
}
|
||||
|
||||
fn run_aggregator(
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)>,
|
||||
elf: &[u8],
|
||||
segment_limit_po2: Option<u32>,
|
||||
) -> Result<(AggregatorCircuitOutput, AggregatorProof), LeeError> {
|
||||
// TODO: add host-side pre-checks before invoking the prover (e.g. no duplicate
|
||||
// nullifiers/commitments, validity windows, public account uniqueness) so obviously
|
||||
// invalid batches are rejected cheaply without spending GPU time.
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
if let Some(po2) = segment_limit_po2 {
|
||||
env_builder.segment_limit_po2(po2);
|
||||
}
|
||||
let mut circuit_outputs = Vec::with_capacity(proofs.len());
|
||||
|
||||
for (circuit_output, proof) in proofs {
|
||||
let inner = borsh::from_slice::<InnerReceipt>(&proof.into_inner())
|
||||
.map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?;
|
||||
let receipt = Receipt::new(inner, circuit_output.to_bytes());
|
||||
env_builder.add_assumption(receipt);
|
||||
circuit_outputs.push(circuit_output);
|
||||
}
|
||||
|
||||
let input = AggregatorCircuitInput {
|
||||
privacy_preserving_circuit_id: PRIVACY_PRESERVING_CIRCUIT_ID,
|
||||
block_id,
|
||||
timestamp,
|
||||
circuit_outputs,
|
||||
};
|
||||
|
||||
env_builder
|
||||
.write(&input)
|
||||
.map_err(|e| LeeError::CircuitProvingError(e.to_string()))?;
|
||||
|
||||
let env = env_builder
|
||||
.build()
|
||||
.map_err(|e| LeeError::CircuitProvingError(e.to_string()))?;
|
||||
|
||||
let prove_info = default_prover()
|
||||
// TODO: succinct compresses all segments into one receipt via recursion — consider
|
||||
// ProverOpts::composite() (no recursion, one receipt per segment) if proving speed
|
||||
// matters more than proof size.
|
||||
.prove_with_opts(env, elf, &ProverOpts::succinct())
|
||||
.map_err(|e| LeeError::CircuitProvingError(e.to_string()))?;
|
||||
|
||||
let proof = AggregatorProof(borsh::to_vec(&prove_info.receipt.inner)?);
|
||||
|
||||
let output: AggregatorCircuitOutput = prove_info
|
||||
.receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
Ok((output, proof))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee_core::{BlockId, PrivacyPreservingCircuitOutput, Timestamp};
|
||||
use test_program_methods::PpeFixture;
|
||||
|
||||
use super::aggregate;
|
||||
use crate::{
|
||||
privacy_preserving_transaction::circuit::Proof,
|
||||
program_methods::{
|
||||
AGGREGATOR_CIRCUIT_ELF, AGGREGATOR_CIRCUIT_ID, AGGREGATOR_CIRCUIT_STRICT_ELF,
|
||||
AGGREGATOR_CIRCUIT_STRICT_ID,
|
||||
},
|
||||
};
|
||||
|
||||
/// Benchmark: aggregate N pre-generated PPE proofs loaded from a fixture file.
|
||||
///
|
||||
/// Generate fixtures first:
|
||||
/// cargo run --release -p ppe_test_data_gen -- --output ppe_fixtures.bin
|
||||
///
|
||||
/// Control via env vars:
|
||||
/// PPE_FIXTURES — path to fixture file (default: ppe_fixtures.bin)
|
||||
/// AGGREGATOR_COUNT — number of fixtures to use (default: all)
|
||||
/// AGGREGATOR_STRICT — set to "1" for the strict variant (default: core)
|
||||
///
|
||||
/// Skips gracefully when the fixture file is absent.
|
||||
///
|
||||
/// Output line (captured by bench_aggregator_cuda.sh):
|
||||
/// [lee::analytics] aggregator n=… variant=… proving_ms=… proof_size_bytes=…
|
||||
#[test]
|
||||
fn bench_aggregator() {
|
||||
let path =
|
||||
std::env::var("PPE_FIXTURES").unwrap_or_else(|_| "ppe_fixtures.bin".to_owned());
|
||||
let mut fixtures = PpeFixture::load_bundle(&path);
|
||||
|
||||
if fixtures.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(s) = std::env::var("AGGREGATOR_COUNT") {
|
||||
let count: usize = s.parse().expect("AGGREGATOR_COUNT must be a number");
|
||||
fixtures.truncate(count);
|
||||
}
|
||||
|
||||
let strict: bool = std::env::var("AGGREGATOR_STRICT")
|
||||
.map(|s| s == "1" || s == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
let (elf, circuit_id) = if strict {
|
||||
(AGGREGATOR_CIRCUIT_STRICT_ELF, AGGREGATOR_CIRCUIT_STRICT_ID)
|
||||
} else {
|
||||
(AGGREGATOR_CIRCUIT_ELF, AGGREGATOR_CIRCUIT_ID)
|
||||
};
|
||||
|
||||
let proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)> = fixtures
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let words: &[u32] = bytemuck::cast_slice(&f.output_bytes);
|
||||
let output: PrivacyPreservingCircuitOutput =
|
||||
risc0_zkvm::serde::from_slice(words).expect("fixture output_bytes invalid");
|
||||
let proof = Proof::from_inner(f.proof_bytes.clone());
|
||||
(output, proof)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let block_id: BlockId = 1;
|
||||
let timestamp = Timestamp::from(1_700_000_000_u64);
|
||||
let segment_limit_po2: Option<u32> = std::env::var("PPE_SEGMENT_LIMIT_PO2")
|
||||
.ok()
|
||||
.map(|s| s.parse().expect("PPE_SEGMENT_LIMIT_PO2 must be a number"));
|
||||
|
||||
let t0 = std::time::Instant::now();
|
||||
let (_, agg_proof) =
|
||||
aggregate(block_id, timestamp, proofs, elf, segment_limit_po2).expect("aggregation should succeed");
|
||||
let proving_ms = t0.elapsed().as_millis();
|
||||
|
||||
let variant = if strict { "strict" } else { "core" };
|
||||
let proof_size = agg_proof.into_inner().len();
|
||||
eprintln!(
|
||||
"[lee::analytics] aggregator n={} variant={variant} proving_ms={proving_ms} proof_size_bytes={proof_size}",
|
||||
fixtures.len(),
|
||||
);
|
||||
|
||||
let _ = circuit_id;
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,7 @@ pub use state::{
|
||||
};
|
||||
pub use validated_state_diff::ValidatedStateDiff;
|
||||
|
||||
pub mod aggregator_circuit;
|
||||
pub mod encoding;
|
||||
pub mod error;
|
||||
mod merkle_tree;
|
||||
|
||||
69
program_methods/guest/src/bin/aggregator_circuit/main.rs
Normal file
69
program_methods/guest/src/bin/aggregator_circuit/main.rs
Normal file
@ -0,0 +1,69 @@
|
||||
//! Aggregator Circuit.
|
||||
//!
|
||||
//! Verifies N privacy-preserving circuit proofs and enforces:
|
||||
//! - Intra-batch uniqueness of nullifiers and commitments.
|
||||
//! - No public account is updated by more than one transaction in the batch.
|
||||
//!
|
||||
//! The full `PrivacyPreservingCircuitOutput` for each transaction is committed to the
|
||||
//! journal so observers can perform state-dependent checks independently.
|
||||
|
||||
use std::{collections::HashSet, convert::Infallible};
|
||||
|
||||
use lee_core::{AggregatorCircuitInput, AggregatorCircuitOutput, Commitment, Nullifier, account::AccountId};
|
||||
use risc0_zkvm::{guest::env, serde::to_vec};
|
||||
|
||||
fn main() {
|
||||
let AggregatorCircuitInput {
|
||||
privacy_preserving_circuit_id,
|
||||
block_id,
|
||||
timestamp,
|
||||
circuit_outputs,
|
||||
} = env::read();
|
||||
|
||||
for output in &circuit_outputs {
|
||||
let output_words =
|
||||
to_vec(output).expect("PrivacyPreservingCircuitOutput serialization should not fail");
|
||||
env::verify(privacy_preserving_circuit_id, &output_words)
|
||||
.unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed"));
|
||||
}
|
||||
|
||||
let mut seen_nullifiers: HashSet<Nullifier> = HashSet::new();
|
||||
for output in &circuit_outputs {
|
||||
for (nullifier, _) in &output.new_nullifiers {
|
||||
assert!(
|
||||
seen_nullifiers.insert(*nullifier),
|
||||
"Duplicate nullifier across transactions in batch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen_commitments: HashSet<Commitment> = HashSet::new();
|
||||
for output in &circuit_outputs {
|
||||
for commitment in &output.new_commitments {
|
||||
assert!(
|
||||
seen_commitments.insert(commitment.clone()),
|
||||
"Duplicate commitment across transactions in batch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen_updated_account_ids: HashSet<AccountId> = HashSet::new();
|
||||
for output in &circuit_outputs {
|
||||
for (pre_state, post_state) in
|
||||
output.public_pre_states.iter().zip(output.public_post_states.iter())
|
||||
{
|
||||
if pre_state.account != *post_state {
|
||||
assert!(
|
||||
seen_updated_account_ids.insert(pre_state.account_id),
|
||||
"Public account updated by multiple transactions in batch"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env::commit(&AggregatorCircuitOutput {
|
||||
block_id,
|
||||
timestamp,
|
||||
circuit_outputs,
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
//! Aggregator Circuit (Strict).
|
||||
//!
|
||||
//! Extends the core aggregator circuit with one additional check proven inside RISC0:
|
||||
//! - Each transaction's validity window contains the provided `block_id` and `timestamp`.
|
||||
|
||||
use std::{collections::HashSet, convert::Infallible};
|
||||
|
||||
use lee_core::{
|
||||
AggregatorCircuitInput, AggregatorCircuitOutput, Commitment, Nullifier, account::AccountId,
|
||||
};
|
||||
use risc0_zkvm::{guest::env, serde::to_vec};
|
||||
|
||||
fn main() {
|
||||
let AggregatorCircuitInput {
|
||||
privacy_preserving_circuit_id,
|
||||
block_id,
|
||||
timestamp,
|
||||
circuit_outputs,
|
||||
} = env::read();
|
||||
|
||||
for output in &circuit_outputs {
|
||||
let output_words =
|
||||
to_vec(output).expect("PrivacyPreservingCircuitOutput serialization should not fail");
|
||||
env::verify(privacy_preserving_circuit_id, &output_words)
|
||||
.unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed"));
|
||||
}
|
||||
|
||||
let mut seen_nullifiers: HashSet<Nullifier> = HashSet::new();
|
||||
for output in &circuit_outputs {
|
||||
for (nullifier, _) in &output.new_nullifiers {
|
||||
assert!(
|
||||
seen_nullifiers.insert(*nullifier),
|
||||
"Duplicate nullifier across transactions in batch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen_commitments: HashSet<Commitment> = HashSet::new();
|
||||
for output in &circuit_outputs {
|
||||
for commitment in &output.new_commitments {
|
||||
assert!(
|
||||
seen_commitments.insert(commitment.clone()),
|
||||
"Duplicate commitment across transactions in batch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for output in &circuit_outputs {
|
||||
assert!(
|
||||
output.block_validity_window.is_valid_for(block_id),
|
||||
"Transaction block validity window does not include the block id"
|
||||
);
|
||||
assert!(
|
||||
output.timestamp_validity_window.is_valid_for(timestamp),
|
||||
"Transaction timestamp validity window does not include the timestamp"
|
||||
);
|
||||
}
|
||||
|
||||
let mut seen_updated_account_ids: HashSet<AccountId> = HashSet::new();
|
||||
for output in &circuit_outputs {
|
||||
for (pre_state, post_state) in
|
||||
output.public_pre_states.iter().zip(output.public_post_states.iter())
|
||||
{
|
||||
if pre_state.account != *post_state {
|
||||
assert!(
|
||||
seen_updated_account_ids.insert(pre_state.account_id),
|
||||
"Public account updated by multiple transactions in batch"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env::commit(&AggregatorCircuitOutput {
|
||||
block_id,
|
||||
timestamp,
|
||||
circuit_outputs,
|
||||
});
|
||||
}
|
||||
12
programs/sequencer_aggregation_circuit/core/Cargo.toml
Normal file
12
programs/sequencer_aggregation_circuit/core/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "sequencer_aggregation_circuit_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
lee_core.workspace = true
|
||||
borsh.workspace = true
|
||||
@ -11,7 +11,9 @@ workspace = true
|
||||
[features]
|
||||
default = []
|
||||
prove = ["lee/prove", "risc0-zkvm/prove"]
|
||||
cuda = ["lee/cuda", "risc0-zkvm/cuda"]
|
||||
ppe = ["prove"]
|
||||
aggregator = ["ppe"]
|
||||
|
||||
[dependencies]
|
||||
lee = { workspace = true }
|
||||
|
||||
110
tools/cycle_bench/src/aggregator.rs
Normal file
110
tools/cycle_bench/src/aggregator.rs
Normal file
@ -0,0 +1,110 @@
|
||||
//! Aggregator circuit bench module.
|
||||
//!
|
||||
//! Measures wall-clock time for batching N privacy-preserving circuit proofs into a
|
||||
//! single aggregated proof, using both the core and strict aggregator variants.
|
||||
//!
|
||||
//! Reported metrics per (N, variant) pair:
|
||||
//! - `pp_prove_ms`: time to generate the N pp-circuit proofs (context for total cost)
|
||||
//! - `agg_prove_ms`: time to run `aggregate()` — the sequencer's batch proving step
|
||||
//! - `agg_proof_bytes`: borsh-serialized `InnerReceipt` of the aggregated proof
|
||||
//! - `pp_proof_bytes_per_tx`: same metric for one pp-proof, for size comparison
|
||||
//!
|
||||
//! Requires `--features aggregator` and a full build (aggregator ELFs must exist in
|
||||
//! `artifacts/program_methods/`).
|
||||
|
||||
#![allow(
|
||||
dead_code,
|
||||
reason = "Stubs are used when the `aggregator` feature is disabled."
|
||||
)]
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(feature = "aggregator")]
|
||||
mod agg_impl;
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct AggregatorBenchResult {
|
||||
pub label: String,
|
||||
pub n_txs: usize,
|
||||
pub strict: bool,
|
||||
/// Total wall-clock time to generate all N pp-circuit proofs (ms).
|
||||
pub pp_prove_ms: Option<f64>,
|
||||
/// Wall-clock time for the `aggregate()` call alone (ms).
|
||||
pub agg_prove_ms: Option<f64>,
|
||||
/// borsh-serialized `InnerReceipt` length of the aggregated proof (bytes).
|
||||
pub agg_proof_bytes: Option<usize>,
|
||||
/// borsh-serialized `InnerReceipt` length of one pp-proof, for comparison (bytes).
|
||||
pub pp_proof_bytes_per_tx: Option<usize>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "aggregator"))]
|
||||
#[must_use]
|
||||
pub const fn run_all() -> Vec<AggregatorBenchResult> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[cfg(feature = "aggregator")]
|
||||
#[must_use]
|
||||
pub fn run_all() -> Vec<AggregatorBenchResult> {
|
||||
let mut results = Vec::new();
|
||||
for n_txs in [1_usize, 3, 5] {
|
||||
for strict in [false, true] {
|
||||
let variant = if strict { "strict" } else { "core" };
|
||||
eprintln!("aggregator: {variant} n={n_txs}");
|
||||
results.push(agg_impl::run(n_txs, strict));
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
pub fn print_table(results: &[AggregatorBenchResult]) {
|
||||
if results.is_empty() {
|
||||
return;
|
||||
}
|
||||
let lw = results
|
||||
.iter()
|
||||
.map(|r| r.label.len())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.max("label".len());
|
||||
|
||||
println!(
|
||||
"\n{:<lw$} {:>5} {:>22} {:>22} {:>12} {:>12} {}",
|
||||
"label",
|
||||
"n_txs",
|
||||
"pp_prove_ms (s)",
|
||||
"agg_prove_ms (s)",
|
||||
"agg_bytes",
|
||||
"pp_bytes/tx",
|
||||
"error",
|
||||
lw = lw,
|
||||
);
|
||||
println!("{}", "-".repeat(lw + 85));
|
||||
for r in results {
|
||||
let pp = fmt_ms(r.pp_prove_ms);
|
||||
let ap = fmt_ms(r.agg_prove_ms);
|
||||
let ab = r
|
||||
.agg_proof_bytes
|
||||
.map_or_else(|| "-".to_owned(), |n| n.to_string());
|
||||
let pb = r
|
||||
.pp_proof_bytes_per_tx
|
||||
.map_or_else(|| "-".to_owned(), |n| n.to_string());
|
||||
let e = r.error.as_deref().unwrap_or("");
|
||||
println!(
|
||||
"{:<lw$} {:>5} {:>22} {:>22} {:>12} {:>12} {}",
|
||||
r.label,
|
||||
r.n_txs,
|
||||
pp,
|
||||
ap,
|
||||
ab,
|
||||
pb,
|
||||
e,
|
||||
lw = lw,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_ms(ms: Option<f64>) -> String {
|
||||
ms.map_or_else(|| "-".to_owned(), |v| format!("{v:.1} ({:.1}s)", v / 1_000.0))
|
||||
}
|
||||
177
tools/cycle_bench/src/aggregator/agg_impl.rs
Normal file
177
tools/cycle_bench/src/aggregator/agg_impl.rs
Normal file
@ -0,0 +1,177 @@
|
||||
//! Feature-gated implementation of aggregator circuit benches.
|
||||
//!
|
||||
//! ELFs are loaded at runtime from `artifacts/program_methods/` so this module
|
||||
//! compiles even before a full RISC0 build. If the ELFs are not present, each
|
||||
//! bench run reports an error rather than panicking.
|
||||
|
||||
use std::{path::PathBuf, time::Instant};
|
||||
|
||||
use authenticated_transfer_core::Instruction;
|
||||
use lee::{
|
||||
aggregator_circuit::aggregate,
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::circuit::{Proof, ProgramWithDependencies},
|
||||
program::Program,
|
||||
program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID},
|
||||
};
|
||||
use lee_core::{
|
||||
BlockId, InputAccountIdentity, NullifierPublicKey, SharedSecretKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
use risc0_zkvm::serde::to_vec;
|
||||
|
||||
use super::AggregatorBenchResult;
|
||||
|
||||
/// Loads an aggregator ELF from `artifacts/program_methods/{name}.bin` at runtime.
|
||||
fn load_aggregator_elf(name: &str) -> anyhow::Result<Vec<u8>> {
|
||||
let artifacts = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../..")
|
||||
.join("artifacts/program_methods");
|
||||
let path = artifacts.join(format!("{name}.bin"));
|
||||
std::fs::read(&path).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"aggregator ELF not found at {}: {e}\n\
|
||||
Run a full RISC0 build (without RISC0_SKIP_BUILD=1) to generate it.",
|
||||
path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates a public-to-private (shielded) auth-transfer pp-proof.
|
||||
///
|
||||
/// The sender is a public account; the recipient is a fresh private account derived
|
||||
/// from `tag`. Distinct tags yield distinct `npk` values → distinct commitments and
|
||||
/// nullifiers, so any number of these proofs can be safely aggregated in one batch.
|
||||
fn prove_shielded_transfer(
|
||||
tag: u8,
|
||||
) -> anyhow::Result<(lee_core::PrivacyPreservingCircuitOutput, Proof)> {
|
||||
let nsk: [u8; 32] = [tag; 32];
|
||||
let d: [u8; 32] = [tag.wrapping_add(64); 32];
|
||||
let z: [u8; 32] = [tag.wrapping_add(128); 32];
|
||||
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_seed(&d, &z);
|
||||
let (ssk, _epk) = SharedSecretKey::encapsulate(&vpk);
|
||||
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&npk, 0);
|
||||
|
||||
let program = Program::new(AUTHENTICATED_TRANSFER_ELF.to_vec())?;
|
||||
let pwd = ProgramWithDependencies::from(program);
|
||||
|
||||
// Public sender with sufficient balance; unique account ID per tag so the
|
||||
// strict aggregator's public-account-uniqueness check passes.
|
||||
let sender = AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: AUTHENTICATED_TRANSFER_ID,
|
||||
balance: 1_000_000,
|
||||
..Account::default()
|
||||
},
|
||||
is_authorized: true,
|
||||
account_id: AccountId::new([tag; 32]),
|
||||
};
|
||||
|
||||
// Fresh private recipient account (zero balance, not yet authorized).
|
||||
let recipient = AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: false,
|
||||
account_id: recipient_account_id,
|
||||
};
|
||||
|
||||
let instruction_data = to_vec(&Instruction::Transfer { amount: 1_000 })?;
|
||||
let identities = vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized { npk, ssk, identifier: 0 },
|
||||
];
|
||||
|
||||
Ok(execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
instruction_data,
|
||||
identities,
|
||||
&pwd,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn run(n_txs: usize, strict: bool) -> AggregatorBenchResult {
|
||||
let elf_name = if strict {
|
||||
"aggregator_circuit_strict"
|
||||
} else {
|
||||
"aggregator_circuit"
|
||||
};
|
||||
let label = format!(
|
||||
"aggregator_{} n={n_txs}",
|
||||
if strict { "strict" } else { "core" }
|
||||
);
|
||||
|
||||
let elf = match load_aggregator_elf(elf_name) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
return AggregatorBenchResult {
|
||||
label,
|
||||
n_txs,
|
||||
strict,
|
||||
pp_prove_ms: None,
|
||||
agg_prove_ms: None,
|
||||
agg_proof_bytes: None,
|
||||
pp_proof_bytes_per_tx: None,
|
||||
error: Some(e.to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generate N pp-proofs with distinct private recipients (tags 1..=N).
|
||||
let pp_started = Instant::now();
|
||||
let proofs: Result<Vec<_>, anyhow::Error> = (0..n_txs)
|
||||
.map(|i| prove_shielded_transfer(u8::try_from(i + 1).unwrap_or(u8::MAX)))
|
||||
.collect();
|
||||
let pp_prove_ms = pp_started.elapsed().as_secs_f64() * 1_000.0;
|
||||
|
||||
let proofs = match proofs {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return AggregatorBenchResult {
|
||||
label,
|
||||
n_txs,
|
||||
strict,
|
||||
pp_prove_ms: Some(pp_prove_ms),
|
||||
agg_prove_ms: None,
|
||||
agg_proof_bytes: None,
|
||||
pp_proof_bytes_per_tx: None,
|
||||
error: Some(e.to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Capture per-tx proof size before the vec is consumed by aggregate().
|
||||
let pp_proof_bytes_per_tx = proofs.first().map(|(_, p)| p.clone().into_inner().len());
|
||||
|
||||
let block_id: BlockId = 1;
|
||||
let timestamp = Timestamp::from(1_700_000_000_u64);
|
||||
|
||||
let agg_started = Instant::now();
|
||||
let result = aggregate(block_id, timestamp, proofs, &elf, None);
|
||||
let agg_prove_ms = agg_started.elapsed().as_secs_f64() * 1_000.0;
|
||||
|
||||
match result {
|
||||
Ok((_output, agg_proof)) => AggregatorBenchResult {
|
||||
label,
|
||||
n_txs,
|
||||
strict,
|
||||
pp_prove_ms: Some(pp_prove_ms),
|
||||
agg_prove_ms: Some(agg_prove_ms),
|
||||
agg_proof_bytes: Some(agg_proof.into_inner().len()),
|
||||
pp_proof_bytes_per_tx,
|
||||
error: None,
|
||||
},
|
||||
Err(e) => AggregatorBenchResult {
|
||||
label,
|
||||
n_txs,
|
||||
strict,
|
||||
pp_prove_ms: Some(pp_prove_ms),
|
||||
agg_prove_ms: Some(agg_prove_ms),
|
||||
agg_proof_bytes: None,
|
||||
pp_proof_bytes_per_tx,
|
||||
error: Some(e.to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -14,10 +14,17 @@
|
||||
feature = "ppe",
|
||||
expect(
|
||||
clippy::arbitrary_source_item_ordering,
|
||||
reason = "PPE module: re-export ordering trips strict lints"
|
||||
)
|
||||
)]
|
||||
#![cfg_attr(
|
||||
any(feature = "ppe", feature = "aggregator"),
|
||||
expect(
|
||||
clippy::print_stderr,
|
||||
reason = "PPE module: re-export ordering and eprintln progress trip strict lints"
|
||||
reason = "PPE/aggregator bench: eprintln progress messages"
|
||||
)
|
||||
)]
|
||||
|
||||
pub mod aggregator;
|
||||
pub mod ppe;
|
||||
pub mod stats;
|
||||
|
||||
@ -27,7 +27,7 @@ use clock_core::{
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
|
||||
ClockAccountData,
|
||||
};
|
||||
use cycle_bench::{ppe, stats::Stats};
|
||||
use cycle_bench::{aggregator, ppe, stats::Stats};
|
||||
use lee::program_methods::{
|
||||
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
|
||||
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, TOKEN_ELF,
|
||||
@ -55,6 +55,12 @@ struct Cli {
|
||||
#[arg(long)]
|
||||
ppe: bool,
|
||||
|
||||
/// Also run aggregator circuit benches: batch N=1,3,5 pp-proofs through both the
|
||||
/// core and strict aggregator circuits. Reports pp-prove time, agg-prove time, and
|
||||
/// proof sizes. Requires --features aggregator at build time and a full RISC0 build.
|
||||
#[arg(long)]
|
||||
aggregator: bool,
|
||||
|
||||
/// Iterations for executor wall-time sampling per case. First iter is
|
||||
/// discarded as warmup, remaining N feed the stats.
|
||||
#[arg(long, default_value_t = 5)]
|
||||
@ -515,6 +521,25 @@ fn main() -> Result<()> {
|
||||
ppe::print_table(&ppe_results);
|
||||
}
|
||||
|
||||
#[cfg(feature = "aggregator")]
|
||||
let agg_results = if cli.aggregator {
|
||||
aggregator::run_all()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
#[cfg(not(feature = "aggregator"))]
|
||||
let agg_results: Vec<aggregator::AggregatorBenchResult> = {
|
||||
if cli.aggregator {
|
||||
eprintln!(
|
||||
"cycle_bench: --aggregator requires --features aggregator at build time. Ignoring."
|
||||
);
|
||||
}
|
||||
Vec::new()
|
||||
};
|
||||
if !agg_results.is_empty() {
|
||||
aggregator::print_table(&agg_results);
|
||||
}
|
||||
|
||||
let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
@ -526,6 +551,7 @@ fn main() -> Result<()> {
|
||||
let combined = serde_json::json!({
|
||||
"standalone": results,
|
||||
"ppe": ppe_results,
|
||||
"aggregator": agg_results,
|
||||
});
|
||||
std::fs::write(&out_path, serde_json::to_string_pretty(&combined)?)?;
|
||||
println!("\nJSON written to {}", out_path.display());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user