diff --git a/Cargo.toml b/Cargo.toml index e9374a9..ad90967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,5 @@ [workspace] -members = [ - "solana/account", - "solana/program-runtime", - "solana/transaction-context", -] +members = ["solana/account", "solana/program-runtime", "solana/svm", "solana/transaction-context"] resolver = "3" [workspace.package] @@ -20,6 +16,7 @@ magic-root-interface = { path = "programs/magic-root-interface" } magic-root-program = { path = "programs/magic-root-program" } solana-account = { path = "solana/account" } solana-program-runtime = { path = "solana/program-runtime" } +solana-svm = { path = "solana/svm" } solana-transaction-context = { path = "solana/transaction-context" } ahash = "0.8.12" @@ -32,6 +29,7 @@ blake3 = "1.8.5" cfg-if = "1.0.4" criterion = "0.7.0" derive_more = "2.1.1" +env_logger = "0.11.8" itertools = "0.14.0" log = "0.4.29" qualifier_attr = "0.2.2" @@ -54,9 +52,11 @@ solana-account-info = "3.1.1" solana-clock = "3.1.0" solana-compute-budget-instruction = "4.1.1" solana-cpi = "3.1.0" +solana-ed25519-program = "3.0.0" solana-epoch-rewards = "3.0.1" solana-epoch-schedule = "3.1.0" solana-feature-gate-interface = { version = "4.0.0", features = ["bincode"] } +solana-fee-calculator = "3.2.0" solana-fee-structure = "3.0.0" solana-frozen-abi = "3.3.0" solana-frozen-abi-macro = "3.3.0" @@ -66,7 +66,12 @@ solana-instruction-error = "2.3.0" solana-instructions-sysvar = "4.0.0" solana-keypair = "3.1.2" solana-last-restart-slot = "3.0.0" -solana-loader-v3-interface = "6.1.0" +solana-loader-v3-interface = "7.0.0" +solana-message = "4.1.1" +solana-msg = "3.1.0" +solana-native-token = "3.0.0" +solana-packet = "4.1.0" +solana-precompile-error = "3.0.0" solana-program-entrypoint = "3.1.1" solana-program-error = "3.0.1" solana-pubkey = "4.2.0" @@ -78,25 +83,27 @@ solana-short-vec = "3.2.1" solana-signature = "3.4.0" solana-signer = "3.0.1" solana-slot-hashes = "3.0.1" -solana-stable-layout = "3.0.0" -solana-sysvar = "3.1.1" -solana-sysvar-id = "3.1.0" -solana-system-interface = ">=3.0.0, <3.2.0" -solana-svm-callback = "4.0.0-rc.1" -solana-svm-feature-set = "4.0.0-rc.1" -solana-svm-log-collector = "4.0.0-rc.1" -solana-svm-measure = "4.0.0-rc.1" -solana-svm-timings = "4.0.0-rc.1" -solana-svm-transaction = "4.0.0-rc.1" -solana-svm-type-overrides = "4.0.0-rc.1" -solana-system-interface = ">=3.0.0, <3.2.0" -solana-system-program = "4.0.0-rc.0" -solana-system-transaction = "3.0.0" -solana-sysvar = "3.1.1" +solana-stable-layout = "3.0.1" +solana-svm-callback = "4.1.1" +solana-svm-feature-set = "4.1.1" +solana-svm-log-collector = "4.1.1" +solana-svm-measure = "4.1.1" +solana-svm-timings = "4.1.1" +solana-svm-transaction = "4.1.1" +solana-svm-type-overrides = "4.1.1" +solana-system-interface = { version = "3.2", features = ["alloc", "bincode", "serde", "wincode"] } +solana-system-program = "4.1.0" +solana-sysvar = "4.0.0" solana-sysvar-id = "3.1.0" solana-transaction = "4.1.1" solana-transaction-error = "3.2.0" +[patch.crates-io] +solana-account = { path = "solana/account" } +solana-program-runtime = { path = "solana/program-runtime" } +solana-svm = { path = "solana/svm" } +solana-transaction-context = { path = "solana/transaction-context" } + [workspace.lints.rust] missing_docs = "deny" diff --git a/solana/README.md b/solana/README.md new file mode 100644 index 0000000..b62f543 --- /dev/null +++ b/solana/README.md @@ -0,0 +1,177 @@ +# Engine Runtime Differences from Agave + +This directory holds the engine's forks of the Agave runtime crates. They are +forked rather than used unmodified because the engine is an execution engine, not +a validator runtime: the code here executes already-loaded transactions and +returns the resulting account changes to the caller. It does not handle consensus, +fork choice, confirmations, validator fee policy, or bank commit ownership — those +concerns belong to the layers above. + +This document is the maintainer reference for the fork. It records which +divergences from upstream are intentional and must be preserved, as opposed to +accidental drift that should be fixed. Before changing SVM execution, account +representation, or VM account mapping, read the relevant section: several of these +behaviors are load-bearing in non-obvious ways, and some must be kept in sync +across more than one crate. The per-crate READMEs point back here rather than +repeating it. + +## Runtime Scope + +The SVM path is caller-owned at the transaction boundary. Concretely: + +- `solana-svm` loads accounts through a caller-provided callback and returns the + mutated account set in the executed transaction. +- Persistence, commit policy, deployment-slot checks, and validator-style batch + decisions all live *outside* these crates. +- Program loading is limited to the programs a transaction actually needs, and it + checks native-loader or `PROGRAM_OWNERS` ownership before execution. +- Rent-state and lamport-balance checks still run around execution — but this + runtime does not own validator state, so that's as far as it goes. + +## Account Model + +`solana-account` replaces the standard shared-data shape with an engine account +representation built around copy-on-write storage. + +`AccountSharedData` is one of: + +- `CoWAccount::Owned`, backed by an `Arc>`. +- `CoWAccount::Borrowed`, a zero-copy view into an aligned external buffer. + +It also carries `DirtyMarkers`, which record field-level changes to data, owner, +lamports, slot, mode, and state flags — these are the writeback signal the caller +reads after execution. + +The borrowed layout is fixed, and documented in full in +`solana/account/README.md`. The invariants that matter here: + +- Borrowed buffers must be 8-byte aligned and stay live for the whole borrow. +- The buffer holds an `AccountHeader`, a shared pubkey prefix, and two account + images; `AccountHeader::sequence` selects the active one. +- `translate()` copies the active image into the shadow image before mutation. +- `commit()` publishes the shadow image by advancing the sequence counter. +- `reset()` discards shadow writes by repointing back to the active image. +- `rollback()` is only valid after a `commit()`. + +Writes try to stay borrowed as long as they fit the borrowed image capacity. A +write that doesn't fit promotes the account to owned heap storage, and a shared +owned buffer goes through `Arc::make_mut` before being mutated. + +The account core also carries engine state that Agave's account code doesn't model +the same way: + +- `AccountMode` distinguishes `ReadOnly`, `Placeholder`, `Authority`, `System`, + `Delegated`, `Ephemeral`, `Transient`, and `Closed`. +- `AccountSharedData::mutable()` is true only for `Delegated`, `Ephemeral`, and + `Transient` — this is the predicate higher layers route persisted-vs-volatile on. +- `StateFlags` include `EXECUTABLE` and `COMPRESSED`. +- `rent_epoch` is *not* stored in `AccountSharedData`; compatibility constructors + ignore it, and runtime serialization masks rent-epoch values for program inputs. + +## Transaction Context + +`solana-transaction-context` keeps transaction accounts in `UnsafeCell` +containers guarded by explicit borrow counters. This keeps account borrows local +to the transaction context while still letting VM access handlers remap account +data when a program writes through a mapped region — something ordinary borrows +can't express. + +Maintainer-visible differences: + +- `TransactionAccounts` tracks touched accounts, the total account-data resize + delta, and the per-instruction lamports delta. +- `AccountRef` and `AccountRefMut` release their custom borrow counters on drop. +- `TransactionAccountViewMut::reserve()` can reserve CoW capacity ahead of a VM + write path. +- `ExecutionRecord` returns the keyed accounts, return data, touched-account + count, and account resize delta after execution. +- `TransactionContext::push()` updates the instructions-sysvar current index when + that sysvar is present. + +All outstanding references must be gone before a transaction context is +deconstructed. If `Rc::try_unwrap` fails during deconstruction, that's a real +lifetime bug, not a tolerable edge case — treat it as one. + +## VM Account Mapping + +Account data is always directly mapped into the SBF VM. The old local-runtime +threading for `virtual_address_space_adjustments` and +`account_data_direct_mapping` is intentionally absent. **Do not** reintroduce +branches that copy account data through the serialized input buffer. + +Serialization still preserves the loader-selected ABI formats: + +- Deprecated-loader accounts use ABI-v0 metadata. +- Loader-v2 and loader-v3 accounts use ABI-v1 metadata. +- ABI-v1 may append the direct account-pointer array when + `direct_account_pointers_in_program_input` is enabled. + +The serialized input buffer carries account metadata, lamports, lengths, owner, +instruction data, and program id — but **not** account data bytes. The data lives +in separate `MemoryRegion`s: + +- Loader-v1 account regions reserve exactly the current data length. +- Loader-v2 and loader-v3 account regions reserve the current length plus + `MAX_PERMITTED_DATA_INCREASE`. +- Non-loader-v1 serialization still adds alignment padding so VM and host + addresses stay aligned across account-region gaps. + +Deserialization reads lamports, owners, and lengths back from the serialized +metadata. It does *not* copy account bytes out of the serialized buffer, because +data mutations already happened through the mapped account regions. ABI-v1 still +enforces realloc limits and the maximum account-data length. + +## Access-Violation Growth + +Writable account data may be mapped read-only at first. That's deliberate for +borrowed accounts and shared owned buffers: the first VM store has to pass through +the transaction-context access-violation handler, so the account can be touched, +translated or made unique, resized if needed, and remapped. + +The handler: + +- Only handles stores. Loads beyond mapped data stay load violations. +- Requires a `MemoryRegion` payload carrying the transaction account index. +- Ignores accesses beyond the address space reserved for that account. +- Updates touched flags and the account resize delta before resizing. +- Grows only to the requested access length — not eagerly to the full permitted + growth range. +- Replaces the region's host pointer, length, and writability after obtaining the + current account-data slice. + +VM execution maps account-region access violations back into account-specific +errors — readonly data modification, account data too small, invalid realloc. +Keep `serialization`, `transaction-context::access_violation_handler`, and +`program-runtime::vm` error remapping in sync; they're three views of the same +contract. + +## CPI Account Synchronization + +CPI helpers follow the same direct-mapped account-data rule: + +- `CallerAccount::serialized_data` is always empty for account data. +- CPI entry syncs caller lamports, owner, and data length into the callee account. + It does not copy data bytes out of caller serialized buffers. +- CPI exit syncs lamports, owner, and length back into the caller's `AccountInfo` + fields. +- If a length or owner change may have moved account storage, CPI replaces the + caller account-data `MemoryRegion` with a fresh region from + `create_memory_region_of_account`. +- Inner-instruction realloc limits are based on the caller's original data length + plus the permitted growth range — except deprecated-loader callers under strict + address checks, which reserve only the original length. + +Any change to account-region layout has to update CPI region replacement and the +VM access-violation path *together*; they can't drift apart. + +## Maintainer Rules + +- Preserve direct account-region mapping as the single runtime behavior. +- Preserve ABI-v0 and ABI-v1 metadata compatibility — but do not restore the old + "copy account data into the serialized input buffer" mode. +- Keep borrowed-account layout changes synchronized across `solana-account`, + `solana-transaction-context`, and the runtime mapping tests. +- Treat dirty markers and touched flags as the writeback signal for the caller. +- Don't add validator flow to these crates. If a behavior needs persistence, batch + commit, consensus, or validator fee decisions, it belongs *outside* this runtime + layer. diff --git a/solana/program-runtime/Cargo.toml b/solana/program-runtime/Cargo.toml index ab69205..92c36d2 100644 --- a/solana/program-runtime/Cargo.toml +++ b/solana/program-runtime/Cargo.toml @@ -4,9 +4,7 @@ name = "solana-program-runtime" authors = { workspace = true } description = "Solana program runtime" documentation = "https://docs.rs/solana-program-runtime" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } +edition = { workspace = true } homepage = { workspace = true } license = { workspace = true } repository = { workspace = true } diff --git a/solana/svm/Cargo.toml b/solana/svm/Cargo.toml new file mode 100644 index 0000000..04bd44a --- /dev/null +++ b/solana/svm/Cargo.toml @@ -0,0 +1,95 @@ +[package] +name = "solana-svm" + +authors = { workspace = true } +description = "Solana SVM" +documentation = "https://docs.rs/solana-svm" +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = "4.1.1" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lib] +crate-type = ["lib"] +name = "solana_svm" + +[features] +# No-op stub retained only so external (patched-in) crates that reference +# `solana-svm/agave-unstable-api` still resolve; the lib is no longer gated on +# it, and the upstream svm-* deps enable their unstable API unconditionally below. +agave-unstable-api = [] +dev-context-only-utils = ["dep:qualifier_attr", "solana-program-runtime/dev-context-only-utils"] +frozen-abi = [ + "dep:solana-frozen-abi", + "dep:solana-frozen-abi-macro", + "solana-program-runtime/frozen-abi" +] +shuttle-test = ["solana-program-runtime/shuttle-test", "solana-svm-type-overrides/shuttle-test"] +svm-internal = ["dep:qualifier_attr"] + +[dependencies] +magic-root-interface = { workspace = true } + +ahash = { workspace = true } +log = { workspace = true } +qualifier_attr = { workspace = true, optional = true } +scc = { workspace = true } +serde = { workspace = true, features = ["rc"] } +thiserror = { workspace = true } + +solana-account = { workspace = true } +solana-clock = { workspace = true } +solana-fee-structure = { workspace = true } +solana-frozen-abi = { workspace = true, optional = true, features = ["frozen-abi"] } +solana-frozen-abi-macro = { workspace = true, optional = true, features = ["frozen-abi"] } +solana-hash = { workspace = true } +solana-instruction = { workspace = true, features = ["std"] } +solana-instructions-sysvar = { workspace = true } +solana-loader-v3-interface = { workspace = true, features = ["bincode"] } +solana-message = { workspace = true } +solana-program-entrypoint = { workspace = true } +solana-program-runtime = { workspace = true } +solana-pubkey = { workspace = true } +solana-rent = { workspace = true } +solana-sdk-ids = { workspace = true } +solana-svm-callback = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-feature-set = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-log-collector = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-measure = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-timings = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-transaction = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-type-overrides = { workspace = true, features = ["agave-unstable-api"] } +solana-system-interface = { workspace = true } +solana-sysvar-id = { workspace = true } +solana-transaction-context = { workspace = true } +solana-transaction-error = { workspace = true } + +[dev-dependencies] +bincode = { workspace = true } +env_logger = { workspace = true } +rand = { workspace = true } +solana-clock = { workspace = true } +solana-ed25519-program = { workspace = true } +solana-epoch-schedule = { workspace = true } +solana-fee-calculator = { workspace = true } +solana-keypair = { workspace = true } +solana-native-token = { workspace = true } +solana-precompile-error = { workspace = true } +solana-program-runtime = { workspace = true, features = ["dev-context-only-utils"] } +solana-pubkey = { workspace = true, features = ["rand"] } +solana-rent = { workspace = true } +solana-sbpf = { workspace = true, features = ["jit"] } +solana-signature = { workspace = true, features = ["rand"] } +solana-signer = { workspace = true } +# See order-crates-for-publishing.py for using this unusual `path = "."` +solana-svm = { path = ".", features = ["dev-context-only-utils", "svm-internal"] } +solana-sysvar = { workspace = true } +solana-transaction = { workspace = true, features = ["dev-context-only-utils"] } +solana-transaction-context = { workspace = true, features = ["dev-context-only-utils"] } + +[lints.rust] +unexpected_cfgs = "allow" diff --git a/solana/svm/README.md b/solana/svm/README.md new file mode 100644 index 0000000..5393362 --- /dev/null +++ b/solana/svm/README.md @@ -0,0 +1,18 @@ +# `solana-svm` + +A fork of Agave's `solana-svm` (`anza-xyz/agave`), reshaped for the engine and +patched in workspace-wide through `[patch.crates-io]`. + +This is the transaction-level entry point into execution. It loads the accounts a +transaction needs — through a caller-provided callback, +`transaction_processing_callback`, rather than owning any storage itself — +executes through `solana-program-runtime`, and hands back the mutated account set. +The callback is the seam: it's how the engine plugs `keeper`/`accountsdb` in as +the account source without this crate knowing anything about them. + +Everything a validator would do *around* execution — committing results, batch +decisions, deployment-slot checks — stays above this crate. It loads, runs, and +returns; nothing more. + +The engine's intentional divergences from upstream are documented for the whole +fork in [`../README.md`](../README.md). diff --git a/solana/svm/src/access_permissions.rs b/solana/svm/src/access_permissions.rs new file mode 100644 index 0000000..2ec0f2f --- /dev/null +++ b/solana/svm/src/access_permissions.rs @@ -0,0 +1,262 @@ +use solana_svm_transaction::svm_message::SVMMessage; +use solana_transaction_error::TransactionError; +use std::sync::Arc; + +use crate::transaction_execution_result::ExecutedTransaction; + +impl ExecutedTransaction { + /// Enforces engine account mutability after successful execution. + pub(crate) fn validate_access(&mut self, tx: &impl SVMMessage) -> bool { + if !self.was_successful() { + return true; + } + let privileged = is_privileged(tx); + let mut accounts = self.loaded_transaction.accounts.iter().enumerate(); + let Some((_, payer)) = accounts.next() else { + // Sanitized transactions always carry a fee payer. + return false; + }; + for (i, (pk, acc)) in accounts { + if !tx.is_writable(i) || acc.mutable() || privileged { + continue; + } + let logs = Arc::make_mut(self.execution_details.log_messages.get_or_insert_default()); + let error = format!("Program log: Immutable account {pk} has been modified"); + logs.push(error); + self.execution_details.status = Err(TransactionError::InvalidWritableAccount); + return false; + } + // The payer is writable in Solana messages even when fees are disabled + // here; reject it only if execution actually changed immutable state. + if payer.1.dirty() && !payer.1.mutable() && !privileged { + let logs = Arc::make_mut(self.execution_details.log_messages.get_or_insert_default()); + logs.push(format!( + "Program log: ({}) Feepayer account's balance has been modified illegally", + payer.0 + )); + self.execution_details.status = Err(TransactionError::InvalidAccountForFee); + return false; + } + true + } +} + +/// Returns true when every instruction is handled by the MagicRoot authority path. +fn is_privileged(tx: &impl SVMMessage) -> bool { + tx.program_instructions_iter() + .all(|(program, _)| *program == magic_root_interface::ID) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + account_loader::LoadedTransaction, + transaction_execution_result::{ExecutedTransaction, TransactionExecutionDetails}, + }, + solana_account::{AccountBuilder, AccountMode, AccountSharedData}, + solana_hash::Hash, + solana_message::{ + LegacyMessage, Message, MessageHeader, SanitizedMessage, + compiled_instruction::CompiledInstruction, + }, + solana_pubkey::Pubkey, + solana_signature::Signature, + solana_transaction::sanitized::SanitizedTransaction, + solana_transaction_error::TransactionResult, + std::collections::HashSet, + }; + + /// A dirtied account in the engine-exclusive `Delegated` (mutable) mode. + fn account(mode: AccountMode) -> AccountSharedData { + let mut acc: AccountSharedData = AccountBuilder::default().mode(mode).build(); + acc.set_data_from_slice(&[1]); + acc + } + + /// Builds a sanitized transaction over `account_keys` whose only writable + /// non-signer accounts are the first `writable_non_signers` after the payer, + /// invoking one instruction per entry in `program_indices`. + /// + /// Layout is `[payer, non-signers.., program..]`: the payer signs and is + /// writable, and `is_writable(i)` follows directly from the header math the + /// engine guard relies on. + fn sanitized_tx( + account_keys: Vec, + writable_non_signers: u8, + program_indices: &[u8], + ) -> SanitizedTransaction { + let non_signers = account_keys.len() as u8 - 1; + let header = MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: non_signers - writable_non_signers, + }; + let instructions = program_indices + .iter() + .map(|&program_id_index| CompiledInstruction { + program_id_index, + accounts: vec![], + data: vec![], + }) + .collect(); + let message = Message { + account_keys, + header, + instructions, + recent_blockhash: Hash::default(), + }; + let sanitized = SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())); + SanitizedTransaction::new_for_tests(sanitized, vec![Signature::new_unique()], false) + } + + /// Wraps executed account state and a status into an `ExecutedTransaction`. + fn executed( + accounts: Vec<(Pubkey, AccountSharedData)>, + status: TransactionResult<()>, + ) -> ExecutedTransaction { + ExecutedTransaction { + loaded_transaction: LoadedTransaction { accounts, ..Default::default() }, + execution_details: TransactionExecutionDetails { + status, + log_messages: None, + inner_instructions: None, + return_data: None, + executed_units: 0, + accounts_data_len_delta: 0, + }, + } + } + + /// Runs the guard over a `[payer, target, program]` transaction, returning + /// its verdict and the (possibly rewritten) execution details. + /// + /// `program` is MagicRoot when `privileged`, so the whole tx takes the + /// authority path; only `target` (index 1) is ever writable in the message. + fn run( + payer: AccountSharedData, + target: AccountSharedData, + writable_target: bool, + privileged: bool, + status: TransactionResult<()>, + ) -> (bool, ExecutedTransaction) { + let payer_key = Pubkey::new_unique(); + let target_key = Pubkey::new_unique(); + let program = if privileged { magic_root_interface::ID } else { Pubkey::new_unique() }; + let tx = sanitized_tx( + vec![payer_key, target_key, program], + writable_target as u8, + &[2], + ); + let mut executed = executed( + vec![ + (payer_key, payer), + (target_key, target), + (program, AccountSharedData::default()), + ], + status, + ); + let verdict = executed.validate_access(&tx); + (verdict, executed) + } + + /// Asserts a log line containing `needle` was recorded. + fn assert_logged(tx: &ExecutedTransaction, needle: &str) { + let logs: &[String] = tx + .execution_details + .log_messages + .as_deref() + .map(Vec::as_slice) + .unwrap_or_default(); + assert!( + logs.iter().any(|l| l.contains(needle)), + "expected {needle:?} in {logs:?}" + ); + } + + #[test] + fn writable_account_guard() { + // A dirty, writable, immutable operand is rejected — unless the tx is + // privileged; mutable or message-read-only operands are always fine. + let cases = [ + // (target, writable, privileged, accepted) + (account(AccountMode::ReadOnly), true, false, false), + (account(AccountMode::Delegated), true, false, true), + (account(AccountMode::ReadOnly), false, false, true), // read-only in the message + (account(AccountMode::ReadOnly), true, true, true), // MagicRoot bypass + ]; + for (i, (target, writable, privileged, accepted)) in cases.into_iter().enumerate() { + let (verdict, executed) = run( + AccountSharedData::default(), + target, + writable, + privileged, + Ok(()), + ); + assert_eq!(verdict, accepted, "case {i}"); + let expected = + if accepted { Ok(()) } else { Err(TransactionError::InvalidWritableAccount) }; + assert_eq!(executed.execution_details.status, expected, "case {i}"); + if !accepted { + assert_logged(&executed, "Immutable account"); + } + } + } + + #[test] + fn fee_payer_guard() { + // A dirty immutable fee payer is rejected unless privileged; a mutable + // payer is always accepted. + let cases = [ + // (payer, privileged, accepted) + (account(AccountMode::ReadOnly), false, false), + (account(AccountMode::Delegated), false, true), + (account(AccountMode::ReadOnly), true, true), // MagicRoot bypass + ]; + for (i, (payer, privileged, accepted)) in cases.into_iter().enumerate() { + let (verdict, executed) = run( + payer, + AccountSharedData::default(), + false, + privileged, + Ok(()), + ); + assert_eq!(verdict, accepted, "case {i}"); + let expected = + if accepted { Ok(()) } else { Err(TransactionError::InvalidAccountForFee) }; + assert_eq!(executed.execution_details.status, expected, "case {i}"); + if !accepted { + assert_logged(&executed, "Feepayer account's balance"); + } + } + } + + #[test] + fn guard_edge_cases() { + // A failed run committed nothing, so a dirty immutable account must not + // rewrite the original error into an access error. + let (verdict, failed) = run( + AccountSharedData::default(), + account(AccountMode::ReadOnly), + true, + false, + Err(TransactionError::AccountInUse), + ); + assert!(verdict); + assert_eq!( + failed.execution_details.status, + Err(TransactionError::AccountInUse) + ); + + // A transaction without a fee payer account cannot be validated. + let tx = sanitized_tx(vec![Pubkey::new_unique(), Pubkey::new_unique()], 0, &[1]); + let mut payerless = executed(vec![], Ok(())); + assert!(!payerless.validate_access(&tx)); + + // Privilege requires *every* instruction to invoke MagicRoot. + let keys = vec![Pubkey::new_unique(), magic_root_interface::ID, Pubkey::new_unique()]; + assert!(is_privileged(&sanitized_tx(keys.clone(), 0, &[1, 1]))); + assert!(!is_privileged(&sanitized_tx(keys, 0, &[1, 2]))); + } +} diff --git a/solana/svm/src/account_loader.rs b/solana/svm/src/account_loader.rs new file mode 100644 index 0000000..5cfea00 --- /dev/null +++ b/solana/svm/src/account_loader.rs @@ -0,0 +1,1421 @@ +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::{field_qualifiers, qualifiers}; +use { + solana_account::{Account, AccountSharedData, PROGRAM_OWNERS, ReadableAccount}, + solana_fee_structure::FeeDetails, + solana_instruction::{BorrowedAccountMeta, BorrowedInstruction}, + solana_instructions_sysvar::construct_instructions_data, + solana_program_runtime::execution_budget::{ + SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionBudget, + }, + solana_pubkey::Pubkey, + solana_sdk_ids::sysvar, + solana_svm_callback::TransactionProcessingCallback, + solana_svm_transaction::svm_message::SVMMessage, + solana_transaction_context::{IndexOfAccount, transaction_accounts::KeyedAccountSharedData}, + solana_transaction_error::{TransactionError, TransactionResult as Result}, +}; + +// Per SIMD-0186, all accounts are assigned a base size of 64 bytes to cover +// the storage cost of metadata. +#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] +pub(crate) const TRANSACTION_ACCOUNT_BASE_SIZE: usize = 64; + +// Per SIMD-0186, resolved address lookup tables are assigned a base size of 8248 +// bytes: 8192 bytes for the maximum table size plus 56 bytes for metadata. +const ADDRESS_LOOKUP_TABLE_BASE_SIZE: usize = 8248; + +/// Result of transaction prechecking before account loading. +pub type TransactionCheckResult = Result; + +#[derive(PartialEq, Eq, Debug)] +pub(crate) enum TransactionLoadResult { + /// All transaction accounts and executable program accounts were resolved. + Loaded(LoadedTransaction), + /// Loading failed before execution could start. + NotLoaded(TransactionError), +} + +/// Transaction limits and metadata computed before account loading. +#[derive(PartialEq, Eq, Debug, Clone)] +#[cfg_attr(feature = "svm-internal", qualifier_attr::field_qualifiers(nonce_address(pub)))] +pub struct CheckedTransactionDetails { + pub(crate) nonce_address: Option, + pub(crate) compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits, +} + +impl Default for CheckedTransactionDetails { + fn default() -> Self { + Self { + nonce_address: None, + compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits { + budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size_limit: 32, + fee_details: FeeDetails::default(), + }, + } + } +} + +impl CheckedTransactionDetails { + /// Creates checked transaction details from caller-provided validation. + pub fn new( + nonce_address: Option, + compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits, + ) -> Self { + Self { + nonce_address, + compute_budget_and_limits, + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub(crate) struct ValidatedTransactionDetails { + pub(crate) compute_budget: SVMTransactionExecutionBudget, + pub(crate) loaded_accounts_bytes_limit: u32, + pub(crate) fee_details: FeeDetails, +} + +#[cfg(feature = "dev-context-only-utils")] +impl Default for ValidatedTransactionDetails { + fn default() -> Self { + Self { + compute_budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_bytes_limit: + solana_program_runtime::execution_budget::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + fee_details: FeeDetails::default(), + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +#[cfg_attr(feature = "dev-context-only-utils", derive(Default))] +pub(crate) struct LoadedTransactionAccount { + pub(crate) account: AccountSharedData, + pub(crate) loaded_size: usize, +} + +impl LoadedTransactionAccount { + fn new(account: AccountSharedData) -> Self { + Self { + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE.saturating_add(account.data().len()), + account, + } + } +} + +/// Accounts and execution metadata needed to run one transaction. +#[derive(PartialEq, Eq, Debug, Clone, Default)] +#[cfg_attr( + feature = "dev-context-only-utils", + field_qualifiers(program_indices(pub), compute_budget(pub)) +)] +pub struct LoadedTransaction { + /// Transaction accounts in message account-key order. + pub accounts: Vec, + pub(crate) program_indices: Vec, + /// Fee metadata carried through for callers that still consume it. + pub fee_details: FeeDetails, + pub(crate) compute_budget: SVMTransactionExecutionBudget, + /// Total loaded account data size charged against the transaction limit. + pub loaded_accounts_data_size: u32, +} + +pub(crate) fn load_transaction( + account_loader: &CB, + message: &impl SVMMessage, + validation_details: ValidatedTransactionDetails, +) -> TransactionLoadResult { + let load_result = load_transaction_accounts( + account_loader, + message, + validation_details.loaded_accounts_bytes_limit, + ); + + match load_result { + Ok(loaded_tx_accounts) => TransactionLoadResult::Loaded(LoadedTransaction { + accounts: loaded_tx_accounts.accounts, + program_indices: loaded_tx_accounts.program_indices, + fee_details: validation_details.fee_details, + compute_budget: validation_details.compute_budget, + loaded_accounts_data_size: loaded_tx_accounts.loaded_accounts_data_size, + }), + Err(err) => TransactionLoadResult::NotLoaded(err), + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +struct LoadedTransactionAccounts { + pub(crate) accounts: Vec, + pub(crate) program_indices: Vec, + pub(crate) loaded_accounts_data_size: u32, +} + +impl LoadedTransactionAccounts { + fn increase_calculated_data_size( + &mut self, + data_size_delta: usize, + requested_loaded_accounts_data_size_limit: u32, + ) -> Result<()> { + let Ok(data_size_delta) = u32::try_from(data_size_delta) else { + return Err(TransactionError::MaxLoadedAccountsDataSizeExceeded); + }; + + self.loaded_accounts_data_size = + self.loaded_accounts_data_size.saturating_add(data_size_delta); + + if self.loaded_accounts_data_size > requested_loaded_accounts_data_size_limit { + Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) + } else { + Ok(()) + } + } +} + +fn load_transaction_accounts( + account_loader: &CB, + message: &impl SVMMessage, + loaded_accounts_bytes_limit: u32, +) -> Result { + let account_keys = message.account_keys(); + + let mut loaded_transaction_accounts = LoadedTransactionAccounts { + accounts: Vec::with_capacity(account_keys.len()), + program_indices: Vec::with_capacity(message.num_instructions()), + loaded_accounts_data_size: 0, + }; + + // Transactions pay a base fee per address lookup table. + loaded_transaction_accounts.increase_calculated_data_size( + message.num_lookup_tables().saturating_mul(ADDRESS_LOOKUP_TABLE_BASE_SIZE), + loaded_accounts_bytes_limit, + )?; + + let mut collect_loaded_account = |key: &Pubkey, loaded_account| -> Result<()> { + let LoadedTransactionAccount { account, loaded_size } = loaded_account; + + loaded_transaction_accounts + .increase_calculated_data_size(loaded_size, loaded_accounts_bytes_limit)?; + + loaded_transaction_accounts.accounts.push((*key, account)); + + Ok(()) + }; + + // Attempt to load all of the transaction accounts + for account_key in account_keys.iter() { + let loaded_account = load_transaction_account(account_loader, message, account_key); + collect_loaded_account(account_key, loaded_account)?; + } + + for (program_id, instruction) in message.program_instructions_iter() { + let Some(program_account) = account_loader.get_account_shared_data(program_id) else { + return Err(TransactionError::ProgramAccountNotFound); + }; + + let owner_id = program_account.0.owner(); + if !PROGRAM_OWNERS.contains(owner_id) { + return Err(TransactionError::InvalidProgramForExecution); + } + + loaded_transaction_accounts + .program_indices + .push(instruction.program_id_index as IndexOfAccount); + } + + Ok(loaded_transaction_accounts) +} + +fn load_transaction_account( + account_loader: &CB, + message: &impl SVMMessage, + account_key: &Pubkey, +) -> LoadedTransactionAccount { + if solana_sdk_ids::sysvar::instructions::check_id(account_key) { + // Since the instructions sysvar is constructed by the SVM and modified + // for each transaction instruction, it cannot be loaded. + return LoadedTransactionAccount { + loaded_size: 0, + account: construct_instructions_account(message), + }; + } + account_loader + .get_account_shared_data(account_key) + .map(|a| LoadedTransactionAccount::new(a.0)) + .unwrap_or_else(|| LoadedTransactionAccount::new(Default::default())) +} + +fn construct_instructions_account(message: &impl SVMMessage) -> AccountSharedData { + let account_keys = message.account_keys(); + let mut decompiled_instructions = Vec::with_capacity(message.num_instructions()); + for (program_id, instruction) in message.program_instructions_iter() { + let accounts = instruction + .accounts + .iter() + .map(|account_index| { + let account_index = usize::from(*account_index); + BorrowedAccountMeta { + is_signer: message.is_signer(account_index), + is_writable: message.is_writable(account_index), + pubkey: account_keys.get(account_index).unwrap(), + } + }) + .collect(); + + decompiled_instructions.push(BorrowedInstruction { + accounts, + data: instruction.data, + program_id, + }); + } + + AccountSharedData::from(Account { + data: construct_instructions_data(&decompiled_instructions).unwrap_or_default(), + owner: sysvar::id(), + ..Account::default() + }) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + rent_calculator::RENT_EXEMPT_RENT_EPOCH, + transaction_account_state_info::TransactionAccountStateInfo, + }, + ahash::AHashMap, + rand::prelude::*, + solana_account::{ + Account, AccountSharedData, ReadableAccount, WritableAccount, state_traits::StateMut, + }, + solana_clock::Slot, + solana_hash::Hash, + solana_instruction::{AccountMeta, Instruction}, + solana_keypair::Keypair, + solana_loader_v3_interface::state::UpgradeableLoaderState, + solana_message::{ + LegacyMessage, Message, MessageHeader, SanitizedMessage, + compiled_instruction::CompiledInstruction, + v0::{LoadedAddresses, LoadedMessage}, + }, + solana_native_token::LAMPORTS_PER_SOL, + solana_program_runtime::execution_budget::{ + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{ + bpf_loader, bpf_loader_upgradeable, native_loader, system_program, sysvar, + }, + solana_signature::Signature, + solana_signer::Signer, + solana_svm_callback::{AccountState, InvokeContextCallback, TransactionProcessingCallback}, + solana_system_interface::instruction as system_instruction, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction_context::{ + transaction::TransactionContext, transaction_accounts::KeyedAccountSharedData, + }, + solana_transaction_error::TransactionError, + std::{ + borrow::Cow, + cell::RefCell, + collections::{HashMap, HashSet}, + sync::Arc, + }, + }; + + fn setup_test_logger() { + let _ = env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("error")) + .format_timestamp_nanos() + .is_test(true) + .try_init(); + } + + #[derive(Clone, Default)] + struct TestCallbacks { + accounts_map: HashMap, + #[allow(clippy::type_complexity)] + inspected_accounts: + RefCell, /* is_writable */ bool)>>>, + } + + impl InvokeContextCallback for TestCallbacks {} + + impl TransactionProcessingCallback for TestCallbacks { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { + self.accounts_map.get(pubkey).map(|(account, slot)| (account.clone(), *slot)) + } + + fn inspect_account( + &self, + address: &Pubkey, + account_state: AccountState, + is_writable: bool, + ) { + let account = match account_state { + AccountState::Dead => None, + AccountState::Alive(account) => Some(account.clone()), + }; + self.inspected_accounts + .borrow_mut() + .entry(*address) + .or_default() + .push((account, is_writable)); + } + } + + fn load_accounts_with_features_and_rent( + tx: Transaction, + accounts: &[KeyedAccountSharedData], + ) -> TransactionLoadResult { + let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); + let mut accounts_map = HashMap::new(); + for (pubkey, account) in accounts { + accounts_map.insert(*pubkey, (account.clone(), 1)); + } + let callbacks = TestCallbacks { + accounts_map, + ..Default::default() + }; + load_transaction( + &callbacks, + &sanitized_tx, + ValidatedTransactionDetails::default(), + ) + } + + fn new_unchecked_sanitized_message(message: Message) -> SanitizedMessage { + SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())) + } + + #[test] + fn test_load_accounts_unknown_program_id() { + let mut accounts: Vec = Vec::new(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = Pubkey::from([5u8; 32]); + + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + accounts.push((key0, account)); + + let account = AccountSharedData::new(2, 1, &Pubkey::default()); + accounts.push((key1, account)); + + let instructions = vec![CompiledInstruction::new(1, &(), vec![0])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[], + Hash::default(), + vec![Pubkey::default()], + instructions, + ); + + let load_results = load_accounts_with_features_and_rent(tx, &accounts); + + assert!(matches!( + load_results, + TransactionLoadResult::NotLoaded(TransactionError::ProgramAccountNotFound), + )); + } + + #[test] + fn test_load_accounts_no_loaders() { + let mut accounts: Vec = Vec::new(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = Pubkey::from([5u8; 32]); + + let mut account = AccountSharedData::new(1, 0, &Pubkey::default()); + account.set_rent_epoch(1); + accounts.push((key0, account)); + + let mut account = AccountSharedData::new(2, 1, &Pubkey::default()); + account.set_rent_epoch(1); + accounts.push((key1, account)); + + let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[key1], + Hash::default(), + vec![native_loader::id()], + instructions, + ); + + let loaded_accounts = load_accounts_with_features_and_rent(tx, &accounts); + + match &loaded_accounts { + TransactionLoadResult::NotLoaded(err) => { + assert_eq!(*err, TransactionError::ProgramAccountNotFound); + } + result => panic!("unexpected result: {result:?}"), + } + } + + #[test] + fn test_load_accounts_bad_owner() { + let mut accounts: Vec = Vec::new(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = Pubkey::from([5u8; 32]); + + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + accounts.push((key0, account)); + + let mut account = AccountSharedData::new(40, 1, &Pubkey::default()); + account.set_executable(true); + accounts.push((key1, account)); + + let instructions = vec![CompiledInstruction::new(1, &(), vec![0])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[], + Hash::default(), + vec![key1], + instructions, + ); + + let load_results = load_accounts_with_features_and_rent(tx, &accounts); + + assert!(matches!( + load_results, + TransactionLoadResult::NotLoaded(TransactionError::InvalidProgramForExecution), + )); + } + + #[test] + fn test_load_accounts_not_executable() { + let mut accounts: Vec = Vec::new(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = Pubkey::from([5u8; 32]); + + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + accounts.push((key0, account)); + + let account = AccountSharedData::new(40, 0, &native_loader::id()); + accounts.push((key1, account)); + + let instructions = vec![CompiledInstruction::new(1, &(), vec![0])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[], + Hash::default(), + vec![key1], + instructions, + ); + + let load_results = load_accounts_with_features_and_rent(tx, &accounts); + + match &load_results { + TransactionLoadResult::Loaded(loaded_transaction) => { + assert_eq!(loaded_transaction.accounts.len(), 2); + assert_eq!(loaded_transaction.accounts[0].1, accounts[0].1); + assert_eq!(loaded_transaction.accounts[1].1, accounts[1].1); + assert_eq!(loaded_transaction.program_indices.len(), 1); + assert_eq!(loaded_transaction.program_indices[0], 1); + } + TransactionLoadResult::NotLoaded(e) => panic!("{e}"), + } + } + + #[test] + fn test_load_accounts_multiple_loaders() { + let mut accounts: Vec = Vec::new(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = bpf_loader_upgradeable::id(); + let key2 = Pubkey::from([6u8; 32]); + + let mut account = AccountSharedData::new(1, 0, &Pubkey::default()); + account.set_rent_epoch(1); + accounts.push((key0, account)); + + let mut account = AccountSharedData::new(40, 1, &Pubkey::default()); + account.set_executable(true); + account.set_rent_epoch(1); + account.set_owner(native_loader::id()); + accounts.push((key1, account)); + + let mut account = AccountSharedData::new(41, 1, &Pubkey::default()); + account.set_executable(true); + account.set_rent_epoch(1); + account.set_owner(key1); + accounts.push((key2, account)); + + let instructions = vec![ + CompiledInstruction::new(1, &(), vec![0]), + CompiledInstruction::new(2, &(), vec![0]), + ]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[], + Hash::default(), + vec![key1, key2], + instructions, + ); + + let loaded_accounts = load_accounts_with_features_and_rent(tx, &accounts); + + match &loaded_accounts { + TransactionLoadResult::Loaded(loaded_transaction) => { + assert_eq!(loaded_transaction.accounts.len(), 3); + assert_eq!(loaded_transaction.accounts[0].1, accounts[0].1); + assert_eq!(loaded_transaction.program_indices.len(), 2); + assert_eq!(loaded_transaction.program_indices[0], 1); + assert_eq!(loaded_transaction.program_indices[1], 2); + } + TransactionLoadResult::NotLoaded(e) => panic!("{e}"), + } + } + + fn load_accounts_no_store( + accounts: &[KeyedAccountSharedData], + tx: Transaction, + ) -> TransactionLoadResult { + let tx = SanitizedTransaction::from_transaction_for_tests(tx); + + let mut accounts_map = HashMap::new(); + for (pubkey, account) in accounts { + accounts_map.insert(*pubkey, (account.clone(), 1)); + } + let callbacks = TestCallbacks { + accounts_map, + ..Default::default() + }; + load_transaction(&callbacks, &tx, ValidatedTransactionDetails::default()) + } + + #[test] + fn test_instructions() { + setup_test_logger(); + let instructions_key = solana_sdk_ids::sysvar::instructions::id(); + let keypair = Keypair::new(); + let instructions = vec![CompiledInstruction::new(1, &(), vec![0, 1])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[solana_pubkey::new_rand(), instructions_key], + Hash::default(), + vec![native_loader::id()], + instructions, + ); + + let load_results = load_accounts_no_store(&[], tx); + assert!(matches!( + load_results, + TransactionLoadResult::NotLoaded(TransactionError::ProgramAccountNotFound), + )); + } + + #[test] + fn test_increase_calculated_data_size() { + let mut acc = LoadedTransactionAccounts { + accounts: vec![], + program_indices: vec![], + loaded_accounts_data_size: 0, + }; + + let data_size: usize = 123; + let requested_data_size_limit = data_size as u32; + + // OK - loaded data size is up to limit + assert!(acc.increase_calculated_data_size(data_size, requested_data_size_limit).is_ok()); + assert_eq!(data_size as u32, acc.loaded_accounts_data_size); + + // fail - loading more data that would exceed limit + let another_byte: usize = 1; + assert_eq!( + acc.increase_calculated_data_size(another_byte, requested_data_size_limit), + Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) + ); + } + + #[test] + fn test_construct_instructions_account() { + let loaded_message = LoadedMessage { + message: Cow::Owned(solana_message::v0::Message::default()), + loaded_addresses: Cow::Owned(LoadedAddresses::default()), + is_writable_account_cache: vec![false], + }; + let message = SanitizedMessage::V0(loaded_message); + let shared_data = construct_instructions_account(&message); + let expected = AccountSharedData::from(Account { + data: construct_instructions_data(&message.decompile_instructions()).unwrap(), + owner: sysvar::id(), + ..Account::default() + }); + assert_eq!(shared_data, expected); + } + + #[test] + fn test_load_transaction_accounts_fee_payer() { + let fee_payer_address = Pubkey::new_unique(); + let message = Message { + account_keys: vec![fee_payer_address], + header: MessageHeader::default(), + instructions: vec![], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + + let fee_payer_balance = 200; + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(fee_payer_balance); + mock_bank.accounts_map.insert(fee_payer_address, (fee_payer_account.clone(), 1)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + assert_eq!( + result.unwrap(), + LoadedTransactionAccounts { + accounts: vec![(fee_payer_address, fee_payer_account)], + program_indices: vec![], + loaded_accounts_data_size: TRANSACTION_ACCOUNT_BASE_SIZE as u32, + } + ); + } + + #[test] + fn test_load_transaction_accounts_native_loader() { + let key1 = Keypair::new(); + let message = Message { + account_keys: vec![key1.pubkey(), native_loader::id()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + mock_bank + .accounts_map + .insert(native_loader::id(), (AccountSharedData::default(), 0)); + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank.accounts_map.insert(key1.pubkey(), (fee_payer_account.clone(), 1)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + + assert_eq!( + result.unwrap_err(), + TransactionError::InvalidProgramForExecution + ); + } + + #[test] + fn test_load_transaction_accounts_program_account_no_data() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key1.pubkey(), key2.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0, 1], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key1.pubkey(), (account_data, 1)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + } + + #[test] + fn test_load_transaction_accounts_invalid_program_for_execution() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key1.pubkey(), key2.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![0, 1], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key1.pubkey(), (account_data, 1)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + } + + #[test] + fn test_load_transaction_accounts_native_loader_owner() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(native_loader::id()); + account_data.set_lamports(1); + account_data.set_executable(true); + mock_bank.accounts_map.insert(key1.pubkey(), (account_data, 1)); + + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), (fee_payer_account.clone(), 1)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + + let loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + + assert_eq!( + result.unwrap(), + LoadedTransactionAccounts { + accounts: vec![ + (key2.pubkey(), fee_payer_account), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].0.clone() + ), + ], + program_indices: vec![1], + loaded_accounts_data_size, + } + ); + } + + #[test] + fn test_load_transaction_accounts_program_account_not_found_after_all_checks() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_executable(true); + mock_bank.accounts_map.insert(key1.pubkey(), (account_data, 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), (account_data, 1)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + } + + #[test] + fn test_load_transaction_accounts_program_account_invalid_program_for_execution_last_check() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(key3.pubkey()); + mock_bank.accounts_map.insert(key1.pubkey(), (account_data, 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), (account_data, 1)); + mock_bank.accounts_map.insert(key3.pubkey(), (AccountSharedData::default(), 0)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + } + + #[test] + fn test_load_transaction_accounts_program_success_complete() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(bpf_loader::id()); + mock_bank.accounts_map.insert(key1.pubkey(), (account_data, 1)); + + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), (fee_payer_account.clone(), 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + mock_bank.accounts_map.insert(bpf_loader::id(), (account_data, 0)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + + let loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + + assert_eq!( + result.unwrap(), + LoadedTransactionAccounts { + accounts: vec![ + (key2.pubkey(), fee_payer_account), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].0.clone() + ), + ], + program_indices: vec![1], + loaded_accounts_data_size, + } + ); + } + + #[test] + fn test_load_transaction_accounts_program_builtin_saturating_add() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key3.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(bpf_loader::id()); + mock_bank.accounts_map.insert(key1.pubkey(), (account_data, 0)); + + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), (fee_payer_account.clone(), 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + mock_bank.accounts_map.insert(bpf_loader::id(), (account_data, 0)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let result = load_transaction_accounts( + &mock_bank, + sanitized_transaction.message(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ); + + let loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 3; + + let mut account_data = AccountSharedData::default(); + account_data.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + assert_eq!( + result.unwrap(), + LoadedTransactionAccounts { + accounts: vec![ + (key2.pubkey(), fee_payer_account), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].0.clone() + ), + (key3.pubkey(), account_data), + ], + program_indices: vec![1, 1], + loaded_accounts_data_size, + } + ); + } + + #[test] + fn test_rent_state_list_len() { + let mint_keypair = Keypair::new(); + let mut bank = TestCallbacks::default(); + let recipient = Pubkey::new_unique(); + let last_block_hash = Hash::new_unique(); + + let mut system_data = AccountSharedData::default(); + system_data.set_lamports(1); + system_data.set_executable(true); + system_data.set_owner(native_loader::id()); + bank.accounts_map.insert(Pubkey::new_from_array([0u8; 32]), (system_data, 0)); + + let mut mint_data = AccountSharedData::default(); + mint_data.set_lamports(2); + bank.accounts_map.insert(mint_keypair.pubkey(), (mint_data, 0)); + bank.accounts_map.insert(recipient, (AccountSharedData::default(), 1)); + let mut tx = Transaction::new_with_payer( + &[system_instruction::transfer( + &mint_keypair.pubkey(), + &recipient, + LAMPORTS_PER_SOL, + )], + Some(&mint_keypair.pubkey()), + ); + tx.sign(&[&mint_keypair], last_block_hash); + let num_accounts = tx.message().account_keys.len(); + let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); + let load_result = + load_transaction(&bank, &sanitized_tx, ValidatedTransactionDetails::default()); + + let TransactionLoadResult::Loaded(loaded_transaction) = load_result else { + panic!("transaction loading failed"); + }; + + let compute_budget = SVMTransactionExecutionBudget { + compute_unit_limit: u64::from(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT), + ..SVMTransactionExecutionBudget::default() + }; + let rent = Rent::default(); + let transaction_context = TransactionContext::new( + loaded_transaction.accounts, + rent.clone(), + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + 1, + ); + + assert_eq!( + TransactionAccountStateInfo::new(&transaction_context, sanitized_tx.message(), &rent,) + .len(), + num_accounts, + ); + } + + #[test] + fn test_load_accounts_success() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key3.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(bpf_loader::id()); + mock_bank.accounts_map.insert(key1.pubkey(), (account_data, 0)); + + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), (fee_payer_account.clone(), 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + mock_bank.accounts_map.insert(bpf_loader::id(), (account_data, 0)); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let load_result = load_transaction( + &mock_bank, + &sanitized_transaction, + ValidatedTransactionDetails::default(), + ); + + let loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 3; + + let mut account_data = AccountSharedData::default(); + account_data.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + + let TransactionLoadResult::Loaded(loaded_transaction) = load_result else { + panic!("transaction loading failed"); + }; + assert_eq!( + loaded_transaction, + LoadedTransaction { + accounts: vec![ + ( + key2.pubkey(), + mock_bank.accounts_map[&key2.pubkey()].0.clone() + ), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].0.clone() + ), + (key3.pubkey(), account_data), + ], + program_indices: vec![1, 1], + fee_details: FeeDetails::default(), + compute_budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size, + } + ); + } + + #[test] + fn test_load_accounts_error() { + let mock_bank = TestCallbacks::default(); + let message = Message { + account_keys: vec![Pubkey::new_from_array([0; 32])], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let load_result = load_transaction( + &mock_bank, + &sanitized_transaction, + ValidatedTransactionDetails::default(), + ); + + assert!(matches!( + load_result, + TransactionLoadResult::NotLoaded(TransactionError::ProgramAccountNotFound), + )); + } + + // note all magic numbers (how many accounts, how many instructions, how big to size buffers) are arbitrary + // other than trying not to swamp programs with blank accounts and keep transaction size below the 64mb limit + #[test] + fn test_load_transaction_accounts_data_sizes() { + let mut rng = rand::rng(); + let mut mock_bank = TestCallbacks::default(); + + // arbitrary accounts + for _ in 0..128 { + let account = AccountSharedData::create_from_existing_shared_data( + 1, + Arc::new(vec![0; rng.random_range(0..128)]), + Pubkey::new_unique(), + rng.random(), + u64::MAX, + ); + mock_bank.accounts_map.insert(Pubkey::new_unique(), (account, 1)); + } + + // fee-payers + let mut fee_payers = vec![]; + for _ in 0..8 { + let fee_payer = Pubkey::new_unique(); + let account = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(vec![0; rng.random_range(0..32)]), + system_program::id(), + rng.random(), + u64::MAX, + ); + mock_bank.accounts_map.insert(fee_payer, (account, 1)); + fee_payers.push(fee_payer); + } + + // programs + let mut loader_owned_accounts = vec![]; + let mut programdata_tracker = AHashMap::new(); + for loader in PROGRAM_OWNERS { + for _ in 0..16 { + let program_id = Pubkey::new_unique(); + let mut account = AccountSharedData::create_from_existing_shared_data( + 1, + Arc::new(vec![0; rng.random_range(0..512)]), + *loader, + rng.random(), + u64::MAX, + ); + + // give half loaderv3 accounts (if they're long enough) a valid programdata + // a quarter a dead pointer and a quarter nothing + // we set executable like a program because after the flag is disabled... + // ...programdata and buffer accounts can be used as program ids without aborting loading + // this will always fail at execution but we are merely testing the data size accounting here + if *loader == bpf_loader_upgradeable::id() && account.data().len() >= 64 { + let programdata_address = Pubkey::new_unique(); + let has_programdata = rng.random(); + + if has_programdata { + let programdata_account = + AccountSharedData::create_from_existing_shared_data( + 1, + Arc::new(vec![0; rng.random_range(0..512)]), + *loader, + rng.random(), + u64::MAX, + ); + programdata_tracker.insert( + program_id, + (programdata_address, programdata_account.data().len()), + ); + mock_bank + .accounts_map + .insert(programdata_address, (programdata_account, 1)); + loader_owned_accounts.push(programdata_address); + } + + if has_programdata || rng.random() { + account + .set_state(&UpgradeableLoaderState::Program { programdata_address }) + .unwrap(); + } + } + + mock_bank.accounts_map.insert(program_id, (account, 1)); + loader_owned_accounts.push(program_id); + } + } + + let mut all_accounts = mock_bank.accounts_map.keys().copied().collect::>(); + + // Append some missing accounts. The current loader materializes them as + // default accounts, so they still contribute the base account size. + for _ in 0..32 { + all_accounts.push(Pubkey::new_unique()); + } + + // now generate arbitrary transactions using this accounts + // we ensure valid fee-payers and that all program ids are loader-owned + // otherwise any account can appear anywhere + // some edge cases we hope to hit (not necessarily all in every run): + // * programs used multiple times as program ids and/or normal accounts are counted once + // * loaderv3 programdata used explicitly zero one or multiple times is counted once + // * loaderv3 programs with missing programdata are allowed through + // * loaderv3 programdata used as program id does nothing weird + // * loaderv3 programdata used as a regular account does nothing weird + // * the programdata conditions hold regardless of ordering + for _ in 0..1024 { + let mut instructions = vec![]; + for _ in 0..rng.random_range(1..8) { + let mut accounts = vec![]; + for _ in 0..rng.random_range(1..16) { + all_accounts.shuffle(&mut rng); + let pubkey = all_accounts[0]; + + accounts.push(AccountMeta { + pubkey, + is_writable: rng.random(), + is_signer: rng.random() && rng.random(), + }); + } + + loader_owned_accounts.shuffle(&mut rng); + let program_id = loader_owned_accounts[0]; + instructions.push(Instruction { + accounts, + program_id, + data: vec![], + }); + } + + fee_payers.shuffle(&mut rng); + let fee_payer = fee_payers[0]; + let transaction = SanitizedTransaction::from_transaction_for_tests( + Transaction::new_with_payer(&instructions, Some(&fee_payer)), + ); + + let mut expected_size = 0; + for pubkey in transaction.account_keys().iter() { + let account_data_len = mock_bank + .accounts_map + .get(pubkey) + .map(|(account, _last_modification_slot)| account.data().len()) + .unwrap_or_default(); + expected_size += TRANSACTION_ACCOUNT_BASE_SIZE + account_data_len; + } + + assert!(expected_size <= MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get() as usize); + + let loaded_transaction_accounts = load_transaction_accounts( + &mock_bank, + &transaction, + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + ) + .unwrap(); + + assert_eq!( + loaded_transaction_accounts.loaded_accounts_data_size, + expected_size as u32, + ); + } + } +} diff --git a/solana/svm/src/lib.rs b/solana/svm/src/lib.rs new file mode 100644 index 0000000..3993798 --- /dev/null +++ b/solana/svm/src/lib.rs @@ -0,0 +1,17 @@ +#![cfg_attr(feature = "frozen-abi", feature(min_specialization))] +#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::arc_with_non_send_sync)] +#![allow(clippy::disallowed_methods)] +#![doc = include_str!("../README.md")] + +mod access_permissions; +pub mod account_loader; +pub mod message_processor; +pub mod program_loader; +pub mod rent_calculator; +pub mod transaction_account_state_info; +pub mod transaction_balances; +pub mod transaction_execution_result; +pub mod transaction_processing_callback; +pub mod transaction_processing_result; +pub mod transaction_processor; diff --git a/solana/svm/src/message_processor.rs b/solana/svm/src/message_processor.rs new file mode 100644 index 0000000..301963e --- /dev/null +++ b/solana/svm/src/message_processor.rs @@ -0,0 +1,614 @@ +use { + solana_program_runtime::invoke_context::InvokeContext, + solana_svm_transaction::svm_message::SVMMessage, solana_transaction_context::IndexOfAccount, + solana_transaction_error::TransactionError, +}; + +/// Process each top-level instruction in a message. +/// +/// The caller provides a transaction context containing the loaded accounts. +/// This function advances instruction state, dispatches either a precompile or +/// program entrypoint, and maps instruction failures to transaction errors. +pub(crate) fn process_message<'ix_data>( + message: &'ix_data impl SVMMessage, + program_indices: &[IndexOfAccount], + invoke_context: &mut InvokeContext<'_, 'ix_data>, + accumulated_consumed_units: &mut u64, +) -> Result<(), TransactionError> { + debug_assert_eq!(program_indices.len(), message.num_instructions()); + for (top_level_instruction_index, ((program_id, instruction), program_account_index)) in + message.program_instructions_iter().zip(program_indices.iter()).enumerate() + { + invoke_context + .prepare_next_top_level_instruction( + message, + &instruction, + *program_account_index, + instruction.data, + ) + .map_err(|err| { + TransactionError::InstructionError(top_level_instruction_index as u8, err) + })?; + + let mut compute_units_consumed = 0; + let result = if invoke_context.is_precompile(program_id) { + invoke_context.process_precompile( + program_id, + instruction.data, + message.instructions_iter().map(|ix| ix.data), + ) + } else { + invoke_context.process_instruction(&mut compute_units_consumed) + }; + + *accumulated_consumed_units = + accumulated_consumed_units.saturating_add(compute_units_consumed); + + result.map_err(|err| { + TransactionError::InstructionError(top_level_instruction_index as u8, err) + })?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_account::{ + Account, AccountSharedData, DUMMY_INHERITABLE_ACCOUNT_FIELDS, ReadableAccount, + }, + solana_ed25519_program::new_ed25519_instruction_with_signature, + solana_hash::Hash, + solana_instruction::{AccountMeta, Instruction, error::InstructionError}, + solana_keypair::Keypair, + solana_message::{AccountKeys, Message, SanitizedMessage}, + solana_precompile_error::PrecompileError, + solana_program_runtime::{ + declare_process_instruction, + execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, + invoke_context::EnvironmentConfig, + loaded_programs::{ + ProgramCacheEntry, ProgramCacheForTxBatch, ProgramRuntimeEnvironments, + }, + solana_sbpf::program::BuiltinFunctionDefinition, + sysvar_cache::SysvarCache, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{ed25519_program, native_loader}, + solana_signer::Signer, + solana_svm_callback::InvokeContextCallback, + solana_svm_feature_set::SVMFeatureSet, + solana_transaction_context::transaction::TransactionContext, + std::{collections::HashSet, sync::Arc}, + }; + + struct MockCallback {} + impl InvokeContextCallback for MockCallback {} + + fn create_loadable_account_for_test(name: &str) -> AccountSharedData { + let (lamports, rent_epoch) = DUMMY_INHERITABLE_ACCOUNT_FIELDS; + AccountSharedData::from(Account { + lamports, + owner: native_loader::id(), + data: name.as_bytes().to_vec(), + executable: true, + rent_epoch, + }) + } + + fn new_sanitized_message(message: Message) -> SanitizedMessage { + SanitizedMessage::try_from_legacy_message(message, &HashSet::new()).unwrap() + } + + fn ed25519_instruction_for_test() -> Instruction { + let keypair = Keypair::new(); + let signature = keypair.sign_message(b"hello"); + let pubkey = keypair.pubkey().to_bytes(); + new_ed25519_instruction_with_signature(b"hello", signature.as_array(), &pubkey) + } + + #[test] + fn test_process_message_readonly_handling() { + #[derive(serde::Serialize, serde::Deserialize)] + enum MockSystemInstruction { + Correct, + TransferLamports { lamports: u64 }, + ChangeData { data: u8 }, + } + + declare_process_instruction!(MockBuiltin, 1, |invoke_context| { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let instruction_data = instruction_context.get_instruction_data(); + if let Ok(instruction) = bincode::deserialize(instruction_data) { + match instruction { + MockSystemInstruction::Correct => Ok(()), + MockSystemInstruction::TransferLamports { lamports } => { + instruction_context + .try_borrow_instruction_account(0)? + .checked_sub_lamports(lamports)?; + instruction_context + .try_borrow_instruction_account(1)? + .checked_add_lamports(lamports)?; + Ok(()) + } + MockSystemInstruction::ChangeData { data } => { + instruction_context + .try_borrow_instruction_account(1)? + .set_data_from_slice(&[data])?; + Ok(()) + } + } + } else { + Err(InstructionError::InvalidInstructionData) + } + }); + + let writable_pubkey = Pubkey::new_unique(); + let readonly_pubkey = Pubkey::new_unique(); + let mock_system_program_id = Pubkey::new_unique(); + + let accounts = vec![ + ( + writable_pubkey, + AccountSharedData::new(100, 1, &mock_system_program_id), + ), + ( + readonly_pubkey, + AccountSharedData::new(0, 1, &mock_system_program_id), + ), + ( + mock_system_program_id, + create_loadable_account_for_test("mock_system_program"), + ), + ]; + let mut transaction_context = + TransactionContext::new(accounts.clone(), Rent::default(), 1, 3, 1); + let program_indices = vec![2]; + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + mock_system_program_id, + Arc::new(ProgramCacheEntry::new_builtin(( + MockBuiltin::vm, + MockBuiltin::codegen, + ))), + ); + let account_keys = (0..transaction_context.get_number_of_accounts()) + .map(|index| *transaction_context.get_key_of_account_at_index(index).unwrap()) + .collect::>(); + let account_metas = vec![ + AccountMeta::new(writable_pubkey, true), + AccountMeta::new_readonly(readonly_pubkey, false), + ]; + + let message = new_sanitized_message(Message::new_with_compiled_instructions( + 1, + 0, + 2, + account_keys.clone(), + Hash::default(), + AccountKeys::new(&account_keys, None).compile_instructions(&[ + Instruction::new_with_bincode( + mock_system_program_id, + &MockSystemInstruction::Correct, + account_metas.clone(), + ), + ]), + )); + let sysvar_cache = SysvarCache::default(); + let feature_set = SVMFeatureSet::all_enabled(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); + assert!(result.is_ok()); + assert_eq!( + transaction_context.accounts().try_borrow(0).unwrap().lamports(), + 100 + ); + assert_eq!( + transaction_context.accounts().try_borrow(1).unwrap().lamports(), + 0 + ); + + let message = new_sanitized_message(Message::new_with_compiled_instructions( + 1, + 0, + 2, + account_keys.clone(), + Hash::default(), + AccountKeys::new(&account_keys, None).compile_instructions(&[ + Instruction::new_with_bincode( + mock_system_program_id, + &MockSystemInstruction::TransferLamports { lamports: 50 }, + account_metas.clone(), + ), + ]), + )); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut transaction_context = + TransactionContext::new(accounts.clone(), Rent::default(), 1, 3, 1); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); + assert_eq!( + result, + Err(TransactionError::InstructionError( + 0, + InstructionError::ReadonlyLamportChange + )) + ); + + let message = new_sanitized_message(Message::new_with_compiled_instructions( + 1, + 0, + 2, + account_keys.clone(), + Hash::default(), + AccountKeys::new(&account_keys, None).compile_instructions(&[ + Instruction::new_with_bincode( + mock_system_program_id, + &MockSystemInstruction::ChangeData { data: 50 }, + account_metas, + ), + ]), + )); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut transaction_context = TransactionContext::new(accounts, Rent::default(), 1, 3, 1); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); + assert_eq!( + result, + Err(TransactionError::InstructionError( + 0, + InstructionError::ReadonlyDataModified + )) + ); + } + + #[test] + fn test_process_message_duplicate_accounts() { + #[derive(serde::Serialize, serde::Deserialize)] + enum MockSystemInstruction { + BorrowFail, + MultiBorrowMut, + DoWork { lamports: u64, data: u8 }, + } + + declare_process_instruction!(MockBuiltin, 1, |invoke_context| { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let instruction_data = instruction_context.get_instruction_data(); + let mut to_account = instruction_context.try_borrow_instruction_account(1)?; + if let Ok(instruction) = bincode::deserialize(instruction_data) { + match instruction { + MockSystemInstruction::BorrowFail => { + let from_account = instruction_context.try_borrow_instruction_account(0)?; + let dup_account = instruction_context.try_borrow_instruction_account(2)?; + if from_account.get_lamports() != dup_account.get_lamports() { + return Err(InstructionError::InvalidArgument); + } + Ok(()) + } + MockSystemInstruction::MultiBorrowMut => { + let lamports_a = + instruction_context.try_borrow_instruction_account(0)?.get_lamports(); + let lamports_b = + instruction_context.try_borrow_instruction_account(2)?.get_lamports(); + if lamports_a != lamports_b { + return Err(InstructionError::InvalidArgument); + } + Ok(()) + } + MockSystemInstruction::DoWork { lamports, data } => { + let mut dup_account = + instruction_context.try_borrow_instruction_account(2)?; + dup_account.checked_sub_lamports(lamports)?; + to_account.checked_add_lamports(lamports)?; + dup_account.set_data_from_slice(&[data])?; + drop(dup_account); + let mut from_account = + instruction_context.try_borrow_instruction_account(0)?; + from_account.checked_sub_lamports(lamports)?; + to_account.checked_add_lamports(lamports)?; + Ok(()) + } + } + } else { + Err(InstructionError::InvalidInstructionData) + } + }); + let mock_program_id = Pubkey::from([2u8; 32]); + let accounts = vec![ + ( + solana_pubkey::new_rand(), + AccountSharedData::new(100, 1, &mock_program_id), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::new(0, 1, &mock_program_id), + ), + ( + mock_program_id, + create_loadable_account_for_test("mock_system_program"), + ), + ]; + let mut transaction_context = + TransactionContext::new(accounts.clone(), Rent::default(), 1, 3, 1); + let program_indices = vec![2]; + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + mock_program_id, + Arc::new(ProgramCacheEntry::new_builtin(( + MockBuiltin::vm, + MockBuiltin::codegen, + ))), + ); + let account_metas = vec![ + AccountMeta::new( + *transaction_context.get_key_of_account_at_index(0).unwrap(), + true, + ), + AccountMeta::new( + *transaction_context.get_key_of_account_at_index(1).unwrap(), + false, + ), + AccountMeta::new( + *transaction_context.get_key_of_account_at_index(0).unwrap(), + false, + ), + ]; + + // Try to borrow mut the same account + let message = new_sanitized_message(Message::new( + &[Instruction::new_with_bincode( + mock_program_id, + &MockSystemInstruction::BorrowFail, + account_metas.clone(), + )], + Some(transaction_context.get_key_of_account_at_index(0).unwrap()), + )); + let sysvar_cache = SysvarCache::default(); + let feature_set = SVMFeatureSet::all_enabled(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); + assert_eq!( + result, + Err(TransactionError::InstructionError( + 0, + InstructionError::AccountBorrowFailed + )) + ); + + // Try to borrow mut the same account in a safe way + let message = new_sanitized_message(Message::new( + &[Instruction::new_with_bincode( + mock_program_id, + &MockSystemInstruction::MultiBorrowMut, + account_metas.clone(), + )], + Some(transaction_context.get_key_of_account_at_index(0).unwrap()), + )); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut transaction_context = + TransactionContext::new(accounts.clone(), Rent::default(), 1, 3, 1); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); + assert!(result.is_ok()); + + // Do work on the same transaction account but at different instruction accounts + let message = new_sanitized_message(Message::new( + &[Instruction::new_with_bincode( + mock_program_id, + &MockSystemInstruction::DoWork { lamports: 10, data: 42 }, + account_metas, + )], + Some(transaction_context.get_key_of_account_at_index(0).unwrap()), + )); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut transaction_context = TransactionContext::new(accounts, Rent::default(), 1, 3, 1); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); + assert!(result.is_ok()); + assert_eq!( + transaction_context.accounts().try_borrow(0).unwrap().lamports(), + 80 + ); + assert_eq!( + transaction_context.accounts().try_borrow(1).unwrap().lamports(), + 20 + ); + assert_eq!( + transaction_context.accounts().try_borrow(0).unwrap().data(), + &vec![42] + ); + } + + #[test] + fn test_precompile() { + let mock_program_id = Pubkey::new_unique(); + declare_process_instruction!(MockBuiltin, 1, |_invoke_context| { + Err(InstructionError::Custom(0xbabb1e)) + }); + + let payer = Pubkey::new_unique(); + let accounts = vec![ + (payer, AccountSharedData::new(1, 0, &native_loader::id())), + ( + ed25519_program::id(), + create_loadable_account_for_test("ed25519_program"), + ), + ( + mock_program_id, + create_loadable_account_for_test("mock_program"), + ), + ]; + let mut transaction_context = TransactionContext::new(accounts, Rent::default(), 1, 2, 2); + + let account_keys = (0..transaction_context.get_number_of_accounts()) + .map(|index| *transaction_context.get_key_of_account_at_index(index).unwrap()) + .collect::>(); + let instructions = vec![ + ed25519_instruction_for_test(), + Instruction::new_with_bytes(mock_program_id, &[], vec![]), + ]; + let message = new_sanitized_message(Message::new_with_compiled_instructions( + 1, + 0, + account_keys.len() as u8 - 1, + account_keys.clone(), + Hash::default(), + AccountKeys::new(&account_keys, None).compile_instructions(&instructions), + )); + + let sysvar_cache = SysvarCache::default(); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + mock_program_id, + Arc::new(ProgramCacheEntry::new_builtin(( + MockBuiltin::vm, + MockBuiltin::codegen, + ))), + ); + + struct MockCallback {} + impl InvokeContextCallback for MockCallback { + fn is_precompile(&self, program_id: &Pubkey) -> bool { + program_id == &ed25519_program::id() + } + + fn process_precompile( + &self, + program_id: &Pubkey, + _data: &[u8], + _instruction_datas: Vec<&[u8]>, + ) -> std::result::Result<(), PrecompileError> { + if self.is_precompile(program_id) { + Ok(()) + } else { + Err(PrecompileError::InvalidPublicKey) + } + } + } + + let feature_set = SVMFeatureSet::all_enabled(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message(&message, &[1, 2], &mut invoke_context, &mut 0); + + assert_eq!( + result, + Err(TransactionError::InstructionError( + 1, + InstructionError::Custom(0xbabb1e) + )) + ); + assert_eq!(transaction_context.get_instruction_trace_length(), 2); + } +} diff --git a/solana/svm/src/program_loader.rs b/solana/svm/src/program_loader.rs new file mode 100644 index 0000000..78bebe9 --- /dev/null +++ b/solana/svm/src/program_loader.rs @@ -0,0 +1,26 @@ +use { + solana_account::{AccountSharedData, ReadableAccount}, + solana_program_runtime::loaded_programs::{ + ProgramCacheEntry, ProgramCacheEntryType, ProgramRuntimeEnvironments, + }, + solana_svm_type_overrides::sync::Arc, +}; + +/// Builds a program-cache entry from an executable account. +/// +/// Invalid program data is kept as a failed-verification entry so execution +/// can report the normal program failure path. +pub fn load_program( + environments: &ProgramRuntimeEnvironments, + program: &AccountSharedData, +) -> Arc { + let environment = environments.get_env_for_execution().clone(); + ProgramCacheEntry::new(environment.clone(), program.data()) + .map(Arc::new) + .unwrap_or_else(|_| { + ProgramCacheEntry { + program: ProgramCacheEntryType::FailedVerification(environment), + } + .into() + }) +} diff --git a/solana/svm/src/rent_calculator.rs b/solana/svm/src/rent_calculator.rs new file mode 100644 index 0000000..b235630 --- /dev/null +++ b/solana/svm/src/rent_calculator.rs @@ -0,0 +1,107 @@ +//! Solana SVM Rent Calculator. +//! +//! Rent management for SVM. + +use { + solana_account::{AccountMode, AccountSharedData, ReadableAccount}, + solana_clock::Epoch, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_transaction_context::{IndexOfAccount, transaction::TransactionContext}, + solana_transaction_error::{TransactionError, TransactionResult}, +}; + +/// When rent is collected from an exempt account, rent_epoch is set to this +/// value. The idea is to have a fixed, consistent value for rent_epoch for all accounts that do not collect rent. +/// This enables us to get rid of the field completely. +pub const RENT_EXEMPT_RENT_EPOCH: Epoch = Epoch::MAX; + +/// Rent state of a Solana account. +#[derive(Debug, PartialEq, Eq)] +pub enum RentState { + /// account.lamports == 0 + Uninitialized, + /// 0 < account.lamports < rent-exempt-minimum + RentPaying { + lamports: u64, // account.lamports() + data_size: usize, // account.data().len() + }, + /// account.lamports >= rent-exempt-minimum + RentExempt, +} + +/// Checks a writable account rent-state transition inside a transaction. +pub fn check_rent_state( + pre_rent_state: Option<&RentState>, + post_rent_state: Option<&RentState>, + transaction_context: &TransactionContext, + index: IndexOfAccount, +) -> TransactionResult<()> { + if let Some((pre_rent_state, post_rent_state)) = pre_rent_state.zip(post_rent_state) { + let expect_msg = "account must exist at TransactionContext index if rent-states are Some"; + check_rent_state_with_account( + pre_rent_state, + post_rent_state, + transaction_context.get_key_of_account_at_index(index).expect(expect_msg), + index, + )?; + } + Ok(()) +} + +/// Checks a rent-state transition for a known account address. +/// +/// The incinerator is exempt from this check. +pub fn check_rent_state_with_account( + pre_rent_state: &RentState, + post_rent_state: &RentState, + address: &Pubkey, + account_index: IndexOfAccount, +) -> TransactionResult<()> { + if !solana_sdk_ids::incinerator::check_id(address) + && !transition_allowed(pre_rent_state, post_rent_state) + { + let account_index = account_index as u8; + Err(TransactionError::InsufficientFundsForRent { account_index }) + } else { + Ok(()) + } +} + +/// Determines the rent state of an account from lamports and data size. +pub fn get_account_rent_state(rent: &Rent, acc: &AccountSharedData) -> RentState { + let (lamports, len) = (acc.lamports(), acc.data().len()); + if lamports == 0 { + RentState::Uninitialized + } else if rent.is_exempt(lamports, len) || acc.is(AccountMode::Ephemeral) { + RentState::RentExempt + } else { + RentState::RentPaying { data_size: len, lamports } + } +} + +/// Returns whether a pre/post rent-state transition is valid. +/// +/// Any state may become uninitialized or rent-exempt. A rent-paying account +/// may remain rent-paying only if it keeps the same data size and is not +/// credited. +pub fn transition_allowed(pre_rent_state: &RentState, post_rent_state: &RentState) -> bool { + match post_rent_state { + RentState::Uninitialized | RentState::RentExempt => true, + RentState::RentPaying { + data_size: post_data_size, + lamports: post_lamports, + } => { + match pre_rent_state { + RentState::Uninitialized | RentState::RentExempt => false, + RentState::RentPaying { + data_size: pre_data_size, + lamports: pre_lamports, + } => { + // Cannot remain RentPaying if resized or credited. + post_data_size == pre_data_size && post_lamports <= pre_lamports + } + } + } + } +} diff --git a/solana/svm/src/transaction_account_state_info.rs b/solana/svm/src/transaction_account_state_info.rs new file mode 100644 index 0000000..06e20ed --- /dev/null +++ b/solana/svm/src/transaction_account_state_info.rs @@ -0,0 +1,222 @@ +use { + crate::rent_calculator::{RentState, check_rent_state, get_account_rent_state}, + solana_rent::Rent, + solana_svm_transaction::svm_message::SVMMessage, + solana_transaction_context::{IndexOfAccount, transaction::TransactionContext}, + solana_transaction_error::TransactionResult as Result, +}; + +#[derive(PartialEq, Debug)] +pub(crate) struct TransactionAccountStateInfo { + rent_state: Option, // None: readonly account +} + +impl TransactionAccountStateInfo { + pub(crate) fn new( + transaction_context: &TransactionContext, + message: &impl SVMMessage, + rent: &Rent, + ) -> Vec { + (0..message.account_keys().len()) + .map(|i| { + let rent_state = if message.is_writable(i) { + let state = transaction_context + .accounts() + .try_borrow(i as IndexOfAccount) + .map(|acc| get_account_rent_state(rent, &acc)) + .ok(); + debug_assert!( + state.is_some(), + "message and transaction context out of sync, fatal" + ); + state + } else { + None + }; + Self { rent_state } + }) + .collect() + } + + pub(crate) fn verify_changes( + pre_state_infos: &[Self], + post_state_infos: &[Self], + transaction_context: &TransactionContext, + ) -> Result<()> { + for (i, (pre_state_info, post_state_info)) in + pre_state_infos.iter().zip(post_state_infos).enumerate() + { + check_rent_state( + pre_state_info.rent_state.as_ref(), + post_state_info.rent_state.as_ref(), + transaction_context, + i as IndexOfAccount, + )?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + solana_account::AccountSharedData, + solana_hash::Hash, + solana_keypair::Keypair, + solana_message::{ + LegacyMessage, Message, MessageHeader, SanitizedMessage, + compiled_instruction::CompiledInstruction, + }, + solana_rent::Rent, + solana_signer::Signer, + solana_transaction_context::transaction::TransactionContext, + solana_transaction_error::TransactionError, + std::collections::HashSet, + }; + + #[test] + fn test_new() { + let rent = Rent::default(); + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + let key4 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key4.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = + SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())); + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + (key3.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, rent.clone(), 20, 20, 1); + let result = TransactionAccountStateInfo::new(&context, &sanitized_message, &rent); + assert_eq!( + result, + vec![ + TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized) + }, + TransactionAccountStateInfo { rent_state: None }, + TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized) + } + ] + ); + } + + #[test] + #[should_panic(expected = "message and transaction context out of sync, fatal")] + fn test_new_panic() { + let rent = Rent::default(); + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + let key4 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key4.pubkey(), key3.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = + SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())); + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + (key3.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, rent.clone(), 20, 20, 1); + let _result = TransactionAccountStateInfo::new(&context, &sanitized_message, &rent); + } + + #[test] + fn test_verify_changes() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let pre_rent_state = vec![ + TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized), + }, + TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized), + }, + ]; + let post_rent_state = vec![TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized), + }]; + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, Rent::default(), 20, 20, 1); + + let result = TransactionAccountStateInfo::verify_changes( + &pre_rent_state, + &post_rent_state, + &context, + ); + assert!(result.is_ok()); + + let pre_rent_state = vec![TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized), + }]; + let post_rent_state = vec![TransactionAccountStateInfo { + rent_state: Some(RentState::RentPaying { data_size: 2, lamports: 5 }), + }]; + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, Rent::default(), 20, 20, 1); + let result = TransactionAccountStateInfo::verify_changes( + &pre_rent_state, + &post_rent_state, + &context, + ); + assert_eq!( + result.err(), + Some(TransactionError::InsufficientFundsForRent { account_index: 0 }) + ); + } +} diff --git a/solana/svm/src/transaction_balances.rs b/solana/svm/src/transaction_balances.rs new file mode 100644 index 0000000..28a817c --- /dev/null +++ b/solana/svm/src/transaction_balances.rs @@ -0,0 +1,60 @@ +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::field_qualifiers; +use solana_account::ReadableAccount; +use solana_transaction_context::transaction_accounts::KeyedAccountSharedData; + +// Use an internal alias so this stays tied to native lamport balances. +type TxNativeBalances = Vec; + +// Implemented for Option to keep call sites branch-free. +pub(crate) trait BalanceCollectionRoutines { + fn collect_pre_balances(&mut self, accounts: &[KeyedAccountSharedData]); + + fn collect_post_balances(&mut self, accounts: &[KeyedAccountSharedData]); +} + +/// Native account balances recorded before and after execution. +#[derive(Debug, Default, Clone)] +#[cfg_attr( + feature = "dev-context-only-utils", + field_qualifiers(native_pre(pub), native_post(pub)) +)] +pub struct BalanceCollector { + native_pre: TxNativeBalances, + native_post: TxNativeBalances, +} + +impl BalanceCollector { + /// Returns recorded pre- and post-execution lamport balances. + pub fn into_vecs(self) -> (TxNativeBalances, TxNativeBalances) { + (self.native_pre, self.native_post) + } + + fn collect_balances(&mut self, accounts: &[KeyedAccountSharedData]) -> TxNativeBalances { + accounts.iter().map(|a| a.1.lamports()).collect() + } +} + +impl BalanceCollectionRoutines for BalanceCollector { + fn collect_pre_balances(&mut self, accounts: &[KeyedAccountSharedData]) { + self.native_pre = self.collect_balances(accounts); + } + + fn collect_post_balances(&mut self, accounts: &[KeyedAccountSharedData]) { + self.native_post = self.collect_balances(accounts); + } +} + +impl BalanceCollectionRoutines for Option { + fn collect_pre_balances(&mut self, accounts: &[KeyedAccountSharedData]) { + if let Some(inner) = self { + inner.collect_pre_balances(accounts) + } + } + + fn collect_post_balances(&mut self, accounts: &[KeyedAccountSharedData]) { + if let Some(inner) = self { + inner.collect_post_balances(accounts) + } + } +} diff --git a/solana/svm/src/transaction_execution_result.rs b/solana/svm/src/transaction_execution_result.rs new file mode 100644 index 0000000..954c1f0 --- /dev/null +++ b/solana/svm/src/transaction_execution_result.rs @@ -0,0 +1,56 @@ +use { + crate::account_loader::LoadedTransaction, + solana_message::inner_instruction::InnerInstructionsList, + solana_transaction_context::transaction::TransactionReturnData, + solana_transaction_error::TransactionResult, std::sync::Arc, +}; + +/// Loaded-account statistics for one transaction. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct TransactionLoadedAccountsStats { + /// Total loaded account data size charged to the transaction. + pub loaded_accounts_data_size: u32, + /// Number of loaded transaction accounts. + pub loaded_accounts_count: usize, +} + +/// A transaction that reached execution, including mutated accounts. +#[derive(Debug, Clone)] +pub struct ExecutedTransaction { + /// Loaded transaction state after execution. + pub loaded_transaction: LoadedTransaction, + /// Execution status and optional recording data. + pub execution_details: TransactionExecutionDetails, +} + +impl ExecutedTransaction { + /// Returns true when execution completed with `Ok(())`. + pub fn was_successful(&self) -> bool { + self.execution_details.was_successful() + } +} + +/// Status and recordings produced by transaction execution. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransactionExecutionDetails { + /// Final execution status. + pub status: TransactionResult<()>, + /// Program log messages when log recording is enabled. + pub log_messages: Option>>, + /// CPI inner instructions when CPI recording is enabled. + pub inner_instructions: Option, + /// Non-empty return data when return-data recording is enabled. + pub return_data: Option, + /// Compute units consumed by top-level instruction execution. + pub executed_units: u64, + /// The change in accounts data len for this transaction. + /// NOTE: This value is valid IFF `status` is `Ok`. + pub accounts_data_len_delta: i64, +} + +impl TransactionExecutionDetails { + /// Returns true when `status` is `Ok(())`. + pub fn was_successful(&self) -> bool { + self.status.is_ok() + } +} diff --git a/solana/svm/src/transaction_processing_callback.rs b/solana/svm/src/transaction_processing_callback.rs new file mode 100644 index 0000000..cf53bea --- /dev/null +++ b/solana/svm/src/transaction_processing_callback.rs @@ -0,0 +1 @@ +pub use solana_svm_callback::{AccountState, InvokeContextCallback, TransactionProcessingCallback}; diff --git a/solana/svm/src/transaction_processing_result.rs b/solana/svm/src/transaction_processing_result.rs new file mode 100644 index 0000000..6e1316c --- /dev/null +++ b/solana/svm/src/transaction_processing_result.rs @@ -0,0 +1,46 @@ +use { + crate::transaction_execution_result::ExecutedTransaction, + solana_transaction_error::TransactionResult, +}; + +/// Result of loading and executing a transaction. +/// +/// `Err` means execution did not produce an `ExecutedTransaction`. `Ok` means +/// execution was attempted; inspect the contained execution status for program +/// success or failure. +pub type TransactionProcessingResult = TransactionResult>; + +/// Convenience methods for nested transaction processing results. +pub trait TransactionProcessingResultExtensions { + /// Returns true when the transaction reached execution. + fn was_processed(&self) -> bool; + /// Returns true when the transaction reached execution and succeeded. + fn was_processed_with_successful_result(&self) -> bool; + /// Returns the executed transaction when execution was attempted. + fn processed_transaction(&self) -> Option<&ExecutedTransaction>; + /// Collapses load and execution status into a single transaction result. + fn flattened_result(&self) -> TransactionResult<()>; +} + +impl TransactionProcessingResultExtensions for TransactionProcessingResult { + fn was_processed(&self) -> bool { + self.is_ok() + } + + fn was_processed_with_successful_result(&self) -> bool { + match self { + Ok(processed_tx) => processed_tx.was_successful(), + Err(_) => false, + } + } + + fn processed_transaction(&self) -> Option<&ExecutedTransaction> { + self.as_deref().ok() + } + + fn flattened_result(&self) -> TransactionResult<()> { + self.as_ref() + .map_err(|err| err.clone()) + .and_then(|processed_tx| processed_tx.execution_details.status.clone()) + } +} diff --git a/solana/svm/src/transaction_processor.rs b/solana/svm/src/transaction_processor.rs new file mode 100644 index 0000000..ebbe25c --- /dev/null +++ b/solana/svm/src/transaction_processor.rs @@ -0,0 +1,924 @@ +#[cfg(test)] +use solana_svm_type_overrides::sync::RwLock; +use solana_transaction_context::transaction_accounts::KeyedAccountSharedData; + +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::{field_qualifiers, qualifiers}; +use { + crate::{ + account_loader::{ + CheckedTransactionDetails, LoadedTransaction, TransactionLoadResult, + ValidatedTransactionDetails, load_transaction, + }, + message_processor::process_message, + program_loader::load_program, + transaction_account_state_info::TransactionAccountStateInfo, + transaction_balances::{BalanceCollectionRoutines, BalanceCollector}, + transaction_execution_result::{ExecutedTransaction, TransactionExecutionDetails}, + transaction_processing_result::TransactionProcessingResult, + }, + solana_account::{AccountSharedData, PROGRAM_OWNERS, ReadableAccount}, + solana_clock::Slot, + solana_hash::Hash, + solana_instruction::TRANSACTION_LEVEL_STACK_HEIGHT, + solana_message::{ + compiled_instruction::CompiledInstruction, + inner_instruction::{InnerInstruction, InnerInstructionsList}, + }, + solana_program_runtime::{ + execution_budget::SVMTransactionExecutionCost, + invoke_context::{EnvironmentConfig, InvokeContext}, + loaded_programs::{ProgramCache, ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, + sysvar_cache::SysvarCache, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_svm_callback::TransactionProcessingCallback, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_log_collector::LogCollector, + solana_svm_transaction::svm_transaction::SVMTransaction, + solana_svm_type_overrides::sync::Arc, + solana_transaction_context::transaction::{ExecutionRecord, TransactionContext}, + solana_transaction_error::TransactionError, + std::{ + fmt::{Debug, Formatter}, + rc::Rc, + }, +}; + +/// Log messages emitted during a transaction. +pub type TransactionLogMessages = Vec; + +/// Result of loading and executing one sanitized transaction. +pub struct LoadAndExecuteSanitizedTransactionOutput { + /// Load or execution result. + /// + /// `Ok` means execution was attempted. The contained transaction can still + /// have a failed execution status. + pub processing_result: TransactionProcessingResult, + /// Native pre/post balances when balance recording is enabled. + pub balance_collector: Option, +} + +/// Controls which execution artifacts are retained in the result. +#[derive(Copy, Clone, Default)] +pub struct ExecutionRecordingConfig { + /// Record inner instructions produced by CPI. + pub enable_cpi_recording: bool, + /// Record program log messages. + pub enable_log_recording: bool, + /// Record non-empty return data. + pub enable_return_data_recording: bool, + /// Record native account balances before and after execution. + pub enable_transaction_balance_recording: bool, +} + +impl ExecutionRecordingConfig { + /// Creates a recording config with every flag set to the same value. + pub fn new_single_setting(option: bool) -> Self { + ExecutionRecordingConfig { + enable_return_data_recording: option, + enable_log_recording: option, + enable_cpi_recording: option, + enable_transaction_balance_recording: option, + } + } +} + +/// Transaction execution options. +#[derive(Default)] +pub struct TransactionProcessingConfig { + /// The maximum number of bytes that log messages can consume. + pub log_messages_bytes_limit: Option, + /// Recording capabilities for transaction execution. + pub recording_config: ExecutionRecordingConfig, +} + +/// Runtime inputs that are shared across a transaction execution. +#[derive(Default)] +pub struct TransactionProcessingEnvironment { + /// Blockhash exposed to programs through the invocation environment. + pub blockhash: Hash, + /// Lamports per signature associated with `blockhash`. + pub blockhash_lamports_per_signature: u64, + /// Retained for API compatibility; the current execution path does not use + /// stake weighting. + pub epoch_total_stake: u64, + /// Runtime feature set used during execution. + pub feature_set: SVMFeatureSet, + /// Runtime environments used for executing already deployed programs. + pub program_runtime_environments_for_execution: ProgramRuntimeEnvironments, + /// Rent calculator used for transaction-context construction and rent-state checks. + pub rent: Rent, +} + +#[cfg_attr( + feature = "dev-context-only-utils", + field_qualifiers(slot(pub), sysvar_cache(pub)) +)] +pub struct TransactionBatchProcessor { + /// Slot associated with this processor. + pub slot: Slot, + + /// Sysvars exposed to programs during execution. + sysvar_cache: SysvarCache, + + /// Shared cache of loaded programs. + pub program_cache: Arc, + + execution_cost: SVMTransactionExecutionCost, +} + +impl Debug for TransactionBatchProcessor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TransactionBatchProcessor") + .field("slot", &self.slot) + .field("sysvar_cache", &self.sysvar_cache) + .field("program_cache", &self.program_cache) + .finish() + } +} + +impl Default for TransactionBatchProcessor { + fn default() -> Self { + Self { + slot: Slot::default(), + sysvar_cache: Default::default(), + program_cache: Arc::new(ProgramCache::default()), + execution_cost: SVMTransactionExecutionCost::default(), + } + } +} + +impl TransactionBatchProcessor { + /// Create a new, uninitialized `TransactionBatchProcessor`. + /// + /// In this context, uninitialized means that the `TransactionBatchProcessor` + /// has been initialized with an empty program cache. The cache contains no + /// programs, including builtins. + pub fn new_uninitialized(slot: Slot, cache: Arc) -> Self { + Self { + slot, + program_cache: cache, + ..Self::default() + } + } + + /// Create a new `TransactionBatchProcessor`. + /// + /// Missing runtime environments are replaced with empty loaders. The + /// program cache is used as provided and does not receive builtins here. + pub fn new(slot: Slot, cache: Arc) -> Self { + Self::new_uninitialized(slot, cache) + } + + /// Sets the base execution cost charged by this processor. + pub fn set_execution_cost(&mut self, cost: SVMTransactionExecutionCost) { + self.execution_cost = cost; + } + + /// Returns mutable access to cached sysvars for the current processor slot. + pub fn sysvar_cache_mut(&mut self) -> &mut SysvarCache { + &mut self.sysvar_cache + } + + /// Loads accounts, prepares programs, and executes one sanitized transaction. + pub fn load_and_execute_sanitized_transaction( + &self, + callbacks: &CB, + tx: &impl SVMTransaction, + details: CheckedTransactionDetails, + environment: &TransactionProcessingEnvironment, + config: &TransactionProcessingConfig, + ) -> LoadAndExecuteSanitizedTransactionOutput { + // Determine a capacity for the internal account cache. This + // over-allocates but avoids ever reallocating, and spares us from + // deduplicating the account keys lists. + + // Create the transaction balance collector if recording is enabled. + let mut balance_collector = config + .recording_config + .enable_transaction_balance_recording + .then(BalanceCollector::default); + + // Create the batch-local program cache. + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(self.slot); + + let details = ValidatedTransactionDetails { + compute_budget: details.compute_budget_and_limits.budget, + loaded_accounts_bytes_limit: details + .compute_budget_and_limits + .loaded_accounts_data_size_limit, + fee_details: details.compute_budget_and_limits.fee_details, + }; + let load_result = load_transaction(callbacks, tx, details); + + let processing_result = match load_result { + TransactionLoadResult::NotLoaded(err) => Err(err), + TransactionLoadResult::Loaded(loaded_transaction) => { + balance_collector.collect_pre_balances(&loaded_transaction.accounts); + self.replenish_program_cache( + &environment.program_runtime_environments_for_execution, + &mut program_cache_for_tx_batch, + &loaded_transaction.accounts, + ); + + let mut executed_tx = self.execute_loaded_transaction( + callbacks, + tx, + loaded_transaction, + &mut program_cache_for_tx_batch, + environment, + config, + ); + if executed_tx.validate_access(tx) { + balance_collector + .collect_post_balances(&executed_tx.loaded_transaction.accounts); + } + if executed_tx.was_successful() { + let cache = program_cache_for_tx_batch.drain_modified_entries(); + self.program_cache.merge(&cache); + } + Ok(Box::new(executed_tx)) + } + }; + + LoadAndExecuteSanitizedTransactionOutput { + processing_result, + balance_collector, + } + } + + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + fn replenish_program_cache( + &self, + environments: &ProgramRuntimeEnvironments, + program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, + accounts: &[KeyedAccountSharedData], + ) { + for (pubkey, acc) in accounts { + if !(acc.executable() && PROGRAM_OWNERS.iter().any(|o| o == acc.owner())) { + continue; + } + let entry = if let Some(entry) = self.program_cache.get(pubkey) { + entry + } else { + let entry = load_program(environments, acc); + self.program_cache.assign_program(*pubkey, entry.clone()); + entry + }; + program_cache_for_tx_batch.replenish(*pubkey, entry); + } + } + + /// Executes a transaction using already loaded accounts. + #[allow(clippy::too_many_arguments)] + fn execute_loaded_transaction( + &self, + callback: &CB, + tx: &impl SVMTransaction, + mut loaded_transaction: LoadedTransaction, + program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, + environment: &TransactionProcessingEnvironment, + config: &TransactionProcessingConfig, + ) -> ExecutedTransaction { + let transaction_accounts = std::mem::take(&mut loaded_transaction.accounts); + + // Ensure the length of accounts matches the expected length from tx.account_keys(). + // This is a sanity check in case that someone starts adding some additional accounts + // since this has been done before. See discussion in PR #4497 for details + debug_assert!(transaction_accounts.len() == tx.account_keys().len()); + + fn transaction_accounts_lamports_sum( + accounts: &[(Pubkey, AccountSharedData)], + ) -> Option { + accounts.iter().try_fold(0u128, |sum, (_, account)| { + sum.checked_add(u128::from(account.lamports())) + }) + } + + let lamports_before_tx = + transaction_accounts_lamports_sum(&transaction_accounts).unwrap_or(0); + + let compute_budget = loaded_transaction.compute_budget; + + let mut transaction_context = TransactionContext::new( + transaction_accounts, + environment.rent.clone(), + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + tx.num_instructions(), + ); + + let pre_account_state_info = + TransactionAccountStateInfo::new(&transaction_context, tx, &environment.rent); + + let log_collector = if config.recording_config.enable_log_recording { + match config.log_messages_bytes_limit { + None => Some(LogCollector::new_ref()), + Some(log_messages_bytes_limit) => Some(LogCollector::new_ref_with_limit(Some( + log_messages_bytes_limit, + ))), + } + } else { + None + }; + + let mut executed_units = 0u64; + + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + program_cache_for_tx_batch, + EnvironmentConfig::new( + environment.blockhash, + environment.blockhash_lamports_per_signature, + callback, + &environment.feature_set, + &environment.program_runtime_environments_for_execution, + &self.sysvar_cache, + ), + log_collector.clone(), + compute_budget, + self.execution_cost, + ); + + let process_result = process_message( + tx, + &loaded_transaction.program_indices, + &mut invoke_context, + &mut executed_units, + ); + + drop(invoke_context); + + let mut status = process_result.and_then(|info| { + let post_account_state_info = + TransactionAccountStateInfo::new(&transaction_context, tx, &environment.rent); + TransactionAccountStateInfo::verify_changes( + &pre_account_state_info, + &post_account_state_info, + &transaction_context, + ) + .map(|_| info) + }); + + let log_messages: Option = + log_collector.and_then(|log_collector| { + Rc::try_unwrap(log_collector) + .map(|log_collector| log_collector.into_inner().into_messages()) + .ok() + }); + + let (execution_record, inner_instructions) = Self::deconstruct_transaction( + transaction_context, + config.recording_config.enable_cpi_recording, + ); + + let ExecutionRecord { + accounts, + return_data, + accounts_resize_delta: accounts_data_len_delta, + .. + } = execution_record; + + if status.is_ok() + && transaction_accounts_lamports_sum(&accounts) + .filter(|lamports_after_tx| lamports_before_tx == *lamports_after_tx) + .is_none() + { + status = Err(TransactionError::UnbalancedTransaction); + } + let status = status.map(|_| ()); + + loaded_transaction.accounts = accounts; + + let return_data = if config.recording_config.enable_return_data_recording + && !return_data.data.is_empty() + { + Some(return_data) + } else { + None + }; + + ExecutedTransaction { + execution_details: TransactionExecutionDetails { + status, + log_messages: log_messages.map(Arc::new), + inner_instructions, + return_data, + executed_units, + accounts_data_len_delta, + }, + loaded_transaction, + } + } + + /// Extract an ExecutionRecord and an InnerInstructionsList from a TransactionContext + fn deconstruct_transaction( + mut transaction_context: TransactionContext, + record_inner_instructions: bool, + ) -> (ExecutionRecord, Option) { + let inner_ix = if record_inner_instructions { + debug_assert!( + transaction_context + .get_instruction_context_at_index_in_trace(0) + .map(|instruction_context| instruction_context.get_stack_height() + == TRANSACTION_LEVEL_STACK_HEIGHT) + .unwrap_or(true) + ); + + let (ix_trace, accounts, ix_data_trace) = transaction_context.take_instruction_trace(); + let mut outer_instructions = Vec::new(); + for ((ix_in_trace, ix_data), ix_accounts) in + ix_trace.into_iter().zip(ix_data_trace.into_iter()).zip(accounts) + { + let stack_height = ix_in_trace.nesting_level.saturating_add(1) as usize; + if stack_height == TRANSACTION_LEVEL_STACK_HEIGHT { + outer_instructions.push(Vec::new()); + } else if let Some(inner_instructions) = outer_instructions.last_mut() { + let stack_height = u8::try_from(stack_height).unwrap_or(u8::MAX); + inner_instructions.push(InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts( + ix_in_trace.program_account_index_in_tx as u8, + ix_data.into_owned(), + ix_accounts.iter().map(|acc| acc.index_in_transaction as u8).collect(), + ), + stack_height, + }); + } else { + debug_assert!(false); + } + } + + Some(outer_instructions) + } else { + None + }; + + let record: ExecutionRecord = transaction_context.into(); + + (record, inner_ix) + } + + pub fn fill_missing_sysvar_cache_entries( + &mut self, + callbacks: &CB, + ) { + self.sysvar_cache.fill_missing_entries(|pubkey, set_sysvar| { + if let Some((account, _slot)) = callbacks.get_account_shared_data(pubkey) { + set_sysvar(account.data()); + } + }); + } + + pub fn reset_sysvar_cache(&mut self) { + self.sysvar_cache.reset(); + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + #[allow(deprecated)] + use solana_sysvar::fees::Fees; + use { + super::*, + solana_account::{WritableAccount, create_account_shared_data_for_test}, + solana_clock::Clock, + solana_epoch_schedule::EpochSchedule, + solana_fee_calculator::FeeCalculator, + solana_fee_structure::FeeDetails, + solana_hash::Hash, + solana_message::{LegacyMessage, Message, MessageHeader, SanitizedMessage}, + solana_program_runtime::{ + execution_budget::SVMTransactionExecutionBudget, loaded_programs::ProgramCacheEntryType, + }, + solana_rent::Rent, + solana_sdk_ids::{bpf_loader, sysvar}, + solana_signature::Signature, + solana_svm_callback::{AccountState, InvokeContextCallback}, + solana_transaction::sanitized::SanitizedTransaction, + solana_transaction_context::transaction::TransactionContext, + std::collections::HashMap, + }; + + fn new_unchecked_sanitized_message(message: Message) -> SanitizedMessage { + SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())) + } + + #[derive(Clone, Default)] + struct MockBankCallback { + account_shared_data: Arc>>, + #[allow(clippy::type_complexity)] + inspected_accounts: + Arc, /* is_writable */ bool)>>>>, + } + + impl InvokeContextCallback for MockBankCallback {} + + impl TransactionProcessingCallback for MockBankCallback { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { + self.account_shared_data + .read() + .unwrap() + .get(pubkey) + .map(|account| (account.clone(), 0)) + } + + fn inspect_account( + &self, + address: &Pubkey, + account_state: AccountState, + is_writable: bool, + ) { + let account = match account_state { + AccountState::Dead => None, + AccountState::Alive(account) => Some(account.clone()), + }; + self.inspected_accounts + .write() + .unwrap() + .entry(*address) + .or_default() + .push((account, is_writable)); + } + } + + #[test] + fn test_inner_instructions_list_from_instruction_trace() { + let mut transaction_context = TransactionContext::new( + vec![( + Pubkey::new_unique(), + AccountSharedData::new(1, 1, &bpf_loader::ID), + )], + Rent::default(), + 4, + 11, + 4, + ); + + // To be uncommented when we reorder the instruction trace + // Four top level instructions + // for i in 0..4 { + // transaction_context + // .configure_instruction_at_index( + // i, + // 0, + // vec![], + // vec![u16::MAX; 256], + // Cow::Owned(vec![i as u8]), + // None, + // ) + // .unwrap(); + // } + + // Execute ix #0 + transaction_context + .configure_top_level_instruction_for_tests(0, vec![], vec![0]) + .unwrap(); + transaction_context.push().unwrap(); + // ix #0 does a CPI + transaction_context.configure_next_cpi_for_tests(0, vec![], vec![0, 0]).unwrap(); + transaction_context.push().unwrap(); + // Returning from everything + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + // Execute ix #1 + transaction_context + .configure_top_level_instruction_for_tests(0, vec![], vec![1]) + .unwrap(); + transaction_context.push().unwrap(); + transaction_context.pop().unwrap(); + // Execute ix #2 + transaction_context + .configure_top_level_instruction_for_tests(0, vec![], vec![2]) + .unwrap(); + transaction_context.push().unwrap(); + // ix #2 does a CPI + transaction_context.configure_next_cpi_for_tests(0, vec![], vec![2, 0]).unwrap(); + transaction_context.push().unwrap(); + // A nested CPI + transaction_context.configure_next_cpi_for_tests(0, vec![], vec![2, 1]).unwrap(); + transaction_context.push().unwrap(); + // Return from nested CPI + transaction_context.pop().unwrap(); + // Return from CPI + transaction_context.pop().unwrap(); + // ix #2 does another CPI + transaction_context.configure_next_cpi_for_tests(0, vec![], vec![2, 2]).unwrap(); + transaction_context.push().unwrap(); + // Return from everything related to ix #2 + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + // Execute ix #3 + transaction_context + .configure_top_level_instruction_for_tests(0, vec![], vec![3]) + .unwrap(); + transaction_context.push().unwrap(); + // ix #3 does a CPI + transaction_context.configure_next_cpi_for_tests(0, vec![], vec![3, 0]).unwrap(); + transaction_context.push().unwrap(); + // ix #3 does a nested CPI + transaction_context.configure_next_cpi_for_tests(0, vec![], vec![3, 1]).unwrap(); + transaction_context.push().unwrap(); + // ix #3 does a second nested CPI + transaction_context.configure_next_cpi_for_tests(0, vec![], vec![3, 2]).unwrap(); + transaction_context.push().unwrap(); + // Return from everything related to ix #3 + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + + let inner_instructions = + TransactionBatchProcessor::deconstruct_transaction(transaction_context, true) + .1 + .unwrap(); + + assert_eq!( + inner_instructions, + vec![ + vec![InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![0, 0], vec![]), + stack_height: 2, + }], + vec![], + vec![ + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![2, 0], vec![]), + stack_height: 2, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![2, 1], vec![]), + stack_height: 3, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![2, 2], vec![]), + stack_height: 2, + }, + ], + vec![ + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![3, 0], vec![]), + stack_height: 2, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![3, 1], vec![]), + stack_height: 3, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![3, 2], vec![]), + stack_height: 4, + }, + ] + ] + ); + } + + #[test] + fn test_execute_loaded_transaction_recordings() { + // Setting all the arguments correctly is too burdensome for testing + // execute_loaded_transaction separately.This function will be tested in an integration + // test with load_and_execute_sanitized_transactions + let message = Message { + account_keys: vec![Pubkey::new_from_array([0; 32])], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + let batch_processor = TransactionBatchProcessor::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let loaded_transaction = LoadedTransaction { + accounts: vec![(Pubkey::new_unique(), AccountSharedData::default())], + program_indices: vec![0], + fee_details: FeeDetails::default(), + compute_budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size: 32, + }; + + let processing_environment = TransactionProcessingEnvironment::default(); + + let mut processing_config = TransactionProcessingConfig::default(); + processing_config.recording_config.enable_log_recording = true; + + let mock_bank = MockBankCallback::default(); + + let executed_tx = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + loaded_transaction.clone(), + &mut program_cache_for_tx_batch, + &processing_environment, + &processing_config, + ); + assert!(executed_tx.execution_details.log_messages.is_some()); + + processing_config.log_messages_bytes_limit = Some(2); + + let executed_tx = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + loaded_transaction.clone(), + &mut program_cache_for_tx_batch, + &processing_environment, + &processing_config, + ); + assert!(executed_tx.execution_details.log_messages.is_some()); + assert!(executed_tx.execution_details.inner_instructions.is_none()); + + processing_config.recording_config.enable_log_recording = false; + processing_config.recording_config.enable_cpi_recording = true; + processing_config.log_messages_bytes_limit = None; + + let executed_tx = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + loaded_transaction, + &mut program_cache_for_tx_batch, + &processing_environment, + &processing_config, + ); + + assert!(executed_tx.execution_details.log_messages.is_none()); + assert!(executed_tx.execution_details.inner_instructions.is_some()); + } + + #[test] + fn test_replenish_program_cache() { + let batch_processor = TransactionBatchProcessor::default(); + let key = Pubkey::new_unique(); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + account_data.set_executable(true); + let accounts = vec![(key, account_data)]; + + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(batch_processor.slot); + + batch_processor.replenish_program_cache( + &Default::default(), + &mut program_cache_for_tx_batch, + &accounts, + ); + + let program = program_cache_for_tx_batch.find(&key).unwrap(); + assert!(matches!( + program.program, + ProgramCacheEntryType::FailedVerification(_) + )); + assert!(batch_processor.program_cache.get(&key).is_some()); + } + + #[test] + #[allow(deprecated)] + fn test_sysvar_cache_initialization1() { + let mock_bank = MockBankCallback::default(); + + let clock = Clock { + slot: 1, + epoch_start_timestamp: 2, + epoch: 3, + leader_schedule_epoch: 4, + unix_timestamp: 5, + }; + let clock_account = create_account_shared_data_for_test(&clock); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::clock::id(), clock_account); + + let epoch_schedule = EpochSchedule::custom(64, 2, true); + let epoch_schedule_account = create_account_shared_data_for_test(&epoch_schedule); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::epoch_schedule::id(), epoch_schedule_account); + + let fees = Fees { + fee_calculator: FeeCalculator { lamports_per_signature: 123 }, + }; + let fees_account = create_account_shared_data_for_test(&fees); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::fees::id(), fees_account); + + let rent = Rent::default(); + let rent_account = create_account_shared_data_for_test(&rent); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::rent::id(), rent_account); + + let mut transaction_processor = TransactionBatchProcessor::default(); + transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); + + let sysvar_cache = &transaction_processor.sysvar_cache; + let cached_clock = sysvar_cache.get_clock(); + let cached_rent = sysvar_cache.get_rent(); + + assert_eq!( + cached_clock.expect("clock sysvar missing in cache"), + clock.into() + ); + assert_eq!( + cached_rent.expect("rent sysvar missing in cache"), + rent.into() + ); + assert!(sysvar_cache.get_slot_hashes().is_err()); + } + + #[test] + #[allow(deprecated)] + fn test_reset_and_fill_sysvar_cache() { + let mock_bank = MockBankCallback::default(); + + let clock = Clock { + slot: 1, + epoch_start_timestamp: 2, + epoch: 3, + leader_schedule_epoch: 4, + unix_timestamp: 5, + }; + let clock_account = create_account_shared_data_for_test(&clock); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::clock::id(), clock_account); + + let epoch_schedule = EpochSchedule::custom(64, 2, true); + let epoch_schedule_account = create_account_shared_data_for_test(&epoch_schedule); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::epoch_schedule::id(), epoch_schedule_account); + + let fees = Fees { + fee_calculator: FeeCalculator { lamports_per_signature: 123 }, + }; + let fees_account = create_account_shared_data_for_test(&fees); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::fees::id(), fees_account); + + let rent = Rent::default(); + let rent_account = create_account_shared_data_for_test(&rent); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::rent::id(), rent_account); + + let mut transaction_processor = TransactionBatchProcessor::default(); + // Fill the sysvar cache + transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); + // Reset the sysvar cache + transaction_processor.reset_sysvar_cache(); + + { + let sysvar_cache = &transaction_processor.sysvar_cache; + // Test that sysvar cache is empty and none of the values are found + assert!(sysvar_cache.get_clock().is_err()); + assert!(sysvar_cache.get_rent().is_err()); + } + + // Refill the cache and test the values are available. + transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); + + let sysvar_cache = &transaction_processor.sysvar_cache; + let cached_clock = sysvar_cache.get_clock(); + let cached_rent = sysvar_cache.get_rent(); + + assert_eq!( + cached_clock.expect("clock sysvar missing in cache"), + clock.into() + ); + assert_eq!( + cached_rent.expect("rent sysvar missing in cache"), + rent.into() + ); + assert!(sysvar_cache.get_slot_hashes().is_err()); + } +} diff --git a/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml b/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml new file mode 100644 index 0000000..3dacc69 --- /dev/null +++ b/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "clock-sysvar-program" +version = "4.0.0-rc.1" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so b/solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so new file mode 100755 index 0000000..dee43a1 Binary files /dev/null and b/solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so differ diff --git a/solana/svm/tests/example-programs/clock-sysvar/src/lib.rs b/solana/svm/tests/example-programs/clock-sysvar/src/lib.rs new file mode 100644 index 0000000..b35d142 --- /dev/null +++ b/solana/svm/tests/example-programs/clock-sysvar/src/lib.rs @@ -0,0 +1,21 @@ +use { + solana_account_info::AccountInfo, + solana_program::program::set_return_data, + solana_program_entrypoint::entrypoint, + solana_program_error::ProgramResult, + solana_pubkey::Pubkey, + solana_sysvar::{clock::Clock, Sysvar}, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + let time_now = Clock::get().unwrap().unix_timestamp; + let return_data = time_now.to_be_bytes(); + set_return_data(&return_data); + Ok(()) +} diff --git a/solana/svm/tests/example-programs/hello-solana/Cargo.toml b/solana/svm/tests/example-programs/hello-solana/Cargo.toml new file mode 100644 index 0000000..4f08a1b --- /dev/null +++ b/solana/svm/tests/example-programs/hello-solana/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "hello-solana-program" +version = "4.0.0-rc.1" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/hello-solana/hello_solana_program.so b/solana/svm/tests/example-programs/hello-solana/hello_solana_program.so new file mode 100755 index 0000000..a9da4ff Binary files /dev/null and b/solana/svm/tests/example-programs/hello-solana/hello_solana_program.so differ diff --git a/solana/svm/tests/example-programs/hello-solana/src/lib.rs b/solana/svm/tests/example-programs/hello-solana/src/lib.rs new file mode 100644 index 0000000..3f6799c --- /dev/null +++ b/solana/svm/tests/example-programs/hello-solana/src/lib.rs @@ -0,0 +1,16 @@ +use { + solana_account_info::AccountInfo, solana_msg::msg, solana_program_entrypoint::entrypoint, + solana_program_error::ProgramResult, solana_pubkey::Pubkey, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + msg!("Hello, Solana!"); + + Ok(()) +} diff --git a/solana/svm/tests/example-programs/simple-transfer/Cargo.toml b/solana/svm/tests/example-programs/simple-transfer/Cargo.toml new file mode 100644 index 0000000..8d93d9d --- /dev/null +++ b/solana/svm/tests/example-programs/simple-transfer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "simple-transfer-program" +version = "4.0.0-rc.1" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so b/solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so new file mode 100755 index 0000000..5132b38 Binary files /dev/null and b/solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so differ diff --git a/solana/svm/tests/example-programs/simple-transfer/src/lib.rs b/solana/svm/tests/example-programs/simple-transfer/src/lib.rs new file mode 100644 index 0000000..1e922c2 --- /dev/null +++ b/solana/svm/tests/example-programs/simple-transfer/src/lib.rs @@ -0,0 +1,29 @@ +use { + solana_account_info::{next_account_info, AccountInfo}, + solana_program::program::invoke, + solana_program_entrypoint::entrypoint, + solana_program_error::ProgramResult, + solana_pubkey::Pubkey, + solana_system_interface::instruction as system_instruction, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let amount = u64::from_be_bytes(data[0..8].try_into().unwrap()); + let accounts_iter = &mut accounts.iter(); + let payer = next_account_info(accounts_iter)?; + let recipient = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + invoke( + &system_instruction::transfer(payer.key, recipient.key, amount), + &[payer.clone(), recipient.clone(), system_program.clone()], + )?; + + Ok(()) +} diff --git a/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml b/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml new file mode 100644 index 0000000..c431ebe --- /dev/null +++ b/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "transfer-from-account" +version = "4.0.0-rc.1" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/transfer-from-account/src/lib.rs b/solana/svm/tests/example-programs/transfer-from-account/src/lib.rs new file mode 100644 index 0000000..4460873 --- /dev/null +++ b/solana/svm/tests/example-programs/transfer-from-account/src/lib.rs @@ -0,0 +1,31 @@ +use { + solana_account_info::{next_account_info, AccountInfo}, + solana_program::program::invoke, + solana_program_entrypoint::entrypoint, + solana_program_error::ProgramResult, + solana_pubkey::Pubkey, + solana_system_interface::instruction as system_instruction, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _data: &[u8], +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let payer = next_account_info(accounts_iter)?; + let recipient = next_account_info(accounts_iter)?; + let data_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + let amount = u64::from_le_bytes(data_account.data.borrow()[0..8].try_into().unwrap()); + + invoke( + &system_instruction::transfer(payer.key, recipient.key, amount), + &[payer.clone(), recipient.clone(), system_program.clone()], + )?; + + Ok(()) +} diff --git a/solana/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so b/solana/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so new file mode 100755 index 0000000..a3ef926 Binary files /dev/null and b/solana/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so differ diff --git a/solana/svm/tests/example-programs/write-to-account/Cargo.toml b/solana/svm/tests/example-programs/write-to-account/Cargo.toml new file mode 100644 index 0000000..122f321 --- /dev/null +++ b/solana/svm/tests/example-programs/write-to-account/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "write-to-account" +version = "4.0.0-rc.1" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/write-to-account/src/lib.rs b/solana/svm/tests/example-programs/write-to-account/src/lib.rs new file mode 100644 index 0000000..0fde9fa --- /dev/null +++ b/solana/svm/tests/example-programs/write-to-account/src/lib.rs @@ -0,0 +1,62 @@ +use { + solana_account_info::{next_account_info, AccountInfo}, + solana_msg::msg, + solana_program_entrypoint::entrypoint, + solana_program_error::{ProgramError, ProgramResult}, + solana_pubkey::Pubkey, + solana_sdk_ids::incinerator, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let target_account_info = next_account_info(accounts_iter)?; + match data[0] { + // print account size + 0 => { + msg!( + "account size {}", + target_account_info.try_borrow_data()?.len() + ); + } + // set account data + 1 => { + let mut account_data = target_account_info.try_borrow_mut_data()?; + account_data[0] = 100; + } + // deallocate account + 2 => { + let incinerator_info = next_account_info(accounts_iter)?; + if !incinerator::check_id(incinerator_info.key) { + return Err(ProgramError::InvalidAccountData); + } + + let mut target_lamports = target_account_info.try_borrow_mut_lamports()?; + let mut incinerator_lamports = incinerator_info.try_borrow_mut_lamports()?; + + **incinerator_lamports = incinerator_lamports + .checked_add(**target_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; + + **target_lamports = target_lamports + .checked_sub(**target_lamports) + .ok_or(ProgramError::InsufficientFunds)?; + } + // reallocate account + 3 => { + let new_size = usize::from_le_bytes(data[1..9].try_into().unwrap()); + target_account_info.realloc(new_size, true)?; + } + // bad ixn + _ => { + return Err(ProgramError::InvalidArgument); + } + } + + Ok(()) +} diff --git a/solana/svm/tests/example-programs/write-to-account/write_to_account_program.so b/solana/svm/tests/example-programs/write-to-account/write_to_account_program.so new file mode 100755 index 0000000..e436426 Binary files /dev/null and b/solana/svm/tests/example-programs/write-to-account/write_to_account_program.so differ diff --git a/solana/transaction-context/Cargo.toml b/solana/transaction-context/Cargo.toml index d0c0a3d..4056bbb 100644 --- a/solana/transaction-context/Cargo.toml +++ b/solana/transaction-context/Cargo.toml @@ -8,7 +8,7 @@ edition = { workspace = true } homepage = { workspace = true } license = { workspace = true } repository = { workspace = true } -version = { workspace = true } +version = "4.1.1" [package.metadata.docs.rs] all-features = true