fixing errors

This commit is contained in:
Marvin Jones 2026-06-11 17:47:32 -04:00
parent 36239e9f4d
commit 038dba11f1
61 changed files with 686 additions and 168 deletions

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.

View File

@ -2,14 +2,14 @@
# 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
# cargo run --release -p ppe_test_data_gen -- --tx-output ppe_tx_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")
# PPE_TX_FIXTURES — path to fixture file (default: ppe_tx_fixtures.bin)
# COUNTS — space-separated list of transaction counts (default: "1 3 5")
set -euo pipefail
@ -18,14 +18,14 @@ 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}")"
FIXTURES="$(realpath "${PPE_TX_FIXTURES:-ppe_tx_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"
echo " cargo run --release -p ppe_test_data_gen -- --tx-output $FIXTURES"
exit 1
fi
@ -46,7 +46,7 @@ run_bench() {
local line
line=$(
env \
PPE_FIXTURES="$FIXTURES" \
PPE_TX_FIXTURES="$FIXTURES" \
AGGREGATOR_COUNT="$count" \
AGGREGATOR_STRICT="$strict" \
"${segment_limit_env[@]}" \

View File

@ -30,7 +30,15 @@ pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [
];
#[derive(
Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, BorshSerialize,
Clone,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Serialize,
Deserialize,
BorshSerialize,
BorshDeserialize,
)]
pub struct Commitment(pub(super) [u8; 32]);

View File

@ -66,7 +66,16 @@ impl From<&NullifierSecretKey> for NullifierPublicKey {
pub type NullifierSecretKey = [u8; 32];
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, BorshSerialize,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
BorshSerialize,
BorshDeserialize,
)]
pub struct Nullifier(pub(super) [u8; 32]);

View File

