simulator_client/injection.rs
1use std::collections::BTreeMap;
2
3use simulator_api::{AccountData, BinaryEncoding, EncodedBinary};
4use solana_address::Address;
5use solana_client::nonblocking::rpc_client::RpcClient;
6use thiserror::Error;
7
8const BPF_LOADER_UPGRADEABLE: &str = "BPFLoaderUpgradeab1e11111111111111111111111";
9
10/// Error returned by [`BacktestSession::modify_program`](crate::BacktestSession::modify_program).
11#[derive(Debug, Error)]
12pub enum ProgramModError {
13 #[error("session has no rpc_endpoint (was the session created?)")]
14 NoRpcEndpoint,
15
16 #[error("invalid program id `{id}`")]
17 InvalidProgramId { id: String },
18
19 #[error("RPC error: {source}")]
20 Rpc {
21 #[source]
22 source: Box<dyn std::error::Error + Send + Sync>,
23 },
24}
25
26/// Build a BPF Loader Upgradeable `ProgramData` account modification from raw ELF bytes.
27///
28/// Returns a map of `{programdata_address: AccountData}` ready to pass to
29/// [`Continue::builder().modify_accounts(...)`](crate::Continue).
30///
31/// The ELF is wrapped in the standard `ProgramData` header format:
32/// ```text
33/// [0..4] variant = 3 (u32 LE, UpgradeableLoaderState::ProgramData)
34/// [4..12] deploy_slot (u64 LE)
35/// [12] upgrade_authority discriminant (0 = None, 1 = Some)
36/// [13..45] upgrade_authority pubkey bytes (32 bytes, present only when Some)
37/// [45..] ELF bytecode
38/// ```
39///
40/// `deploy_slot` should be set to `start_slot.saturating_sub(1)` so the program
41/// appears deployed *before* the first executed slot. Deploying at `start_slot`
42/// itself triggers the SVM's same-slot restriction, marking the program unloaded.
43///
44/// `lamports` must be at least rent-exempt for the resulting account size.
45/// With `upgrade_authority = None` the data length is `13 + elf.len()`;
46/// with `Some(authority)` it is `45 + elf.len()`.
47/// Fetch the exact minimum with
48/// `rpc.get_minimum_balance_for_rent_exemption(data_len).await?`.
49///
50/// ## Example
51///
52/// ```no_run
53/// use simulator_client::build_program_injection;
54/// use solana_address::Address;
55///
56/// let program_id: Address = "YourProgramId...".parse().unwrap();
57/// // Compute the programdata PDA using solana_loader_v3_interface::get_program_data_address,
58/// // then convert to Address.
59/// let programdata_addr: Address = "ProgramDataAddr...".parse().unwrap();
60/// let elf = std::fs::read("my_program.so").unwrap();
61/// let deploy_slot = 399_834_991; // start_slot - 1
62///
63/// let mods = build_program_injection(programdata_addr, &elf, deploy_slot, None, 10_000_000_000);
64/// // Pass mods to Continue::builder().modify_accounts(mods).build()
65/// ```
66pub fn build_program_injection(
67 programdata_address: Address,
68 elf: &[u8],
69 deploy_slot: u64,
70 upgrade_authority: Option<Address>,
71 lamports: u64,
72) -> BTreeMap<Address, AccountData> {
73 let data = build_programdata_bytes(elf, deploy_slot, upgrade_authority.as_ref());
74
75 let account = AccountData {
76 space: data.len() as u64,
77 data: EncodedBinary::from_bytes(&data, BinaryEncoding::Base64),
78 executable: false,
79 lamports,
80 owner: BPF_LOADER_UPGRADEABLE
81 .parse::<Address>()
82 .expect("valid BPF loader address"),
83 };
84
85 let mut map = BTreeMap::new();
86 map.insert(programdata_address, account);
87 map
88}
89
90/// Serialize ELF bytes into a `ProgramData` account data blob.
91///
92/// Exposed as a building block if you need to construct the account yourself
93/// (e.g. to set a custom lamport amount after calling
94/// `rpc.get_minimum_balance_for_rent_exemption`).
95pub fn build_programdata_bytes(
96 elf: &[u8],
97 deploy_slot: u64,
98 upgrade_authority: Option<&Address>,
99) -> Vec<u8> {
100 let header_len = if upgrade_authority.is_some() { 45 } else { 13 };
101 let mut data = Vec::with_capacity(header_len + elf.len());
102
103 // variant = 3 (UpgradeableLoaderState::ProgramData)
104 data.extend_from_slice(&3u32.to_le_bytes());
105 // deployment slot
106 data.extend_from_slice(&deploy_slot.to_le_bytes());
107
108 match upgrade_authority {
109 None => {
110 data.push(0); // Option::None
111 }
112 Some(authority) => {
113 data.push(1); // Option::Some
114 data.extend_from_slice(authority.as_ref());
115 }
116 }
117
118 data.extend_from_slice(elf);
119 data
120}
121
122/// Build a program-data account modification, fetching the deploy slot and rent
123/// exemption from the given RPC client.
124///
125/// Equivalent to [`BacktestSession::modify_program`](crate::BacktestSession::modify_program)
126/// but usable without holding a session — pass any RPC client that can see the
127/// current cluster state.
128pub async fn modify_program_via_rpc(
129 rpc: &RpcClient,
130 program_id: &str,
131 elf: &[u8],
132) -> Result<BTreeMap<Address, AccountData>, ProgramModError> {
133 let program_addr: Address =
134 program_id
135 .parse()
136 .map_err(|_| ProgramModError::InvalidProgramId {
137 id: program_id.to_string(),
138 })?;
139 let programdata_addr = solana_loader_v3_interface::get_program_data_address(&program_addr);
140
141 let slot = rpc.get_slot().await.map_err(|e| ProgramModError::Rpc {
142 source: Box::new(e),
143 })?;
144 let deploy_slot = slot.saturating_sub(1);
145
146 let existing = rpc
147 .get_account(&programdata_addr)
148 .await
149 .map_err(|e| ProgramModError::Rpc {
150 source: Box::new(e),
151 })?;
152
153 let upgrade_authority = if existing.data.get(12).copied() == Some(1) {
154 existing.data.get(13..45).and_then(|b| {
155 let bytes: [u8; 32] = b.try_into().ok()?;
156 Some(Address::from(bytes))
157 })
158 } else {
159 None
160 };
161
162 let data_len = upgrade_authority.map_or(13, |_| 45) + elf.len();
163 let lamports = rpc
164 .get_minimum_balance_for_rent_exemption(data_len)
165 .await
166 .map_err(|e| ProgramModError::Rpc {
167 source: Box::new(e),
168 })?;
169
170 Ok(build_program_injection(
171 programdata_addr,
172 elf,
173 deploy_slot,
174 upgrade_authority,
175 lamports,
176 ))
177}