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
18 changes: 15 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["solana/account"]
members = ["solana/account", "solana/transaction-context"]
resolver = "3"

[workspace.package]
Expand All @@ -15,6 +15,7 @@ version = "0.1.0"
magic-root-interface = { path = "programs/magic-root-interface" }
magic-root-program = { path = "programs/magic-root-program" }
solana-account = { path = "solana/account" }
solana-transaction-context = { path = "solana/transaction-context" }

ahash = "0.8.12"
arc-swap = "1.9.1"
Expand All @@ -33,18 +34,29 @@ tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] }
wincode = "0.5.1"
zstd = { version = "0.13.3", default-features = false }

agave-feature-set = { version = "3.1.14", features = ["agave-unstable-api"] }
agave-transaction-view = { version = "3.1.13", features = ["agave-unstable-api"] }
agave-feature-set = "4.1.1"
agave-precompiles = "4.1.1"
agave-syscalls = { package = "solana-syscalls", version = "4.1.1", default-features = false }
agave-transaction-view = "4.1.1"
solana-account-info = "3.1.1"
solana-clock = "3.1.0"
solana-compute-budget-instruction = "4.1.1"
solana-cpi = "3.1.0"
solana-hash = "4.3.0"
solana-instruction = "3.4.0"
solana-instruction-error = "2.3.0"
solana-instructions-sysvar = "4.0.0"
solana-program-entrypoint = "3.1.1"
solana-program-error = "3.0.1"
solana-pubkey = "4.2.0"
solana-rent = "4.0.0-rc.1"
solana-sbpf = "0.13.1"
solana-sdk-ids = "3.1.0"
solana-sha256-hasher = "3.1.0"
solana-short-vec = "3.2.1"
solana-signature = "3.4.0"
solana-sysvar = "4.0.0"
solana-system-interface = "3.2.0"
solana-transaction-error = "3.2.0"


Expand Down
50 changes: 50 additions & 0 deletions solana/transaction-context/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[package]
name = "solana-transaction-context"

authors = { workspace = true }
description = "Solana data shared between program runtime and built-in programs as well as SBF programs."
documentation = "https://docs.rs/solana-transaction-context"
edition = { workspace = true }
homepage = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version = { workspace = true }

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg=docsrs"]
targets = ["x86_64-unknown-linux-gnu"]

[features]
# No-op stub retained only so external (patched-in) crates that reference
# `solana-transaction-context/agave-unstable-api` still resolve; the lib is no
# longer gated on it.
agave-unstable-api = []
bincode = ["dep:bincode", "serde", "solana-account/bincode"]
dev-context-only-utils = ["bincode", "dep:qualifier_attr", "solana-account/dev-context-only-utils"]
serde = ["serde/derive", "solana-pubkey/serde"]

[dependencies]
solana-account = { workspace = true }
solana-instruction = { workspace = true, features = ["std"] }
solana-instructions-sysvar = { workspace = true }
solana-pubkey = { workspace = true }

[target.'cfg(not(any(target_arch = "sbf", target_arch = "bpf")))'.dependencies]
bincode = { workspace = true, optional = true }
qualifier_attr = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
solana-rent = { workspace = true }
solana-sbpf = { workspace = true }
solana-sdk-ids = { workspace = true }
solana-signature = { workspace = true, optional = true }

[dev-dependencies]
solana-account-info = { workspace = true }
solana-program-entrypoint = { workspace = true }
solana-system-interface = { workspace = true }
solana-transaction-context = { path = ".", features = ["dev-context-only-utils"] }
static_assertions = "1.1.0"

[lints.rust]
unexpected_cfgs = "allow"
20 changes: 20 additions & 0 deletions solana/transaction-context/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# `solana-transaction-context`

A fork of Agave's `solana-transaction-context` (`anza-xyz/agave`), reshaped for
the engine and patched in workspace-wide through `[patch.crates-io]`.

These are the low-level structures a single transaction executes over:
`TransactionContext`, `InstructionContext`, the `TransactionAccounts` store with
its `AccountRef`/`AccountRefMut` views, `ExecutionRecord`, and the account and
transaction limits.

The detail that drives the design: accounts live in `UnsafeCell` behind explicit
borrow counters rather than ordinary Rust borrows. That keeps account borrows
*local to the context*, while still letting the VM's access handlers remap an
account's data underneath an active borrow when a program writes through a mapped
region — something the normal borrow checker can't express. The counters are what
make that safe at runtime.

