Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ solana-rpc-client = "=2.3.1"
solana-rpc-client-api = "=2.3.1"
solana-transaction-status-client-types = "=2.3.1"
solana-loader-v3-interface = "5.0.0"
solana-sdk-ids = "2.2.1"
solana-system-interface = "1.0.0"
tokio = { version = "1.29.1", features = ["full"] }
bincode = "1.3.3"
Expand Down
102 changes: 77 additions & 25 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use solana_loader_v3_interface::{get_program_data_address, state::UpgradeableLoa
use solana_program::get_address_from_keypair_or_config;
use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable};
use solana_transaction_status_client_types::UiTransactionEncoding;
use std::{
io::Read,
Expand Down Expand Up @@ -725,33 +726,83 @@ pub fn get_file_hash(filepath: &str) -> Result<String, std::io::Error> {
pub fn get_buffer_hash(url: Option<String>, buffer_address: Pubkey) -> anyhow::Result<String> {
let client = get_client(url, None);
let offset = UpgradeableLoaderState::size_of_buffer_metadata();
let account_data = client.get_account_data(&buffer_address)?[offset..].to_vec();
let data = client.get_account_data(&buffer_address)?;
let account_data = data
.get(offset..)
.ok_or_else(|| {
anyhow::anyhow!(
"Buffer account {} appears invalid or incomplete. Expected at least {} bytes for metadata.",
buffer_address,
offset
)
})?
.to_vec();

let program_hash = get_binary_hash(account_data);
Ok(program_hash)
}

pub fn get_program_hash(client: &RpcClient, program_id: Pubkey) -> anyhow::Result<String> {
// First check if the program account exists
if client.get_account(&program_id).is_err() {
return Err(anyhow!("Program {} is not deployed", program_id));
}

let program_buffer = get_program_data_address(&program_id);
let account = client
.get_account(&program_id)
.map_err(|e| anyhow!("Program {} is not deployed: {}", program_id, e))?;

let owner = account.owner;

match owner {
// Check if the program is owned by the upgradeable loader (Loader-v3)
// If so, the program data is in a separate program data account
owner_id if owner_id == bpf_loader_upgradeable::id() => {
let program_buffer = get_program_data_address(&program_id);

// Get the program data account
let data = client.get_account_data(&program_buffer).map_err(|e| {
anyhow!(
"Could not find program data for {}: {}. This could mean:\n\
1. The program is not deployed\n\
2. The program is not upgradeable\n\
3. The program was deployed with a different loader",
program_id,
e
)
})?;

// Then check if the program data account exists
match client.get_account_data(&program_buffer) {
Ok(data) => {
let offset = UpgradeableLoaderState::size_of_programdata_metadata();
let account_data = data[offset..].to_vec();
let program_hash = get_binary_hash(account_data);
Ok(program_hash)

let account_data = data
.get(offset..)
.ok_or_else(|| {
anyhow!(
"Program data account appears corrupted or incomplete. Expected at least {} bytes for metadata.",
offset
)
})?
.to_vec();

Ok(get_binary_hash(account_data))
}
Err(_) => Err(anyhow!(
"Could not find program data for {}. This could mean:\n\
1. The program is not deployed\n\
2. The program is not upgradeable\n\
3. The program was deployed with a different loader",
program_id

// Check if the program is owned by the legacy BPF loaders (v1/v2)
// If so, the program data is stored in the program account's data
owner_id if owner_id == bpf_loader_deprecated::id() || owner_id == bpf_loader::id() => {
let program_data = account.data;

if program_data.is_empty() {
return Err(anyhow!(
"Program {} has no data (legacy loader account empty)",
program_id
));
}

Ok(get_binary_hash(program_data))
}

// Unsupported loader
_ => Err(anyhow!(
"Unknown or unsupported program loader. \
Program {} is owned by {}. Supported loaders: BPF Loader v1, v2, or Upgradeable (loader-v3).",
program_id,
owner
)),
}
}
Expand All @@ -772,7 +823,9 @@ pub fn get_docker_resource_limits() -> Option<(String, String)> {
} else {
// Print message to user that they can set these environment variables to limit docker resources
println!("No Docker resource limits are set.");
println!("You can set the SVB_DOCKER_MEMORY_LIMIT and SVB_DOCKER_CPU_LIMIT environment variables to limit Docker resources.");
println!(
"You can set the SVB_DOCKER_MEMORY_LIMIT and SVB_DOCKER_CPU_LIMIT environment variables to limit Docker resources."
);
println!("For example: SVB_DOCKER_MEMORY_LIMIT=2g SVB_DOCKER_CPU_LIMIT=2.");
}
memory.zip(cpus)
Expand Down Expand Up @@ -1086,10 +1139,7 @@ pub fn verify_from_image(

let executable_hash: String = get_file_hash(program_filepath.as_str())?;
let client = get_client(network, config_path);
let program_buffer = get_program_data_address(&program_id);
let offset = UpgradeableLoaderState::size_of_programdata_metadata();
let account_data = &client.get_account_data(&program_buffer)?[offset..];
let program_hash = get_binary_hash(account_data.to_vec());
let program_hash = get_program_hash(&client, program_id)?;
println!("Executable hash: {}", executable_hash);
println!("Program hash: {}", program_hash);

Expand Down Expand Up @@ -1392,7 +1442,9 @@ pub async fn verify_from_repo(
check_signal(container_id_opt, temp_dir_opt);
let genesis_hash = get_genesis_hash(connection)?;
if genesis_hash != MAINNET_GENESIS_HASH {
return Err(anyhow!("Remote verification only works with mainnet. Please omit the --remote flag to verify locally."));
return Err(anyhow!(
"Remote verification only works with mainnet. Please omit the --remote flag to verify locally."
));
}

let uploader = get_address_from_keypair_or_config(
Expand Down
36 changes: 36 additions & 0 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,40 @@ mod tests {
}
Ok(())
}

#[test]
fn test_get_program_hash_legacy_loader() -> anyhow::Result<()> {
const SPL_TOKEN_PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
const EXPECTED_HASH: &str =
"d5a1793250f0f22efc7174bd8399570636e667655179642b2e90b0fb80e09106";
let args = ["get-program-hash", SPL_TOKEN_PROGRAM_ID];
let child = std::process::Command::new("./target/debug/solana-verify")
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to execute solana-verify command")?;

let output = child
.wait_with_output()
.context("Failed to wait for solana-verify command")?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"get-program-hash for legacy loader program failed: {}",
stderr
);
}

let stdout = String::from_utf8_lossy(&output.stdout);
let hash = stdout.trim();
assert_eq!(
hash, EXPECTED_HASH,
"Program hash {} does not match expected value {}",
hash, EXPECTED_HASH
);
Ok(())
}
}