use std::{any::Any, marker::ConstParamTy};
use auto_impl::auto_impl;
use solana_address::Address;
use solana_transaction::sanitized::SanitizedTransaction;
use crate::{Svm, SvmTypesHost, state_change_sink::AccountChange};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum TransactionOrigin {
User,
InternalAirdrop,
Batch {
stage_index: usize,
transaction_index: usize,
},
}
impl TransactionOrigin {
pub const fn is_user(self) -> bool {
matches!(self, Self::User)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ConstParamTy)]
pub enum InspectMode {
NoInspector,
DynInspector,
}
impl InspectMode {
pub const fn is_live(self) -> bool {
match self {
Self::DynInspector => true,
Self::NoInspector => false,
}
}
}
#[auto_impl(Box, &mut)]
pub trait Inspector<T: SvmTypesHost>: Send + Sync {
fn accepts_origin(&self, origin: TransactionOrigin) -> bool {
origin.is_user()
}
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn initialize(&mut self, _svm: &Svm<'_, T>) {}
fn transaction_start(&mut self, _svm: &Svm<'_, T>, _tx: &SanitizedTransaction) {}
fn instruction_start(&mut self, _index: usize, _program_id: &Address) {}
fn instruction_end(&mut self, _index: usize, _program_id: &Address) {}
fn cpi_start(&mut self, _depth: u8, _program_id: &Address) {}
fn cpi_end(&mut self, _depth: u8, _program_id: &Address) {}
fn log(&mut self, _message: &str) {}
fn account_change(&mut self, _change: AccountChange<'_>) {}
fn transaction_end(
&mut self,
_svm: &Svm<'_, T>,
_result: &solana_transaction_error::TransactionResult<()>,
) {
}
}
impl<T: SvmTypesHost> dyn Inspector<T> + '_ {
pub fn downcast_ref<U: 'static>(&self) -> Option<&U> {
self.as_any().downcast_ref::<U>()
}
pub fn downcast_mut<U: 'static>(&mut self) -> Option<&mut U> {
self.as_any_mut().downcast_mut::<U>()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OriginFilter {
UserOnly,
All,
BatchStages,
InternalAirdrop,
NonUser,
}
impl OriginFilter {
pub const fn allows(self, origin: TransactionOrigin) -> bool {
match (self, origin) {
(Self::All, _) => true,
(Self::UserOnly, TransactionOrigin::User) => true,
(Self::BatchStages, TransactionOrigin::Batch { .. }) => true,
(Self::InternalAirdrop, TransactionOrigin::InternalAirdrop) => true,
(Self::NonUser, TransactionOrigin::User) => false,
(Self::NonUser, _) => true,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct OriginFiltered<I> {
inner: I,
filter: OriginFilter,
}
impl<I> OriginFiltered<I> {
pub const fn new(inner: I, filter: OriginFilter) -> Self {
Self { inner, filter }
}
pub const fn inner(&self) -> &I {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut I {
&mut self.inner
}
pub const fn filter(&self) -> OriginFilter {
self.filter
}
pub fn into_inner(self) -> I {
self.inner
}
}
impl<T: SvmTypesHost, I: Inspector<T> + 'static> Inspector<T> for OriginFiltered<I> {
fn accepts_origin(&self, origin: TransactionOrigin) -> bool {
self.filter.allows(origin) && self.inner.accepts_origin(origin)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn initialize(&mut self, svm: &Svm<'_, T>) {
self.inner.initialize(svm);
}
fn transaction_start(&mut self, svm: &Svm<'_, T>, tx: &SanitizedTransaction) {
self.inner.transaction_start(svm, tx);
}
fn instruction_start(&mut self, index: usize, program_id: &Address) {
self.inner.instruction_start(index, program_id);
}
fn instruction_end(&mut self, index: usize, program_id: &Address) {
self.inner.instruction_end(index, program_id);
}
fn cpi_start(&mut self, depth: u8, program_id: &Address) {
self.inner.cpi_start(depth, program_id);
}
fn cpi_end(&mut self, depth: u8, program_id: &Address) {
self.inner.cpi_end(depth, program_id);
}
fn log(&mut self, message: &str) {
self.inner.log(message);
}
fn account_change(&mut self, change: AccountChange<'_>) {
self.inner.account_change(change);
}
fn transaction_end(
&mut self,
svm: &Svm<'_, T>,
result: &solana_transaction_error::TransactionResult<()>,
) {
self.inner.transaction_end(svm, result);
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use solana_keypair::Keypair;
use solana_message::{Message, VersionedMessage};
use solana_signer::Signer;
use solana_system_interface::instruction::transfer;
use solana_transaction::versioned::VersionedTransaction;
use solana_transaction_error::TransactionResult;
use super::*;
use crate::{BaseSvmTypes, Hpsvm, Svm};
fn sanitized_transfer(svm: &Svm<'_, BaseSvmTypes>) -> SanitizedTransaction {
let payer = Keypair::new();
let recipient = Address::new_unique();
let message =
Message::new(&[transfer(&payer.pubkey(), &recipient, 1)], Some(&payer.pubkey()));
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer])
.expect("unsigned transfer should sanitize");
svm.sanitize_transaction_no_verify(tx).expect("sanitize should succeed")
}
#[derive(Default)]
struct RecordingInspector {
events: Vec<String>,
}
impl Inspector<BaseSvmTypes> for RecordingInspector {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn transaction_start(&mut self, _svm: &Svm<'_, BaseSvmTypes>, _tx: &SanitizedTransaction) {
self.events.push("start".to_string());
}
fn instruction_start(&mut self, index: usize, program_id: &Address) {
self.events.push(format!("instruction_start {index} {program_id}"));
}
fn instruction_end(&mut self, index: usize, program_id: &Address) {
self.events.push(format!("instruction_end {index} {program_id}"));
}
fn transaction_end(
&mut self,
_svm: &Svm<'_, BaseSvmTypes>,
result: &TransactionResult<()>,
) {
self.events.push(format!("end {}", result.is_ok()));
}
}
struct OriginRecorder {
origins: Mutex<Vec<TransactionOrigin>>,
}
impl OriginRecorder {
fn origins(&self) -> Vec<TransactionOrigin> {
self.origins.lock().expect("origin recorder lock").clone()
}
}
impl Inspector<BaseSvmTypes> for OriginRecorder {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn accepts_origin(&self, origin: TransactionOrigin) -> bool {
self.origins.lock().expect("origin recorder lock").push(origin);
true
}
}
fn origins() -> [TransactionOrigin; 4] {
[
TransactionOrigin::User,
TransactionOrigin::InternalAirdrop,
TransactionOrigin::Batch { stage_index: 0, transaction_index: 0 },
TransactionOrigin::Batch { stage_index: 3, transaction_index: 7 },
]
}
#[test]
fn only_the_user_origin_is_a_user_origin() {
assert!(TransactionOrigin::User.is_user());
assert!(!TransactionOrigin::InternalAirdrop.is_user());
assert!(!TransactionOrigin::Batch { stage_index: 0, transaction_index: 0 }.is_user());
assert!(!TransactionOrigin::Batch { stage_index: 9, transaction_index: 9 }.is_user());
}
#[test]
fn default_accepts_origin_forwards_user_traffic_only() {
let inspector = RecordingInspector::default();
assert!(inspector.accepts_origin(TransactionOrigin::User));
for origin in origins().into_iter().filter(|origin| !origin.is_user()) {
assert!(!inspector.accepts_origin(origin), "origin {origin:?}");
}
}
#[test]
fn origin_filter_allows_exactly_its_documented_origins() {
let batch = TransactionOrigin::Batch { stage_index: 1, transaction_index: 2 };
let cases = [
(OriginFilter::UserOnly, TransactionOrigin::User, true),
(OriginFilter::UserOnly, TransactionOrigin::InternalAirdrop, false),
(OriginFilter::UserOnly, batch, false),
(OriginFilter::All, TransactionOrigin::User, true),
(OriginFilter::All, TransactionOrigin::InternalAirdrop, true),
(OriginFilter::All, batch, true),
(OriginFilter::BatchStages, batch, true),
(OriginFilter::BatchStages, TransactionOrigin::User, false),
(OriginFilter::BatchStages, TransactionOrigin::InternalAirdrop, false),
(OriginFilter::InternalAirdrop, TransactionOrigin::InternalAirdrop, true),
(OriginFilter::InternalAirdrop, TransactionOrigin::User, false),
(OriginFilter::InternalAirdrop, batch, false),
(OriginFilter::NonUser, TransactionOrigin::User, false),
(OriginFilter::NonUser, TransactionOrigin::InternalAirdrop, true),
(OriginFilter::NonUser, batch, true),
];
for (filter, origin, expected) in cases {
assert_eq!(filter.allows(origin), expected, "{filter:?} on {origin:?}");
}
}
#[test]
fn origin_filtered_forwards_hooks_only_for_allowed_origins() {
let svm = Hpsvm::new();
let tx = sanitized_transfer(&svm);
let program_id = Address::new_unique();
let mut allowed =
OriginFiltered::new(RecordingInspector::default(), OriginFilter::UserOnly);
assert!(allowed.accepts_origin(TransactionOrigin::User));
allowed.transaction_start(&svm, &tx);
allowed.instruction_start(0, &program_id);
allowed.instruction_end(0, &program_id);
assert_eq!(
allowed.inner().events,
vec![
"start".to_string(),
format!("instruction_start 0 {program_id}"),
format!("instruction_end 0 {program_id}"),
]
);
let blocked = OriginFiltered::new(RecordingInspector::default(), OriginFilter::BatchStages);
assert!(!blocked.accepts_origin(TransactionOrigin::User));
assert!(blocked.inner().events.is_empty());
}
#[test]
fn nested_origin_filtered_decorators_combine_their_filters() {
type Nested = OriginFiltered<OriginFiltered<RecordingInspector>>;
let batch = TransactionOrigin::Batch { stage_index: 0, transaction_index: 0 };
let nested = Nested::new(
OriginFiltered::new(RecordingInspector::default(), OriginFilter::All),
OriginFilter::UserOnly,
);
assert!(nested.accepts_origin(TransactionOrigin::User));
assert!(!nested.accepts_origin(batch));
assert!(!nested.accepts_origin(TransactionOrigin::InternalAirdrop));
}
#[test]
fn boxed_inspector_downcasts_to_its_concrete_type_only() {
let boxed: Box<dyn Inspector<BaseSvmTypes>> = Box::new(RecordingInspector::default());
assert!(boxed.downcast_ref::<RecordingInspector>().is_some());
assert!(boxed.downcast_ref::<OriginRecorder>().is_none());
let mut reboxed: Box<dyn Inspector<BaseSvmTypes>> =
Box::new(OriginFiltered::new(RecordingInspector::default(), OriginFilter::All));
assert!(reboxed.downcast_ref::<OriginFiltered<RecordingInspector>>().is_some());
assert!(reboxed.downcast_mut::<OriginFiltered<RecordingInspector>>().is_some());
assert!(reboxed.downcast_ref::<RecordingInspector>().is_none());
}
#[test]
fn mutably_borrowed_inspector_forwards_every_hook() {
let svm = Hpsvm::new();
let tx = sanitized_transfer(&svm);
let program_id = Address::new_unique();
let mut inspector = RecordingInspector::default();
let borrowed: &mut dyn Inspector<BaseSvmTypes> = &mut inspector;
borrowed.transaction_start(&svm, &tx);
borrowed.instruction_start(2, &program_id);
borrowed.instruction_end(2, &program_id);
assert_eq!(
inspector.events,
vec![
"start".to_string(),
format!("instruction_start 2 {program_id}"),
format!("instruction_end 2 {program_id}"),
]
);
}
#[test]
fn every_hook_fires_by_direct_call() {
let svm = Hpsvm::new();
let tx = sanitized_transfer(&svm);
let program_id = Address::new_unique();
let mut inspector = RecordingInspector::default();
inspector.initialize(&svm);
inspector.transaction_start(&svm, &tx);
inspector.instruction_start(0, &program_id);
inspector.instruction_end(0, &program_id);
inspector.cpi_start(1, &program_id);
inspector.cpi_end(1, &program_id);
inspector.log("hello");
inspector.account_change(AccountChange { address: program_id, before: None, after: None });
assert_eq!(
inspector.events,
vec![
"start".to_string(),
format!("instruction_start 0 {program_id}"),
format!("instruction_end 0 {program_id}"),
]
);
}
#[test]
fn origin_recorder_observes_every_consulted_origin() {
let recorder = OriginRecorder { origins: Mutex::new(Vec::new()) };
for origin in origins() {
assert!(recorder.accepts_origin(origin));
}
assert_eq!(recorder.origins(), origins());
}
#[test]
fn empty_implementation_accepts_every_hook_as_a_no_op() {
struct Empty;
impl Inspector<BaseSvmTypes> for Empty {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
let svm = Hpsvm::new();
let tx = sanitized_transfer(&svm);
let program_id = Address::new_unique();
let mut inspector = Empty;
assert!(inspector.accepts_origin(TransactionOrigin::User));
inspector.initialize(&svm);
inspector.transaction_start(&svm, &tx);
inspector.instruction_start(0, &program_id);
inspector.instruction_end(0, &program_id);
inspector.cpi_start(1, &program_id);
inspector.cpi_end(1, &program_id);
inspector.log("hello");
inspector.account_change(AccountChange { address: program_id, before: None, after: None });
}
}