mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-27 04:11:08 +00:00
test(lee): add Deploy validation-failure and chained-call coverage
Adds tests for the remaining execute_deploy failure modes (invalid bytecode, wrong target account, wrong number of accounts) and a generic chained_call_forwarder test guest to exercise Deploy invoked via a chained call rather than only top-level. The chained-call test is #[ignore]d: it surfaces a real limitation, not a bug in the test. 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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<u32>);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (target_program_id, instruction_data),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_lee_inputs::<Instruction>();
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user