From c7b415b2f4b57b46b0544197c7d95c4fd553219d Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 12 Nov 2025 19:55:02 -0300 Subject: [PATCH] add max depth reached error for chained calls --- nssa/src/error.rs | 3 ++ nssa/src/public_transaction/transaction.rs | 8 +++- nssa/src/state.rs | 45 +++++++++++++++++-- .../guest/src/bin/chain_caller.rs | 22 ++++----- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 8ed9657..2299731 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -54,4 +54,7 @@ pub enum NssaError { #[error("Program already exists")] ProgramAlreadyExists, + + #[error("Chain of calls too long")] + MaxChainedCallsDepthExceeded, } diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index cfee8db..199d60d 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -11,6 +11,7 @@ use crate::{ V02State, error::NssaError, public_transaction::{Message, WitnessSet}, + state::MAX_NUMBER_CHAINED_CALLS, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -18,7 +19,6 @@ pub struct PublicTransaction { message: Message, witness_set: WitnessSet, } -const MAX_NUMBER_CHAINED_CALLS: usize = 10; impl PublicTransaction { pub fn new(message: Message, witness_set: WitnessSet) -> Self { @@ -183,7 +183,11 @@ impl PublicTransaction { }; } - Ok(state_diff) + if chained_calls.is_empty() { + Ok(state_diff) + } else { + Err(NssaError::MaxChainedCallsDepthExceeded) + } } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index b0f60eb..d53609c 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -10,6 +10,8 @@ use nssa_core::{ }; use std::collections::{HashMap, HashSet}; +pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; + pub(crate) struct CommitmentSet { merkle_tree: MerkleTree, commitments: HashMap, @@ -251,6 +253,7 @@ pub mod tests { program::Program, public_transaction, signature::PrivateKey, + state::MAX_NUMBER_CHAINED_CALLS, }; use nssa_core::{ @@ -2079,7 +2082,7 @@ pub mod tests { } #[test] - fn test_chained_call() { + fn test_chained_call_succeeds() { let program = Program::chain_caller(); let key = PrivateKey::try_new([1; 32]).unwrap(); let address = Address::from(&PublicKey::new_from_private_key(&key)); @@ -2091,8 +2094,8 @@ pub mod tests { let from_key = key; let to = Address::new([2; 32]); let amount: u128 = 37; - let instruction: (u128, ProgramId) = - (amount, Program::authenticated_transfer_program().id()); + let instruction: (u128, ProgramId, u32) = + (amount, Program::authenticated_transfer_program().id(), 2); let expected_to_post = Account { program_owner: Program::chain_caller().id(), @@ -2118,4 +2121,40 @@ pub mod tests { assert_eq!(from_post.balance, initial_balance - 2 * amount); assert_eq!(to_post, expected_to_post); } + + #[test] + fn test_execution_fails_if_chained_calls_exceeds_depth() { + let program = Program::chain_caller(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let address = Address::from(&PublicKey::new_from_private_key(&key)); + let initial_balance = 100; + let initial_data = [(address, initial_balance)]; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let from = address; + let from_key = key; + let to = Address::new([2; 32]); + let amount: u128 = 0; + let instruction: (u128, ProgramId, u32) = ( + amount, + Program::authenticated_transfer_program().id(), + MAX_NUMBER_CHAINED_CALLS as u32 + 1, + ); + + let message = public_transaction::Message::try_new( + program.id(), + vec![to, from], //The chain_caller program permutes the account order in the chain call + vec![0], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx); + assert!(matches!( + result, + Err(NssaError::MaxChainedCallsDepthExceeded) + )); + } } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index c4a548b..5ebb6e6 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -3,14 +3,14 @@ use nssa_core::program::{ }; use risc0_zkvm::serde::to_vec; -type Instruction = (u128, ProgramId); +type Instruction = (u128, ProgramId, u32); -/// A program that calls another program twice. +/// A program that calls another program `num_chain_calls` times. /// It permutes the order of the input accounts on the subsequent call fn main() { let ProgramInput { pre_states, - instruction: (balance, program_id), + instruction: (balance, program_id, num_chain_calls), } = read_nssa_inputs::(); let [sender_pre, receiver_pre] = match pre_states.try_into() { @@ -20,19 +20,21 @@ fn main() { let instruction_data = to_vec(&balance).unwrap(); - let chained_call = vec![ + let mut chained_call = vec![ ChainedCall { program_id, instruction_data: instruction_data.clone(), account_indices: vec![0, 1], - }, - ChainedCall { - program_id, - instruction_data, - account_indices: vec![1, 0], // <- Account order permutation here - }, + }; + num_chain_calls as usize - 1 ]; + chained_call.push(ChainedCall { + program_id, + instruction_data, + account_indices: vec![1, 0], // <- Account order permutation here + }); + write_nssa_outputs_with_chained_call( vec![sender_pre.clone(), receiver_pre.clone()], vec![sender_pre.account, receiver_pre.account],