refactor(lee): drop the loader guest binary, native-only Deploy

Deploy is not intended to ever run privately, and public dispatch
already always takes the native fast-path for it, so the guest ELF
had no real execution path left. Removes lez/programs/loader's guest
binary crate, its [[bin]] wiring, programs::loader(), and the
now-pointless guest-vs-native equivalence test along with the
test-only Program::execute_for_test it depended on.

loader_core (Instruction, ProgramData, execute_deploy) stays — native
dispatch still calls it directly. Deploy is now honestly just native
dispatch logic with a program-shaped interface, not a program with a
guest binary nobody executes.
This commit is contained in:
Marvin Jones
2026-08-22 17:39:23 -04:00
parent dcc0c4b950
commit d81ea687ca
9 changed files with 3 additions and 148 deletions
Generated
-9
View File
@@ -6378,14 +6378,6 @@ dependencies = [
"serde",
]
[[package]]
name = "loader_program"
version = "0.1.0"
dependencies = [
"lee_core",
"loader_core",
]
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -9442,7 +9434,6 @@ dependencies = [
"faucet_core",
"lee",
"lee_core",
"loader_core",
"ping_core",
"risc0-zkvm",
"sequencer_stake_core",
-2
View File
@@ -57,7 +57,6 @@ members = [
"lez/programs/ping_sender",
"lez/programs/ping_receiver",
"lez/programs/sequencer_stake",
"lez/programs/loader",
"lez/programs/loader/core",
"lez/cross_zone",
@@ -127,7 +126,6 @@ wrapped_token_core = { path = "lez/programs/wrapped_token/core" }
ping_core = { path = "lez/programs/ping_core" }
sequencer_stake_core = { path = "lez/programs/sequencer_stake/core" }
loader_core = { path = "lez/programs/loader/core" }
loader_program = { path = "lez/programs/loader" }
cross_zone = { path = "lez/cross_zone" }
build_utils = { path = "build_utils" }
test_programs = { path = "test_programs" }
Binary file not shown.
+1 -40
View File
@@ -57,25 +57,10 @@ impl Program {
caller_account_id: Option<AccountId>,
pre_states: &[AccountWithMetadata],
instruction_data: &InstructionData,
) -> Result<ProgramOutput, LeeError> {
self.execute_with_session_limit(
caller_program_id,
pre_states,
instruction_data,
MAX_NUM_CYCLES_PUBLIC_EXECUTION,
)
}
fn execute_with_session_limit(
&self,
caller_program_id: Option<ProgramId>,
pre_states: &[AccountWithMetadata],
instruction_data: &InstructionData,
session_limit: u64,
) -> Result<ProgramOutput, LeeError> {
// Write inputs to the program
let mut env_builder = ExecutorEnv::builder();
env_builder.session_limit(Some(session_limit));
env_builder.session_limit(Some(MAX_NUM_CYCLES_PUBLIC_EXECUTION));
Self::write_inputs(
AccountId::from(self.id),
caller_account_id,
@@ -125,29 +110,5 @@ impl Program {
}
}
#[cfg(feature = "test-utils")]
impl Program {
/// Test-only: like `execute`, but with a session limit far above the production
/// `MAX_NUM_CYCLES_PUBLIC_EXECUTION` cap.
///
/// Exists so tests can run a real, possibly large guest program to completion — e.g.
/// comparing the loader guest's actual execution against its native dispatch fast-path
/// (see `RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID`) — without hitting the budget that exists
/// specifically to bound production dispatch cost, which this is deliberately not testing.
pub fn execute_for_test(
&self,
caller_program_id: Option<ProgramId>,
pre_states: &[AccountWithMetadata],
instruction_data: &InstructionData,
) -> Result<ProgramOutput, LeeError> {
self.execute_with_session_limit(
caller_program_id,
pre_states,
instruction_data,
MAX_NUM_CYCLES_PUBLIC_EXECUTION * 64,
)
}
}
#[cfg(test)]
mod tests;
-7
View File
@@ -89,11 +89,6 @@ name = "sequencer_stake"
path = "sequencer_stake/src/main.rs"
required-features = ["programs"]
[[bin]]
name = "loader"
path = "loader/src/main.rs"
required-features = ["programs"]
[features]
# TODO: Uncomment once https://github.com/risc0/risc0/issues/3772 is resolved.
# default = ["artifacts"]
@@ -125,7 +120,6 @@ programs = [
"dep:wrapped_token_core",
"dep:ping_core",
"dep:sequencer_stake_core",
"dep:loader_core",
]
[dependencies]
@@ -147,7 +141,6 @@ bridge_lock_core = { workspace = true, optional = true }
wrapped_token_core = { workspace = true, optional = true }
ping_core = { workspace = true, optional = true }
sequencer_stake_core = { workspace = true, optional = true }
loader_core = { workspace = true, optional = true }
amm_program = { path = "amm", optional = true }
associated_token_account_program = { path = "associated_token_account", optional = true }
-9
View File
@@ -1,9 +0,0 @@
[package]
name = "loader_program"
version = "0.1.0"
edition = "2024"
license = { workspace = true }
[dependencies]
lee_core.workspace = true
loader_core.workspace = true
-31
View File
@@ -1,31 +0,0 @@
use lee_core::program::{ProgramInput, ProgramOutput, read_lee_inputs};
use loader_core::Instruction;
fn main() {
let (
ProgramInput {
self_program_id,
caller_program_id,
pre_states,
instruction,
},
instruction_words,
) = read_lee_inputs::<Instruction>();
let pre_states_clone = pre_states.clone();
let post_states = match instruction {
Instruction::Deploy { bytecode } => {
loader_core::execute_deploy(self_program_id, pre_states, bytecode)
}
};
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
pre_states_clone,
post_states,
)
.write();
}
+2 -9
View File
@@ -13,8 +13,8 @@ mod inner {
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, BRIDGE_ELF, BRIDGE_ID,
BRIDGE_LOCK_ELF, BRIDGE_LOCK_ID, CLOCK_ELF, CLOCK_ID, CROSS_ZONE_INBOX_ELF,
CROSS_ZONE_INBOX_ID, CROSS_ZONE_OUTBOX_ELF, CROSS_ZONE_OUTBOX_ID, FAUCET_ELF, FAUCET_ID,
LOADER_ELF, LOADER_ID, PINATA_ELF, PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID,
PING_RECEIVER_ELF, PING_RECEIVER_ID, PING_SENDER_ELF, PING_SENDER_ID, SEQUENCER_STAKE_ELF,
PINATA_ELF, PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, PING_RECEIVER_ELF,
PING_RECEIVER_ID, PING_SENDER_ELF, PING_SENDER_ID, SEQUENCER_STAKE_ELF,
SEQUENCER_STAKE_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID, WRAPPED_TOKEN_ELF,
WRAPPED_TOKEN_ID,
};
@@ -133,12 +133,6 @@ mod inner {
Program::new_unchecked(SEQUENCER_STAKE_ID, Cow::Borrowed(SEQUENCER_STAKE_ELF))
}
#[must_use]
#[inline]
pub const fn loader() -> Program {
Program::new_unchecked(LOADER_ID, Cow::Borrowed(LOADER_ELF))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -189,7 +183,6 @@ mod inner {
(BRIDGE_LOCK_ELF, BRIDGE_LOCK_ID),
(WRAPPED_TOKEN_ELF, WRAPPED_TOKEN_ID),
(SEQUENCER_STAKE_ELF, SEQUENCER_STAKE_ID),
(LOADER_ELF, LOADER_ID),
];
for (elf, expected_id) in cases {
let program = Program::new((*elf).into()).unwrap();
-41
View File
@@ -3731,44 +3731,3 @@ fn loader_rejects_redeploying_an_already_deployed_program() {
"Redeploying to an already-claimed program account should fail, but got: {result:?}"
);
}
/// Runs the real `loader_program` guest ELF end-to-end (via `Program::execute_for_test`, which
/// uses a session limit well above production's `MAX_NUM_CYCLES_PUBLIC_EXECUTION` since running
/// a real guest to completion for this comparison is the whole point) and checks its output
/// against calling `execute_deploy` natively with the same inputs.
///
/// This doesn't re-verify `execute_deploy`'s own logic — the guest and the native dispatch path
/// both call that exact function, so it can't diverge between them. What this catches is drift
/// in the thin wrapper code on each side: the guest's `read_lee_inputs`/`ProgramOutput::write`
/// glue in `loader_program::main`, versus dispatch's manual `risc0_zkvm::serde::from_slice` and
/// `ProgramOutput::new(..)` construction in `from_public_transaction`.
#[test]
fn loader_native_execution_matches_real_guest_execution() {
let loader = programs::loader();
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 pre_states = vec![lee_core::account::AccountWithMetadata::new(
Account::default(),
false,
target,
)];
let instruction_data =
lee::program::Program::serialize_instruction(loader_core::Instruction::Deploy {
bytecode: bytecode.clone(),
})
.unwrap();
let guest_output = loader
.execute_for_test(None, &pre_states, &instruction_data)
.expect("real guest execution should succeed");
let native_post_states = loader_core::execute_deploy(loader.id(), pre_states.clone(), bytecode);
assert_eq!(guest_output.self_program_id, loader.id());
assert_eq!(guest_output.caller_program_id, None);
assert_eq!(guest_output.pre_states, pre_states);
assert_eq!(guest_output.post_states, native_post_states);
}