#[cfg(not(test))]
use {prost::Message, std::ffi::c_int};
use {
protosol::protos::{
CostContext as ProtoCostContext, CostResult as ProtoCostResult,
SanitizedTransaction as ProtoSanitizedTransaction, TxnCostMode as ProtoTxnCostMode,
},
solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost},
solana_message::SimpleAddressLoader,
solana_packet::PACKET_DATA_SIZE,
solana_runtime_transaction::runtime_transaction::RuntimeTransaction,
solana_signature::Signature,
solana_svm::conformance::{
feature_set::feature_set_from_proto, versioned_message::versioned_message_from_proto,
},
solana_transaction::{
sanitized::{MessageHash, SanitizedTransaction},
versioned::VersionedTransaction,
},
};
fn runtime_transaction_from_proto(
proto_tx: &ProtoSanitizedTransaction,
) -> RuntimeTransaction<SanitizedTransaction> {
let proto_message = proto_tx
.message
.as_ref()
.expect("missing transaction message");
let message = versioned_message_from_proto(proto_message);
let mut signatures: Vec<Signature> = proto_tx
.signatures
.iter()
.map(|s| {
Signature::from(<[u8; 64]>::try_from(s.as_slice()).expect("invalid signature bytes"))
})
.collect();
if signatures.is_empty() {
signatures.push(Signature::default());
}
let versioned_tx = VersionedTransaction {
signatures,
message,
};
let serialized_size =
bincode::serialized_size(&versioned_tx).expect("failed to compute serialized size");
assert!(
serialized_size <= PACKET_DATA_SIZE as u64,
"transaction exceeds max packet size",
);
RuntimeTransaction::try_create(
versioned_tx,
MessageHash::Compute,
None,
SimpleAddressLoader::Disabled,
&std::collections::HashSet::new(),
)
.expect("failed to create RuntimeTransaction")
}
fn cost_result_to_proto<Tx>(cost: &TransactionCost<'_, Tx>) -> ProtoCostResult
where
Tx: solana_runtime_transaction::transaction_meta::TransactionMeta,
{
ProtoCostResult {
has_cost: true,
signature_cost: cost.signature_cost(),
write_lock_cost: cost.write_lock_cost(),
data_bytes_cost: u64::from(cost.data_bytes_cost()),
programs_execution_cost: cost.programs_execution_cost(),
loaded_accounts_data_size_cost: cost.loaded_accounts_data_size_cost(),
allocated_accounts_data_size: cost.allocated_accounts_data_size(),
total_cost: cost.sum(),
}
}
pub fn execute_cost(input: &ProtoCostContext) -> ProtoCostResult {
let proto_tx = input
.tx
.as_ref()
.expect("ProtoCostContext missing transaction");
let runtime_tx = runtime_transaction_from_proto(proto_tx);
let feature_set = input
.features
.as_ref()
.map(feature_set_from_proto)
.unwrap_or_default();
let cost = if input.mode == ProtoTxnCostMode::Actual as i32 {
CostModel::calculate_cost_for_executed_transaction(
&runtime_tx,
u64::from(input.actual_programs_execution_cost),
input.actual_loaded_accounts_data_size_bytes,
&feature_set,
)
} else {
CostModel::calculate_cost(&runtime_tx, &feature_set)
};
cost_result_to_proto(&cost)
}
#[cfg(not(test))]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sol_compat_txn_cost_v1(
out_ptr: *mut u8,
out_psz: *mut u64,
in_ptr: *mut u8,
in_sz: u64,
) -> c_int {
if out_ptr.is_null() || out_psz.is_null() {
return 0;
}
let in_slice = if in_sz == 0 {
&[]
} else if in_ptr.is_null() {
return 0;
} else {
unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }
};
let Ok(cost_context) = ProtoCostContext::decode(in_slice) else {
return 0;
};
let cost_result = execute_cost(&cost_context);
let out_vec = cost_result.encode_to_vec();
let out_cap = unsafe { *out_psz } as usize;
if out_vec.len() > out_cap {
return 0;
}
let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, out_cap) };
out_slice[..out_vec.len()].copy_from_slice(&out_vec);
unsafe { *out_psz = out_vec.len() as u64 };
1
}
#[cfg(test)]
mod tests {
use {
super::*,
agave_feature_set::FEATURE_NAMES,
protosol::protos::{
CompiledInstruction as ProtoCompiledInstruction, FeatureSet as ProtoFeatureSet,
MessageHeader as ProtoMessageHeader, SanitizedTransaction as ProtoSanitizedTransaction,
TransactionMessage as ProtoTransactionMessage,
},
solana_pubkey::Pubkey,
};
const VOTE_PROGRAM_ID: [u8; 32] = [
7, 97, 72, 29, 53, 116, 116, 187, 124, 77, 118, 36, 235, 211, 189, 179, 216, 53, 94, 115,
209, 16, 67, 252, 13, 163, 83, 128, 0, 0, 0, 0,
];
const SYSTEM_PROGRAM_ID: [u8; 32] = [0; 32];
fn feature_u64(pubkey: &Pubkey) -> u64 {
let b = pubkey.to_bytes();
u64::from_le_bytes(b[..8].try_into().unwrap())
}
fn all_features() -> ProtoFeatureSet {
ProtoFeatureSet {
features: FEATURE_NAMES.keys().map(feature_u64).collect(),
}
}
fn simple_transfer_tx() -> ProtoSanitizedTransaction {
let msg = ProtoTransactionMessage {
is_legacy: true,
header: Some(ProtoMessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
}),
account_keys: vec![vec![1; 32], vec![2; 32], SYSTEM_PROGRAM_ID.to_vec()],
recent_blockhash: vec![0; 32],
instructions: vec![ProtoCompiledInstruction {
program_id_index: 2,
accounts: vec![0, 1],
data: vec![2, 0, 0, 0],
}],
address_table_lookups: vec![],
};
ProtoSanitizedTransaction {
message: Some(msg),
message_hash: vec![0; 32],
signatures: vec![vec![0; 64]],
}
}
fn vote_tx() -> ProtoSanitizedTransaction {
let msg = ProtoTransactionMessage {
is_legacy: true,
header: Some(ProtoMessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
}),
account_keys: vec![vec![1; 32], vec![2; 32], VOTE_PROGRAM_ID.to_vec()],
recent_blockhash: vec![0; 32],
instructions: vec![ProtoCompiledInstruction {
program_id_index: 2,
accounts: vec![0, 1],
data: vec![0; 16],
}],
address_table_lookups: vec![],
};
ProtoSanitizedTransaction {
message: Some(msg),
message_hash: vec![0; 32],
signatures: vec![vec![0; 64]],
}
}
fn estimate_context(tx: ProtoSanitizedTransaction) -> ProtoCostContext {
ProtoCostContext {
tx: Some(tx),
features: Some(all_features()),
mode: ProtoTxnCostMode::Estimate as i32,
actual_programs_execution_cost: 0,
actual_loaded_accounts_data_size_bytes: 0,
}
}
fn assert_has_cost(ctx: &ProtoCostContext) -> ProtoCostResult {
let result = execute_cost(ctx);
assert!(result.has_cost, "expected has_cost to be true");
result
}
#[test]
fn test_estimate_simple_transfer() {
let result = assert_has_cost(&estimate_context(simple_transfer_tx()));
assert!(result.signature_cost > 0);
assert!(result.write_lock_cost > 0);
assert!(result.programs_execution_cost > 0);
let component_sum = result.signature_cost
+ result.write_lock_cost
+ result.data_bytes_cost
+ result.programs_execution_cost
+ result.loaded_accounts_data_size_cost;
assert_eq!(result.total_cost, component_sum);
}
#[test]
fn test_estimate_vote() {
let result = assert_has_cost(&estimate_context(vote_tx()));
assert!(result.total_cost > 0);
assert!(result.signature_cost > 0);
}
#[test]
fn test_actual_mode_uses_provided_costs() {
let exec_cost: u32 = 5000;
let loaded_size: u32 = 65536;
let ctx = ProtoCostContext {
tx: Some(simple_transfer_tx()),
features: Some(all_features()),
mode: ProtoTxnCostMode::Actual as i32,
actual_programs_execution_cost: exec_cost,
actual_loaded_accounts_data_size_bytes: loaded_size,
};
let result = assert_has_cost(&ctx);
assert_eq!(result.programs_execution_cost, u64::from(exec_cost));
let estimate = assert_has_cost(&estimate_context(simple_transfer_tx()));
assert_ne!(
result.loaded_accounts_data_size_cost,
estimate.loaded_accounts_data_size_cost,
);
}
#[test]
#[should_panic(expected = "ProtoCostContext missing transaction")]
fn test_missing_transaction_panics() {
let ctx = ProtoCostContext {
tx: None,
features: Some(all_features()),
mode: ProtoTxnCostMode::Estimate as i32,
actual_programs_execution_cost: 0,
actual_loaded_accounts_data_size_bytes: 0,
};
execute_cost(&ctx);
}
#[test]
#[should_panic(expected = "transaction exceeds max packet size")]
fn test_oversized_transaction_panics() {
let msg = ProtoTransactionMessage {
is_legacy: true,
header: Some(ProtoMessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
}),
account_keys: vec![vec![1; 32], SYSTEM_PROGRAM_ID.to_vec()],
recent_blockhash: vec![0; 32],
instructions: vec![ProtoCompiledInstruction {
program_id_index: 1,
accounts: vec![0],
data: vec![0u8; 1300],
}],
address_table_lookups: vec![],
};
let tx = ProtoSanitizedTransaction {
message: Some(msg),
message_hash: vec![0; 32],
signatures: vec![vec![0; 64]],
};
let ctx = estimate_context(tx);
execute_cost(&ctx);
}
}