mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-26 03:41:13 +00:00
feat(lee): add loader program with native Deploy dispatch fast-path
Introduces a new loader_program/loader_core crate pair implementing a Deploy instruction that claims a program's ProgramData PDA account (image_id, segment_number, update_auth, elf_segment), unifying deployment with ordinary PublicTransaction dispatch instead of the separate ProgramDeploymentTransaction path. Measured against every real program in this repo, computing a program's image_id inside the zkVM costs ~1,400-1,500 cycles per byte of deployed bytecode, pushing real deployments to 500M-900M cycles against the 32M public-execution cap (vs. ~27ms natively, since ProgramDeploymentTransaction's equivalent check runs as a plain host function today). To keep the unified dispatch path viable, Deploy is special-cased in from_public_transaction: calls targeting the reserved RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID run loader_core::execute_deploy natively instead of through the interpreted guest executor, wrapped in catch_unwind since the shared execute_deploy logic validates via assert!/expect() like every other guest program, relying on that boundary instead of the zkVM's own panic-to-Result conversion. The loader guest binary is kept buildable and covered by a test that runs it for real and asserts its output matches the native path exactly, so the two can't silently drift apart.
This commit is contained in:
Generated
+24
@@ -5595,6 +5595,7 @@ dependencies = [
|
||||
"hex-literal 1.1.0",
|
||||
"k256 0.13.4",
|
||||
"lee_core",
|
||||
"loader_core",
|
||||
"log",
|
||||
"rand 0.8.6",
|
||||
"risc0-binfmt",
|
||||
@@ -6365,6 +6366,26 @@ version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "loader_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"bytemuck",
|
||||
"lee_core",
|
||||
"risc0-binfmt",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loader_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"loader_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -9421,6 +9442,7 @@ dependencies = [
|
||||
"faucet_core",
|
||||
"lee",
|
||||
"lee_core",
|
||||
"loader_core",
|
||||
"ping_core",
|
||||
"risc0-zkvm",
|
||||
"sequencer_stake_core",
|
||||
@@ -10974,6 +10996,7 @@ dependencies = [
|
||||
"lee",
|
||||
"lee_core",
|
||||
"libp2p",
|
||||
"loader_core",
|
||||
"log",
|
||||
"logos-blockchain-core",
|
||||
"logos-blockchain-http-api-common",
|
||||
@@ -10984,6 +11007,7 @@ dependencies = [
|
||||
"ping_core",
|
||||
"programs",
|
||||
"rand 0.8.6",
|
||||
"risc0-binfmt",
|
||||
"risc0-zkvm",
|
||||
"sequencer_core_metrics",
|
||||
"sequencer_stake_core",
|
||||
|
||||
@@ -57,6 +57,8 @@ members = [
|
||||
"lez/programs/ping_sender",
|
||||
"lez/programs/ping_receiver",
|
||||
"lez/programs/sequencer_stake",
|
||||
"lez/programs/loader",
|
||||
"lez/programs/loader/core",
|
||||
"lez/cross_zone",
|
||||
|
||||
"test_programs",
|
||||
@@ -124,6 +126,8 @@ bridge_lock_core = { path = "lez/programs/bridge_lock/core" }
|
||||
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" }
|
||||
@@ -141,6 +145,7 @@ tokio-util = "0.7.18"
|
||||
async-trait = "0.1"
|
||||
trait-variant = "0.1.2"
|
||||
risc0-zkvm = { version = "3.0.5", default-features = false, features = ['std'] }
|
||||
risc0-binfmt = "3.0.2"
|
||||
risc0-build = "3.0.5"
|
||||
kameo = "0.22.2"
|
||||
kameo_actors = "0.8.1"
|
||||
|
||||
Binary file not shown.
@@ -22,6 +22,7 @@ hex.workspace = true
|
||||
k256.workspace = true
|
||||
risc0-binfmt = "3.0.2"
|
||||
log.workspace = true
|
||||
loader_core.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
build_utils.workspace = true
|
||||
|
||||
@@ -19,6 +19,26 @@ pub const DEFAULT_PROGRAM_OWNER: AccountId = AccountId::new([0; 32]);
|
||||
/// `program_owner` for program `Account`s.
|
||||
pub const PROGRAM_STORAGE_OWNER: AccountId = AccountId::new([0xFF; 32]);
|
||||
|
||||
/// Reserved `AccountId` for the native "Deploy" dispatch shortcut.
|
||||
///
|
||||
/// `SHA256(domain_separator || label)`, where `domain_separator` is
|
||||
/// `/LEE/v0.3/AccountId/State/` and `label` is `DeploymentProgram`, each padded with trailing
|
||||
/// zero bytes to 32 bytes before concatenation — the same domain-separation construction used
|
||||
/// throughout this module, just with no variable input, since this is a single fixed address
|
||||
/// rather than a per-caller derivation.
|
||||
///
|
||||
/// Dispatch recognizes this exact `AccountId` and runs the deploy logic as native Rust instead
|
||||
/// of interpreting a guest ELF: computing a program's image id inside the zkVM costs roughly
|
||||
/// 1,400-1,500 cycles per byte of deployed bytecode (measured against every real program in
|
||||
/// this repo), pushing a real deployment to 500M-900M cycles against the 32M public-execution
|
||||
/// cap, whereas the equivalent native computation costs low tens of milliseconds. A caller
|
||||
/// targeting this address converts it to the `ProgramId` a `Message`/`ChainedCall` expects via
|
||||
/// the existing `From<AccountId> for ProgramId` bijection.
|
||||
pub const RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID: AccountId = AccountId::new([
|
||||
89, 158, 44, 108, 43, 137, 255, 57, 188, 48, 148, 179, 39, 111, 31, 202, 167, 23, 56, 0, 167,
|
||||
29, 152, 150, 161, 186, 155, 209, 69, 138, 145, 201,
|
||||
]);
|
||||
|
||||
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
|
||||
|
||||
pub type ProgramId = [u32; 8];
|
||||
|
||||
@@ -57,10 +57,25 @@ 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(MAX_NUM_CYCLES_PUBLIC_EXECUTION));
|
||||
env_builder.session_limit(Some(session_limit));
|
||||
Self::write_inputs(
|
||||
AccountId::from(self.id),
|
||||
caller_account_id,
|
||||
@@ -110,5 +125,29 @@ 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;
|
||||
|
||||
@@ -8,8 +8,8 @@ use lee_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, PublicAction, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
CallerData, ChainedCall, Claim, DEFAULT_PROGRAM_OWNER, ProgramId,
|
||||
compute_public_authorized_pdas, validate_execution,
|
||||
CallerData, ChainedCall, Claim, DEFAULT_PROGRAM_OWNER, ProgramId, ProgramOutput,
|
||||
RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID, compute_public_authorized_pdas, validate_execution,
|
||||
},
|
||||
};
|
||||
use log::debug;
|
||||
@@ -112,16 +112,11 @@ impl ValidatedStateDiff {
|
||||
LeeError::MaxChainedCallsDepthExceeded
|
||||
);
|
||||
|
||||
let Some(program_account) = state.get_program(chained_call.program_account_id) else {
|
||||
return Err(LeeError::InvalidInput("Unknown program".into()));
|
||||
};
|
||||
// Recover the real `ProgramId` (RISC0 image id) from the account's address: on this
|
||||
// branch every program account lives at the direct `AccountId::from(program_id)`
|
||||
// bijection, so this round-trip is exact. Needed wherever execution/PDA derivation
|
||||
// requires the underlying image id rather than the dispatch-facing `AccountId`.
|
||||
let program_id = ProgramId::from(chained_call.program_account_id);
|
||||
let program =
|
||||
Program::new_unchecked(program_id, Cow::Owned(program_account.data.to_vec()));
|
||||
|
||||
debug!(
|
||||
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
|
||||
@@ -129,11 +124,50 @@ impl ValidatedStateDiff {
|
||||
chained_call.pre_states,
|
||||
chained_call.instruction_data
|
||||
);
|
||||
let mut program_output = program.execute(
|
||||
caller_data.caller_account_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
|
||||
let mut program_output = if chained_call.program_account_id
|
||||
== RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID
|
||||
{
|
||||
// Runs `Deploy` as native Rust instead of interpreting a guest ELF — see
|
||||
// `RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID`'s doc comment for why.
|
||||
//
|
||||
// `execute_deploy` validates its input via `assert!`/`.expect(...)`, exactly
|
||||
// like every guest program in this codebase, relying here on `catch_unwind` to
|
||||
// play the same role the zkVM executor plays for a real guest: converting a
|
||||
// rejected input into a graceful `Err` instead of unwinding past this call.
|
||||
let loader_core::Instruction::Deploy { bytecode } =
|
||||
risc0_zkvm::serde::from_slice(&chained_call.instruction_data).map_err(|e| {
|
||||
LeeError::InvalidInput(format!("invalid Deploy instruction: {e}"))
|
||||
})?;
|
||||
let deploy_pre_states = chained_call.pre_states.clone();
|
||||
let post_states = std::panic::catch_unwind(|| {
|
||||
loader_core::execute_deploy(program_id, deploy_pre_states, bytecode)
|
||||
})
|
||||
.map_err(|_panic_payload| {
|
||||
LeeError::ProgramExecutionFailed("Deploy rejected the given input".into())
|
||||
})?;
|
||||
ProgramOutput::new(
|
||||
chained_call.program_account_id,
|
||||
caller_data.caller_account_id,
|
||||
chained_call.instruction_data.clone(),
|
||||
chained_call.pre_states.clone(),
|
||||
post_states,
|
||||
)
|
||||
} else {
|
||||
let Some(program_account) = state.get_program(chained_call.program_account_id)
|
||||
else {
|
||||
return Err(LeeError::InvalidInput("Unknown program".into()));
|
||||
};
|
||||
let program = Program::new_unchecked(
|
||||
program_id,
|
||||
Cow::Owned(program_account.data.to_vec()),
|
||||
);
|
||||
program.execute(
|
||||
caller_data.caller_account_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?
|
||||
};
|
||||
debug!(
|
||||
"Program {:?} output: {:?}",
|
||||
chained_call.program_account_id, program_output
|
||||
|
||||
@@ -89,6 +89,11 @@ 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"]
|
||||
@@ -120,6 +125,7 @@ programs = [
|
||||
"dep:wrapped_token_core",
|
||||
"dep:ping_core",
|
||||
"dep:sequencer_stake_core",
|
||||
"dep:loader_core",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -141,6 +147,7 @@ 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 }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "loader_program"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
lee_core.workspace = true
|
||||
loader_core.workspace = true
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "loader_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
lee_core.workspace = true
|
||||
serde = { workspace = true, default-features = false }
|
||||
borsh.workspace = true
|
||||
bytemuck.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
risc0-binfmt.workspace = true
|
||||
@@ -0,0 +1,135 @@
|
||||
pub use lee_core::program::PdaSeed;
|
||||
use lee_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, Claim, ProgramId},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const DEPLOY_SEED_DOMAIN_SEPARATOR: [u8; 32] = *b"/LEZ/v0.3/LoaderDeploySeed/00000";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Deploys a new program, claiming its `ProgramData` account as a PDA of the loader.
|
||||
///
|
||||
/// Required accounts (1):
|
||||
/// - The target `ProgramData` PDA account (must be `Account::default()`)
|
||||
Deploy { bytecode: Vec<u8> },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, borsh::BorshSerialize, borsh::BorshDeserialize)]
|
||||
pub struct ProgramData {
|
||||
pub image_id: ProgramId,
|
||||
pub segment_number: u32,
|
||||
pub update_auth: AccountId,
|
||||
pub elf_segment: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for ProgramData {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
||||
borsh::BorshDeserialize::try_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ProgramData> for Data {
|
||||
fn from(program_data: &ProgramData) -> Self {
|
||||
let mut data = Vec::with_capacity(std::mem::size_of_val(program_data));
|
||||
borsh::BorshSerialize::serialize(program_data, &mut data)
|
||||
.expect("borsh serialization should not fail");
|
||||
Self::try_from(data).expect("elf must fit under DATA_MAX_LENGTH")
|
||||
}
|
||||
}
|
||||
|
||||
/// Derives the PDA seed for a deployed program's `ProgramData` account.
|
||||
///
|
||||
/// Combines the program's content-derived identity (`image_id`), its position in a (currently
|
||||
/// always single-segment) split (`segment_number`), and who may redeploy it (`update_auth`).
|
||||
///
|
||||
/// Domain-separated from other PDA-seed derivations in the codebase so that a `deploy_pda_seed`
|
||||
/// output can never collide with a seed meant for a different purpose, even if the input triple
|
||||
/// happened to coincide.
|
||||
#[must_use]
|
||||
pub fn deploy_pda_seed(
|
||||
image_id: ProgramId,
|
||||
segment_number: u32,
|
||||
update_auth: AccountId,
|
||||
) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = [0_u8; 32 + 32 + 4 + 32];
|
||||
bytes[0..32].copy_from_slice(&DEPLOY_SEED_DOMAIN_SEPARATOR);
|
||||
let image_id_bytes: &[u8] =
|
||||
bytemuck::try_cast_slice(&image_id).expect("ProgramId should be castable to &[u8]");
|
||||
bytes[32..64].copy_from_slice(image_id_bytes);
|
||||
bytes[64..68].copy_from_slice(&segment_number.to_le_bytes());
|
||||
bytes[68..].copy_from_slice(update_auth.as_ref());
|
||||
|
||||
PdaSeed::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.expect("Hash output must be exactly 32 bytes long"),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn deploy_account_id(
|
||||
loader_program_id: ProgramId,
|
||||
image_id: ProgramId,
|
||||
segment_number: u32,
|
||||
update_auth: AccountId,
|
||||
) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&loader_program_id,
|
||||
&deploy_pda_seed(image_id, segment_number, update_auth),
|
||||
)
|
||||
}
|
||||
|
||||
/// Executes the `Deploy` instruction: verifies `bytecode` decodes as a valid RISC0 program
|
||||
/// binary, derives its `ProgramData` PDA, and claims it.
|
||||
///
|
||||
/// Shared, target-independent logic: called both from the guest binary (`loader_program`) and,
|
||||
/// natively, from dispatch's `RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID` shortcut (see that
|
||||
/// constant's doc comment in `lee_core::program`) — the two must stay identical, so this is the
|
||||
/// single implementation both wrap around.
|
||||
#[must_use]
|
||||
pub fn execute_deploy(
|
||||
self_program_id: ProgramId,
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
bytecode: Vec<u8>,
|
||||
) -> Vec<AccountPostState> {
|
||||
let image_id: ProgramId = risc0_binfmt::compute_image_id(&bytecode)
|
||||
.expect("bytecode must decode as a valid RISC0 program binary")
|
||||
.into();
|
||||
let segment_number = 0_u32;
|
||||
let update_auth = AccountId::default();
|
||||
let seed = deploy_pda_seed(image_id, segment_number, update_auth);
|
||||
let pda = AccountId::for_public_pda(&self_program_id, &seed);
|
||||
|
||||
let [target] = pre_states
|
||||
.try_into()
|
||||
.expect("Deploy requires exactly 1 account");
|
||||
|
||||
assert_eq!(target.account_id, pda, "wrong deployment target account");
|
||||
assert_eq!(
|
||||
target.account,
|
||||
Account::default(),
|
||||
"program already deployed"
|
||||
);
|
||||
|
||||
let program_data = ProgramData {
|
||||
image_id,
|
||||
segment_number,
|
||||
update_auth,
|
||||
elf_segment: bytecode,
|
||||
};
|
||||
|
||||
vec![AccountPostState::new_claimed(
|
||||
Account {
|
||||
data: Data::from(&program_data),
|
||||
..Account::default()
|
||||
},
|
||||
Claim::Pda(seed),
|
||||
)]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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();
|
||||
}
|
||||
+11
-3
@@ -13,9 +13,10 @@ 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,
|
||||
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,
|
||||
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,
|
||||
SEQUENCER_STAKE_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID, WRAPPED_TOKEN_ELF,
|
||||
WRAPPED_TOKEN_ID,
|
||||
};
|
||||
use lee::program::Program;
|
||||
|
||||
@@ -132,6 +133,12 @@ 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::*;
|
||||
@@ -182,6 +189,7 @@ 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();
|
||||
|
||||
@@ -70,3 +70,5 @@ lee = { workspace = true, features = ["test-utils"] }
|
||||
key_protocol.workspace = true
|
||||
token_core.workspace = true
|
||||
ping_core.workspace = true
|
||||
loader_core.workspace = true
|
||||
risc0-binfmt.workspace = true
|
||||
|
||||
@@ -12,7 +12,10 @@ use kameo::actor::Spawn as _;
|
||||
use lee::{
|
||||
Account, AccountId, Data, PrivateKey, PublicKey, PublicTransaction, V03State, program::Program,
|
||||
};
|
||||
use lee_core::{account::Nonce, program::PdaSeed};
|
||||
use lee_core::{
|
||||
account::Nonce,
|
||||
program::{PdaSeed, ProgramId, RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID},
|
||||
};
|
||||
use logos_blockchain_core::{
|
||||
events::DepositRecreatedNotes,
|
||||
mantle::{
|
||||
@@ -3662,3 +3665,110 @@ fn the_bootstrap_sequencer_can_request_an_unstake_of_its_genesis_stake() {
|
||||
Some(system_accounts::DEFAULT_MINIMUM_SEQUENCER_STAKE)
|
||||
);
|
||||
}
|
||||
|
||||
fn deploy_transaction(target: AccountId, bytecode: Vec<u8>) -> PublicTransaction {
|
||||
let loader_id: ProgramId = RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID.into();
|
||||
let message = lee::public_transaction::Message::try_new(
|
||||
loader_id,
|
||||
vec![target],
|
||||
vec![],
|
||||
loader_core::Instruction::Deploy { bytecode },
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loader_deploys_program() {
|
||||
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());
|
||||
|
||||
assert_eq!(state.get_account_by_id(target), Account::default());
|
||||
|
||||
let tx = deploy_transaction(target, bytecode.clone());
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 1, 0)
|
||||
.expect("Deploy should succeed against an unclaimed target");
|
||||
|
||||
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.segment_number, 0);
|
||||
assert_eq!(program_data.update_auth, AccountId::default());
|
||||
assert_eq!(program_data.elf_segment, bytecode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loader_rejects_redeploying_an_already_deployed_program() {
|
||||
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 tx = deploy_transaction(target, bytecode.clone());
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 1, 0)
|
||||
.expect("First deploy should succeed");
|
||||
|
||||
let tx = deploy_transaction(target, bytecode);
|
||||
let result = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user