The engine's intentional divergences from upstream are documented for the whole
fork in [`../README.md`](../README.md) — see "Transaction Context", "VM Account
Mapping", and "Access-Violation Growth".
275 changes: 275 additions & 0 deletions solana/transaction-context/src/instruction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
use {
crate::{
IndexOfAccount,
instruction_accounts::{BorrowedInstructionAccount, InstructionAccount},
transaction::TransactionContext,
vm_addresses::{
GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS, GUEST_INSTRUCTION_DATA_BASE_ADDRESS,
GUEST_REGION_SIZE,
},
vm_slice::VmSlice,
},
solana_account::ReadableAccount,
solana_instruction::error::InstructionError,
solana_pubkey::Pubkey,
std::collections::HashSet,
};

/// Instruction shared between runtime and programs.
#[repr(C)]
#[derive(Debug)]
pub struct InstructionFrame {
/// Reserved field for alignment and potential future usage.
pub reserved: u16,
pub program_account_index_in_tx: u16,
pub nesting_level: u16,
/// This is the index of the parent instruction if this is a CPI and u16::MAX if this is a
/// top-level instruction
pub index_of_caller_instruction: u16,
pub instruction_accounts: VmSlice<InstructionAccount>,
pub instruction_data: VmSlice<u8>,
}

impl Default for InstructionFrame {
fn default() -> Self {
InstructionFrame {
nesting_level: 0,
program_account_index_in_tx: 0,
index_of_caller_instruction: u16::MAX,
// Using u64::MAX as the default pointer value, since it shall never be accessible.
instruction_accounts: VmSlice::new(0, 0),
instruction_data: VmSlice::new(0, 0),
reserved: 0,
}
}
}

#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))]
impl InstructionFrame {
pub fn configure_vm_slices(
&mut self,
instruction_index: u64,
instruction_accounts_len: usize,
instruction_data_len: u64,
) {
let common_offset = GUEST_REGION_SIZE.saturating_mul(instruction_index);

// Instruction data slice
self.instruction_data = VmSlice::new(
GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(common_offset),
instruction_data_len,
);

// Instruction accounts slice
self.instruction_accounts = VmSlice::new(
GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS.saturating_add(common_offset),
instruction_accounts_len as u64,
);
}
}

/// View interface to read instructions.
#[derive(Debug)]
pub struct InstructionContext<'a, 'ix_data> {
pub(crate) transaction_context: &'a TransactionContext<'ix_data>,
// The rest of the fields are redundant shortcuts
pub(crate) index_in_trace: usize,
pub(crate) nesting_level: usize,
pub(crate) index_of_caller_instruction: usize,
pub(crate) program_account_index_in_tx: IndexOfAccount,
pub(crate) instruction_accounts: &'a [InstructionAccount],
pub(crate) dedup_map: &'a [u16],
pub(crate) instruction_data: &'ix_data [u8],
}

impl<'a> InstructionContext<'a, '_> {
/// How many Instructions were on the trace before this one was pushed
pub fn get_index_in_trace(&self) -> usize {
self.index_in_trace
}

/// Returns the index of the instruction that called into this one.
pub fn get_index_of_caller(&self) -> usize {
self.index_of_caller_instruction
}

/// How many Instructions were on the stack after this one was pushed
///
/// That is the number of nested parent Instructions plus one (itself).
pub fn get_stack_height(&self) -> usize {
self.nesting_level.saturating_add(1)
}

/// Number of accounts in this Instruction (without program accounts)
pub fn get_number_of_instruction_accounts(&self) -> IndexOfAccount {
self.instruction_accounts.len() as IndexOfAccount
}

/// Assert that enough accounts were supplied to this Instruction
pub fn check_number_of_instruction_accounts(
&self,
expected_at_least: IndexOfAccount,
) -> Result<(), InstructionError> {
if self.get_number_of_instruction_accounts() < expected_at_least {
Err(InstructionError::MissingAccount)
} else {
Ok(())
}
}

/// Data parameter for the programs `process_instruction` handler
pub fn get_instruction_data(&self) -> &[u8] {
self.instruction_data
}

/// Translates the given instruction wide program_account_index into a transaction wide index
pub fn get_index_of_program_account_in_transaction(
&self,
) -> Result<IndexOfAccount, InstructionError> {
if self.program_account_index_in_tx == u16::MAX {
Err(InstructionError::MissingAccount)
} else {
Ok(self.program_account_index_in_tx)
}
}

