use std::{collections::BTreeMap, time::Duration};
use simulator_api::{AccountData, BinaryEncoding, EncodedBinary};
use solana_address::Address;
use solana_client::nonblocking::rpc_client::RpcClient;
use thiserror::Error;
use tokio_retry::{
Retry,
strategy::{ExponentialBackoff, jitter},
};
use tracing::warn;
use crate::error::err_chain;
const MODIFY_RPC_RETRIES: usize = 10;
const MODIFY_RPC_BACKOFF_BASE: u64 = 2;
const MODIFY_RPC_BACKOFF_FACTOR_MS: u64 = 500;
const MODIFY_RPC_MAX_DELAY: Duration = Duration::from_secs(10);
const BPF_LOADER_UPGRADEABLE: &str = "BPFLoaderUpgradeab1e11111111111111111111111";
#[derive(Debug, Error)]
pub enum ProgramModError {
#[error("session has no rpc_endpoint (was the session created?)")]
NoRpcEndpoint,
#[error("invalid program id `{id}`")]
InvalidProgramId { id: String },
#[error("RPC error: {source}")]
Rpc {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
}
pub fn build_program_injection(
programdata_address: Address,
elf: &[u8],
deploy_slot: u64,
upgrade_authority: Option<Address>,
lamports: u64,
) -> BTreeMap<Address, AccountData> {
let data = build_programdata_bytes(elf, deploy_slot, upgrade_authority.as_ref());
let account = AccountData {
space: data.len() as u64,
data: EncodedBinary::from_bytes(&data, BinaryEncoding::Base64),
executable: false,
lamports,
owner: BPF_LOADER_UPGRADEABLE
.parse::<Address>()
.expect("valid BPF loader address"),
};
let mut map = BTreeMap::new();
map.insert(programdata_address, account);
map
}
pub fn build_programdata_bytes(
elf: &[u8],
deploy_slot: u64,
upgrade_authority: Option<&Address>,
) -> Vec<u8> {
let header_len = if upgrade_authority.is_some() { 45 } else { 13 };
let mut data = Vec::with_capacity(header_len + elf.len());
data.extend_from_slice(&3u32.to_le_bytes());
data.extend_from_slice(&deploy_slot.to_le_bytes());
match upgrade_authority {
None => {
data.push(0); }
Some(authority) => {
data.push(1); data.extend_from_slice(authority.as_ref());
}
}
data.extend_from_slice(elf);
data
}
pub async fn modify_program_via_rpc(
rpc: &RpcClient,
program_id: &str,
elf: &[u8],
) -> Result<BTreeMap<Address, AccountData>, ProgramModError> {
let program_addr: Address =
program_id
.parse()
.map_err(|_| ProgramModError::InvalidProgramId {
id: program_id.to_string(),
})?;
let programdata_addr = solana_loader_v3_interface::get_program_data_address(&program_addr);
let strategy = ExponentialBackoff::from_millis(MODIFY_RPC_BACKOFF_BASE)
.factor(MODIFY_RPC_BACKOFF_FACTOR_MS)
.max_delay(MODIFY_RPC_MAX_DELAY)
.map(jitter)
.take(MODIFY_RPC_RETRIES);
Retry::spawn(strategy, || async {
modify_program_via_rpc_once(rpc, programdata_addr, elf)
.await
.inspect_err(|e| {
warn!(
program_id,
error = %err_chain(e),
"modify_program_via_rpc attempt failed"
)
})
})
.await
}
async fn modify_program_via_rpc_once(
rpc: &RpcClient,
programdata_addr: Address,
elf: &[u8],
) -> Result<BTreeMap<Address, AccountData>, ProgramModError> {
let slot = rpc.get_slot().await.map_err(|e| ProgramModError::Rpc {
source: Box::new(e),
})?;
let deploy_slot = slot.saturating_sub(1);
let existing = rpc
.get_account(&programdata_addr)
.await
.map_err(|e| ProgramModError::Rpc {
source: Box::new(e),
})?;
let upgrade_authority = if existing.data.get(12).copied() == Some(1) {
existing.data.get(13..45).and_then(|b| {
let bytes: [u8; 32] = b.try_into().ok()?;
Some(Address::from(bytes))
})
} else {
None
};
let data_len = upgrade_authority.map_or(13, |_| 45) + elf.len();
let lamports = rpc
.get_minimum_balance_for_rent_exemption(data_len)
.await
.map_err(|e| ProgramModError::Rpc {
source: Box::new(e),
})?;
Ok(build_program_injection(
programdata_addr,
elf,
deploy_slot,
upgrade_authority,
lamports,
))
}