diff --git a/account-decoder/src/lib.rs b/account-decoder/src/lib.rs index de06324c36..1771dccb9d 100644 --- a/account-decoder/src/lib.rs +++ b/account-decoder/src/lib.rs @@ -4,6 +4,7 @@ extern crate lazy_static; extern crate serde_derive; pub mod parse_account_data; +pub mod parse_bpf_loader; pub mod parse_config; pub mod parse_nonce; pub mod parse_stake; diff --git a/account-decoder/src/parse_account_data.rs b/account-decoder/src/parse_account_data.rs index a677e89b50..9b649dc8fe 100644 --- a/account-decoder/src/parse_account_data.rs +++ b/account-decoder/src/parse_account_data.rs @@ -1,4 +1,5 @@ use crate::{ + parse_bpf_loader::parse_bpf_upgradeable_loader, parse_config::parse_config, parse_nonce::parse_nonce, parse_stake::parse_stake, @@ -13,6 +14,7 @@ use std::collections::HashMap; use thiserror::Error; lazy_static! { + static ref BPF_UPGRADEABLE_LOADER_PROGRAM_ID: Pubkey = solana_sdk::bpf_loader_upgradeable::id(); static ref CONFIG_PROGRAM_ID: Pubkey = solana_config_program::id(); static ref STAKE_PROGRAM_ID: Pubkey = solana_stake_program::id(); static ref SYSTEM_PROGRAM_ID: Pubkey = system_program::id(); @@ -21,6 +23,10 @@ lazy_static! { static ref VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id(); pub static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); + m.insert( + *BPF_UPGRADEABLE_LOADER_PROGRAM_ID, + ParsableAccount::BpfUpgradeableLoader, + ); m.insert(*CONFIG_PROGRAM_ID, ParsableAccount::Config); m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce); m.insert(*TOKEN_PROGRAM_ID, ParsableAccount::SplToken); @@ -60,6 +66,7 @@ pub struct ParsedAccount { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum ParsableAccount { + BpfUpgradeableLoader, Config, Nonce, SplToken, @@ -84,6 +91,9 @@ pub fn parse_account_data( .ok_or(ParseAccountError::ProgramNotParsable)?; let additional_data = additional_data.unwrap_or_default(); let parsed_json = match program_name { + ParsableAccount::BpfUpgradeableLoader => { + serde_json::to_value(parse_bpf_upgradeable_loader(data)?)? + } ParsableAccount::Config => serde_json::to_value(parse_config(data, pubkey)?)?, ParsableAccount::Nonce => serde_json::to_value(parse_nonce(data)?)?, ParsableAccount::SplToken => { diff --git a/account-decoder/src/parse_bpf_loader.rs b/account-decoder/src/parse_bpf_loader.rs new file mode 100644 index 0000000000..d12753dcf7 --- /dev/null +++ b/account-decoder/src/parse_bpf_loader.rs @@ -0,0 +1,181 @@ +use crate::{ + parse_account_data::{ParsableAccount, ParseAccountError}, + UiAccountData, UiAccountEncoding, +}; +use bincode::{deserialize, serialized_size}; +use solana_sdk::{bpf_loader_upgradeable::UpgradeableLoaderState, pubkey::Pubkey}; + +pub fn parse_bpf_upgradeable_loader( + data: &[u8], +) -> Result { + let account_state: UpgradeableLoaderState = deserialize(data).map_err(|_| { + ParseAccountError::AccountNotParsable(ParsableAccount::BpfUpgradeableLoader) + })?; + let parsed_account = match account_state { + UpgradeableLoaderState::Uninitialized => BpfUpgradeableLoaderAccountType::Uninitialized, + UpgradeableLoaderState::Buffer { authority_address } => { + let offset = if authority_address.is_some() { + UpgradeableLoaderState::buffer_data_offset().unwrap() + } else { + // This case included for code completeness; in practice, a Buffer account will + // always have authority_address.is_some() + UpgradeableLoaderState::buffer_data_offset().unwrap() + - serialized_size(&Pubkey::default()).unwrap() as usize + }; + BpfUpgradeableLoaderAccountType::Buffer(UiBuffer { + authority: authority_address.map(|pubkey| pubkey.to_string()), + data: UiAccountData::Binary( + base64::encode(&data[offset as usize..]), + UiAccountEncoding::Base64, + ), + }) + } + UpgradeableLoaderState::Program { + programdata_address, + } => BpfUpgradeableLoaderAccountType::Program(UiProgram { + program_data: programdata_address.to_string(), + }), + UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address, + } => { + let offset = if upgrade_authority_address.is_some() { + UpgradeableLoaderState::programdata_data_offset().unwrap() + } else { + UpgradeableLoaderState::programdata_data_offset().unwrap() + - serialized_size(&Pubkey::default()).unwrap() as usize + }; + BpfUpgradeableLoaderAccountType::ProgramData(UiProgramData { + slot, + authority: upgrade_authority_address.map(|pubkey| pubkey.to_string()), + data: UiAccountData::Binary( + base64::encode(&data[offset as usize..]), + UiAccountEncoding::Base64, + ), + }) + } + }; + Ok(parsed_account) +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase", tag = "type", content = "info")] +pub enum BpfUpgradeableLoaderAccountType { + Uninitialized, + Buffer(UiBuffer), + Program(UiProgram), + ProgramData(UiProgramData), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiBuffer { + pub authority: Option, + pub data: UiAccountData, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiProgram { + pub program_data: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiProgramData { + pub slot: u64, + pub authority: Option, + pub data: UiAccountData, +} + +#[cfg(test)] +mod test { + use super::*; + use bincode::serialize; + use solana_sdk::pubkey::Pubkey; + + #[test] + fn test_parse_bpf_upgradeable_loader_accounts() { + let bpf_loader_state = UpgradeableLoaderState::Uninitialized; + let account_data = serialize(&bpf_loader_state).unwrap(); + assert_eq!( + parse_bpf_upgradeable_loader(&account_data).unwrap(), + BpfUpgradeableLoaderAccountType::Uninitialized + ); + + let program = vec![7u8; 64]; // Arbitrary program data + + let authority = Pubkey::new_unique(); + let bpf_loader_state = UpgradeableLoaderState::Buffer { + authority_address: Some(authority), + }; + let mut account_data = serialize(&bpf_loader_state).unwrap(); + account_data.extend_from_slice(&program); + assert_eq!( + parse_bpf_upgradeable_loader(&account_data).unwrap(), + BpfUpgradeableLoaderAccountType::Buffer(UiBuffer { + authority: Some(authority.to_string()), + data: UiAccountData::Binary(base64::encode(&program), UiAccountEncoding::Base64), + }) + ); + + // This case included for code completeness; in practice, a Buffer account will always have + // authority_address.is_some() + let bpf_loader_state = UpgradeableLoaderState::Buffer { + authority_address: None, + }; + let mut account_data = serialize(&bpf_loader_state).unwrap(); + account_data.extend_from_slice(&program); + assert_eq!( + parse_bpf_upgradeable_loader(&account_data).unwrap(), + BpfUpgradeableLoaderAccountType::Buffer(UiBuffer { + authority: None, + data: UiAccountData::Binary(base64::encode(&program), UiAccountEncoding::Base64), + }) + ); + + let programdata_address = Pubkey::new_unique(); + let bpf_loader_state = UpgradeableLoaderState::Program { + programdata_address, + }; + let account_data = serialize(&bpf_loader_state).unwrap(); + assert_eq!( + parse_bpf_upgradeable_loader(&account_data).unwrap(), + BpfUpgradeableLoaderAccountType::Program(UiProgram { + program_data: programdata_address.to_string(), + }) + ); + + let authority = Pubkey::new_unique(); + let slot = 42; + let bpf_loader_state = UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: Some(authority), + }; + let mut account_data = serialize(&bpf_loader_state).unwrap(); + account_data.extend_from_slice(&program); + assert_eq!( + parse_bpf_upgradeable_loader(&account_data).unwrap(), + BpfUpgradeableLoaderAccountType::ProgramData(UiProgramData { + slot, + authority: Some(authority.to_string()), + data: UiAccountData::Binary(base64::encode(&program), UiAccountEncoding::Base64), + }) + ); + + let bpf_loader_state = UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: None, + }; + let mut account_data = serialize(&bpf_loader_state).unwrap(); + account_data.extend_from_slice(&program); + assert_eq!( + parse_bpf_upgradeable_loader(&account_data).unwrap(), + BpfUpgradeableLoaderAccountType::ProgramData(UiProgramData { + slot, + authority: None, + data: UiAccountData::Binary(base64::encode(&program), UiAccountEncoding::Base64), + }) + ); + } +} diff --git a/transaction-status/src/parse_bpf_loader.rs b/transaction-status/src/parse_bpf_loader.rs index 2f16712bce..a99f82b4ea 100644 --- a/transaction-status/src/parse_bpf_loader.rs +++ b/transaction-status/src/parse_bpf_loader.rs @@ -1,8 +1,11 @@ -use crate::parse_instruction::{ParsableProgram, ParseInstructionError, ParsedInstructionEnum}; +use crate::parse_instruction::{ + check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum, +}; use bincode::deserialize; use serde_json::json; use solana_sdk::{ - instruction::CompiledInstruction, loader_instruction::LoaderInstruction, pubkey::Pubkey, + instruction::CompiledInstruction, loader_instruction::LoaderInstruction, + loader_upgradeable_instruction::UpgradeableLoaderInstruction, pubkey::Pubkey, }; pub fn parse_bpf_loader( @@ -34,9 +37,114 @@ pub fn parse_bpf_loader( } } +pub fn parse_bpf_upgradeable_loader( + instruction: &CompiledInstruction, + account_keys: &[Pubkey], +) -> Result { + let bpf_upgradeable_loader_instruction: UpgradeableLoaderInstruction = + deserialize(&instruction.data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::BpfUpgradeableLoader) + })?; + match instruction.accounts.iter().max() { + Some(index) if (*index as usize) < account_keys.len() => {} + _ => { + // Runtime should prevent this from ever happening + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::BpfUpgradeableLoader, + )); + } + } + match bpf_upgradeable_loader_instruction { + UpgradeableLoaderInstruction::InitializeBuffer => { + check_num_bpf_upgradeable_loader_accounts(&instruction.accounts, 1)?; + let mut value = json!({ + "account": account_keys[instruction.accounts[0] as usize].to_string(), + }); + let map = value.as_object_mut().unwrap(); + if instruction.accounts.len() > 1 { + map.insert( + "authority".to_string(), + json!(account_keys[instruction.accounts[1] as usize].to_string()), + ); + } + Ok(ParsedInstructionEnum { + instruction_type: "initializeBuffer".to_string(), + info: value, + }) + } + UpgradeableLoaderInstruction::Write { offset, bytes } => { + check_num_bpf_upgradeable_loader_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "write".to_string(), + info: json!({ + "offset": offset, + "bytes": base64::encode(bytes), + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "authority": account_keys[instruction.accounts[1] as usize].to_string(), + }), + }) + } + UpgradeableLoaderInstruction::DeployWithMaxDataLen { max_data_len } => { + check_num_bpf_upgradeable_loader_accounts(&instruction.accounts, 8)?; + Ok(ParsedInstructionEnum { + instruction_type: "deployWithMaxDataLen".to_string(), + info: json!({ + "maxDataLen": max_data_len, + "payerAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "programDataAccount": account_keys[instruction.accounts[1] as usize].to_string(), + "programAccount": account_keys[instruction.accounts[2] as usize].to_string(), + "bufferAccount": account_keys[instruction.accounts[3] as usize].to_string(), + "rentSysvar": account_keys[instruction.accounts[4] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[5] as usize].to_string(), + "systemProgram": account_keys[instruction.accounts[6] as usize].to_string(), + "authority": account_keys[instruction.accounts[7] as usize].to_string(), + }), + }) + } + UpgradeableLoaderInstruction::Upgrade => { + check_num_bpf_upgradeable_loader_accounts(&instruction.accounts, 7)?; + Ok(ParsedInstructionEnum { + instruction_type: "upgrade".to_string(), + info: json!({ + "programDataAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "programAccount": account_keys[instruction.accounts[1] as usize].to_string(), + "bufferAccount": account_keys[instruction.accounts[2] as usize].to_string(), + "spillAccount": account_keys[instruction.accounts[3] as usize].to_string(), + "rentSysvar": account_keys[instruction.accounts[4] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[5] as usize].to_string(), + "authority": account_keys[instruction.accounts[6] as usize].to_string(), + }), + }) + } + UpgradeableLoaderInstruction::SetAuthority => { + check_num_bpf_upgradeable_loader_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "setAuthority".to_string(), + info: json!({ + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "authority": account_keys[instruction.accounts[1] as usize].to_string(), + "newAuthority": if instruction.accounts.len() > 2 { + Some(account_keys[instruction.accounts[2] as usize].to_string()) + } else { + None + }, + }), + }) + } + } +} + +fn check_num_bpf_upgradeable_loader_accounts( + accounts: &[u8], + num: usize, +) -> Result<(), ParseInstructionError> { + check_num_accounts(accounts, num, ParsableProgram::BpfUpgradeableLoader) +} + #[cfg(test)] mod test { use super::*; + use serde_json::Value; use solana_sdk::{message::Message, pubkey}; #[test] @@ -96,4 +204,153 @@ mod test { }; assert!(parse_bpf_loader(&bad_compiled_instruction, &account_keys).is_err()); } + + #[test] + fn test_parse_bpf_upgradeable_loader_instructions() { + let mut keys: Vec = vec![]; + for _ in 0..8 { + keys.push(Pubkey::new_unique()); + } + let offset = 4242; + let bytes = vec![8; 99]; + let max_data_len = 54321; + + let instructions = solana_sdk::bpf_loader_upgradeable::create_buffer( + &keys[0], + &keys[1], + &keys[2], + 55, + max_data_len, + ) + .unwrap(); + let message = Message::new(&instructions, None); + assert_eq!( + parse_bpf_upgradeable_loader(&message.instructions[1], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "initializeBuffer".to_string(), + info: json!({ + "account": keys[1].to_string(), + "authority": keys[2].to_string(), + }), + } + ); + assert!(parse_bpf_upgradeable_loader(&message.instructions[1], &keys[0..2]).is_err()); + + let instruction = + solana_sdk::bpf_loader_upgradeable::write(&keys[1], &keys[0], offset, bytes.clone()); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "write".to_string(), + info: json!({ + "offset": offset, + "bytes": base64::encode(&bytes), + "account": keys[1].to_string(), + "authority": keys[0].to_string(), + }), + } + ); + assert!(parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..1]).is_err()); + + let instructions = solana_sdk::bpf_loader_upgradeable::deploy_with_max_program_len( + &keys[0], + &keys[1], + &keys[4], + &keys[2], + 55, + max_data_len, + ) + .unwrap(); + let message = Message::new(&instructions, None); + assert_eq!( + parse_bpf_upgradeable_loader(&message.instructions[1], &keys[0..8]).unwrap(), + ParsedInstructionEnum { + instruction_type: "deployWithMaxDataLen".to_string(), + info: json!({ + "maxDataLen": max_data_len, + "payerAccount": keys[0].to_string(), + "programAccount": keys[1].to_string(), + "authority": keys[2].to_string(), + "programDataAccount": keys[3].to_string(), + "bufferAccount": keys[4].to_string(), + "rentSysvar": keys[5].to_string(), + "clockSysvar": keys[6].to_string(), + "systemProgram": keys[7].to_string(), + }), + } + ); + assert!(parse_bpf_upgradeable_loader(&message.instructions[1], &keys[0..7]).is_err()); + + let instruction = + solana_sdk::bpf_loader_upgradeable::upgrade(&keys[2], &keys[3], &keys[0], &keys[4]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..7]).unwrap(), + ParsedInstructionEnum { + instruction_type: "upgrade".to_string(), + info: json!({ + "authority": keys[0].to_string(), + "programDataAccount": keys[1].to_string(), + "programAccount": keys[2].to_string(), + "bufferAccount": keys[3].to_string(), + "spillAccount": keys[4].to_string(), + "rentSysvar": keys[5].to_string(), + "clockSysvar": keys[6].to_string(), + }), + } + ); + assert!(parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..6]).is_err()); + + let instruction = + solana_sdk::bpf_loader_upgradeable::set_buffer_authority(&keys[1], &keys[0], &keys[2]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setAuthority".to_string(), + info: json!({ + "account": keys[1].to_string(), + "authority": keys[0].to_string(), + "newAuthority": keys[2].to_string(), + }), + } + ); + assert!(parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..1]).is_err()); + + let instruction = solana_sdk::bpf_loader_upgradeable::set_upgrade_authority( + &keys[1], + &keys[0], + Some(&keys[2]), + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setAuthority".to_string(), + info: json!({ + "account": keys[1].to_string(), + "authority": keys[0].to_string(), + "newAuthority": keys[2].to_string(), + }), + } + ); + assert!(parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..1]).is_err()); + + let instruction = + solana_sdk::bpf_loader_upgradeable::set_upgrade_authority(&keys[1], &keys[0], None); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setAuthority".to_string(), + info: json!({ + "account": keys[1].to_string(), + "authority": keys[0].to_string(), + "newAuthority": Value::Null, + }), + } + ); + assert!(parse_bpf_upgradeable_loader(&message.instructions[0], &keys[0..1]).is_err()); + } } diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index 9e2b595da0..ab9d3326cc 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -1,6 +1,9 @@ use crate::{ - parse_bpf_loader::parse_bpf_loader, parse_stake::parse_stake, parse_system::parse_system, - parse_token::parse_token, parse_vote::parse_vote, + parse_bpf_loader::{parse_bpf_loader, parse_bpf_upgradeable_loader}, + parse_stake::parse_stake, + parse_system::parse_system, + parse_token::parse_token, + parse_vote::parse_vote, }; use inflector::Inflector; use serde_json::Value; @@ -14,6 +17,7 @@ use thiserror::Error; lazy_static! { static ref BPF_LOADER_PROGRAM_ID: Pubkey = solana_sdk::bpf_loader::id(); + static ref BPF_UPGRADEABLE_LOADER_PROGRAM_ID: Pubkey = solana_sdk::bpf_loader_upgradeable::id(); static ref MEMO_V1_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_memo_v1_0::id().to_string()).unwrap(); static ref MEMO_V3_PROGRAM_ID: Pubkey = @@ -28,6 +32,10 @@ lazy_static! { m.insert(*MEMO_V3_PROGRAM_ID, ParsableProgram::SplMemo); m.insert(*TOKEN_PROGRAM_ID, ParsableProgram::SplToken); m.insert(*BPF_LOADER_PROGRAM_ID, ParsableProgram::BpfLoader); + m.insert( + *BPF_UPGRADEABLE_LOADER_PROGRAM_ID, + ParsableProgram::BpfUpgradeableLoader, + ); m.insert(*STAKE_PROGRAM_ID, ParsableProgram::Stake); m.insert(*SYSTEM_PROGRAM_ID, ParsableProgram::System); m.insert(*VOTE_PROGRAM_ID, ParsableProgram::Vote); @@ -73,6 +81,7 @@ pub enum ParsableProgram { SplMemo, SplToken, BpfLoader, + BpfUpgradeableLoader, Stake, System, Vote, @@ -92,6 +101,9 @@ pub fn parse( ParsableProgram::BpfLoader => { serde_json::to_value(parse_bpf_loader(instruction, account_keys)?)? } + ParsableProgram::BpfUpgradeableLoader => { + serde_json::to_value(parse_bpf_upgradeable_loader(instruction, account_keys)?)? + } ParsableProgram::Stake => serde_json::to_value(parse_stake(instruction, account_keys)?)?, ParsableProgram::System => serde_json::to_value(parse_system(instruction, account_keys)?)?, ParsableProgram::Vote => serde_json::to_value(parse_vote(instruction, account_keys)?)?,