Skip to main content

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}