use {
super::{
parallel::{historical_fetch_worker_count, should_parallelize_historical_fetch},
rpc::{fetch_signatures_for_address, fetch_transaction},
MAX_IDL_BUFFER_BYTES,
},
crate::fetch::FetchTuning,
anyhow::{anyhow, bail, Context, Result},
base64::Engine,
flate2::read::{GzDecoder, ZlibDecoder},
solana_pubkey::{pubkey, Pubkey},
solana_rpc_client::rpc_client::RpcClient,
solana_rpc_client_api::response::RpcConfirmedTransactionStatusWithSignature,
solana_signature::Signature,
solana_transaction_status_client_types::{
EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiInstruction, UiMessage,
UiTransaction,
},
std::{fmt, io::Read, str::FromStr, thread},
};
const PMP_PROGRAM_ID: Pubkey = pubkey!("ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S");
const IDL_SEED: &str = "idl";
const SEED_SIZE: usize = 16;
const METADATA_SEED_PADDING: usize = 14;
const PMP_BUFFER_HEADER_SIZE: usize = 1 + 32 + 32 + 1 + SEED_SIZE + METADATA_SEED_PADDING;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
enum PmpInstruction {
Write = 0,
Initialize = 1,
SetAuthority = 2,
SetData = 3,
SetImmutable = 4,
Trim = 5,
Close = 6,
Allocate = 7,
Extend = 8,
}
impl TryFrom<u8> for PmpInstruction {
type Error = PmpFetchError;
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
match value {
0 => Ok(Self::Write),
1 => Ok(Self::Initialize),
2 => Ok(Self::SetAuthority),
3 => Ok(Self::SetData),
4 => Ok(Self::SetImmutable),
5 => Ok(Self::Trim),
6 => Ok(Self::Close),
7 => Ok(Self::Allocate),
8 => Ok(Self::Extend),
_ => Err(PmpFetchError::invalid_transaction(format!(
"unknown PMP instruction {value}"
))),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
enum MetadataDataSource {
Direct = 0,
Url = 1,
External = 2,
}
impl TryFrom<u8> for MetadataDataSource {
type Error = PmpFetchError;
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
match value {
0 => Ok(Self::Direct),
1 => Ok(Self::Url),
2 => Ok(Self::External),
_ => Err(PmpFetchError::invalid_transaction(format!(
"unsupported metadata data source {value}"
))),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
enum MetadataFormat {
None = 0,
Json = 1,
Yaml = 2,
Toml = 3,
}
impl TryFrom<u8> for MetadataFormat {
type Error = PmpFetchError;
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Json),
2 => Ok(Self::Yaml),
3 => Ok(Self::Toml),
_ => Err(PmpFetchError::invalid_transaction(format!(
"unsupported PMP metadata format {value}"
))),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
enum MetadataEncoding {
None = 0,
Utf8 = 1,
Base58 = 2,
Base64 = 3,
}
impl TryFrom<u8> for MetadataEncoding {
type Error = PmpFetchError;
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Utf8),
2 => Ok(Self::Base58),
3 => Ok(Self::Base64),
_ => Err(PmpFetchError::invalid_transaction(format!(
"unsupported PMP metadata encoding {value}"
))),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
enum MetadataCompression {
None = 0,
Gzip = 1,
Zlib = 2,
}
impl TryFrom<u8> for MetadataCompression {
type Error = PmpFetchError;
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Gzip),
2 => Ok(Self::Zlib),
_ => Err(PmpFetchError::invalid_transaction(format!(
"unsupported PMP metadata compression {value}"
))),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PmpHistoryWarningKind {
UnsupportedDataSource,
MissingBuffer,
InvalidBufferInstruction,
InvalidTransaction,
RpcError,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PmpHistoryWarning {
pub slot: u64,
pub kind: PmpHistoryWarningKind,
pub detail: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PmpHistoricalIdl {
pub slot: u64,
pub signature: String,
pub idl_data: Vec<u8>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PmpHistoricalFetch {
pub idls: Vec<PmpHistoricalIdl>,
pub warnings: Vec<PmpHistoryWarning>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PmpMetadataHeader {
encoding: MetadataEncoding,
compression: MetadataCompression,
format: MetadataFormat,
data_source: MetadataDataSource,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct BufferWrite {
offset: usize,
bytes: Vec<u8>,
}
#[derive(Clone, Debug)]
struct PriorWriteReconstruction {
writes: Vec<BufferWrite>,
saw_allocate: bool,
}
#[derive(Clone, Debug)]
struct PmpFetchError {
kind: PmpHistoryWarningKind,
detail: String,
}
impl PmpFetchError {
fn unsupported_data_source(detail: impl Into<String>) -> Self {
Self {
kind: PmpHistoryWarningKind::UnsupportedDataSource,
detail: detail.into(),
}
}
fn missing_buffer(detail: impl Into<String>) -> Self {
Self {
kind: PmpHistoryWarningKind::MissingBuffer,
detail: detail.into(),
}
}
fn invalid_buffer_instruction(detail: impl Into<String>) -> Self {
Self {
kind: PmpHistoryWarningKind::InvalidBufferInstruction,
detail: detail.into(),
}
}
fn invalid_transaction(detail: impl Into<String>) -> Self {
Self {
kind: PmpHistoryWarningKind::InvalidTransaction,
detail: detail.into(),
}
}
}
impl fmt::Display for PmpFetchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.detail.fmt(f)
}
}
pub fn pmp_metadata_address(program_id: &Pubkey, authority: Option<&Pubkey>) -> Pubkey {
let mut padded_seed = [0u8; SEED_SIZE];
padded_seed[..IDL_SEED.len()].copy_from_slice(IDL_SEED.as_bytes());
let authority_seed: &[u8] = authority.map(|key| key.as_ref()).unwrap_or(&[]);
Pubkey::find_program_address(
&[program_id.as_ref(), authority_seed, &padded_seed],
&PMP_PROGRAM_ID,
)
.0
}
pub fn fetch_pmp_historical_idls(
client: &RpcClient,
program_id: &Pubkey,
authority: Option<&Pubkey>,
signatures: &[RpcConfirmedTransactionStatusWithSignature],
tuning: &FetchTuning,
) -> PmpHistoricalFetch {
let mut result = PmpHistoricalFetch::default();
let metadata_address = pmp_metadata_address(program_id, authority);
for sig in signatures {
let Ok(signature) = Signature::from_str(&sig.signature) else {
result.warnings.push(PmpHistoryWarning {
slot: sig.slot,
kind: PmpHistoryWarningKind::InvalidTransaction,
detail: format!("invalid signature {}", sig.signature),
});
continue;
};
match fetch_pmp_idl_from_transaction(
client,
&metadata_address,
&signature,
sig.slot,
tuning,
) {
Ok(idls) => result
.idls
.extend(idls.into_iter().map(|idl_data| PmpHistoricalIdl {
slot: sig.slot,
signature: sig.signature.clone(),
idl_data,
})),
Err(err) => result.warnings.push(PmpHistoryWarning {
slot: sig.slot,
kind: err.kind,
detail: err.detail,
}),
}
}
result
}
fn fetch_pmp_idl_from_transaction(
client: &RpcClient,
metadata_address: &Pubkey,
signature: &Signature,
slot: u64,
tuning: &FetchTuning,
) -> std::result::Result<Vec<Vec<u8>>, PmpFetchError> {
let transaction =
fetch_transaction(client, signature, tuning).map_err(|err| PmpFetchError {
kind: PmpHistoryWarningKind::RpcError,
detail: err.to_string(),
})?;
let ui_tx = parse_transaction_data(transaction)
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?;
let account_keys = account_keys(&ui_tx.message)
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?;
let instructions = flatten_compiled_instructions(&ui_tx.message, &account_keys);
let mut recovered = Vec::new();
for instruction in instructions {
let Some(program_id) = instruction.program_id(&account_keys) else {
continue;
};
if program_id != PMP_PROGRAM_ID {
continue;
}
if instruction.accounts.first().copied() != Some(*metadata_address) {
continue;
}
match instruction
.kind()
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?
{
PmpInstruction::Initialize => {
let (header, inline_bytes) = parse_initialize_data(
instruction
.data_bytes()
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?,
)?;
if !inline_bytes.is_empty() {
recovered.push(header.decode_direct(inline_bytes)?);
continue;
}
let reconstructed =
reconstruct_prior_writes(client, metadata_address, slot, tuning)?;
if !reconstructed.saw_allocate {
return Err(PmpFetchError::missing_buffer(
"metadata account missing prior allocate for PMP initialize",
));
}
let staged_bytes = apply_buffer_writes_to_bytes(reconstructed.writes)
.map_err(|err| PmpFetchError::invalid_buffer_instruction(err.to_string()))?;
recovered.push(header.decode_direct(&staged_bytes)?);
}
PmpInstruction::SetData => {
let (header, inline_bytes) = parse_set_data(
instruction
.data_bytes()
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?,
)?;
if !inline_bytes.is_empty() {
recovered.push(header.decode_direct(inline_bytes)?);
continue;
}
let buffer_address = instruction.accounts.get(2).copied().ok_or_else(|| {
PmpFetchError::missing_buffer("buffer account missing for PMP set-data")
})?;
if buffer_address == PMP_PROGRAM_ID {
return Err(PmpFetchError::missing_buffer(
"buffer account missing for PMP set-data",
));
}
let buffer_bytes = reconstruct_buffer_bytes(client, &buffer_address, slot, tuning)?;
recovered.push(header.decode_direct(&buffer_bytes)?);
}
_ => {}
}
}
Ok(recovered)
}
fn reconstruct_buffer_bytes(
client: &RpcClient,
buffer_address: &Pubkey,
before_or_at_slot: u64,
tuning: &FetchTuning,
) -> std::result::Result<Vec<u8>, PmpFetchError> {
let writes_in_order =
reconstruct_prior_writes(client, buffer_address, before_or_at_slot, tuning)?.writes;
apply_buffer_writes_to_bytes(writes_in_order).map_err(|err| {
PmpFetchError::invalid_buffer_instruction(format!("buffer account {buffer_address} {err}"))
})
}
fn reconstruct_prior_writes(
client: &RpcClient,
account_address: &Pubkey,
before_or_at_slot: u64,
tuning: &FetchTuning,
) -> std::result::Result<PriorWriteReconstruction, PmpFetchError> {
let mut eligible = fetch_signatures_for_address(
client,
account_address,
None,
None,
Some(before_or_at_slot),
tuning.max_signatures,
)
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?
.into_iter()
.map(|status| {
let signature = Signature::from_str(&status.signature)
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?;
Ok((status.slot, signature))
})
.collect::<std::result::Result<Vec<_>, PmpFetchError>>()?;
eligible.reverse();
fetch_buffer_writes_in_order(client, account_address, &eligible, tuning)
}
fn apply_buffer_writes_to_bytes(buffer_writes: Vec<BufferWrite>) -> Result<Vec<u8>> {
let mut highest_end = 0usize;
for write in &buffer_writes {
highest_end = highest_end.max(write.offset + write.bytes.len());
}
if highest_end == 0 {
bail!("did not contain any recoverable PMP buffer write");
}
if highest_end > MAX_IDL_BUFFER_BYTES {
bail!(
"reconstructed PMP buffer exceeds safety limit of {} bytes",
MAX_IDL_BUFFER_BYTES
);
}
let mut buffer = vec![0u8; highest_end];
for write in buffer_writes {
let end = write.offset + write.bytes.len();
buffer[write.offset..end].copy_from_slice(&write.bytes);
}
let header_end = PMP_BUFFER_HEADER_SIZE;
if buffer.len() < header_end {
bail!("does not decode as a PMP buffer");
}
Ok(buffer[header_end..].to_vec())
}
fn fetch_buffer_writes_in_order(
client: &RpcClient,
buffer_address: &Pubkey,
signatures: &[(u64, Signature)],
tuning: &FetchTuning,
) -> std::result::Result<PriorWriteReconstruction, PmpFetchError> {
if signatures.is_empty() {
return Ok(PriorWriteReconstruction {
writes: Vec::new(),
saw_allocate: false,
});
}
let results = if should_parallelize_historical_fetch(signatures.len(), tuning) {
fetch_buffer_writes_parallel(client, buffer_address, signatures, tuning)?
} else {
signatures
.iter()
.map(|(_, signature)| {
extract_buffer_writes_for_signature(client, buffer_address, signature, tuning)
})
.collect::<std::result::Result<Vec<_>, _>>()?
};
let mut writes = Vec::new();
let mut saw_allocate = false;
for ops in results {
for op in ops {
match op {
BufferReplayOp::Write(write) => writes.push(write),
BufferReplayOp::Allocate => {
saw_allocate = true;
writes.clear();
}
BufferReplayOp::Extend => {}
BufferReplayOp::Trim => {}
}
}
}
Ok(PriorWriteReconstruction {
writes,
saw_allocate,
})
}
fn fetch_buffer_writes_parallel(
client: &RpcClient,
buffer_address: &Pubkey,
signatures: &[(u64, Signature)],
tuning: &FetchTuning,
) -> std::result::Result<Vec<Vec<BufferReplayOp>>, PmpFetchError> {
let worker_count = historical_fetch_worker_count(signatures.len(), tuning);
if worker_count <= 1 {
return signatures
.iter()
.map(|(_, signature)| {
extract_buffer_writes_for_signature(client, buffer_address, signature, tuning)
})
.collect();
}
let chunk_size = signatures.len().div_ceil(worker_count);
thread::scope(|scope| {
let mut handles = Vec::new();
for chunk in signatures.chunks(chunk_size) {
handles.push(scope.spawn(move || {
chunk
.iter()
.map(|(_, signature)| {
extract_buffer_writes_for_signature(
client,
buffer_address,
signature,
tuning,
)
})
.collect::<std::result::Result<Vec<_>, _>>()
}));
}
let mut results = Vec::new();
for handle in handles {
let chunk_result = handle.join().expect("PMP buffer fetch worker panicked")?;
results.extend(chunk_result);
}
Ok(results)
})
}
fn extract_buffer_writes_for_signature(
client: &RpcClient,
buffer_address: &Pubkey,
signature: &Signature,
tuning: &FetchTuning,
) -> std::result::Result<Vec<BufferReplayOp>, PmpFetchError> {
let transaction =
fetch_transaction(client, signature, tuning).map_err(|err| PmpFetchError {
kind: PmpHistoryWarningKind::RpcError,
detail: err.to_string(),
})?;
let ui_tx = parse_transaction_data(transaction)
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?;
let account_keys = account_keys(&ui_tx.message)
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?;
let instructions = flatten_compiled_instructions(&ui_tx.message, &account_keys);
instructions
.into_iter()
.filter_map(|instruction| {
let program_id = instruction.program_id(&account_keys)?;
if program_id != PMP_PROGRAM_ID {
return None;
}
if instruction.accounts.first().copied() != Some(*buffer_address) {
return None;
}
Some(instruction)
})
.filter_map(|instruction| {
let kind = match instruction.kind() {
Ok(kind) => kind,
Err(err) => {
return Some(Err(PmpFetchError::invalid_transaction(err.to_string())));
}
};
match kind {
PmpInstruction::Write => Some(
instruction
.data_bytes()
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))
.and_then(parse_write_data)
.map(BufferReplayOp::Write),
),
PmpInstruction::Allocate => Some(Ok(BufferReplayOp::Allocate)),
PmpInstruction::Extend => Some(Ok(BufferReplayOp::Extend)),
PmpInstruction::Trim => Some(Ok(BufferReplayOp::Trim)),
PmpInstruction::Initialize
| PmpInstruction::SetData
| PmpInstruction::SetAuthority
| PmpInstruction::SetImmutable
| PmpInstruction::Close => None,
}
})
.collect()
}
fn parse_transaction_data(
transaction: EncodedConfirmedTransactionWithStatusMeta,
) -> Result<UiTransaction> {
match transaction.transaction {
EncodedTransactionWithStatusMeta {
transaction: EncodedTransaction::Json(ui_tx),
..
} => Ok(ui_tx),
_ => Err(anyhow!("invalid transaction format")),
}
}
fn account_keys(message: &UiMessage) -> Result<Vec<Pubkey>> {
match message {
UiMessage::Raw(raw) => raw
.account_keys
.iter()
.map(|key| Pubkey::from_str(key))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into),
UiMessage::Parsed(parsed) => parsed
.account_keys
.iter()
.map(|key| Pubkey::from_str(&key.pubkey))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into),
}
}
fn flatten_compiled_instructions(
message: &UiMessage,
account_keys: &[Pubkey],
) -> Vec<CompiledInstructionView> {
match message {
UiMessage::Raw(raw) => raw
.instructions
.iter()
.map(|ix| CompiledInstructionView::from_raw(ix, account_keys))
.collect(),
UiMessage::Parsed(parsed) => parsed
.instructions
.iter()
.filter_map(|ix| CompiledInstructionView::from_parsed(ix, account_keys))
.collect(),
}
}
#[derive(Clone, Debug)]
struct CompiledInstructionView {
program_id_index: u8,
accounts: Vec<Pubkey>,
data: std::result::Result<Vec<u8>, bs58::decode::Error>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum BufferReplayOp {
Allocate,
Extend,
Trim,
Write(BufferWrite),
}
impl CompiledInstructionView {
fn from_raw(ix: &UiCompiledInstruction, account_keys: &[Pubkey]) -> Self {
Self {
program_id_index: ix.program_id_index,
accounts: ix
.accounts
.iter()
.filter_map(|index| account_keys.get(*index as usize).copied())
.collect(),
data: bs58::decode(&ix.data).into_vec(),
}
}
fn from_parsed(ix: &UiInstruction, account_keys: &[Pubkey]) -> Option<Self> {
match ix {
UiInstruction::Compiled(compiled) => Some(Self::from_raw(compiled, account_keys)),
_ => None,
}
}
fn program_id(&self, account_keys: &[Pubkey]) -> Option<Pubkey> {
account_keys.get(self.program_id_index as usize).copied()
}
fn kind(&self) -> Result<PmpInstruction> {
let data = self.data_bytes()?;
let value = data
.first()
.copied()
.ok_or_else(|| anyhow!("empty PMP instruction data"))?;
PmpInstruction::try_from(value).map_err(|err| anyhow!(err.detail))
}
fn data_bytes(&self) -> Result<&[u8]> {
self.data
.as_deref()
.map_err(|err| anyhow!("invalid base58 PMP instruction data: {err}"))
}
}
fn parse_write_data(data: &[u8]) -> std::result::Result<BufferWrite, PmpFetchError> {
if data.len() < 5 || data[0] != PmpInstruction::Write as u8 {
return Err(PmpFetchError::invalid_buffer_instruction(
"invalid PMP buffer write instruction",
));
}
let offset = u32::from_le_bytes(data[1..5].try_into().map_err(|_| {
PmpFetchError::invalid_buffer_instruction("invalid PMP buffer write offset")
})?) as usize;
Ok(BufferWrite {
offset,
bytes: data[5..].to_vec(),
})
}
fn parse_initialize_data(
data: &[u8],
) -> std::result::Result<(PmpMetadataHeader, &[u8]), PmpFetchError> {
if data.len() < 1 + SEED_SIZE + 4 || data[0] != PmpInstruction::Initialize as u8 {
return Err(PmpFetchError::invalid_transaction(
"invalid PMP initialize instruction",
));
}
let seed = parse_seed(&data[1..1 + SEED_SIZE])
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?;
if seed != IDL_SEED {
return Err(PmpFetchError::invalid_transaction(format!(
"unsupported PMP metadata seed {seed}"
)));
}
let header = PmpMetadataHeader {
encoding: MetadataEncoding::try_from(data[1 + SEED_SIZE])?,
compression: MetadataCompression::try_from(data[1 + SEED_SIZE + 1])?,
format: MetadataFormat::try_from(data[1 + SEED_SIZE + 2])?,
data_source: MetadataDataSource::try_from(data[1 + SEED_SIZE + 3])?,
};
Ok((header, &data[1 + SEED_SIZE + 4..]))
}
fn parse_set_data(data: &[u8]) -> std::result::Result<(PmpMetadataHeader, &[u8]), PmpFetchError> {
if data.len() < 5 || data[0] != PmpInstruction::SetData as u8 {
return Err(PmpFetchError::invalid_transaction(
"invalid PMP set-data instruction",
));
}
Ok((
PmpMetadataHeader {
encoding: MetadataEncoding::try_from(data[1])?,
compression: MetadataCompression::try_from(data[2])?,
format: MetadataFormat::try_from(data[3])?,
data_source: MetadataDataSource::try_from(data[4])?,
},
&data[5..],
))
}
fn parse_seed(bytes: &[u8]) -> Result<String> {
let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
std::str::from_utf8(&bytes[..end])
.map(|s| s.to_string())
.context("invalid UTF-8 metadata seed")
}
impl PmpMetadataHeader {
fn decode_direct(self, bytes: &[u8]) -> std::result::Result<Vec<u8>, PmpFetchError> {
if self.format != MetadataFormat::Json {
return Err(PmpFetchError::invalid_transaction(format!(
"unsupported PMP metadata format {:?}",
self.format
)));
}
if self.data_source != MetadataDataSource::Direct {
return Err(PmpFetchError::unsupported_data_source(format!(
"unsupported metadata data source {:?}",
self.data_source
)));
}
let decoded = decode_metadata_data(bytes, self.encoding)?;
let decompressed = decompress_metadata_data(&decoded, self.compression)?;
serde_json::from_slice::<serde_json::Value>(&decompressed).map_err(|err| {
PmpFetchError::invalid_transaction(format!(
"PMP metadata payload is not valid JSON: {err}"
))
})?;
Ok(decompressed)
}
}
fn decode_metadata_data(
bytes: &[u8],
encoding: MetadataEncoding,
) -> std::result::Result<Vec<u8>, PmpFetchError> {
match encoding {
MetadataEncoding::None => {
let text = std::str::from_utf8(bytes)
.map_err(|_| PmpFetchError::invalid_transaction("invalid hex metadata payload"))?;
hex_decode(text).map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))
}
MetadataEncoding::Utf8 => Ok(bytes.to_vec()),
MetadataEncoding::Base58 => bs58::decode(bytes).into_vec().map_err(|err| {
PmpFetchError::invalid_transaction(format!("invalid base58 metadata payload: {err}"))
}),
MetadataEncoding::Base64 => base64::engine::general_purpose::STANDARD
.decode(bytes)
.map_err(|err| {
PmpFetchError::invalid_transaction(format!(
"invalid base64 metadata payload: {err}"
))
}),
}
}
fn decompress_metadata_data(
bytes: &[u8],
compression: MetadataCompression,
) -> std::result::Result<Vec<u8>, PmpFetchError> {
match compression {
MetadataCompression::None => Ok(bytes.to_vec()),
MetadataCompression::Gzip => {
let mut decoder = GzDecoder::new(bytes);
read_bounded_decompressed(&mut decoder)
}
MetadataCompression::Zlib => {
let mut decoder = ZlibDecoder::new(bytes);
read_bounded_decompressed(&mut decoder)
}
}
}
fn read_bounded_decompressed<R: Read>(
decoder: &mut R,
) -> std::result::Result<Vec<u8>, PmpFetchError> {
let mut out = Vec::new();
decoder
.take(MAX_IDL_BUFFER_BYTES as u64 + 1)
.read_to_end(&mut out)
.map_err(|err| PmpFetchError::invalid_transaction(err.to_string()))?;
if out.len() > MAX_IDL_BUFFER_BYTES {
return Err(PmpFetchError::invalid_transaction(format!(
"decompressed PMP metadata exceeds safety limit of {} bytes",
MAX_IDL_BUFFER_BYTES
)));
}
Ok(out)
}
fn hex_decode(hex: &str) -> Result<Vec<u8>> {
if !hex.len().is_multiple_of(2) {
bail!("hex metadata payload has odd length");
}
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(anyhow::Error::from))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_pmp_address_matches_ts_logic() {
let program = Pubkey::from_str("2uA3amp95zsEHUpo8qnLMhcFAUsiKVEcKHXS1JetFjU5").unwrap();
assert_eq!(
pmp_metadata_address(&program, None).to_string(),
"FquHyG5PSt6GNyzAm7LFupqoKkCbbsLTUVvzdrJmy4MU"
);
}
#[test]
fn decode_utf8_zlib_json_payload() {
use {
flate2::{write::ZlibEncoder, Compression},
std::io::Write,
};
let input = br#"{"name":"example"}"#;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(input).unwrap();
let compressed = encoder.finish().unwrap();
let decoded = PmpMetadataHeader {
encoding: MetadataEncoding::Utf8,
compression: MetadataCompression::Zlib,
format: MetadataFormat::Json,
data_source: MetadataDataSource::Direct,
}
.decode_direct(&compressed)
.unwrap();
assert_eq!(decoded, input);
}
#[test]
fn parse_inline_set_data() {
let instruction = [
PmpInstruction::SetData as u8,
1,
0,
MetadataFormat::Json as u8,
MetadataDataSource::Direct as u8,
b'{',
b'}',
];
let (header, payload) = parse_set_data(&instruction).unwrap();
assert_eq!(
header,
PmpMetadataHeader {
encoding: MetadataEncoding::Utf8,
compression: MetadataCompression::None,
format: MetadataFormat::Json,
data_source: MetadataDataSource::Direct,
}
);
assert_eq!(payload, b"{}");
}
#[test]
fn ordered_buffer_writes_apply_newest_last() {
let header_len = PMP_BUFFER_HEADER_SIZE;
let initial = vec![0u8; header_len + 4];
let out = apply_buffer_writes_to_bytes(vec![
BufferWrite {
offset: 0,
bytes: initial,
},
BufferWrite {
offset: header_len,
bytes: b"new!".to_vec(),
},
])
.unwrap();
assert_eq!(out, b"new!");
}
#[test]
fn parse_initialize_inline_payload_is_raw() {
let header_len = 1 + SEED_SIZE + 4;
let mut data = vec![0u8; header_len];
data[0] = PmpInstruction::Initialize as u8;
data[1..1 + IDL_SEED.len()].copy_from_slice(IDL_SEED.as_bytes());
data[1 + SEED_SIZE] = MetadataEncoding::Utf8 as u8;
data[1 + SEED_SIZE + 1] = MetadataCompression::None as u8;
data[1 + SEED_SIZE + 2] = MetadataFormat::Json as u8;
data[1 + SEED_SIZE + 3] = MetadataDataSource::Direct as u8;
data.extend_from_slice(b"{\"k\":1}");
let (header, payload) = parse_initialize_data(&data).unwrap();
assert_eq!(header.format, MetadataFormat::Json);
assert_eq!(payload, b"{\"k\":1}");
}
#[test]
fn parse_initialize_buffer_path_has_empty_payload() {
let mut data = vec![0u8; 1 + SEED_SIZE + 4];
data[0] = PmpInstruction::Initialize as u8;
data[1..1 + IDL_SEED.len()].copy_from_slice(IDL_SEED.as_bytes());
let (_, payload) = parse_initialize_data(&data).unwrap();
assert!(payload.is_empty());
}
}