diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs index bc1463ce8..6a21666b5 100644 --- a/lez/sequencer/core/src/tests.rs +++ b/lez/sequencer/core/src/tests.rs @@ -2907,3 +2907,113 @@ fn loader_rejects_redeploying_an_already_deployed_program() { "Redeploying to an already-claimed program account should fail, but got: {result:?}" ); } + +#[test] +fn loader_rejects_invalid_bytecode() { + let mut state = V03State::new(); + + // execute_deploy panics on compute_image_id before it ever looks at the target account, so + // any account works here. + let bytecode = b"this is not a valid RISC0 program binary".to_vec(); + let target = AccountId::new([7; 32]); + + let tx = deploy_transaction(target, bytecode); + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!( + result.is_err(), + "Deploying invalid bytecode should fail, but got: {result:?}" + ); +} + +#[test] +fn loader_rejects_wrong_target_account() { + let mut state = V03State::new(); + + let bytecode = test_programs::claimer().elf().to_vec(); + // Deliberately not the PDA this bytecode's image_id would derive to. + let wrong_target = AccountId::new([7; 32]); + + let tx = deploy_transaction(wrong_target, bytecode); + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!( + result.is_err(), + "Deploying to the wrong target account should fail, but got: {result:?}" + ); +} + +#[test] +fn loader_rejects_wrong_number_of_accounts() { + let loader_id: ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into(); + let mut state = V03State::new(); + + let bytecode = test_programs::claimer().elf().to_vec(); + let image_id: ProgramId = risc0_binfmt::compute_image_id(&bytecode).unwrap().into(); + let target = loader_core::deploy_account_id(loader_id, image_id, 0, AccountId::default()); + let extra = AccountId::new([9; 32]); + + let message = lee::public_transaction::Message::try_new( + loader_id, + vec![target, extra], + vec![], + loader_core::Instruction::Deploy { bytecode }, + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!( + result.is_err(), + "Deploying with the wrong number of accounts should fail, but got: {result:?}" + ); +} + +#[test] +#[ignore = "known limitation: the forwarding program has to carry the deployed bytecode through \ + its own instruction_data to build the chained call, which blows the interpreted \ + 32M-cycle public-execution cap for any realistically-sized program (the native \ + Deploy fast-path only covers the loader's own execution, not the caller's). Root \ + cause is ChainedCall/Message still referencing programs by ProgramId rather than \ + AccountId, which also means dispatch can't locate a Deploy-created (PDA-addressed) \ + program at all; tracked for marvin/program-as-account-3-1."] +fn loader_deploys_program_via_chained_call() { + let loader_id: ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into(); + let forwarder = test_programs::chained_call_forwarder(); + let mut state = V03State::new().with_programs([forwarder.clone()]); + + let bytecode = test_programs::claimer().elf().to_vec(); + let image_id: ProgramId = risc0_binfmt::compute_image_id(&bytecode).unwrap().into(); + let target = loader_core::deploy_account_id(loader_id, image_id, 0, AccountId::default()); + + let inner_instruction_data = lee::program::Program::serialize_instruction( + loader_core::Instruction::Deploy { + bytecode: bytecode.clone(), + }, + ) + .unwrap(); + + let message = lee::public_transaction::Message::try_new( + forwarder.id(), + vec![target], + vec![], + (loader_id, inner_instruction_data), + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + state + .transition_from_public_transaction(&tx, 1, 0) + .expect("Deploy via chained call should succeed"); + + let deployed = state.get_account_by_id(target); + assert_eq!(deployed.program_owner, RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID); + + let program_data = loader_core::ProgramData::try_from(&deployed.data) + .expect("deployed account data should decode as ProgramData"); + assert_eq!(program_data.image_id, image_id); + assert_eq!(program_data.elf_segment, bytecode); +} diff --git a/test_programs/guest/src/bin/chained_call_forwarder.rs b/test_programs/guest/src/bin/chained_call_forwarder.rs new file mode 100644 index 000000000..23ceb8baf --- /dev/null +++ b/test_programs/guest/src/bin/chained_call_forwarder.rs @@ -0,0 +1,45 @@ +use lee_core::program::{ + AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_lee_inputs, +}; + +/// Forwards a single chained call to `target_program_id` with `instruction_data`, passing +/// through whatever `pre_states` this program itself was invoked with unchanged. +/// +/// Exists purely as test infrastructure: lets a test exercise "program X invokes program Y via +/// a chained call" for an arbitrary Y and instruction, without needing a purpose-built guest for +/// every target program under test. +type Instruction = (ProgramId, Vec); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (target_program_id, instruction_data), + }, + instruction_words, + ) = read_lee_inputs::(); + + let post_states = pre_states + .iter() + .map(|pre| AccountPostState::new(pre.account.clone())) + .collect(); + + let chained_call = ChainedCall { + program_id: target_program_id, + instruction_data, + pre_states: pre_states.clone(), + pda_seeds: vec![], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + post_states, + ) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/test_programs/src/lib.rs b/test_programs/src/lib.rs index f1df3e961..d6ad8d306 100644 --- a/test_programs/src/lib.rs +++ b/test_programs/src/lib.rs @@ -56,6 +56,19 @@ pub const fn pda_spend_proxy() -> Program { Program::new_unchecked(PDA_SPEND_PROXY_ID, Cow::Borrowed(PDA_SPEND_PROXY_ELF)) } +#[must_use] +#[inline] +pub const fn chained_call_forwarder() -> Program { + use guests::{CHAINED_CALL_FORWARDER_ELF, CHAINED_CALL_FORWARDER_ID, CHAINED_CALL_FORWARDER_PATH}; + + let _unused = CHAINED_CALL_FORWARDER_PATH; + + Program::new_unchecked( + CHAINED_CALL_FORWARDER_ID, + Cow::Borrowed(CHAINED_CALL_FORWARDER_ELF), + ) +} + #[must_use] #[inline] pub const fn time_locked_transfer() -> Program {