add test for empty intersection in circuit

This commit is contained in:
Sergio Chouhy 2026-03-25 17:32:11 -03:00
parent abc30c0ce0
commit 79d70b3a66
31 changed files with 131 additions and 11 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -205,14 +205,16 @@ impl ValidityWindow {
self.to
}
/// Sets the inclusive lower bound. Returns `Err` if the updated window would be empty or inverted.
/// Sets the inclusive lower bound. Returns `Err` if the updated window would be empty or
/// inverted.
pub fn set_from(&mut self, id: Option<BlockId>) -> Result<(), InvalidWindow> {
let prev = self.from;
self.from = id;
self.check_window().inspect_err(|_| self.from = prev)
}
/// Sets the exclusive upper bound. Returns `Err` if the updated window would be empty or inverted.
/// Sets the exclusive upper bound. Returns `Err` if the updated window would be empty or
/// inverted.
pub fn set_to(&mut self, id: Option<BlockId>) -> Result<(), InvalidWindow> {
let prev = self.to;
self.to = id;

View File

@ -174,12 +174,13 @@ mod tests {
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
use nssa_core::{
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
};
use super::*;
use crate::{
error::NssaError,
privacy_preserving_transaction::circuit::execute_and_prove,
program::Program,
state::{
@ -364,4 +365,46 @@ mod tests {
.unwrap();
assert_eq!(recipient_post, expected_private_account_2);
}
#[test]
fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&account_keys.npk()),
);
let validity_window_chain_caller = Program::validity_window_chain_caller();
let validity_window = Program::validity_window();
let instruction = Program::serialize_instruction((
Some(1_u64),
Some(4_u64),
validity_window.id(),
Some(4_u64),
Some(7_u64),
))
.unwrap();
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let program_with_deps = ProgramWithDependencies::new(
validity_window_chain_caller,
[(validity_window.id(), validity_window)].into(),
);
let result = execute_and_prove(
vec![pre],
instruction,
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![],
vec![None],
&program_with_deps,
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
}

View File

@ -292,6 +292,12 @@ mod tests {
// `program_methods`
Self::new(VALIDITY_WINDOW_ELF.to_vec()).unwrap()
}
#[must_use]
pub fn validity_window_chain_caller() -> Self {
use test_program_methods::VALIDITY_WINDOW_CHAIN_CALLER_ELF;
Self::new(VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec()).unwrap()
}
}
#[test]

View File

@ -19,6 +19,10 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.balance = account_post.balance.saturating_sub(balance_to_burn);
ProgramOutput::new(instruction_words, vec![pre], vec![AccountPostState::new(account_post)])
.write();
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(account_post)],
)
.write();
}

View File

@ -16,6 +16,10 @@ fn main() {
.checked_add(1)
.expect("Balance overflow");
ProgramOutput::new(instruction_words, vec![pre], vec![AccountPostState::new(account_post)])
.write();
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(account_post)],
)
.write();
}

View File

@ -13,6 +13,10 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.nonce.public_account_nonce_increment();
ProgramOutput::new(instruction_words, vec![pre], vec![AccountPostState::new(account_post)])
.write();
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(account_post)],
)
.write();
}

View File

@ -13,6 +13,10 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7];
ProgramOutput::new(instruction_words, vec![pre], vec![AccountPostState::new(account_post)])
.write();
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(account_post)],
)
.write();
}

View File

@ -0,0 +1,53 @@
use nssa_core::program::{
AccountPostState, BlockId, ChainedCall, ProgramId, ProgramInput, ProgramOutput,
read_nssa_inputs,
};
use risc0_zkvm::serde::to_vec;
/// A program that sets a validity window on its output and chains to another program with a
/// potentially different validity window.
///
/// Instruction: (from_id, until_id, chained_program_id, chained_from, chained_until)
/// The initial output uses [from_id, until_id) and chains to `chained_program_id` with
/// [chained_from, chained_until).
type Instruction = (
Option<BlockId>,
Option<BlockId>,
ProgramId,
Option<BlockId>,
Option<BlockId>,
);
fn main() {
let (
ProgramInput {
pre_states,
instruction: (from_id, until_id, chained_program_id, chained_from, chained_until),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let [pre] = <[_; 1]>::try_from(pre_states.clone())
.unwrap_or_else(|_| panic!("Expected exactly one pre state"));
let post = pre.account.clone();
let chained_instruction = to_vec(&(chained_from, chained_until)).unwrap();
let chained_call = ChainedCall {
program_id: chained_program_id,
instruction_data: chained_instruction,
pre_states,
pda_seeds: vec![],
};
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(post)],
)
.valid_from_id(from_id)
.unwrap()
.valid_until_id(until_id)
.unwrap()
.with_chained_calls(vec![chained_call])
.write();
}