/// Translates the given instruction wide instruction_account_index into a transaction wide index
pub fn get_index_of_instruction_account_in_transaction(
&self,
instruction_account_index: IndexOfAccount,
) -> Result<IndexOfAccount, InstructionError> {
Ok(self
.instruction_accounts
.get(instruction_account_index as usize)
.ok_or(InstructionError::MissingAccount)?
.index_in_transaction as IndexOfAccount)
}

/// Get the index of account in instruction from the index in transaction
pub fn get_index_of_account_in_instruction(
&self,
index_in_transaction: IndexOfAccount,
) -> Result<IndexOfAccount, InstructionError> {
self.dedup_map
.get(index_in_transaction as usize)
.and_then(|idx| {
if *idx as usize >= self.instruction_accounts.len() {
None
} else {
Some(*idx as IndexOfAccount)
}
})
.ok_or(InstructionError::MissingAccount)
}

/// Returns `Some(instruction_account_index)` if this is a duplicate
/// and `None` if it is the first account with this key
pub fn is_instruction_account_duplicate(
&self,
instruction_account_index: IndexOfAccount,
) -> Result<Option<IndexOfAccount>, InstructionError> {
let index_in_transaction =
self.get_index_of_instruction_account_in_transaction(instruction_account_index)?;
let first_instruction_account_index =
self.get_index_of_account_in_instruction(index_in_transaction)?;

Ok(
if first_instruction_account_index == instruction_account_index {
None
} else {
Some(first_instruction_account_index)
},
)
}

/// Gets the key of the last program account of this Instruction
pub fn get_program_key(&self) -> Result<&'a Pubkey, InstructionError> {
self.get_index_of_program_account_in_transaction()
.and_then(|index_in_transaction| {
self.transaction_context.get_key_of_account_at_index(index_in_transaction)
})
}

/// Get the owner of the program account of this instruction
pub fn get_program_owner(&self) -> Result<Pubkey, InstructionError> {
self.get_index_of_program_account_in_transaction()
.and_then(|index_in_transaction| {
self.transaction_context.accounts.try_borrow(index_in_transaction)
})
.map(|acc| *acc.owner())
}

/// Gets an instruction account of this Instruction
pub fn try_borrow_instruction_account(
&self,
index_in_instruction: IndexOfAccount,
) -> Result<BorrowedInstructionAccount<'_, '_>, InstructionError> {
let instruction_account = *self
.instruction_accounts
.get(index_in_instruction as usize)
.ok_or(InstructionError::MissingAccount)?;

let account = self
.transaction_context
.accounts
.try_borrow_mut(instruction_account.index_in_transaction)?;

Ok(BorrowedInstructionAccount {
transaction_context: self.transaction_context,
instruction_account,
account,
index_in_transaction_of_instruction_program: self.program_account_index_in_tx,
})
}

/// Returns whether an instruction account is a signer
pub fn is_instruction_account_signer(
&self,
instruction_account_index: IndexOfAccount,
) -> Result<bool, InstructionError> {
Ok(self
.instruction_accounts
.get(instruction_account_index as usize)
.ok_or(InstructionError::MissingAccount)?
.is_signer())
}

/// Returns whether an instruction account is writable
pub fn is_instruction_account_writable(
&self,
instruction_account_index: IndexOfAccount,
) -> Result<bool, InstructionError> {
Ok(self
.instruction_accounts
.get(instruction_account_index as usize)
.ok_or(InstructionError::MissingAccount)?
.is_writable())
}

/// Calculates the set of all keys of signer instruction accounts in this Instruction
pub fn get_signers(&self) -> Result<HashSet<Pubkey>, InstructionError> {
let mut result = HashSet::new();
for instruction_account in self.instruction_accounts.iter() {
if instruction_account.is_signer() {
result.insert(
*self
.transaction_context
.get_key_of_account_at_index(instruction_account.index_in_transaction)?,
);
}
}
Ok(result)
}

pub fn instruction_accounts(&self) -> &[InstructionAccount] {
self.instruction_accounts
}

pub fn get_key_of_instruction_account(
&self,
index_in_instruction: IndexOfAccount,
) -> Result<&'a Pubkey, InstructionError> {
self.get_index_of_instruction_account_in_transaction(index_in_instruction)
.and_then(|idx| self.transaction_context.get_key_of_account_at_index(idx))
}
}
Loading