lez-fuzzing/fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs

229 lines
8.9 KiB
Rust
Raw Normal View History

#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
//! Fuzz target: privacy-preserving encoding invariants.
//!
//! Tests that `to_bytes` / `from_bytes` round-trips work correctly for the
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
//! privacy-preserving `Message` type, and that `from_circuit_output`
2026-06-16 17:37:36 +08:00
//! maps each circuit-output field onto the resulting `Message` unchanged.
//!
//! `PrivacyPreservingTransaction` is also tested for serialisation stability
//! (non-empty, deterministic bytes) without requiring a real ZK receipt.
use nssa::{
AccountId, PrivateKey, PublicKey,
PrivacyPreservingTransaction,
privacy_preserving_transaction::{
Message as PPMessage,
WitnessSet as PPWitnessSet,
circuit::Proof,
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
message::PublicActionWithID,
},
};
use nssa_core::{
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
PrivacyPreservingCircuitOutput, PublicAction,
account::{Account, AccountWithMetadata, Nonce},
program::{BlockValidityWindow, TimestampValidityWindow},
};
/// Build a minimal `Message` with no private state.
fn minimal_message() -> PPMessage {
let addr = AccountId::from(
&PublicKey::new_from_private_key(
&PrivateKey::try_new([1_u8; 32]).expect("known-good"),
),
);
PPMessage {
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
public_actions: vec![PublicActionWithID {
account_id: addr,
post_state: Account::default(),
}],
nonces: vec![Nonce::from(0_u128)],
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
private_actions: vec![],
block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
}
}
fuzz_props::fuzz_entry!(|data: &[u8]| {
// ── INVARIANT [MessageEncodingRoundtrip] ──────────────────────────────────
// `Message::to_bytes()` followed by `Message::from_bytes()` must reproduce
// the original message. Catches mutations that return `vec![]`, `vec![0]`,
// or `vec![1]` — these break round-trip identity.
{
let msg = minimal_message();
let encoded = msg.to_bytes();
// Non-empty: catches `→ vec![]`
assert!(
!encoded.is_empty(),
"INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
Message::to_bytes must not return an empty vec",
);
let decoded = PPMessage::from_bytes(&encoded)
.expect("INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
from_bytes(to_bytes(msg)) must succeed");
let re_encoded = decoded.to_bytes();
assert_eq!(
encoded,
re_encoded,
"INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
encode(decode(encode(msg))) != encode(msg)",
);
}
// ── INVARIANT [TxEncodingNonEmpty] / [TxEncodingDeterministic] ────────────
// `PrivacyPreservingTransaction::to_bytes()` must return a non-empty byte
// slice and be deterministic. Catches mutations that return `vec![]` etc.
{
let key = PrivateKey::try_new([1_u8; 32]).expect("known-good");
let msg = minimal_message();
let proof = Proof::from_inner(vec![0xDE_u8, 0xAD, 0xBE, 0xEF]);
let ws = PPWitnessSet::for_message(&msg, proof, &[&key]);
let tx = PrivacyPreservingTransaction::new(msg, ws);
let bytes1 = tx.to_bytes();
assert!(
!bytes1.is_empty(),
"INVARIANT VIOLATION [TxEncodingNonEmpty]: \
PrivacyPreservingTransaction::to_bytes must not be empty",
);
let bytes2 = tx.to_bytes();
assert_eq!(
bytes1,
bytes2,
"INVARIANT VIOLATION [TxEncodingDeterministic]: \
to_bytes must be deterministic called twice, got different results",
);
// Verify round-trip for the full transaction:
let decoded = PrivacyPreservingTransaction::from_bytes(&bytes1)
.expect("INVARIANT VIOLATION: round-trip decode must succeed");
assert_eq!(
bytes1,
decoded.to_bytes(),
"INVARIANT VIOLATION [TxEncodingDeterministic]: \
encode(decode(encode(tx))) != encode(tx)",
);
}
2026-06-16 17:37:36 +08:00
// ── INVARIANT [CircuitOutputMapping] ──────────────────────────────────────
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
// `from_circuit_output` carries each circuit-output field onto the resulting
// `Message` unchanged — every public action's pre-state account id is paired with its
// post-state, private actions are carried verbatim — and threads through the
// caller-supplied nonces. The function performs no validation of its own, so assert
// the field mapping, which catches a mutation that drops, swaps, or defaults any
// carried field.
{
2026-06-16 17:37:36 +08:00
let addr = AccountId::from(
&PublicKey::new_from_private_key(
&PrivateKey::try_new([1_u8; 32]).expect("known-good"),
),
);
let nonces = vec![Nonce::from(7_u128)];
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
let pre_state = Account::default();
let post_state = Account {
balance: 42,
..Account::default()
};
2026-06-16 17:37:36 +08:00
let output = PrivacyPreservingCircuitOutput {
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
public_actions: vec![PublicAction {
pre: AccountWithMetadata::new(pre_state, true, addr),
post: post_state.clone(),
}],
private_actions: vec![],
block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
};
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
let msg = PPMessage::from_circuit_output(nonces.clone(), output);
2026-06-16 17:37:36 +08:00
assert_eq!(
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
msg.public_account_ids(),
vec![addr],
2026-06-16 17:37:36 +08:00
"INVARIANT VIOLATION [CircuitOutputMapping]: \
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
public action account ids not carried from the circuit output's pre-states",
);
2026-06-16 17:37:36 +08:00
assert_eq!(
msg.nonces, nonces,
"INVARIANT VIOLATION [CircuitOutputMapping]: nonces not threaded through unchanged",
);
assert_eq!(
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
msg.public_actions,
vec![PublicActionWithID {
account_id: addr,
post_state,
}],
2026-06-16 17:37:36 +08:00
"INVARIANT VIOLATION [CircuitOutputMapping]: \
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
public post-states not carried from the circuit output",
2026-06-16 17:37:36 +08:00
);
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
assert!(
msg.private_actions.is_empty(),
2026-06-16 17:37:36 +08:00
"INVARIANT VIOLATION [CircuitOutputMapping]: \
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
private actions must be carried verbatim (here: empty)",
);
}
// ── Raw fuzz decode tests ─────────────────────────────────────────────────
// Fuzz the Message decoder for no-panic and canonical round-trip.
{
// No-panic on arbitrary bytes:
let _ = PPMessage::from_bytes(data);
// Canonical round-trip: if fuzz bytes decode, re-encoding must reproduce them.
if let Ok(msg) = PPMessage::from_bytes(data) {
let re_encoded = msg.to_bytes();
assert_eq!(
data,
re_encoded.as_slice(),
"INVARIANT VIOLATION: PP Message decoded from raw bytes but \
re-encoding differs (non-canonical encoding accepted)",
);
}
}
// ── Varied-size message round-trips ──────────────────────────────────────
// Verify round-trip for several multi-account messages.
for n_accounts in [0, 1, 2, 3] {
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
let mut public_actions = Vec::new();
let mut nonces = Vec::new();
for i in 0..n_accounts {
let key_bytes = [i + 1_u8; 32];
if let Ok(key) = PrivateKey::try_new(key_bytes) {
let pk = PublicKey::new_from_private_key(&key);
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
public_actions.push(PublicActionWithID {
account_id: AccountId::from(&pk),
post_state: Account::default(),
});
nonces.push(Nonce::from(i as u128));
}
}
let msg = PPMessage {
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
public_actions,
nonces,
chore: sync with LEZ main (bundled public/private actions) Upstream LEZ merged the parallel Message vectors into action structs (f16c3bd1e, 75326d031, e13b4cb23): * `Message` now carries `public_actions: Vec<PublicActionWithID>` (account id paired with its post-state) and `private_actions: Vec<PrivateAction>` (nullifier + root + commitment + encrypted post state bundled per action) instead of six parallel vectors. * `PrivacyPreservingCircuitOutput` mirrors the same bundling (`public_actions: Vec<PublicAction>` with pre/post halves). * `Message::try_from_circuit_output(ids, nonces, output)` became the infallible `Message::from_circuit_output(nonces, output)`. * `EncryptionScheme::encrypt` keys the note on the nullifier and dropped the output-index argument. * `V03State::default()` seeds the commitment set with the protocol's dummy commitment, so `root_history` contains the live root from genesis — a nullifier bound to the live root is valid on the first transaction (previously the history started empty). Harness changes: * `synthesize_passing_proof` reconstructs the journal from bundled `PublicAction`s, mirroring the validator byte-for-byte. * `arb_privacy_preserving_tx` generates 1..=3 deduplicated `PrivateAction`s (the length-mismatch and empty-commitments shapes are unrepresentable under the paired layout and were dropped) and pairs every public account id with its post-state. * `arb_conflicting_nullifier_pair` builds the shared-nullifier pair as two private actions with distinct commitments. * Encoding/witness targets build messages via the action structs; the circuit-output-mapping invariant asserts the pre-id/post pairing. * Generator-contract and proof-synthesis tests updated accordingly (48/48 pass, with and without RISC0_DEV_MODE). Bump the pinned LEZ SHA to d6e4ae69 and refresh both lockfiles. Fixes the scheduled lez-compat failure (run 30977103292).
2026-08-05 09:45:21 +02:00
private_actions: vec![],
block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
};
let encoded = msg.to_bytes();
assert!(
!encoded.is_empty(),
"INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
Message::to_bytes must not be empty for a {n_accounts}-account message",
);
let decoded = PPMessage::from_bytes(&encoded)
.expect("round-trip must succeed for well-formed message");
assert_eq!(
encoded,
decoded.to_bytes(),
"INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
round-trip failed for {n_accounts}-account message",
);
}
});