Skip to main content

simulator_client/
injection.rs

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