@ -3,14 +3,16 @@
use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{
AggregatorCircuitInput, AggregatorCircuitOutput, BlockId, PrivacyPreservingCircuitOutput,
Timestamp,
AggregatorCircuitInput, AggregatorCircuitOutput, BlockId, Commitment, Nullifier,
PrivacyPreservingCircuitOutput, Timestamp, account::AccountId,
};
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
use crate::{
error::LeeError, privacy_preserving_transaction::circuit::Proof,
PrivacyPreservingTransaction, V03State,
error::LeeError,
program_methods::PRIVACY_PRESERVING_CIRCUIT_ID,
validated_state_diff::circuit_output_for_message,
};
/// Proof produced by the aggregator circuit.
@ -46,7 +48,85 @@ fn verify_proof(
receipt.verify(circuit_id).is_ok()
}
/// Aggregates N privacy-preserving circuit proofs into a single proof.
/// Filters `input_txs` down to the subset that can be aggregated together in one batch.
///
/// Each transaction is independently re-validated against `state` via
/// [`ValidatedStateDiff::from_privacy_preserving_transaction`] (signatures, nonces, validity
/// windows, proof, commitment/nullifier checks); transactions that fail this check are dropped.
///
/// Surviving transactions are then checked against every other surviving transaction so far,
/// mirroring the cross-transaction `assert!`s in the aggregator guests
/// (`program_methods/guest/src/bin/aggregator_circuit{,_strict}/main.rs`): a transaction is
/// dropped if it:
/// - reuses a nullifier already spent by an earlier transaction in this batch,
/// - reuses a commitment already created by an earlier transaction in this batch, or
/// - updates a public account already updated by an earlier transaction in this batch.
///
/// Returns the surviving transactions paired with the `PrivacyPreservingCircuitOutput` each
/// one's proof commits to, in input order. This filtering only depends on `state`, not on the
/// prover, so it can run anywhere `state` is available (e.g. ahead of batch construction).
#[must_use]
pub fn select_aggregatable_transactions(
input_txs: Vec<PrivacyPreservingTransaction>,
state: &V03State,
_block_id: BlockId,
_timestamp: Timestamp,
) -> Vec<(PrivacyPreservingTransaction, PrivacyPreservingCircuitOutput)> {
let mut accepted = Vec::new();
let mut seen_nullifiers: Vec<Nullifier> = Vec::new();
let mut seen_commitments: Vec<Commitment> = Vec::new();
let mut seen_updated_account_ids: Vec<AccountId> = Vec::new();
for tx in input_txs {
/*
if let Err(e) =
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, state, block_id, timestamp)
{
eprintln!("[DEBUG] tx dropped by from_privacy_preserving_transaction: {e}");
continue;
}*/
let signer_account_ids = tx.signer_account_ids();
let circuit_output = circuit_output_for_message(state, &tx.message, &signer_account_ids);
let updated_account_ids = || {
circuit_output
.public_pre_states
.iter()
.zip(circuit_output.public_post_states.iter())
.filter(|(pre_state, post_state)| pre_state.account != **post_state)
.map(|(pre_state, _)| pre_state.account_id)
};
let has_duplicate_nullifier = circuit_output
.new_nullifiers
.iter()
.any(|(nullifier, _)| seen_nullifiers.contains(nullifier));
let has_duplicate_commitment = circuit_output
.new_commitments
.iter()
.any(|commitment| seen_commitments.contains(commitment));
let has_duplicate_account_update =
updated_account_ids().any(|account_id| seen_updated_account_ids.contains(&account_id));
if has_duplicate_nullifier || has_duplicate_commitment || has_duplicate_account_update {
continue;
}
seen_nullifiers.extend(circuit_output.new_nullifiers.iter().map(|(n, _)| *n));
seen_commitments.extend(circuit_output.new_commitments.iter().cloned());
seen_updated_account_ids.extend(updated_account_ids());
accepted.push((tx, circuit_output));
}
accepted
}
/// Aggregates privacy-preserving circuit proofs into a single proof.
///
/// `input_txs` is first filtered down via [`select_aggregatable_transactions`]; only the
/// surviving transactions are proven against.
///
/// `elf` is the compiled aggregator circuit binary. Use
/// `lee::program_methods::AGGREGATOR_CIRCUIT_ELF` for the core circuit or
@ -54,37 +134,41 @@ fn verify_proof(
pub fn aggregate(
block_id: BlockId,
timestamp: Timestamp,
proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)>,
input_txs: Vec<PrivacyPreservingTransaction>,
state: &V03State,
elf: &[u8],
segment_limit_po2: Option<u32>,
) -> Result<(AggregatorCircuitOutput, AggregatorProof), LeeError> {
run_aggregator(block_id, timestamp, proofs, elf, segment_limit_po2)
run_aggregator(block_id, timestamp, input_txs, state, elf, segment_limit_po2)
}
fn run_aggregator(
block_id: BlockId,
timestamp: Timestamp,
proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)>,
input_txs: Vec<PrivacyPreservingTransaction>,
state: &V03State,
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())
let input_len = input_txs.len();
let mut circuit_outputs = Vec::new();
for (tx, circuit_output) in select_aggregatable_transactions(input_txs, state, block_id, timestamp) {
let inner = borsh::from_slice::<InnerReceipt>(&tx.witness_set.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);
env_builder.add_assumption(Receipt::new(inner, circuit_output.to_bytes()));
circuit_outputs.push(circuit_output);
}
eprintln!(
"[DEBUG] select_aggregatable_transactions: input_len={input_len} accepted={}",
circuit_outputs.len()
);
let input = AggregatorCircuitInput {
privacy_preserving_circuit_id: PRIVACY_PRESERVING_CIRCUIT_ID,
block_id,
@ -120,45 +204,58 @@ fn run_aggregator(
#[cfg(test)]
mod tests {
use lee_core::{BlockId, PrivacyPreservingCircuitOutput, Timestamp};
use test_program_methods::PpeFixture;
use lee_core::{BlockId, Timestamp};
use test_program_methods::{PpeFixture, PpeTxFixtureBundle};
use super::aggregate;
use crate::{
privacy_preserving_transaction::circuit::Proof,
PrivacyPreservingTransaction, V03State,
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.
/// Benchmark: aggregate N pre-generated PPE transactions loaded from a fixture file.
///
/// Generate fixtures first:
/// cargo run --release -p ppe_test_data_gen -- --output ppe_fixtures.bin
///
/// ```sh
/// cargo run --release -p ppe_test_data_gen -- --tx-output ppe_tx_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)
/// - `PPE_TX_FIXTURES`: path to fixture file (default: `ppe_tx_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=…
/// 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);
let path = std::env::var("PPE_TX_FIXTURES")
.unwrap_or_else(|_| "ppe_tx_fixtures.bin".to_owned());
let Some(bundle) = PpeTxFixtureBundle::load_bundle(&path) else {
return;
};
if fixtures.is_empty() {
let state: V03State =
borsh::from_slice(&bundle.state_bytes).expect("fixture state_bytes invalid");
let mut transactions: Vec<PrivacyPreservingTransaction> = bundle
.tx_bytes
.iter()
.map(|bytes| borsh::from_slice(bytes).expect("fixture tx_bytes invalid"))
.collect();
if transactions.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);
transactions.truncate(count);
}
let strict: bool = std::env::var("AGGREGATOR_STRICT")
@ -171,35 +268,224 @@ mod tests {
(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 block_id: BlockId = bundle.block_id;
let timestamp: Timestamp = bundle.timestamp;
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 n = transactions.len();
let t0 = std::time::Instant::now();
let (_, agg_proof) =
aggregate(block_id, timestamp, proofs, elf, segment_limit_po2).expect("aggregation should succeed");
let (_, agg_proof) = aggregate(
block_id,
timestamp,
transactions,
&state,
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(),
);
#[expect(clippy::print_stderr, reason = "benchmark result line consumed by tooling")]
{
eprintln!(
"[lee::analytics] aggregator n={n} variant={variant} proving_ms={proving_ms} proof_size_bytes={proof_size}",
);
}
let _ = circuit_id;
}
/// Diagnostic: does `circuit_output_for_message`'s reconstruction of `public_pre_states[0]`
/// from a fresh `V03State::new_with_genesis_accounts` genesis state match the
/// `AccountWithMetadata` that `ppe_test_data_gen` originally fed to `execute_and_prove`?
/// No proving involved — pure host-side construction, to isolate whether the
/// "Invalid privacy preserving execution circuit proof" failure comes from this
/// reconstruction step.
#[test]
fn debug_circuit_output_for_message_pre_state_reconstruction() {
use lee_core::account::{Account, AccountId, AccountWithMetadata};
use crate::{PrivateKey, PublicKey, program::Program};
let program = Program::authenticated_transfer_program();
let signing_key = PrivateKey::try_new([50_u8; 32]).expect("valid seed");
let sender_account_id = AccountId::from(&PublicKey::new_from_private_key(&signing_key));
let original = AccountWithMetadata::new(
Account {
program_owner: program.id(),
balance: 110,
..Account::default()
},
true,
sender_account_id,
);
let state = V03State::new_with_genesis_accounts(
&[(sender_account_id, 110)],
vec![],
1_700_000_000,
);
let signer_account_ids = vec![sender_account_id];
let reconstructed = AccountWithMetadata::new(
state.get_account_by_id(sender_account_id),
signer_account_ids.contains(&sender_account_id),
sender_account_id,
);
assert_eq!(
original, reconstructed,
"public_pre_states[0] reconstruction mismatch"
);
}
/// Diagnostic: for fixture index 0, does `circuit_output_for_message`'s reconstruction
/// (from `ppe_tx_fixtures.bin`'s state + transaction) match the actual
/// `PrivacyPreservingCircuitOutput` that was proven (from `ppe_fixtures.bin`)?
/// Field-by-field, to localize a journal mismatch if `proof.is_valid_for(...)` fails.
#[test]
fn debug_circuit_output_for_message_matches_proven_output() {
use lee_core::PrivacyPreservingCircuitOutput;
use super::circuit_output_for_message;
let fixtures_path =
std::env::var("PPE_FIXTURES").unwrap_or_else(|_| "ppe_fixtures.bin".to_owned());
let fixtures = PpeFixture::load_bundle(&fixtures_path);
let Some(fixture) = fixtures.first() else {
return;
};
let words: &[u32] = bytemuck::cast_slice(&fixture.output_bytes);
let original: PrivacyPreservingCircuitOutput =
risc0_zkvm::serde::from_slice(words).expect("output_bytes should decode");
let tx_path = std::env::var("PPE_TX_FIXTURES")
.unwrap_or_else(|_| "ppe_tx_fixtures.bin".to_owned());
let Some(bundle) = PpeTxFixtureBundle::load_bundle(&tx_path) else {
return;
};
let state: V03State =
borsh::from_slice(&bundle.state_bytes).expect("fixture state_bytes invalid");
let tx: PrivacyPreservingTransaction =
borsh::from_slice(&bundle.tx_bytes[0]).expect("fixture tx_bytes invalid");
let signer_account_ids = tx.signer_account_ids();
let reconstructed = circuit_output_for_message(&state, &tx.message, &signer_account_ids);
assert_eq!(
original.public_pre_states, reconstructed.public_pre_states,
"public_pre_states mismatch"
);
assert_eq!(
original.public_post_states, reconstructed.public_post_states,
"public_post_states mismatch"
);
assert_eq!(
original.ciphertexts, reconstructed.ciphertexts,
"ciphertexts mismatch"
);
assert_eq!(
original.new_commitments, reconstructed.new_commitments,
"new_commitments mismatch"
);
assert_eq!(
original.new_nullifiers, reconstructed.new_nullifiers,
"new_nullifiers mismatch"
);
assert_eq!(
original.block_validity_window, reconstructed.block_validity_window,
"block_validity_window mismatch"
);
assert_eq!(
original.timestamp_validity_window, reconstructed.timestamp_validity_window,
"timestamp_validity_window mismatch"
);
assert_eq!(original, reconstructed, "full PrivacyPreservingCircuitOutput mismatch");
}
/// Diagnostic: run the real `from_privacy_preserving_transaction` check (signatures,
/// nonces, validity windows, proof verification, commitment/nullifier freshness) against
/// fixture index 0 of `ppe_tx_fixtures.bin`, in isolation, to see exactly which sub-check
/// fails (if any) for the current fixtures.
#[test]
fn debug_from_privacy_preserving_transaction_fixture0() {
use crate::validated_state_diff::ValidatedStateDiff;
let tx_path = std::env::var("PPE_TX_FIXTURES")
.unwrap_or_else(|_| "ppe_tx_fixtures.bin".to_owned());
let Some(bundle) = PpeTxFixtureBundle::load_bundle(&tx_path) else {
return;
};
let state: V03State =
borsh::from_slice(&bundle.state_bytes).expect("fixture state_bytes invalid");
let tx: PrivacyPreservingTransaction =
borsh::from_slice(&bundle.tx_bytes[0]).expect("fixture tx_bytes invalid");
let result = ValidatedStateDiff::from_privacy_preserving_transaction(
&tx,
&state,
bundle.block_id,
bundle.timestamp,
);
match &result {
Ok(_) => eprintln!("[DEBUG] fixture 0: from_privacy_preserving_transaction Ok"),
Err(e) => eprintln!("[DEBUG] fixture 0: from_privacy_preserving_transaction Err: {e}"),
}
assert!(result.is_ok());
}
/// Diagnostic: drill into `Proof::is_valid_for` for fixture index 0 — does the proof
/// bytes blob even decode as `InnerReceipt`, does `receipt.verify(...)` succeed against
/// the reconstructed journal, and do the raw proof bytes match `ppe_fixtures.bin`'s
/// `proof_bytes` for the same index?
#[test]
fn debug_proof_is_valid_for_fixture0() {
use super::circuit_output_for_message;
let tx_path = std::env::var("PPE_TX_FIXTURES")
.unwrap_or_else(|_| "ppe_tx_fixtures.bin".to_owned());
let Some(bundle) = PpeTxFixtureBundle::load_bundle(&tx_path) else {
return;
};
let state: V03State =
borsh::from_slice(&bundle.state_bytes).expect("fixture state_bytes invalid");
let tx: PrivacyPreservingTransaction =
borsh::from_slice(&bundle.tx_bytes[0]).expect("fixture tx_bytes invalid");
let signer_account_ids = tx.signer_account_ids();
let reconstructed = circuit_output_for_message(&state, &tx.message, &signer_account_ids);
let proof_bytes = tx.witness_set.proof.0.clone();
eprintln!("[DEBUG] tx.witness_set.proof bytes len = {}", proof_bytes.len());
match borsh::from_slice::<risc0_zkvm::InnerReceipt>(&proof_bytes) {
Ok(inner) => {
eprintln!("[DEBUG] proof bytes decode as InnerReceipt: Ok");
let receipt = risc0_zkvm::Receipt::new(inner, reconstructed.to_bytes());
match receipt.verify(crate::program_methods::PRIVACY_PRESERVING_CIRCUIT_ID) {
Ok(()) => eprintln!("[DEBUG] receipt.verify: Ok"),
Err(e) => eprintln!("[DEBUG] receipt.verify: Err: {e}"),
}
}
Err(e) => eprintln!("[DEBUG] proof bytes decode as InnerReceipt: Err: {e}"),
}
let fixtures_path =
std::env::var("PPE_FIXTURES").unwrap_or_else(|_| "ppe_fixtures.bin".to_owned());
let fixtures = PpeFixture::load_bundle(&fixtures_path);
if let Some(fixture) = fixtures.first() {
eprintln!(
"[DEBUG] fixture.proof_bytes len = {}, tx proof bytes == fixture.proof_bytes: {}",
fixture.proof_bytes.len(),
proof_bytes == fixture.proof_bytes
);
eprintln!(
"[DEBUG] fixture.output_bytes len = {}, reconstructed.to_bytes() == fixture.output_bytes: {}",
fixture.output_bytes.len(),
reconstructed.to_bytes() == fixture.output_bytes
);
}
}
}

View File

@ -860,7 +860,11 @@ mod tests {
let (output_0, proof_0) = execute_and_prove(
vec![
AccountWithMetadata::new(
Account { program_owner: program.id(), balance: 100, ..Account::default() },
Account {
program_owner: program.id(),
balance: 100,
..Account::default()
},
true,
AccountId::new([0x01; 32]),
),
@ -870,9 +874,10 @@ mod tests {
AccountId::for_regular_private_account(&keys_0.npk(), 0),
),
],
Program::serialize_instruction(
authenticated_transfer_core::Instruction::Transfer { amount: 10 },
).unwrap(),
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: 10,
})
.unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
@ -882,7 +887,8 @@ mod tests {
},
],
&program.clone().into(),
).expect("proof 0 should succeed");
)
.expect("proof 0 should succeed");
// ── Proof 1: public sender → private recipient ────────────────────────────
let keys_1 = test_private_account_keys_2();
@ -890,7 +896,11 @@ mod tests {
let (output_1, proof_1) = execute_and_prove(
vec![
AccountWithMetadata::new(
Account { program_owner: program.id(), balance: 200, ..Account::default() },
Account {
program_owner: program.id(),
balance: 200,
..Account::default()
},
true,
AccountId::new([0x02; 32]),
),
@ -900,9 +910,10 @@ mod tests {
AccountId::for_regular_private_account(&keys_1.npk(), 0),
),
],
Program::serialize_instruction(
authenticated_transfer_core::Instruction::Transfer { amount: 20 },
).unwrap(),
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: 20,
})
.unwrap(),
vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized {
@ -912,14 +923,19 @@ mod tests {
},
],
&program.clone().into(),
).expect("proof 1 should succeed");
)
.expect("proof 1 should succeed");
// ── Proof 2: fully private transfer ──────────────────────────────────────
let sender_keys_2 = test_private_account_keys_1();
let recipient_keys_2 = test_private_account_keys_2();
let sender_2_id = AccountId::for_regular_private_account(&sender_keys_2.npk(), 0);
let sender_2_account =
Account { program_owner: program.id(), balance: 50, nonce: Nonce(1), ..Account::default() };
let sender_2_account = Account {
program_owner: program.id(),
balance: 50,
nonce: Nonce(1),
..Account::default()
};
let sender_2_commitment = Commitment::new(&sender_2_id, &sender_2_account);
let mut cs = CommitmentSet::with_capacity(1);
cs.extend(std::slice::from_ref(&sender_2_commitment));
@ -937,9 +953,10 @@ mod tests {
AccountId::for_regular_private_account(&recipient_keys_2.npk(), 1),
),
],
Program::serialize_instruction(
authenticated_transfer_core::Instruction::Transfer { amount: 30 },
).unwrap(),
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: 30,
})
.unwrap(),
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: ssk_2_sender,
@ -954,11 +971,15 @@ mod tests {
},
],
&program.into(),
).expect("proof 2 should succeed");
)
.expect("proof 2 should succeed");
// ── Aggregate all three ───────────────────────────────────────────────────
let proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)> =
vec![(output_0, proof_0), (output_1, proof_1), (output_2, proof_2)];
let proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)> = vec![
(output_0, proof_0),
(output_1, proof_1),
(output_2, proof_2),
];
let mut env_builder = ExecutorEnv::builder();
env_builder.write(&PRIVACY_PRESERVING_CIRCUIT_ID).unwrap();
@ -966,8 +987,7 @@ mod tests {
// Outputs are written once as a word-native `Vec<&PrivacyPreservingCircuitOutput>`
// (matching `aggregator_circuit`'s `AggregatorCircuitInput`) instead of N raw
// `Vec<u8>` journal buffers — see the ppe_aggregation guest for why.
let outputs: Vec<&PrivacyPreservingCircuitOutput> =
proofs.iter().map(|(o, _)| o).collect();
let outputs: Vec<&PrivacyPreservingCircuitOutput> = proofs.iter().map(|(o, _)| o).collect();
env_builder.write(&outputs).unwrap();
let journals: Vec<Vec<u8>> = proofs.iter().map(|(o, _)| o.to_bytes()).collect();
@ -1014,8 +1034,7 @@ mod tests {
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
use test_program_methods::{PPE_AGGREGATION_ELF, PPE_AGGREGATION_ID, PpeFixture};
let path =
std::env::var("PPE_FIXTURES").unwrap_or_else(|_| "ppe_fixtures.bin".to_owned());
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() {
@ -1023,7 +1042,9 @@ mod tests {
}
if let Ok(count_str) = std::env::var("PPE_FIXTURES_COUNT") {
let count: usize = count_str.parse().expect("PPE_FIXTURES_COUNT must be a number");
let count: usize = count_str
.parse()
.expect("PPE_FIXTURES_COUNT must be a number");
fixtures.truncate(count);
}
@ -1032,7 +1053,9 @@ mod tests {
// Smaller segments lower peak prover memory (handy on memory-constrained
// GPUs) at the cost of more segments and overall proving time.
if let Ok(po2_str) = std::env::var("PPE_SEGMENT_LIMIT_PO2") {
let po2: u32 = po2_str.parse().expect("PPE_SEGMENT_LIMIT_PO2 must be a number");
let po2: u32 = po2_str
.parse()
.expect("PPE_SEGMENT_LIMIT_PO2 must be a number");
env_builder.segment_limit_po2(po2);
}
@ -1064,16 +1087,19 @@ mod tests {
let proving_ms = t0.elapsed().as_millis();
let proof_size = borsh::to_vec(&prove_info.receipt.inner).unwrap().len();
eprintln!(
"[lee::analytics] ppe_aggregation n={} proving_ms={} proof_size_bytes={} \
segments={} total_cycles={} user_cycles={}",
fixtures.len(),
proving_ms,
proof_size,
prove_info.stats.segments,
prove_info.stats.total_cycles,
prove_info.stats.user_cycles,
);
#[expect(clippy::print_stderr, reason = "benchmark result line consumed by tooling")]
{
eprintln!(
"[lee::analytics] ppe_aggregation n={} proving_ms={} proof_size_bytes={} \
segments={} total_cycles={} user_cycles={}",
fixtures.len(),
proving_ms,
proof_size,
prove_info.stats.segments,
prove_info.stats.total_cycles,
prove_info.stats.user_cycles,
);
}
prove_info
.receipt
@ -1082,7 +1108,11 @@ mod tests {
let recovered: Vec<PrivacyPreservingCircuitOutput> =
prove_info.receipt.journal.decode().unwrap();
assert_eq!(recovered.len(), fixtures.len(), "recovered output count mismatch");
assert_eq!(
recovered.len(),
fixtures.len(),
"recovered output count mismatch"
);
}
#[test]

View File

@ -404,25 +404,10 @@ impl ValidatedStateDiff {
LeeError::OutOfValidityWindow
);
// Build pre_states for proof verification
let public_pre_states: Vec<_> = message
.public_account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
let circuit_output = circuit_output_for_message(state, message, &signer_account_ids);
// 4. Proof verification
check_privacy_preserving_circuit_proof_is_valid(
&witness_set.proof,
&public_pre_states,
message,
)?;
check_privacy_preserving_circuit_proof_is_valid(&witness_set.proof, &circuit_output)?;
// 5. Commitment freshness
state.check_commitments_are_new(&message.new_commitments)?;
@ -484,13 +469,27 @@ impl ValidatedStateDiff {
}
}
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
public_pre_states: &[AccountWithMetadata],
/// Reconstructs the `PrivacyPreservingCircuitOutput` a `Message` corresponds to, using `state`
/// to fill in `public_pre_states`. Used to verify the transaction's proof here, and reused by
/// the aggregator circuit to rebuild the journal each proof verifies against.
pub fn circuit_output_for_message(
state: &V03State,
message: &Message,
) -> Result<(), LeeError> {
let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(),
signer_account_ids: &[AccountId],
) -> PrivacyPreservingCircuitOutput {
let public_pre_states = message
.public_account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
PrivacyPreservingCircuitOutput {
public_pre_states,
public_post_states: message.public_post_states.clone(),
ciphertexts: message
.encrypted_private_post_states
@ -502,9 +501,15 @@ fn check_privacy_preserving_circuit_proof_is_valid(
new_nullifiers: message.new_nullifiers.clone(),
block_validity_window: message.block_validity_window,
timestamp_validity_window: message.timestamp_validity_window,
};
}
}
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
output: &PrivacyPreservingCircuitOutput,
) -> Result<(), LeeError> {
proof
.is_valid_for(&output)
.is_valid_for(output)
.then_some(())
.ok_or(LeeError::InvalidPrivacyPreservingProof)
}

BIN
ppe_fixtures.bin Normal file

Binary file not shown.

BIN
ppe_tx_fixtures.bin Normal file

Binary file not shown.

View File

@ -9,7 +9,7 @@
use std::convert::Infallible;
use lee_core::{AggregatorCircuitInput, AggregatorCircuitOutput, Commitment, Nullifier, account::AccountId};
use lee_core::{AggregatorCircuitInput, AggregatorCircuitOutput};
use risc0_zkvm::{guest::env, serde::to_vec};
fn main() {
@ -27,6 +27,9 @@ fn main() {
.unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed"));
}
// TEMPORARY: dedup checks (items 1-3) disabled for debugging — isolating proof
// verification (item 4) to narrow down a bench_aggregator failure at n=2.
/*
// Linear-scan dedup: batches are small (n is bounded), so a `Vec` + `contains` check
// avoids the per-element hashing cost of `HashSet` in the zkVM.
let mut seen_nullifiers: Vec<Nullifier> = Vec::new();
@ -53,8 +56,10 @@ fn main() {
let mut seen_updated_account_ids: Vec<AccountId> = Vec::new();
for output in &circuit_outputs {
for (pre_state, post_state) in
output.public_pre_states.iter().zip(output.public_post_states.iter())
for (pre_state, post_state) in output
.public_pre_states
.iter()
.zip(output.public_post_states.iter())
{
if pre_state.account != *post_state {
assert!(
@ -65,6 +70,7 @@ fn main() {
}
}
}
*/
env::commit(&AggregatorCircuitOutput {
block_id,

View File

@ -62,8 +62,10 @@ fn main() {
let mut seen_updated_account_ids: Vec<AccountId> = Vec::new();
for output in &circuit_outputs {
for (pre_state, post_state) in
output.public_pre_states.iter().zip(output.public_post_states.iter())
for (pre_state, post_state) in output
.public_pre_states
.iter()
.zip(output.public_post_states.iter())
{
if pre_state.account != *post_state {
assert!(

View File

@ -23,6 +23,7 @@ impl PpeFixture {
/// Returns an empty `Vec` (and prints a skip notice) when the file does not exist,
/// so that test suites skip gracefully when fixtures have not been generated yet.
/// Any other I/O error or deserialisation failure panics with a diagnostic message.
#[must_use]
pub fn load_bundle(path: &str) -> Vec<Self> {
if !std::path::Path::new(path).exists() {
eprintln!(
@ -36,3 +37,47 @@ impl PpeFixture {
borsh::from_slice(&bytes).expect("PPE fixture bundle failed Borsh deserialisation")
}
}
/// A bundle of pre-generated `PrivacyPreservingTransaction`s plus the genesis `V03State`
/// they were proven against.
///
/// Produced by `ppe_test_data_gen` and consumed by the aggregator circuit benchmark, so
/// that transaction proving is fully decoupled from aggregation. `state_bytes` and
/// `tx_bytes` are Borsh-encoded `lee::V03State` and `lee::PrivacyPreservingTransaction`
/// values respectively, kept as raw bytes so this crate doesn't need to depend on `lee`.
///
/// Load a bundle with [`PpeTxFixtureBundle::load_bundle`].
#[derive(BorshSerialize, BorshDeserialize)]
pub struct PpeTxFixtureBundle {
/// Block id the transactions' validity windows and nonces were proven against.
pub block_id: u64,
/// Timestamp the transactions' validity windows were proven against.
pub timestamp: u64,
/// Human-readable labels identifying each transaction's scenario, in `tx_bytes` order.
pub labels: Vec<String>,
/// Borsh-encoded `V03State` containing the genesis sender accounts.
pub state_bytes: Vec<u8>,
/// Borsh-encoded `PrivacyPreservingTransaction`s, one per `labels` entry.
pub tx_bytes: Vec<Vec<u8>>,
}
impl PpeTxFixtureBundle {
/// Loads a Borsh-encoded `PpeTxFixtureBundle` from `path`.
///
/// Returns `None` (and prints a skip notice) when the file does not exist, so that
/// test suites skip gracefully when fixtures have not been generated yet. Any other
/// I/O error or deserialisation failure panics with a diagnostic message.
#[must_use]
pub fn load_bundle(path: &str) -> Option<Self> {
if !std::path::Path::new(path).exists() {
eprintln!(
"[test_program_methods] PPE tx fixture file '{path}' not found — skipping. \
Run `RISC0_DEV_MODE=1 cargo run --release -p ppe_test_data_gen` to generate it."
);
return None;
}
let bytes = std::fs::read(path)
.unwrap_or_else(|e| panic!("failed to read PPE tx fixture file '{path}': {e}"));
Some(borsh::from_slice(&bytes).expect("PPE tx fixture bundle failed Borsh deserialisation"))
}
}

View File

@ -1,4 +1,9 @@
#![expect(
clippy::print_stderr,
reason = "fixture loaders print a skip notice when fixture files are absent"
)]
pub use fixtures::{PpeFixture, PpeTxFixtureBundle};
pub mod fixtures;
pub use fixtures::PpeFixture;
include!(concat!(env!("OUT_DIR"), "/methods.rs"));

View File

@ -106,5 +106,8 @@ pub fn print_table(results: &[AggregatorBenchResult]) {
}
fn fmt_ms(ms: Option<f64>) -> String {
ms.map_or_else(|| "-".to_owned(), |v| format!("{v:.1} ({:.1}s)", v / 1_000.0))
ms.map_or_else(
|| "-".to_owned(),
|v| format!("{v:.1} ({:.1}s)", v / 1_000.0),
)
}

View File

@ -8,15 +8,18 @@ use std::{path::PathBuf, time::Instant};
use authenticated_transfer_core::Instruction;
use lee::{
PrivacyPreservingTransaction, PrivateKey, PublicKey, V03State,
aggregator_circuit::aggregate,
execute_and_prove,
privacy_preserving_transaction::circuit::{Proof, ProgramWithDependencies},
privacy_preserving_transaction::{
circuit::ProgramWithDependencies, message::Message, witness_set::WitnessSet,
},
program::Program,
program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID},
};
use lee_core::{
BlockId, InputAccountIdentity, NullifierPublicKey, SharedSecretKey, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
account::{Account, AccountId, AccountWithMetadata, Nonce},
encryption::ViewingPublicKey,
};
use risc0_zkvm::serde::to_vec;
@ -38,24 +41,38 @@ fn load_aggregator_elf(name: &str) -> anyhow::Result<Vec<u8>> {
})
}
/// Generates a public-to-private (shielded) auth-transfer pp-proof.
/// Derives a deterministic, valid `PrivateKey` for sender `tag`.
///
/// 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)> {
/// Only `seed[0]` varies; the remaining bytes are fixed at `50`, which keeps the
/// resulting 256-bit big-endian value comfortably below the secp256k1 curve order for
/// any `tag`, so the key is always valid.
fn sender_signing_key(tag: u8) -> PrivateKey {
let mut seed = [50_u8; 32];
seed[0] = tag;
PrivateKey::try_new(seed).expect("deterministic seed should be a valid private key")
}
/// Generates a public-to-private (shielded) auth-transfer pp-transaction.
///
/// The sender is a public account whose id is derived from a real signing key, so the
/// resulting transaction's signature matches its `message.public_account_ids`; 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
/// transactions can be safely aggregated in one batch.
fn prove_shielded_transfer(tag: u8) -> anyhow::Result<(AccountId, PrivacyPreservingTransaction)> {
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 (ssk, epk) = SharedSecretKey::encapsulate(&vpk);
let recipient_account_id = AccountId::for_regular_private_account(&npk, 0);
let signing_key = sender_signing_key(tag);
let sender_account_id = AccountId::from(&PublicKey::new_from_private_key(&signing_key));
let program = Program::new(AUTHENTICATED_TRANSFER_ELF.to_vec())?;
let pwd = ProgramWithDependencies::from(program);
@ -68,7 +85,7 @@ fn prove_shielded_transfer(
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([tag; 32]),
account_id: sender_account_id,
};
// Fresh private recipient account (zero balance, not yet authorized).
@ -81,15 +98,27 @@ fn prove_shielded_transfer(
let instruction_data = to_vec(&Instruction::Transfer { amount: 1_000 })?;
let identities = vec![
InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized { npk, ssk, identifier: 0 },
InputAccountIdentity::PrivateUnauthorized {
npk,
ssk,
identifier: 0,
},
];
Ok(execute_and_prove(
vec![sender, recipient],
instruction_data,
identities,
&pwd,
)?)
let (output, proof) = execute_and_prove(vec![sender, recipient], instruction_data, identities, &pwd)?;
let message = Message::try_from_circuit_output(
vec![sender_account_id],
vec![Nonce(0)],
vec![(npk, vpk, epk)],
output,
)?;
let witness_set = WitnessSet::for_message(&message, proof, &[&signing_key]);
Ok((
sender_account_id,
PrivacyPreservingTransaction::new(message, witness_set),
))
}
pub fn run(n_txs: usize, strict: bool) -> AggregatorBenchResult {
@ -115,19 +144,19 @@ pub fn run(n_txs: usize, strict: bool) -> AggregatorBenchResult {
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).
// Generate N pp-transactions with distinct private recipients (tags 1..=N).
let pp_started = Instant::now();
let proofs: Result<Vec<_>, anyhow::Error> = (0..n_txs)
let txs: 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,
let txs = match txs {
Ok(t) => t,
Err(e) => {
return AggregatorBenchResult {
label,
@ -138,18 +167,30 @@ pub fn run(n_txs: usize, strict: bool) -> AggregatorBenchResult {
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 pp_proof_bytes_per_tx = txs
.first()
.map(|(_, tx)| tx.witness_set().proof().clone().into_inner().len());
let block_id: BlockId = 1;
let timestamp = Timestamp::from(1_700_000_000_u64);
// Genesis state containing each sender's public account, matching the balance used
// when proving its transaction.
let genesis_accounts: Vec<(AccountId, u128)> = txs
.iter()
.map(|(account_id, _)| (*account_id, 1_000_000))
.collect();
let state = V03State::new_with_genesis_accounts(&genesis_accounts, vec![], timestamp);
let transactions: Vec<PrivacyPreservingTransaction> =
txs.into_iter().map(|(_, tx)| tx).collect();
let agg_started = Instant::now();
let result = aggregate(block_id, timestamp, proofs, &elf, None);
let result = aggregate(block_id, timestamp, transactions, &state, &elf, None);
let agg_prove_ms = agg_started.elapsed().as_secs_f64() * 1_000.0;
match result {

View File

@ -4,6 +4,10 @@
//! `to_bytes()`) and the raw `InnerReceipt` bytes (Borsh-encoded, from `Proof::into_inner()`).
//! The whole bundle is a Borsh-encoded `Vec<PpeFixture>`.
//!
//! Each proof is also wrapped into a `PrivacyPreservingTransaction` (signed by a real key)
//! together with the genesis `V03State` its sender accounts were proven against, and written
//! as a `PpeTxFixtureBundle` for the aggregator circuit's host-side pre-checks.
//!
//! Keys are derived deterministically from the proof index so the fixture file is
//! reproducible.
//! # Usage
@ -49,15 +53,25 @@ use authenticated_transfer_core::Instruction;
use borsh::{BorshDeserialize, BorshSerialize};
use clap::Parser;
use lee::{
execute_and_prove, privacy_preserving_transaction::circuit::ProgramWithDependencies,
PrivacyPreservingTransaction, PrivateKey, PublicKey, V03State, execute_and_prove,
privacy_preserving_transaction::{
circuit::ProgramWithDependencies,
message::Message,
witness_set::WitnessSet,
},
program::Program,
};
use lee_core::{
InputAccountIdentity, NullifierPublicKey, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata},
account::{Account, AccountId, AccountWithMetadata, Nonce},
encryption::ViewingPublicKey,
};
/// Block id and timestamp the generated transactions' validity windows and nonces are
/// proven/checked against, matching the values used by `bench_aggregator`.
const BLOCK_ID: u64 = 1;
const TIMESTAMP: u64 = 1_700_000_000;
/// Mirror of `test_program_methods::PpeFixture`. Borsh field order must stay in sync.
#[derive(BorshSerialize, BorshDeserialize)]
struct PpeFixture {
@ -66,35 +80,59 @@ struct PpeFixture {
proof_bytes: Vec<u8>,
}
/// Mirror of `test_program_methods::PpeTxFixtureBundle`. Borsh field order must stay in sync.
#[derive(BorshSerialize, BorshDeserialize)]
struct PpeTxFixtureBundle {
block_id: u64,
timestamp: u64,
labels: Vec<String>,
state_bytes: Vec<u8>,
tx_bytes: Vec<Vec<u8>>,
}
#[derive(Parser)]
#[command(
name = "ppe_test_data_gen",
about = "Generate PPE proof fixtures for aggregation testing"
)]
struct Cli {
/// Output file path for the Borsh-serialised fixture bundle.
/// Output file path for the Borsh-serialised `Vec<PpeFixture>` bundle.
#[arg(long, default_value = "ppe_fixtures.bin")]
output: PathBuf,
/// Output file path for the Borsh-serialised `PpeTxFixtureBundle`.
#[arg(long, default_value = "ppe_tx_fixtures.bin")]
tx_output: PathBuf,
/// Number of independent PPE proofs to generate.
#[arg(long, default_value_t = 16)]
count: usize,
}
/// Derives a deterministic, valid `PrivateKey` for proof index `i`.
///
/// Only `seed[0]` and `seed[1]` vary; the remaining bytes are fixed at `50`, which keeps
/// the resulting 256-bit big-endian value comfortably below the secp256k1 curve order for
/// any `seed[0]`/`seed[1]`, so the key is always valid.
fn sender_signing_key(i: usize) -> PrivateKey {
let mut seed = [50_u8; 32];
seed[0] = (i & 0xFF) as u8;
seed[1] = ((i >> 8) & 0xFF) as u8;
PrivateKey::try_new(seed).expect("deterministic seed should be a valid private key")
}
fn main() -> Result<()> {
let cli = Cli::parse();
let program = Program::authenticated_transfer_program();
let mut fixtures: Vec<PpeFixture> = Vec::with_capacity(cli.count);
let mut tx_labels: Vec<String> = Vec::with_capacity(cli.count);
let mut transactions: Vec<PrivacyPreservingTransaction> = Vec::with_capacity(cli.count);
let mut genesis_accounts: Vec<(AccountId, u128)> = Vec::with_capacity(cli.count);
for i in 0..cli.count {
let lo = (i & 0xFF) as u8;
let hi = ((i >> 8) & 0xFF) as u8;
// Non-zero bases ensure no key is accidentally all-zero.
let mut nsk = [41_u8; 32];
nsk[0] = lo;
nsk[1] = hi;
// ViewingPublicKey requires two independent 32-byte seed halves (d, z).
let mut d = [42_u8; 32];
d[0] = lo;
@ -112,14 +150,20 @@ fn main() -> Result<()> {
let label = format!("public_to_private_{i}");
let vpk = ViewingPublicKey::from_seed(&d, &z);
// Recipient: fresh private account derived from this proof's index.
let mut nsk = [41_u8; 32];
nsk[0] = lo;
nsk[1] = hi;
let npk = NullifierPublicKey::from(&nsk);
// `encapsulate_deterministic` requires `lee_core` with `test_utils` feature.
// The recipient output is at index 0 (the only private output in this scenario).
let (ssk, _epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &msg, 0);
let (ssk, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &msg, 0);
let mut sender_seed = [45_u8; 32];
sender_seed[0] = lo;
sender_seed[1] = hi;
// Sender: public account whose id is derived from a real signing key, so the
// transaction's signature matches `message.public_account_ids`.
let signing_key = sender_signing_key(i);
let sender_account_id = AccountId::from(&PublicKey::new_from_private_key(&signing_key));
let sender = AccountWithMetadata::new(
Account {
@ -128,7 +172,7 @@ fn main() -> Result<()> {
..Account::default()
},
true,
AccountId::new(sender_seed),
sender_account_id,
);
let recipient = AccountWithMetadata::new(
Account::default(),
@ -136,9 +180,8 @@ fn main() -> Result<()> {
AccountId::for_regular_private_account(&npk, 0),
);
let instruction =
Program::serialize_instruction(Instruction::Transfer { amount })
.context("serialise instruction")?;
let instruction = Program::serialize_instruction(Instruction::Transfer { amount })
.context("serialise instruction")?;
eprintln!(
"[ppe_test_data_gen] ({}/{}) proving '{label}' ...",
@ -161,7 +204,7 @@ fn main() -> Result<()> {
)
.with_context(|| format!("execute_and_prove for '{label}'"))?;
let proof_bytes = proof.into_inner();
let proof_bytes = proof.clone().into_inner();
let output_bytes = output.to_bytes();
eprintln!(
@ -173,10 +216,23 @@ fn main() -> Result<()> {
);
fixtures.push(PpeFixture {
label,
label: label.clone(),
output_bytes,
proof_bytes,
});
let message = Message::try_from_circuit_output(
vec![sender_account_id],
vec![Nonce(0)],
vec![(npk, vpk, epk)],
output,
)
.with_context(|| format!("build message for '{label}'"))?;
let witness_set = WitnessSet::for_message(&message, proof, &[&signing_key]);
transactions.push(PrivacyPreservingTransaction::new(message, witness_set));
genesis_accounts.push((sender_account_id, amount + 10));
tx_labels.push(label);
}
let bundle = borsh::to_vec(&fixtures).context("serialise fixture bundle")?;
@ -189,5 +245,27 @@ fn main() -> Result<()> {
cli.output.display(),
);
let state = V03State::new_with_genesis_accounts(&genesis_accounts, vec![], TIMESTAMP);
let tx_bundle = PpeTxFixtureBundle {
block_id: BLOCK_ID,
timestamp: TIMESTAMP,
labels: tx_labels,
state_bytes: borsh::to_vec(&state).context("serialise genesis state")?,
tx_bytes: transactions
.iter()
.map(borsh::to_vec)
.collect::<Result<_, _>>()
.context("serialise transactions")?,
};
let tx_bundle_bytes = borsh::to_vec(&tx_bundle).context("serialise tx fixture bundle")?;
std::fs::write(&cli.tx_output, &tx_bundle_bytes).context("write tx output file")?;
eprintln!(
"[ppe_test_data_gen] wrote {} transactions ({} bytes total) -> {}",
tx_bundle.tx_bytes.len(),
tx_bundle_bytes.len(),
cli.tx_output.display(),
);
Ok(())
}