simulator_client/injection.rs
1use std::collections::BTreeMap;
2
3use simulator_api::{AccountData, BinaryEncoding, EncodedBinary};
4use solana_address::Address;
5use thiserror::Error;
6
7const BPF_LOADER_UPGRADEABLE: &str = "BPFLoaderUpgradeab1e11111111111111111111111";
8
9/// Error returned by [`BacktestSession::modify_program`](crate::BacktestSession::modify_program).
10#[derive(Debug, Error)]
11pub enum ProgramModError {
12 #[error("session has no rpc_endpoint (was the session created?)")]
13 NoRpcEndpoint,
14
15 #[error("invalid program id `{id}`")]
16 InvalidProgramId { id: String },
17
18 #[error("RPC error: {source}")]
19 Rpc {
20 #[source]
21 source: Box<dyn std::error::Error + Send + Sync>,
22 },
23}
24
25/// Build a BPF Loader Upgradeable `ProgramData` account modification from raw ELF bytes.
26///
27/// Returns a map of `{programdata_address: AccountData}` ready to pass to
28/// [`Continue::builder().modify_accounts(...)`](crate::Continue).
29///
30/// The ELF is wrapped in the standard `ProgramData` header format:
31/// ```text
32/// [0..4] variant = 3 (u32 LE, UpgradeableLoaderState::ProgramData)
33/// [4..12] deploy_slot (u64 LE)
34/// [12] upgrade_authority discriminant (0 = None, 1 = Some)
35/// [13..45] upgrade_authority pubkey bytes (32 bytes, present only when Some)
36/// [45..] ELF bytecode
37/// ```
38///
39/// `deploy_slot` should be set to `start_slot.saturating_sub(1)` so the program
40/// appears deployed *before* the first executed slot. Deploying at `start_slot`
41/// itself triggers the SVM's same-slot restriction, marking the program unloaded.
42///
43/// `lamports` must be at least rent-exempt for the resulting account size.
44/// With `upgrade_authority = None` the data length is `13 + elf.len()`;
45/// with `Some(authority)` it is `45 + elf.len()`.
46/// Fetch the exact minimum with
47/// `rpc.get_minimum_balance_for_rent_exemption(data_len).await?`.
48///
49/// ## Example
50///
51/// ```no_run
52/// use simulator_client::build_program_injection;
53/// use solana_address::Address;
54///
55/// let program_id: Address = "YourProgramId...".parse().unwrap();
56/// // Compute the programdata PDA using solana_loader_v3_interface::get_program_data_address,
57/// // then convert to Address.
58/// let programdata_addr: Address = "ProgramDataAddr...".parse().unwrap();
59/// let elf = std::fs::read("my_program.so").unwrap();
60/// let deploy_slot = 399_834_991; // start_slot - 1
61///
62/// let mods = build_program_injection(programdata_addr, &elf, deploy_slot, None, 10_000_000_000);
63/// // Pass mods to Continue::builder().modify_accounts(mods).build()
64/// ```
65pub fn build_program_injection(
66 programdata_address: Address,
67 elf: &[u8],
68 deploy_slot: u64,
69 upgrade_authority: Option<Address>,
70 lamports: u64,
71) -> BTreeMap<Address, AccountData> {
72 let data = build_programdata_bytes(elf, deploy_slot, upgrade_authority.as_ref());
73
74 let account = AccountData {
75 space: data.len() as u64,
76 data: EncodedBinary::from_bytes(&data, BinaryEncoding::Base64),
77 executable: false,
78 lamports,
79 owner: BPF_LOADER_UPGRADEABLE
80 .parse::<Address>()
81 .expect("valid BPF loader address"),
82 };
83
84 let mut map = BTreeMap::new();
85 map.insert(programdata_address, account);
86 map
87}
88
89/// Serialize ELF bytes into a `ProgramData` account data blob.
90///
91/// Exposed as a building block if you need to construct the account yourself
92/// (e.g. to set a custom lamport amount after calling
93/// `rpc.get_minimum_balance_for_rent_exemption`).
94pub fn build_programdata_bytes(
95 elf: &[u8],
96 deploy_slot: u64,
97 upgrade_authority: Option<&Address>,
98) -> Vec<u8> {
99 let header_len = if upgrade_authority.is_some() { 45 } else { 13 };
100 let mut data = Vec::with_capacity(header_len + elf.len());
101
102 // variant = 3 (UpgradeableLoaderState::ProgramData)
103 data.extend_from_slice(&3u32.to_le_bytes());
104 // deployment slot
105 data.extend_from_slice(&deploy_slot.to_le_bytes());
106
107 match upgrade_authority {
108 None => {
109 data.push(0); // Option::None
110 }
111 Some(authority) => {
112 data.push(1); // Option::Some
113 data.extend_from_slice(authority.as_ref());
114 }
115 }
116
117 data.extend_from_slice(elf);
118 data
119}