diff --git a/Cargo.toml b/Cargo.toml index 25185a6..39b090f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "solana/account", "solana/program-runtime", + "solana/svm", "solana/transaction-context", "solana/transaction-view", ] @@ -21,6 +22,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" @@ -33,6 +35,7 @@ blake3 = "1.8.5" cfg-if = "1.0.4" criterion = "0.8.2" derive_more = "2.1.1" +env_logger = "0.11.8" itertools = "0.13.0" qualifier_attr = "0.2.2" rand = "0.9.2" @@ -56,9 +59,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-hash = "4.3.0" solana-instruction = "3.4.0" @@ -66,7 +71,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" @@ -77,27 +87,27 @@ solana-short-vec = "=3.2.2" solana-signature = "=3.4.1" 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 = { version = "=4.1.1", features = ["agave-unstable-api"] } +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] agave-transaction-view = { path = "solana/transaction-view" } +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..0c8dbbe --- /dev/null +++ b/solana/README.md @@ -0,0 +1,165 @@ +# Engine Runtime Differences from Agave + +This directory contains the Agave runtime forks required by the engine. These +crates execute caller-loaded transactions and return account changes; they do +not own consensus, fork choice, confirmation, persistence, or validator commit +policy. + +The differences below are intentional compatibility constraints for account +representation, transaction context, serialization, VM mapping, and CPI. + +## Runtime boundary + +- `solana-svm` loads accounts through a caller callback and returns execution + results and mutated accounts. +- Persistence, commit decisions, and deployment policy remain outside the fork. +- Program loading is limited to programs required by the transaction and checks + native-loader or `PROGRAM_OWNERS` ownership. +- Rent-state and lamport-balance checks remain part of execution. + +## Account representation + +`solana-account` replaces the shared-data representation with copy-on-write +storage. `AccountSharedData` contains either an owned `Arc>` or a borrowed +view into an aligned external buffer. `DirtyMarkers` record changes to data, +owner, lamports, slot, mode, and state flags for higher-layer writeback. + +Borrowed storage has these invariants: + +- The buffer is 8-byte aligned and remains live for the borrow. +- One header and pubkey prefix are followed by two account images. +- `AccountHeader::sequence` selects the active image. +- `translate` copies active state into the shadow image before mutation. +- `commit` publishes the shadow image; `reset` abandons it. +- `rollback` is valid only after `commit`. + +Writes remain borrowed while they fit the image capacity. Growth beyond that +capacity promotes the account to owned storage. Shared owned data becomes unique +through `Arc::make_mut` before mutation. + +`AccountMode` contains `ReadOnly`, `Placeholder`, `System`, `Delegated`, +`Ephemeral`, `Transient`, and `Closed`. Only delegated and ephemeral accounts are +mutable by user programs. Transient accounts remain persistent but immutable +after the transaction that legally transitions them from delegated. The +transaction access guard recognizes that transition through the mode dirty +marker; a freshly loaded transient account has a clean marker and remains +immutable. The same transaction-local exception lets a legal mode transition +close an account. Ephemeral accounts remain persistent until then. `StateFlags` +contains `EXECUTABLE` and `COMPRESSED`. +`AccountSharedData` does not store `rent_epoch`; compatibility APIs return or +ignore the masked value required by their interface. + +## Transaction context + +`solana-transaction-context` stores accounts in `UnsafeCell`s guarded by explicit +borrow counters. This permits the VM access handler to remap account data while +runtime borrow rules remain enforced. + +`TransactionAccounts` records touched accounts, total account-data resize, and +instruction lamport deltas. `AccountRef` and `AccountRefMut` release their +counters on drop. `ExecutionRecord` returns keyed accounts, return data, touched +count, and resize delta. All references must be released before context +deconstruction; failure of `Rc::try_unwrap` indicates a lifetime bug. + +## Transaction parsing and Engine-private transactions + +`agave-transaction-view` parses Legacy, v0, V1, and Engine-private Magicblock +transactions directly from their serialized bytes. Legacy and v0 retain the +standard Solana wire layouts, while V1 retains the Agave V1 layout. All three +accept serialized sizes through `u16::MAX` bytes, inclusive. The compact-u16 +parser supports the complete canonical one-, two-, and three-byte encoding, so +instruction data and other framed arrays are no longer limited by the former +two-byte parser assumption. + +Frame offsets and total lengths are stored as `u32`. Fallible parsing uses +checked range arithmetic and validates every frame before unchecked iterators +or typed views access the original bytes. The engine is guaranteed not to run +on 16-bit targets, so conversion from validated `u32` offsets to `usize` is +direct. + +Magicblock is private transaction version 127 and reuses the V1 layout with a +distinct prefix. Its signatures follow the V1 message at the end of the byte +stream. The Engine transaction composer compiles account operations as V1, +writes the Magicblock prefix, signs the exact message range, and verifies that +the first static account is the configured Engine authority. Magicblock +transactions may be at most 16 MiB and raise only the SVM instruction-trace +limit to 255; standard versions retain their existing structural limits. The +account accessor uses this private path so a 64 KiB account payload can be split +into patch instructions and executed atomically without relaxing standard +transaction policy. + +Address lookup tables are intentionally disabled. Any transaction containing a +lookup table entry fails sanitization with `AddressLookupMismatch`; an empty v0 +lookup list remains valid and resolves without loaded addresses. Sequencing and +simulation therefore resolve transactions without supplying loaded addresses. + +The crate-specific wire and safety contracts are documented in +[`transaction-view/README.md`](transaction-view/README.md). Keep its version +prefix, signed message range, size limits, sanitizer, Engine composer, and SVM +trace-limit override synchronized. + +## VM account mapping + +Account data is always mapped directly into the SBF VM. Do not restore the +removed `virtual_address_space_adjustments` or `account_data_direct_mapping` +branches that copied account data through serialized program input. + +Serialization retains loader ABI metadata: + +- Deprecated-loader accounts use ABI v0. +- Loader-v2 and loader-v3 accounts use ABI v1. +- ABI v1 optionally includes direct account pointers. + +The serialized input contains metadata, lamports, lengths, owners, instruction +data, and program id. Account data resides in separate `MemoryRegion`s. +Deprecated-loader regions reserve the current length; newer loaders also reserve +`MAX_PERMITTED_DATA_INCREASE`. Deserialization reads mutable metadata but does +not copy account bytes back from the input buffer. + +## Access-violation growth + +Writable borrowed or shared-owned account data may initially be mapped +read-only. The first VM store enters the transaction-context handler, which: + +- handles stores only and requires an account-index region payload; +- rejects accesses outside the account's reserved address range; +- records touch and resize deltas before growing data; +- grows only to the requested access length; and +- replaces the region host pointer, length, and writability. + +Keep serialization, `TransactionContext::access_violation_handler`, and VM error +mapping synchronized. They jointly map growth failures to account-specific +readonly, size, and realloc errors. + +## CPI synchronization + +`CallerAccount::serialized_data` remains empty. CPI entry and exit synchronize +lamports, owner, and data length, while account bytes remain directly mapped. +When storage can move, CPI replaces the caller `MemoryRegion` with one created +from the current account. + +Strict syscall parameter-address checks are always enforced. CPI rejects +`AccountInfo` fields whose key, owner, lamports, data, or data-length pointers do +not reference the canonical VM locations for the passed account. This is required +because account bytes are mapped directly into the VM and cannot be protected by +copy-back serialization. + +Inner-instruction growth uses the caller's original length plus the permitted +increase. Deprecated loaders reserve only the original length. Any account-region +layout change must update CPI pointer checks, region replacement, and VM access +handling together. + +## Maintenance constraints + +- Preserve direct account-region mapping as the only runtime path. +- Preserve ABI v0 and ABI v1 metadata compatibility. +- Keep borrowed layout changes synchronized across account, transaction-context, + serialization, and mapping code. +- Preserve full compact-u16 parsing and checked `u32` transaction framing. +- Keep Magicblock construction and execution policy synchronized with + `agave-transaction-view`. +- Do not enable address lookup resolution without revisiting ingress, + sanitization, scheduling, and simulation together. +- Treat dirty markers and touched flags as the caller's writeback signal. +- Keep persistence, consensus, validator fee policy, and batch commit decisions + outside these runtime crates. diff --git a/solana/program-runtime/Cargo.toml b/solana/program-runtime/Cargo.toml index a213e3f..a5d2a83 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 index 0916c0d..23f2567 100644 --- a/solana/svm/Cargo.toml +++ b/solana/svm/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "solana-svm" + +authors = { workspace = true } description = "Solana SVM" documentation = "https://docs.rs/solana-svm" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } +edition = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +repository = { workspace = true } +version = "4.1.1" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] @@ -17,104 +18,67 @@ crate-type = ["lib"] name = "solana_svm" [features] -default = ["metrics"] +# 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 = [] -dummy-for-ci-check = ["metrics"] 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", -] -metrics = [ - "solana-bpf-loader-program/metrics", - "solana-program-runtime/metrics", -] -shuttle-test = [ - "solana-bpf-loader-program/shuttle-test", - "solana-program-runtime/shuttle-test", - "solana-svm-type-overrides/shuttle-test", -] +# Compatibility stub forwarded to program-runtime; neither fork consumes frozen +# ABI metadata. +frozen-abi = ["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 } -percentage = { workspace = true } qualifier_attr = { workspace = true, optional = true } serde = { workspace = true, features = ["rc"] } + 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-loader-v4-interface = { workspace = true } solana-message = { workspace = true } -solana-nonce = { workspace = true } -solana-nonce-account = { workspace = true, features = ["wincode"] } -solana-program-pack = { 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 } -solana-svm-feature-set = { workspace = true } -solana-svm-log-collector = { workspace = true } -solana-svm-measure = { workspace = true } -solana-svm-timings = { workspace = true } -solana-svm-transaction = { workspace = true } -solana-svm-type-overrides = { 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-transaction = { workspace = true, features = ["agave-unstable-api"] } +solana-svm-type-overrides = { workspace = true, features = ["agave-unstable-api"] } solana-system-interface = { workspace = true } solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } -spl-generic-token = { workspace = true } -thiserror = { workspace = true } [dev-dependencies] -assert_matches = { workspace = true } bincode = { workspace = true } env_logger = { workspace = true } -libsecp256k1 = { workspace = true } -openssl = { workspace = true } rand = { workspace = true } -shuttle = { workspace = true } -solana-bpf-loader-program = { path = "../programs/bpf_loader", default-features = false, features = ["agave-unstable-api"] } solana-clock = { workspace = true } -solana-compute-budget = { path = "../compute-budget", features = ["agave-unstable-api"] } -solana-compute-budget-interface = { workspace = true } -solana-compute-budget-program = { path = "../programs/compute-budget", features = ["agave-unstable-api"] } 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-binaries = { path = "../program-binaries", features = ["agave-unstable-api"] } -solana-program-runtime = { path = "../program-runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } +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-secp256k1-program = { workspace = true, features = ["bincode"] } -solana-secp256r1-program = { workspace = true, features = ["openssl-vendored"] } 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 = ["agave-unstable-api", "dev-context-only-utils", "svm-internal"] } -solana-syscalls = { path = "../syscalls", features = ["agave-unstable-api"] } -solana-system-program = { path = "../programs/system", features = ["agave-unstable-api"] } -solana-system-transaction = { workspace = true } +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 = { path = "../transaction-context", features = ["agave-unstable-api", "bincode", "dev-context-only-utils"] } -spl-token-interface = { workspace = true } -test-case = { workspace = true } +solana-transaction-context = { workspace = true, features = ["dev-context-only-utils"] } -[lints] -workspace = true +[lints.rust] +unexpected_cfgs = "allow" diff --git a/solana/svm/README.md b/solana/svm/README.md new file mode 100644 index 0000000..1afa450 --- /dev/null +++ b/solana/svm/README.md @@ -0,0 +1,16 @@ +# `solana-svm` + +This Agave fork is the transaction-level execution entry point. Workspace +`[patch.crates-io]` entries force the dependency graph to use this copy. + +The SVM loads required accounts through the caller's +`transaction_processing_callback`, loads required programs, executes through +`solana-program-runtime`, and returns processing results and mutated accounts. +It owns no account storage. + +Persistence, commit decisions, deployment policy, and validator batch behavior +remain above this crate. Engine-specific runtime differences are documented in +[`../README.md`](../README.md). + +The `frozen-abi` feature is retained as a no-op compatibility stub and forwards +to the corresponding program-runtime feature. diff --git a/solana/svm/doc/diagrams/context.svg b/solana/svm/doc/diagrams/context.svg deleted file mode 100644 index b2ec4f2..0000000 --- a/solana/svm/doc/diagrams/context.svg +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -bank - -transactionprocessor - -programruntime - -BPFVM - -accounts-db - -runtime - -SVM - - \ No newline at end of file diff --git a/solana/svm/doc/diagrams/context.tex b/solana/svm/doc/diagrams/context.tex deleted file mode 100644 index ec493b9..0000000 --- a/solana/svm/doc/diagrams/context.tex +++ /dev/null @@ -1,35 +0,0 @@ -%%\documentclass[dvisvgm]{minimal} -\documentclass{minimal} - -\usepackage{tikz} -\usetikzlibrary{graphs, graphdrawing, shapes.misc} -\usegdlibrary{trees} - -\begin{document} - -\tikzset{terminal/.style={ - % The shape: - rectangle, - rounded corners=3mm, - minimum size=20mm, - % The rest - very thick,draw=black!50, - top color=white,bottom color=black!20, - font=\ttfamily}, -} - -\begin{tikzpicture} - \graph[tree layout, grow'=right, level sep=10mm] { - bank [terminal] - -> { - { a/"transaction processor"[terminal, orient=right, orient tail=bank] - -> b/"program runtime"[terminal] - -> c/"BPF VM"[terminal] }, , , , , , , , - d/"accounts-db"[terminal, orient=down, orient tail=bank] - }; - runtime [draw] // { bank }; - SVM [draw] // { a, b, c } - }; -\end{tikzpicture} - -\end{document} diff --git a/solana/svm/doc/spec.md b/solana/svm/doc/spec.md deleted file mode 100644 index 16c311b..0000000 --- a/solana/svm/doc/spec.md +++ /dev/null @@ -1,311 +0,0 @@ -# Solana Virtual Machine specification - -# Introduction - -Several components of the Solana Validator are involved in processing -a transaction (or a batch of transactions). Collectively, the -components responsible for transaction execution are designated as -Solana Virtual Machine (SVM). SVM packaged as a stand-alone library -can be used in applications outside the Solana Validator. - -This document represents the SVM specification. It covers the API -of using SVM in projects unrelated to Solana Validator and the -internal workings of the SVM, including the descriptions of the inner -data flow, data structures, and algorithms involved in the execution -of transactions. The document’s target audience includes both external -users and the developers of the SVM. - -## Use cases - -We envision the following applications for SVM - -- **Transaction execution in Solana Validator** - - This is the primary use case for the SVM. It remains a major - component of the Agave Validator, but with clear interface and - isolated from dependencies on other components. - - The SVM is currently viewed as realizing two stages of the - Transaction Engine Execution pipeline as described in Solana - Architecture documentation - [https://docs.solana.com/validator/runtime#execution](https://docs.solana.com/validator/runtime#execution), - namely ‘load accounts’ and ‘execute’ stages. - -- **SVM Rollups** - - Rollups that need to execute a block but don’t need the other - components of the validator can benefit from SVM, as it can reduce - hardware requirements and decentralize the network. This is - especially useful for Ephemeral Rollups since the cost of compute - will be higher as a new rollup is created for every user session - in applications like gaming. - -- **SVM Fraud Proofs for Diet Clients** - - A succinct proof of an invalid state transition by the supermajority (SIMD-65) - -- **Validator Sidecar for JSON-RPC** - - The RPC needs to be separated from the validator. - `simulateTransaction` requires replaying the transactions and - accessing necessary account data. - -- **SVM-based Avalanche subnet** - - The SVM would need to be isolated to run within a subnet since the - consensus and networking functionality would rely on Avalanche - modules. - -- **Modified SVM (SVM+)** - - An SVM type with all the current functionality and extended - instructions for custom use cases. This would form a superset of - the current SVM. - -# System Context - -In this section, SVM is represented as a single entity. We describe its -interfaces to the parts of the Solana Validator external to SVM. - -In the context of Solana Validator, the main entity external to SVM is -bank. It creates an SVM, submits transactions for execution and -receives results of transaction execution from SVM. - -![context diagram](/svm/doc/diagrams/context.svg "System Context") - -## Interfaces - -In this section, we describe the API of using the SVM both in Solana -Validator and in third-party applications. - -The interface to SVM is represented by the -`transaction_processor::TransactionBatchProcessor` struct. To create -a `TransactionBatchProcessor` object the client need to specify the -`slot`, `epoch`, and `program_cache`. - -- `slot: Slot` is a u64 value representing the ordinal number of a - particular blockchain state in context of which the transactions - are executed. This value is used to locate the on-chain program - versions used in the transaction execution. -- `epoch: Epoch` is a u64 value representing the ordinal number of - a Solana epoch, in which the slot was created. This is another - index used to locate the onchain programs used in the execution of - transactions in the batch. -- `program_cache: Arc>>` is a reference to - a ProgramCache instance. All on chain programs used in transaction - batch execution are loaded from the program cache. - -In addition, `TransactionBatchProcessor` needs an instance of -`SysvarCache` and a set of pubkeys of builtin program IDs. - -The main entry point to the SVM is the method -`load_and_execute_sanitized_transactions`. - -The method `load_and_execute_sanitized_transactions` takes the -following arguments: - -- `callbacks`: A `TransactionProcessingCallback` trait instance which allows - the transaction processor to summon information about accounts, most - importantly loading them for transaction execution. -- `sanitized_txs`: A slice of sanitized transactions. -- `check_results`: A mutable slice of transaction check results. -- `environment`: The runtime environment for transaction batch processing. -- `config`: Configurations for customizing transaction processing behavior. - -The method returns a `LoadAndExecuteSanitizedTransactionsOutput`, which is -defined below in more detail. - -An integration test `svm_integration` contains an example of -instantiating `TransactionBatchProcessor` and calling its method -`load_and_execute_sanitized_transactions`. - -### `TransactionProcessingCallback` - -Downstream consumers of the SVM must implement the -`TransactionProcessingCallback` trait in order to provide the transaction -processor with the ability to load accounts and retrieve other account-related -information. - -```rust -pub trait TransactionProcessingCallback { - fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)>; - - fn add_builtin_account(&self, _name: &str, _program_id: &Pubkey) {} -} -``` - -Consumers can customize this plug-in to use their own Solana account source, -caching, and more. - -### `SVMTransaction` - -An SVM transaction is a transaction that has undergone the -various checks required to evaluate a transaction against the Solana protocol -ruleset. Some of these rules include signature verification and validation -of account indices (`num_readonly_signers`, etc.). - -A `SVMTransaction` is a trait that can access: - -- `signatures`: the hash of the transaction message encrypted using - the signing key (for each signer in the transaction). -- `static_account_keys`: Slice of `Pubkey` of accounts used in the transaction. -- `account_keys`: Pubkeys of all accounts used in the transaction, including - those from address table lookups. -- `recent_blockhash`: Hash of a recent block. -- `instructions_iter`: An iterator over the transaction's instructions. -- `message_address_table_lookups`: An iterator over the transaction's - address table lookups. These are only used in V0 transactions, for legacy - transactions the iterator is empty. - -### `TransactionCheckResult` - -Simply stores details about a transaction, including whether or not it contains -a nonce, the nonce it contains (if applicable), and the lamports per signature -to charge for fees. - -### `TransactionProcessingEnvironment` - -The transaction processor requires consumers to provide values describing -the runtime environment to use for processing transactions. - -- `blockhash`: The blockhash to use for the transaction batch. -- `feature_set`: Runtime feature set to use for the transaction batch. -- `epoch_total_stake`: The total stake for the current epoch. -- `fee_structure`: Fee structure to use for assessing transaction fees. -- `lamports_per_signature`: Lamports per signature to charge per transaction. -- `rent_collector`: Rent collector to use for the transaction batch. - -### `TransactionProcessingConfig` - -Consumers can provide various configurations to adjust the default behavior of -the transaction processor. - -- `account_overrides`: Encapsulates overridden accounts, typically used for - transaction simulation. -- `compute_budget`: The compute budget to use for transaction execution. -- `check_program_deployment_slot`: Whether or not to check a program's - deployment slot when replenishing a program cache instance. -- `log_messages_bytes_limit`: The maximum number of bytes that log messages can - consume. -- `limit_to_load_programs`: Whether to limit the number of programs loaded for - the transaction batch. -- `recording_config`: Recording capabilities for transaction execution. - -### `LoadAndExecuteSanitizedTransactionsOutput` - -The output of the transaction batch processor's -`load_and_execute_sanitized_transactions` method. - -- `error_metrics`: Error metrics for transactions that were processed. -- `execute_timings`: Timings for transaction batch execution. -- `processing_results`: Vector of results indicating whether a transaction was - processed or could not be processed for some reason. Note that processed - transactions can still have failed! - -# Functional Model - -In this section, we describe the functionality (logic) of the SVM in -terms of its components, relationships among components, and their -interactions. - -On a high level the control flow of SVM consists of loading program -accounts, checking and verifying the loaded accounts, creating -invocation context and invoking RBPF on programs implementing the -instructions of a transaction. The SVM needs to have access to an account -database, and a sysvar cache via traits implemented for the corresponding -objects passed to it. The results of transaction execution are -consumed by bank in Solana Validator use case. However, bank structure -should not be part of the SVM. - -In bank context `load_and_execute_sanitized_transactions` is called from -`simulate_transaction` where a single transaction is executed, and -from `load_execute_and_commit_transactions` which receives a batch of -transactions from its caller. - -Steps of `load_and_execute_sanitized_transactions` - -1. Steps of preparation for execution - - filter executable program accounts and build program accounts map (explain) - - add builtin programs to program accounts map - - replenish program cache using the program accounts map - - Gather all required programs to load from the cache. - - Lock the global program cache and initialize the local program cache. - - Perform loading tasks to load all required programs from the cache, - loading, verifying, and compiling (where necessary) each program. - - A helper module - `program_loader` - provides utilities for loading - programs from on-chain, namely `load_program_with_pubkey`. - - Return the replenished local program cache. - -2. Load accounts (call to `load_accounts` function) - - For each `SVMTransaction` and `TransactionCheckResult`, we: - - Calculate the number of signatures in transaction and its cost. - - Call `load_transaction_accounts` - - The function is interwined with the struct `SVMInstruction` - - Load accounts from accounts DB - - Extract data from accounts - - Verify if we've reached the maximum account data size - - Validate the fee payer and the loaded accounts - - Validate the programs accounts that have been loaded and checks if they are builtin programs. - - Return `struct LoadedTransaction` containing the accounts (pubkey and data), - indices to the executable accounts in `TransactionContext` (or `InstructionContext`), - the transaction rent, and the `struct RentDebit`. - - Generate a `RollbackAccounts` struct which holds fee-subtracted fee payer account and pre-execution nonce state used for rolling back account state on execution failure. - - Returns `TransactionLoadedResult`, containing the `LoadTransaction` we obtained from `loaded_transaction_accounts` - -3. Execute each loaded transactions - 1. Compute the sum of transaction accounts' balances. This sum is - invariant in the transaction execution. - 2. Obtain rent state of each account before the transaction - execution. This is later used in verifying the account state - changes (step #7). - 3. Create a new log_collector. `LogCollector` is defined in - solana-program-runtime crate. - 4. Obtain last blockhash and lamports per signature. This - information is read from blockhash_queue maintained in Bank. The - information is taken in parameters to - `MessageProcessor::process_message`. - 5. Make two local variables that will be used as output parameters - of `MessageProcessor::process_message`. One will contain the - number of executed units (the number of compute unites consumed - in the transaction). Another is a container of `ProgramCacheForTxBatch`. - The latter is initialized with the slot, and - the clone of environments of `programs_loaded_for_tx_batch` - - `programs_loaded_for_tx_batch` contains a reference to all the `ProgramCacheEntry`s - necessary for the transaction. It maintains an `Arc` to the programs in the global - `ProgramCacheEntry` data structure. - 6. Call `MessageProcessor::process_message` to execute the - transaction. `MessageProcessor` is contained in - solana-program-runtime crate. The result of processing message - is either `ProcessedMessageInfo` which is an i64 wrapped in a - struct meaning the change in accounts data length, or a - `TransactionError`, if any of instructions failed to execute - correctly. - 7. Verify transaction accounts' `RentState` changes (`verify_changes` function) - - If the account `RentState` post-transaction processing is rent exempt or uninitialized, the verification will pass, regardless of the pre-transaction `RentState`. - - If the account `RentState` pre-transaction is rent paying: - - It may remain rent paying only if its size has not changed and its balance has not increased. - - If the account `RentState` pre-transaction is rent exempt or uninitialized: - - It cannot become rent paying. - 8. Extract log messages. - 9. Extract inner instructions (`Vec>`). - 10. Extract `ExecutionRecord` components from transaction context. - 11. Check balances of accounts to match the sum of balances before - transaction execution. - 12. Update loaded transaction accounts to new accounts. - 13. Extract changes in accounts data sizes - 14. Extract return data - 15. Return `TransactionExecutionResult` with wrapping the extracted - information in `TransactionExecutionDetails`. - -4. Prepare the results of loading and executing transactions. - - This includes the following steps for each transactions - 1. Dump flattened result to info log for an account whose pubkey is - in the transaction's debug keys. - 2. Collect logs of the transaction execution for each executed - transaction, unless Bank's `transaction_log_collector_config` is - set to `None`. - 3. Finally, increment various statistical counters, and update - timings passed as a mutable reference to - `load_and_execute_transactions` in arguments. The counters are - packed in the struct `LoadAndExecuteTransactionsOutput`. diff --git a/solana/svm/src/access_permissions.rs b/solana/svm/src/access_permissions.rs new file mode 100644 index 0000000..06a1faf --- /dev/null +++ b/solana/svm/src/access_permissions.rs @@ -0,0 +1,281 @@ +use solana_account::{AccountMode, AccountSharedData, DirtyMarkers}; +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 access_is_valid(&mut self, tx: &impl SVMMessage) -> bool { + if !self.was_successful() { + return false; + } + 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; + }; + let logs = Arc::make_mut(self.execution_details.log_messages.get_or_insert_default()); + for (i, (pk, acc)) in accounts { + if !tx.is_writable(i) || writable(acc) || privileged { + continue; + } + 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() && !writable(&payer.1) { + let error = format!("Program log: ({}) Feepayer account is readonly", payer.0); + logs.push(error); + self.execution_details.status = Err(TransactionError::InvalidAccountForFee); + return false; + } + true + } +} + +/// Returns whether an unprivileged transaction may leave the account modified. +/// +/// Mutable modes are writable directly. `Transient` and `Closed` are writable +/// only when the mode dirty marker records a lifecycle transition in the +/// current transaction. +fn writable(account: &AccountSharedData) -> bool { + if account.mutable() { + return true; + } + matches!(account.mode(), AccountMode::Transient | AccountMode::Closed) + && account.markers().contains(DirtyMarkers::MODE) +} + +/// Returns true when every instruction is handled by the MagicRoot authority path. +fn is_privileged(tx: &impl SVMMessage) -> bool { + tx.program_instructions_iter().all(|(id, _)| *id == 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.access_is_valid(&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() { + let mut transitioned = account(AccountMode::Delegated); + transitioned.set_mode(AccountMode::Transient).unwrap(); + let mut closed = account(AccountMode::Ephemeral); + closed.set_mode(AccountMode::Closed).unwrap(); + + // A dirty, writable, immutable operand is rejected — unless the tx is + // privileged or it legally entered a transaction-final mode. Mutable or + // message-read-only operands are always fine. + let cases = [ + // (target, writable, privileged, accepted) + (account(AccountMode::ReadOnly), true, false, false), + (account(AccountMode::Transient), true, false, false), + (transitioned, true, false, true), + (closed, true, false, true), + (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::Transient), false, false), + (account(AccountMode::Delegated), false, true), + ]; + 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"); + } + } + } + + #[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.access_is_valid(&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 index a0db387..5cfea00 100644 --- a/solana/svm/src/account_loader.rs +++ b/solana/svm/src/account_loader.rs @@ -1,36 +1,16 @@ #[cfg(feature = "dev-context-only-utils")] use qualifier_attr::{field_qualifiers, qualifiers}; use { - crate::{ - account_overrides::AccountOverrides, - rent_calculator::{ - RENT_EXEMPT_RENT_EPOCH, check_rent_state_with_account, get_account_rent_state, - }, - rollback_accounts::RollbackAccounts, - transaction_error_metrics::TransactionErrorMetrics, - }, - ahash::{AHashMap, AHashSet}, - solana_account::{ - Account, AccountSharedData, ReadableAccount, WritableAccount, state_traits::StateMut, - }, - solana_clock::Slot, + solana_account::{Account, AccountSharedData, PROGRAM_OWNERS, ReadableAccount}, solana_fee_structure::FeeDetails, solana_instruction::{BorrowedAccountMeta, BorrowedInstruction}, solana_instructions_sysvar::construct_instructions_data, - solana_loader_v3_interface::state::UpgradeableLoaderState, - solana_nonce::state::State as NonceState, - solana_nonce_account::{SystemAccountKind, get_system_account_kind}, solana_program_runtime::execution_budget::{ SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionBudget, }, solana_pubkey::Pubkey, - solana_rent::Rent, - solana_sdk_ids::{ - bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, - sysvar::{self, slot_history}, - }, - solana_svm_callback::{AccountState, TransactionProcessingCallback}, - solana_svm_feature_set::SVMFeatureSet, + 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}, @@ -41,46 +21,29 @@ use { #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] pub(crate) const TRANSACTION_ACCOUNT_BASE_SIZE: usize = 64; -// Valid program owners (loaders). -pub const PROGRAM_OWNERS: &[Pubkey] = &[ - bpf_loader_upgradeable::id(), - bpf_loader::id(), - bpf_loader_deprecated::id(), - loader_v4::id(), -]; - // 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; -// for the load instructions +/// Result of transaction prechecking before account loading. pub type TransactionCheckResult = Result; -type TransactionValidationResult = Result; #[derive(PartialEq, Eq, Debug)] pub(crate) enum TransactionLoadResult { - /// All transaction accounts were loaded successfully + /// All transaction accounts and executable program accounts were resolved. Loaded(LoadedTransaction), - /// Some transaction accounts needed for execution were unable to be loaded - /// but the fee payer and any nonce account needed for fee collection were - /// loaded successfully - FeesOnly(FeesOnlyTransaction), - /// Some transaction accounts needed for fee collection were unable to be - /// loaded + /// 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)) -)] +#[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, } -#[cfg(feature = "dev-context-only-utils")] impl Default for CheckedTransactionDetails { fn default() -> Self { Self { @@ -95,6 +58,7 @@ impl Default for CheckedTransactionDetails { } impl CheckedTransactionDetails { + /// Creates checked transaction details from caller-provided validation. pub fn new( nonce_address: Option, compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits, @@ -108,23 +72,19 @@ impl CheckedTransactionDetails { #[derive(PartialEq, Eq, Debug, Clone)] pub(crate) struct ValidatedTransactionDetails { - pub(crate) rollback_accounts: RollbackAccounts, pub(crate) compute_budget: SVMTransactionExecutionBudget, pub(crate) loaded_accounts_bytes_limit: u32, pub(crate) fee_details: FeeDetails, - pub(crate) loaded_fee_payer_account: LoadedTransactionAccount, } #[cfg(feature = "dev-context-only-utils")] impl Default for ValidatedTransactionDetails { fn default() -> Self { Self { - rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), loaded_accounts_bytes_limit: solana_program_runtime::execution_budget::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), fee_details: FeeDetails::default(), - loaded_fee_payer_account: LoadedTransactionAccount::default(), } } } @@ -136,355 +96,76 @@ pub(crate) struct LoadedTransactionAccount { pub(crate) loaded_size: usize, } -#[derive(PartialEq, Eq, Debug, Clone)] -#[cfg_attr(feature = "dev-context-only-utils", derive(Default))] +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(compute_budget(pub)) + 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 rollback_accounts: RollbackAccounts, pub(crate) compute_budget: SVMTransactionExecutionBudget, + /// Total loaded account data size charged against the transaction limit. pub loaded_accounts_data_size: u32, } -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct FeesOnlyTransaction { - pub load_error: TransactionError, - pub rollback_accounts: RollbackAccounts, - pub fee_details: FeeDetails, - pub loaded_accounts_data_size: u32, -} - -// This is an internal SVM type that tracks account changes throughout a -// transaction batch and obviates the need to load accounts from accounts-db -// more than once. It effectively wraps an `impl TransactionProcessingCallback` -// type, and itself implements `TransactionProcessingCallback`, behaving -// exactly like the implementor of the trait, but also returning up-to-date -// account states mid-batch. -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -pub(crate) struct AccountLoader<'a, CB: TransactionProcessingCallback> { - loaded_accounts: AHashMap, - callbacks: &'a CB, - pub(crate) feature_set: &'a SVMFeatureSet, -} - -impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> { - // create a new AccountLoader for the transaction batch - #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] - pub(crate) fn new_with_loaded_accounts_capacity( - account_overrides: Option<&'a AccountOverrides>, - callbacks: &'a CB, - feature_set: &'a SVMFeatureSet, - capacity: usize, - ) -> AccountLoader<'a, CB> { - let mut loaded_accounts = AHashMap::with_capacity(capacity); - - // SlotHistory may be overridden for simulation. - // No other uses of AccountOverrides are expected. - if let Some(slot_history) = - account_overrides.and_then(|overrides| overrides.get(&slot_history::id())) - { - loaded_accounts.insert(slot_history::id(), (slot_history.clone(), 0)); - } - - Self { - loaded_accounts, - callbacks, - feature_set, - } - } - - // Load an account either from our own store or accounts-db and inspect it on behalf of Bank. - // Inspection is required prior to any modifications to the account. This function is used - // by load_transaction() and validate_transaction_fee_payer() for that purpose. It returns - // a different type than other AccountLoader load functions, which should prevent accidental - // mix and match of them. - pub(crate) fn load_transaction_account( - &mut self, - account_key: &Pubkey, - is_writable: bool, - ) -> Option { - let account = self.load_account(account_key); - - // Inspect prior to collecting rent, since rent collection can modify - // the account. - // - // Note that though rent collection is disabled, we still set the rent - // epoch of rent exempt if the account is rent-exempt but its rent epoch - // is not set to u64::MAX. In other words, an account can be updated - // during rent collection. Therefore, we must inspect prior to collecting rent. - self.callbacks.inspect_account( - account_key, - if let Some(ref account) = account { - AccountState::Alive(account) - } else { - AccountState::Dead - }, - is_writable, - ); - - account.map(|account| LoadedTransactionAccount { - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE.saturating_add(account.data().len()), - account, - }) - } - - // Load an account as above, with no inspection and no LoadedTransactionAccount wrapper. - // This is a general purpose function suitable for usage outside initial transaction loading. - pub(crate) fn load_account(&mut self, account_key: &Pubkey) -> Option { - match self.do_load(account_key) { - // Exists, from AccountLoader. - (Some((account, _last_modification_slot)), false) => Some(account), - // Not allocated, but has an AccountLoader placeholder already. - (None, false) => None, - // Exists in accounts-db. Store it in AccountLoader for future loads. - (Some((account, last_modification_slot)), true) => { - self.loaded_accounts - .insert(*account_key, (account.clone(), last_modification_slot)); - Some(account) - } - // Does not exist and has never been seen. - (None, true) => { - self.loaded_accounts - .insert(*account_key, (AccountSharedData::default(), 0)); - None - } - } - } - - // Internal helper for core loading logic to prevent code duplication. Returns a bool - // indicating whether an accounts-db lookup was performed, which allows wrappers with - // &mut self to insert the account. Wrappers with &self ignore it. - fn do_load(&self, account_key: &Pubkey) -> (Option<(AccountSharedData, Slot)>, bool) { - if let Some((account, slot)) = self.loaded_accounts.get(account_key) { - // If lamports is 0, a previous transaction deallocated this account. - // We return None instead of the account we found so it can be created fresh. - // We *never* remove accounts, or else we would fetch stale state from accounts-db. - let option_account = if account.lamports() == 0 { - None - } else { - Some((account.clone(), *slot)) - }; - - (option_account, false) - } else if let Some((account, slot)) = self.callbacks.get_account_shared_data(account_key) { - (Some((account, slot)), true) - } else { - (None, true) - } - } - - pub(crate) fn update_accounts_for_failed_tx( - &mut self, - rollback_accounts: &RollbackAccounts, - current_slot: Slot, - ) { - for (account_address, account) in rollback_accounts { - self.loaded_accounts - .insert(*account_address, (account.clone(), current_slot)); - } - } - - pub(crate) fn update_accounts_for_successful_tx( - &mut self, - message: &impl SVMMessage, - transaction_accounts: &[KeyedAccountSharedData], - current_slot: Slot, - ) { - for (i, (address, account)) in (0..message.account_keys().len()).zip(transaction_accounts) { - if !message.is_writable(i) { - continue; - } - - // Accounts that are invoked and also not passed as an instruction - // account to a program don't need to be stored because it's assumed - // to be impossible for a committable transaction to modify an - // invoked account if said account isn't passed to some program. - if message.is_invoked(i) && !message.is_instruction_account(i) { - continue; - } - - self.loaded_accounts - .insert(*address, (account.clone(), current_slot)); - } - } -} - -// Program loaders and parsers require a type that impls TransactionProcessingCallback, -// because they are used in both SVM and by Bank. We impl it, with the consequence -// that if we fall back to accounts-db, we cannot store the state for future loads. -// In practice, all accounts we load this way will already be in our accounts store. -impl TransactionProcessingCallback for AccountLoader<'_, CB> { - fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { - self.do_load(pubkey).0 - } -} - -// NOTE this is a required subtrait of TransactionProcessingCallback. -// It may make sense to break out a second subtrait just for the above two functions, -// but this would be a nontrivial breaking change and require careful consideration. -impl solana_svm_callback::InvokeContextCallback - for AccountLoader<'_, CB> -{ -} - -/// Set the rent epoch to u64::MAX if the account is rent exempt. -/// -/// TODO: This function is used to update the rent epoch of an account. Once we -/// completely switched to lthash, where rent_epoch is ignored in accounts -/// hashing, we can remove this function. -pub fn update_rent_exempt_status_for_account(rent: &Rent, account: &mut AccountSharedData) { - // Now that rent fee collection is disabled, we won't collect rent for any - // account. If there are any rent paying accounts, their `rent_epoch` won't - // change either. However, if the account itself is rent-exempted but its - // `rent_epoch` is not u64::MAX, we will set its `rent_epoch` to u64::MAX. - // In such case, the behavior stays the same as before. - if account.rent_epoch() != RENT_EXEMPT_RENT_EPOCH - && rent.is_exempt(account.lamports(), account.data().len()) - { - account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); - } -} - -/// Check whether the payer_account is capable of paying the fee. The -/// side effect is to subtract the fee amount from the payer_account -/// balance of lamports. If the payer_account is not able to pay the -/// fee, the error_metrics is incremented, and a specific error is -/// returned. -pub fn validate_fee_payer( - payer_address: &Pubkey, - payer_account: &mut AccountSharedData, - payer_index: IndexOfAccount, - error_metrics: &mut TransactionErrorMetrics, - rent: &Rent, - fee: u64, -) -> Result<()> { - if payer_account.lamports() == 0 { - error_metrics.account_not_found += 1; - return Err(TransactionError::AccountNotFound); - } - let system_account_kind = get_system_account_kind(payer_account).ok_or_else(|| { - error_metrics.invalid_account_for_fee += 1; - TransactionError::InvalidAccountForFee - })?; - let min_balance = match system_account_kind { - SystemAccountKind::System => 0, - SystemAccountKind::Nonce => { - // Should we ever allow a fees charge to zero a nonce account's - // balance. The state MUST be set to uninitialized in that case - rent.minimum_balance(NonceState::size()) - } - }; - - payer_account - .lamports() - .checked_sub(min_balance) - .and_then(|v| v.checked_sub(fee)) - .ok_or_else(|| { - error_metrics.insufficient_funds += 1; - TransactionError::InsufficientFundsForFee - })?; - - let payer_pre_rent_state = - get_account_rent_state(rent, payer_account.lamports(), payer_account.data().len()); - payer_account - .checked_sub_lamports(fee) - .map_err(|_| TransactionError::InsufficientFundsForFee)?; - - let payer_post_rent_state = - get_account_rent_state(rent, payer_account.lamports(), payer_account.data().len()); - check_rent_state_with_account( - &payer_pre_rent_state, - &payer_post_rent_state, - payer_address, - payer_index, - ) -} - pub(crate) fn load_transaction( - account_loader: &mut AccountLoader, + account_loader: &CB, message: &impl SVMMessage, - validation_result: TransactionValidationResult, - error_metrics: &mut TransactionErrorMetrics, - rent: &Rent, + validation_details: ValidatedTransactionDetails, ) -> TransactionLoadResult { - match validation_result { - Err(e) => TransactionLoadResult::NotLoaded(e), - Ok(tx_details) => { - let mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(tx_details.loaded_accounts_bytes_limit); - - let load_result = load_transaction_accounts( - account_loader, - message, - tx_details.loaded_fee_payer_account, - &mut loaded_transaction_data_size, - error_metrics, - rent, - ); - - match load_result { - Ok(accounts) => TransactionLoadResult::Loaded(LoadedTransaction { - accounts, - fee_details: tx_details.fee_details, - rollback_accounts: tx_details.rollback_accounts, - compute_budget: tx_details.compute_budget, - loaded_accounts_data_size: loaded_transaction_data_size.into(), - }), - Err(err) => TransactionLoadResult::FeesOnly(FeesOnlyTransaction { - load_error: err, - fee_details: tx_details.fee_details, - loaded_accounts_data_size: if account_loader - .feature_set - .define_ltds_fee_only_semantics - { - loaded_transaction_data_size.into() - } else { - tx_details.rollback_accounts.data_size() as u32 - }, - rollback_accounts: tx_details.rollback_accounts, - }), - } - } + 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 LoadedTransactionDataSize { - loaded_accounts_data_size: u32, - requested_loaded_accounts_data_size_limit: u32, +struct LoadedTransactionAccounts { + pub(crate) accounts: Vec, + pub(crate) program_indices: Vec, + pub(crate) loaded_accounts_data_size: u32, } -impl LoadedTransactionDataSize { - fn with_max_size(requested_loaded_accounts_data_size_limit: u32) -> Self { - Self { - loaded_accounts_data_size: 0, - requested_loaded_accounts_data_size_limit, - } - } - +impl LoadedTransactionAccounts { fn increase_calculated_data_size( &mut self, data_size_delta: usize, - error_metrics: &mut TransactionErrorMetrics, + requested_loaded_accounts_data_size_limit: u32, ) -> Result<()> { - // this branch is unreachable in practice (though not by construction), - // since it would imply an account >4gb in size let Ok(data_size_delta) = u32::try_from(data_size_delta) else { - self.loaded_accounts_data_size = u32::MAX; - error_metrics.max_loaded_accounts_data_size_exceeded += 1; return Err(TransactionError::MaxLoadedAccountsDataSizeExceeded); }; - self.loaded_accounts_data_size = self - .loaded_accounts_data_size - .saturating_add(data_size_delta); + self.loaded_accounts_data_size = + self.loaded_accounts_data_size.saturating_add(data_size_delta); - if self.loaded_accounts_data_size > self.requested_loaded_accounts_data_size_limit { - error_metrics.max_loaded_accounts_data_size_exceeded += 1; + if self.loaded_accounts_data_size > requested_loaded_accounts_data_size_limit { Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) } else { Ok(()) @@ -492,144 +173,77 @@ impl LoadedTransactionDataSize { } } -impl From for u32 { - fn from(value: LoadedTransactionDataSize) -> Self { - value - .loaded_accounts_data_size - .min(value.requested_loaded_accounts_data_size_limit) - } -} - fn load_transaction_accounts( - account_loader: &mut AccountLoader, + account_loader: &CB, message: &impl SVMMessage, - loaded_fee_payer_account: LoadedTransactionAccount, - loaded_tx_data_size: &mut LoadedTransactionDataSize, - error_metrics: &mut TransactionErrorMetrics, - rent: &Rent, -) -> Result> { + loaded_accounts_bytes_limit: u32, +) -> Result { let account_keys = message.account_keys(); - let mut loaded_transaction_accounts = Vec::with_capacity(account_keys.len()); - let mut additional_loaded_accounts: AHashSet = AHashSet::new(); + + 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_tx_data_size.increase_calculated_data_size( - message - .num_lookup_tables() - .saturating_mul(ADDRESS_LOOKUP_TABLE_BASE_SIZE), - error_metrics, + 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 = - |account_loader: &mut AccountLoader, key: &Pubkey, loaded_account| -> Result<()> { - let LoadedTransactionAccount { - account, - loaded_size, - } = loaded_account; - - loaded_tx_data_size.increase_calculated_data_size(loaded_size, error_metrics)?; - - // This has been annotated branch-by-branch because collapsing the logic is infeasible. - // Its purpose is to ensure programdata accounts are counted once and *only* once per - // transaction. By checking account_keys, we never double-count a programdata account - // that was explicitly included in the transaction. We also use a hashset to gracefully - // handle cases that LoaderV3 presumably makes impossible, such as self-referential - // program accounts or multiply-referenced programdata accounts, for added safety. - // - // If in the future LoaderV3 programs are migrated to LoaderV4, this entire code block - // can be deleted. - // - // If this is a valid LoaderV3 program... - if bpf_loader_upgradeable::check_id(account.owner()) - && let Ok(UpgradeableLoaderState::Program { - programdata_address, - }) = account.state() - { - // ...its programdata was not already counted and will not later be counted... - if !account_keys.iter().any(|key| programdata_address == *key) - && !additional_loaded_accounts.contains(&programdata_address) - { - // ...and the programdata account exists (if it doesn't, it is *not* a load failure)... - if let Some(programdata_account) = - account_loader.load_account(&programdata_address) - { - // ...count programdata toward this transaction's total size. - loaded_tx_data_size.increase_calculated_data_size( - TRANSACTION_ACCOUNT_BASE_SIZE - .saturating_add(programdata_account.data().len()), - error_metrics, - )?; - additional_loaded_accounts.insert(programdata_address); - } - } - } + let mut collect_loaded_account = |key: &Pubkey, loaded_account| -> Result<()> { + let LoadedTransactionAccount { account, loaded_size } = loaded_account; - loaded_transaction_accounts.push((*key, account)); + loaded_transaction_accounts + .increase_calculated_data_size(loaded_size, loaded_accounts_bytes_limit)?; - Ok(()) - }; + loaded_transaction_accounts.accounts.push((*key, account)); - // Since the fee payer is always the first account, collect it first. - // We can use it directly because it was already loaded during validation. - collect_loaded_account( - account_loader, - message.fee_payer(), - loaded_fee_payer_account, - )?; + Ok(()) + }; - // Attempt to load and collect remaining non-fee payer accounts. - for (account_index, account_key) in account_keys.iter().enumerate().skip(1) { - let loaded_account = - load_transaction_account(account_loader, message, account_key, account_index, rent); - collect_loaded_account(account_loader, account_key, loaded_account)?; + // 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, _) in message.program_instructions_iter() { - let Some(program_account) = account_loader.load_account(program_id) else { - error_metrics.account_not_found += 1; + 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.owner(); - if !native_loader::check_id(owner_id) && !PROGRAM_OWNERS.contains(owner_id) { - error_metrics.invalid_program_for_execution += 1; + 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: &mut AccountLoader, + account_loader: &CB, message: &impl SVMMessage, account_key: &Pubkey, - account_index: usize, - rent: &Rent, ) -> LoadedTransactionAccount { - let is_writable = message.is_writable(account_index); 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. - LoadedTransactionAccount { + return LoadedTransactionAccount { loaded_size: 0, account: construct_instructions_account(message), - } - } else if let Some(mut loaded_account) = - account_loader.load_transaction_account(account_key, is_writable) - { - if is_writable { - update_rent_exempt_status_for_account(rent, &mut loaded_account.account); - } - loaded_account - } else { - let mut default_account = AccountSharedData::default(); - default_account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); - LoadedTransactionAccount { - loaded_size: default_account.data().len(), - account: default_account, - } + }; } + 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 { @@ -657,7 +271,7 @@ fn construct_instructions_account(message: &impl SVMMessage) -> AccountSharedDat } AccountSharedData::from(Account { - data: construct_instructions_data(&decompiled_instructions), + data: construct_instructions_data(&decompiled_instructions).unwrap_or_default(), owner: sysvar::id(), ..Account::default() }) @@ -667,9 +281,16 @@ fn construct_instructions_account(message: &impl SVMMessage) -> AccountSharedDat mod tests { use { super::*, - crate::transaction_account_state_info::TransactionAccountStateInfo, + crate::{ + rent_calculator::RENT_EXEMPT_RENT_EPOCH, + transaction_account_state_info::TransactionAccountStateInfo, + }, + ahash::AHashMap, rand::prelude::*, - solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, + solana_account::{ + Account, AccountSharedData, ReadableAccount, WritableAccount, state_traits::StateMut, + }, + solana_clock::Slot, solana_hash::Hash, solana_instruction::{AccountMeta, Instruction}, solana_keypair::Keypair, @@ -680,7 +301,6 @@ mod tests { v0::{LoadedAddresses, LoadedMessage}, }, solana_native_token::LAMPORTS_PER_SOL, - solana_nonce::{self as nonce, versions::Versions as NonceVersions}, solana_program_runtime::execution_budget::{ DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, }, @@ -691,13 +311,13 @@ mod tests { }, solana_signature::Signature, solana_signer::Signer, - solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, - solana_system_transaction::transfer, + 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, TransactionResult as Result}, + solana_transaction_error::TransactionError, std::{ borrow::Cow, cell::RefCell, @@ -713,32 +333,19 @@ mod tests { .try_init(); } - #[derive(Clone)] + #[derive(Clone, Default)] struct TestCallbacks { accounts_map: HashMap, #[allow(clippy::type_complexity)] inspected_accounts: RefCell, /* is_writable */ bool)>>>, - feature_set: SVMFeatureSet, - } - - impl Default for TestCallbacks { - fn default() -> Self { - Self { - accounts_map: HashMap::default(), - inspected_accounts: RefCell::default(), - feature_set: SVMFeatureSet::all_enabled(), - } - } } 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)) + self.accounts_map.get(pubkey).map(|(account, slot)| (account.clone(), *slot)) } fn inspect_account( @@ -759,26 +366,11 @@ mod tests { } } - impl<'a> From<&'a TestCallbacks> for AccountLoader<'a, TestCallbacks> { - fn from(callbacks: &'a TestCallbacks) -> AccountLoader<'a, TestCallbacks> { - AccountLoader::new_with_loaded_accounts_capacity( - None, - callbacks, - &callbacks.feature_set, - 0, - ) - } - } - fn load_accounts_with_features_and_rent( tx: Transaction, accounts: &[KeyedAccountSharedData], - rent: &Rent, - error_metrics: &mut TransactionErrorMetrics, - feature_set: SVMFeatureSet, ) -> TransactionLoadResult { let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); - let fee_payer_account = accounts[0].1.clone(); let mut accounts_map = HashMap::new(); for (pubkey, account) in accounts { accounts_map.insert(*pubkey, (account.clone(), 1)); @@ -787,20 +379,10 @@ mod tests { accounts_map, ..Default::default() }; - let mut account_loader: AccountLoader = (&callbacks).into(); - account_loader.feature_set = &feature_set; load_transaction( - &mut account_loader, + &callbacks, &sanitized_tx, - Ok(ValidatedTransactionDetails { - loaded_fee_payer_account: LoadedTransactionAccount { - account: fee_payer_account, - ..LoadedTransactionAccount::default() - }, - ..ValidatedTransactionDetails::default() - }), - error_metrics, - rent, + ValidatedTransactionDetails::default(), ) } @@ -811,7 +393,6 @@ mod tests { #[test] fn test_load_accounts_unknown_program_id() { let mut accounts: Vec = Vec::new(); - let mut error_metrics = TransactionErrorMetrics::default(); let keypair = Keypair::new(); let key0 = keypair.pubkey(); @@ -832,29 +413,17 @@ mod tests { instructions, ); - let feature_set = SVMFeatureSet::all_enabled(); - let load_results = load_accounts_with_features_and_rent( - tx, - &accounts, - &Rent::default(), - &mut error_metrics, - feature_set, - ); + let load_results = load_accounts_with_features_and_rent(tx, &accounts); - assert_eq!(error_metrics.account_not_found.0, 1); assert!(matches!( load_results, - TransactionLoadResult::FeesOnly(FeesOnlyTransaction { - load_error: TransactionError::ProgramAccountNotFound, - .. - }), + TransactionLoadResult::NotLoaded(TransactionError::ProgramAccountNotFound), )); } #[test] fn test_load_accounts_no_loaders() { let mut accounts: Vec = Vec::new(); - let mut error_metrics = TransactionErrorMetrics::default(); let keypair = Keypair::new(); let key0 = keypair.pubkey(); @@ -877,22 +446,11 @@ mod tests { instructions, ); - let feature_set = SVMFeatureSet::all_enabled(); - let loaded_accounts = load_accounts_with_features_and_rent( - tx, - &accounts, - &Rent::default(), - &mut error_metrics, - feature_set, - ); + let loaded_accounts = load_accounts_with_features_and_rent(tx, &accounts); match &loaded_accounts { - TransactionLoadResult::FeesOnly(fees_only_tx) => { - assert_eq!(error_metrics.account_not_found.0, 1); - assert_eq!( - fees_only_tx.load_error, - TransactionError::ProgramAccountNotFound, - ); + TransactionLoadResult::NotLoaded(err) => { + assert_eq!(*err, TransactionError::ProgramAccountNotFound); } result => panic!("unexpected result: {result:?}"), } @@ -901,7 +459,6 @@ mod tests { #[test] fn test_load_accounts_bad_owner() { let mut accounts: Vec = Vec::new(); - let mut error_metrics = TransactionErrorMetrics::default(); let keypair = Keypair::new(); let key0 = keypair.pubkey(); @@ -923,29 +480,17 @@ mod tests { instructions, ); - let feature_set = SVMFeatureSet::all_enabled(); - let load_results = load_accounts_with_features_and_rent( - tx, - &accounts, - &Rent::default(), - &mut error_metrics, - feature_set, - ); + let load_results = load_accounts_with_features_and_rent(tx, &accounts); - assert_eq!(error_metrics.invalid_program_for_execution.0, 1); assert!(matches!( load_results, - TransactionLoadResult::FeesOnly(FeesOnlyTransaction { - load_error: TransactionError::InvalidProgramForExecution, - .. - }), + TransactionLoadResult::NotLoaded(TransactionError::InvalidProgramForExecution), )); } #[test] fn test_load_accounts_not_executable() { let mut accounts: Vec = Vec::new(); - let mut error_metrics = TransactionErrorMetrics::default(); let keypair = Keypair::new(); let key0 = keypair.pubkey(); @@ -966,23 +511,16 @@ mod tests { instructions, ); - let feature_set = SVMFeatureSet::all_enabled(); - let load_results = load_accounts_with_features_and_rent( - tx, - &accounts, - &Rent::default(), - &mut error_metrics, - feature_set, - ); + let load_results = load_accounts_with_features_and_rent(tx, &accounts); - assert_eq!(error_metrics.invalid_program_for_execution.0, 0); 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::FeesOnly(fees_only_tx) => panic!("{}", fees_only_tx.load_error), TransactionLoadResult::NotLoaded(e) => panic!("{e}"), } } @@ -990,7 +528,6 @@ mod tests { #[test] fn test_load_accounts_multiple_loaders() { let mut accounts: Vec = Vec::new(); - let mut error_metrics = TransactionErrorMetrics::default(); let keypair = Keypair::new(); let key0 = keypair.pubkey(); @@ -1025,22 +562,16 @@ mod tests { instructions, ); - let feature_set = SVMFeatureSet::all_enabled(); - let loaded_accounts = load_accounts_with_features_and_rent( - tx, - &accounts, - &Rent::default(), - &mut error_metrics, - feature_set, - ); + let loaded_accounts = load_accounts_with_features_and_rent(tx, &accounts); - assert_eq!(error_metrics.account_not_found.0, 0); 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::FeesOnly(fees_only_tx) => panic!("{}", fees_only_tx.load_error), TransactionLoadResult::NotLoaded(e) => panic!("{e}"), } } @@ -1048,11 +579,9 @@ mod tests { fn load_accounts_no_store( accounts: &[KeyedAccountSharedData], tx: Transaction, - account_overrides: Option<&AccountOverrides>, ) -> TransactionLoadResult { let tx = SanitizedTransaction::from_transaction_for_tests(tx); - let mut error_metrics = TransactionErrorMetrics::default(); let mut accounts_map = HashMap::new(); for (pubkey, account) in accounts { accounts_map.insert(*pubkey, (account.clone(), 1)); @@ -1061,20 +590,7 @@ mod tests { accounts_map, ..Default::default() }; - let feature_set = SVMFeatureSet::all_enabled(); - let mut account_loader = AccountLoader::new_with_loaded_accounts_capacity( - account_overrides, - &callbacks, - &feature_set, - 0, - ); - load_transaction( - &mut account_loader, - &tx, - Ok(ValidatedTransactionDetails::default()), - &mut error_metrics, - &Rent::default(), - ) + load_transaction(&callbacks, &tx, ValidatedTransactionDetails::default()) } #[test] @@ -1091,228 +607,34 @@ mod tests { instructions, ); - let load_results = load_accounts_no_store(&[], tx, None); + let load_results = load_accounts_no_store(&[], tx); assert!(matches!( load_results, - TransactionLoadResult::FeesOnly(FeesOnlyTransaction { - load_error: TransactionError::ProgramAccountNotFound, - .. - }), + TransactionLoadResult::NotLoaded(TransactionError::ProgramAccountNotFound), )); } - #[test] - fn test_overrides() { - setup_test_logger(); - let mut account_overrides = AccountOverrides::default(); - let slot_history_id = sysvar::slot_history::id(); - let account = AccountSharedData::new(42, 0, &Pubkey::default()); - account_overrides.set_slot_history(Some(account)); - - let keypair = Keypair::new(); - let account = AccountSharedData::new(1_000_000, 0, &Pubkey::default()); - - let mut program_account = AccountSharedData::default(); - program_account.set_lamports(1); - program_account.set_executable(true); - program_account.set_owner(native_loader::id()); - - let instructions = vec![CompiledInstruction::new(2, &(), vec![0])]; - let tx = Transaction::new_with_compiled_instructions( - &[&keypair], - &[slot_history_id], - Hash::default(), - vec![bpf_loader::id()], - instructions, - ); - - let loaded_accounts = load_accounts_no_store( - &[ - (keypair.pubkey(), account), - (bpf_loader::id(), program_account), - ], - tx, - Some(&account_overrides), - ); - match &loaded_accounts { - TransactionLoadResult::Loaded(loaded_transaction) => { - assert_eq!(loaded_transaction.accounts[0].0, keypair.pubkey()); - assert_eq!(loaded_transaction.accounts[1].0, slot_history_id); - assert_eq!(loaded_transaction.accounts[1].1.lamports(), 42); - } - TransactionLoadResult::FeesOnly(fees_only_tx) => panic!("{}", fees_only_tx.load_error), - TransactionLoadResult::NotLoaded(e) => panic!("{e}"), - } - } - #[test] fn test_increase_calculated_data_size() { - let mut error_metrics = TransactionErrorMetrics::default(); - let data_size: usize = 123; - let requested_data_size_limit = data_size as u32 + 1; - let mut acc = LoadedTransactionDataSize::with_max_size(requested_data_size_limit); - - // OK - loaded data size is under limit - assert!( - acc.increase_calculated_data_size(data_size, &mut error_metrics) - .is_ok() - ); - assert_eq!(data_size as u32, acc.clone().into()); - - // OK - loaded data size meets limit - assert!( - acc.increase_calculated_data_size(1, &mut error_metrics) - .is_ok() - ); - assert_eq!(requested_data_size_limit, acc.clone().into()); + let mut acc = LoadedTransactionAccounts { + accounts: vec![], + program_indices: vec![], + loaded_accounts_data_size: 0, + }; - // fail - loading more data would exceed limit - // data size helper reports the limit only - assert_eq!( - acc.increase_calculated_data_size(1, &mut error_metrics), - Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) - ); - assert_eq!(requested_data_size_limit, acc.into()); + let data_size: usize = 123; + let requested_data_size_limit = data_size as u32; - let mut acc = LoadedTransactionDataSize::with_max_size(requested_data_size_limit); + // 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 - adding a huge number exceeds limit - // data size helper correctly reports we hit the limit + // fail - loading more data that would exceed limit + let another_byte: usize = 1; assert_eq!( - acc.increase_calculated_data_size(u32::MAX as usize + 1, &mut error_metrics), + acc.increase_calculated_data_size(another_byte, requested_data_size_limit), Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) ); - assert_eq!(requested_data_size_limit, acc.into()); - } - - struct ValidateFeePayerTestParameter { - is_nonce: bool, - payer_init_balance: u64, - fee: u64, - expected_result: Result<()>, - payer_post_balance: u64, - } - fn validate_fee_payer_account(test_parameter: ValidateFeePayerTestParameter, rent: &Rent) { - let payer_account_keys = Keypair::new(); - let mut account = if test_parameter.is_nonce { - AccountSharedData::new_data( - test_parameter.payer_init_balance, - &NonceVersions::new(NonceState::Initialized(nonce::state::Data::default())), - &system_program::id(), - ) - .unwrap() - } else { - AccountSharedData::new(test_parameter.payer_init_balance, 0, &system_program::id()) - }; - let result = validate_fee_payer( - &payer_account_keys.pubkey(), - &mut account, - 0, - &mut TransactionErrorMetrics::default(), - rent, - test_parameter.fee, - ); - - assert_eq!(result, test_parameter.expected_result); - assert_eq!(account.lamports(), test_parameter.payer_post_balance); - } - - #[test] - fn test_validate_fee_payer() { - let rent = Rent { - lamports_per_byte: 1, - ..Rent::default() - }; - let min_balance = rent.minimum_balance(NonceState::size()); - let fee = 5_000; - - // If payer account has sufficient balance, expect successful fee deduction, - // regardless feature gate status, or if payer is nonce account. - { - for (is_nonce, min_balance) in [(true, min_balance), (false, 0)] { - validate_fee_payer_account( - ValidateFeePayerTestParameter { - is_nonce, - payer_init_balance: min_balance + fee, - fee, - expected_result: Ok(()), - payer_post_balance: min_balance, - }, - &rent, - ); - } - } - - // If payer account has no balance, expected AccountNotFound Error - // regardless feature gate status, or if payer is nonce account. - { - for is_nonce in [true, false] { - validate_fee_payer_account( - ValidateFeePayerTestParameter { - is_nonce, - payer_init_balance: 0, - fee, - expected_result: Err(TransactionError::AccountNotFound), - payer_post_balance: 0, - }, - &rent, - ); - } - } - - // If payer account has insufficient balance, expect InsufficientFundsForFee error - // regardless feature gate status, or if payer is nonce account. - { - for (is_nonce, min_balance) in [(true, min_balance), (false, 0)] { - validate_fee_payer_account( - ValidateFeePayerTestParameter { - is_nonce, - payer_init_balance: min_balance + fee - 1, - fee, - expected_result: Err(TransactionError::InsufficientFundsForFee), - payer_post_balance: min_balance + fee - 1, - }, - &rent, - ); - } - } - - // normal payer account has balance of u64::MAX, so does fee; since it does not require - // min_balance, expect successful fee deduction, regardless of feature gate status - { - validate_fee_payer_account( - ValidateFeePayerTestParameter { - is_nonce: false, - payer_init_balance: u64::MAX, - fee: u64::MAX, - expected_result: Ok(()), - payer_post_balance: 0, - }, - &rent, - ); - } - } - - #[test] - fn test_validate_nonce_fee_payer_with_checked_arithmetic() { - let rent = Rent { - lamports_per_byte: 1, - ..Rent::default() - }; - - // nonce payer account has balance of u64::MAX, so does fee; due to nonce account - // requires additional min_balance, expect InsufficientFundsForFee error if feature gate is - // enabled - validate_fee_payer_account( - ValidateFeePayerTestParameter { - is_nonce: true, - payer_init_balance: u64::MAX, - fee: u64::MAX, - expected_result: Err(TransactionError::InsufficientFundsForFee), - payer_post_balance: u64::MAX, - }, - &rent, - ); } #[test] @@ -1325,7 +647,7 @@ mod tests { 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()), + data: construct_instructions_data(&message.decompile_instructions()).unwrap(), owner: sysvar::id(), ..Account::default() }); @@ -1348,37 +670,25 @@ mod tests { 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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - - let mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - + 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( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount { - loaded_size: fee_payer_account.data().len(), - account: fee_payer_account.clone(), - }, - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); assert_eq!( - vec![(fee_payer_address, fee_payer_account)], result.unwrap(), + LoadedTransactionAccounts { + accounts: vec![(fee_payer_address, fee_payer_account)], + program_indices: vec![], + loaded_accounts_data_size: TRANSACTION_ACCOUNT_BASE_SIZE as u32, + } ); - assert_eq!(0, loaded_transaction_data_size.loaded_accounts_data_size); } #[test] @@ -1402,16 +712,7 @@ mod tests { .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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - - let mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - + 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()], @@ -1419,20 +720,14 @@ mod tests { ); let result = load_transaction_accounts( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount { - account: fee_payer_account.clone(), - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, - }, - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); assert_eq!( result.unwrap_err(), - TransactionError::ProgramAccountNotFound + TransactionError::InvalidProgramForExecution ); } @@ -1456,28 +751,16 @@ mod tests { 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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - - let mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - + 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( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount::default(), - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); @@ -1503,28 +786,16 @@ mod tests { 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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - - let mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - + 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( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount::default(), - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); assert_eq!( @@ -1555,22 +826,11 @@ mod tests { 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)); + 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_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - - let mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - + 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()], @@ -1578,32 +838,26 @@ mod tests { ); let result = load_transaction_accounts( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount { - account: fee_payer_account.clone(), - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, - }, - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); - let expected_loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + let loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; assert_eq!( - vec![ - (key2.pubkey(), fee_payer_account), - ( - key1.pubkey(), - mock_bank.accounts_map[&key1.pubkey()].0.clone() - ), - ], result.unwrap(), - ); - assert_eq!( - expected_loaded_accounts_data_size, - loaded_transaction_data_size.loaded_accounts_data_size + 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, + } ); } @@ -1627,37 +881,26 @@ mod tests { 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)); + 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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - - let mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - + 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( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount::default(), - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); - assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); } #[test] @@ -1683,38 +926,21 @@ mod tests { 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)); + 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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - - let mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - + 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( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount::default(), - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); assert_eq!( @@ -1745,63 +971,44 @@ mod tests { 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)); + 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)); + 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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - + 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 mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - let result = load_transaction_accounts( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount { - account: fee_payer_account.clone(), - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, - }, - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); - let expected_loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + let loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; assert_eq!( - vec![ - (key2.pubkey(), fee_payer_account), - ( - key1.pubkey(), - mock_bank.accounts_map[&key1.pubkey()].0.clone() - ), - ], result.unwrap(), - ); - assert_eq!( - expected_loaded_accounts_data_size, - loaded_transaction_data_size.loaded_accounts_data_size + 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, + } ); } @@ -1835,66 +1042,47 @@ mod tests { 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)); + 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)); + 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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - + 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 mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - let result = load_transaction_accounts( - &mut account_loader, + &mock_bank, sanitized_transaction.message(), - LoadedTransactionAccount { - account: fee_payer_account.clone(), - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, - }, - &mut loaded_transaction_data_size, - &mut error_metrics, - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ); - let expected_loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + 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!( - vec![ - (key2.pubkey(), fee_payer_account), - ( - key1.pubkey(), - mock_bank.accounts_map[&key1.pubkey()].0.clone() - ), - (key3.pubkey(), account_data), - ], result.unwrap(), - ); - assert_eq!( - expected_loaded_accounts_data_size, - loaded_transaction_data_size.loaded_accounts_data_size + 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, + } ); } @@ -1909,28 +1097,25 @@ mod tests { 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)); + 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 account_loader = (&bank).into(); - - let tx = transfer(&mint_keypair, &recipient, LAMPORTS_PER_SOL, last_block_hash); + 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 mut error_metrics = TransactionErrorMetrics::default(); - let load_result = load_transaction( - &mut account_loader, - &sanitized_tx, - Ok(ValidatedTransactionDetails::default()), - &mut error_metrics, - &Rent::default(), - ); + let load_result = + load_transaction(&bank, &sanitized_tx, ValidatedTransactionDetails::default()); let TransactionLoadResult::Loaded(loaded_transaction) = load_result else { panic!("transaction loading failed"); @@ -1986,50 +1171,30 @@ mod tests { 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)); + 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)); + 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 mut account_loader = (&mock_bank).into(); - - let mut error_metrics = TransactionErrorMetrics::default(); - + 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 validation_result = Ok(ValidatedTransactionDetails { - loaded_fee_payer_account: LoadedTransactionAccount { - account: fee_payer_account, - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, - }, - ..ValidatedTransactionDetails::default() - }); - let load_result = load_transaction( - &mut account_loader, + &mock_bank, &sanitized_transaction, - validation_result, - &mut error_metrics, - &Rent::default(), + ValidatedTransactionDetails::default(), ); - let loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + 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); @@ -2051,8 +1216,8 @@ mod tests { ), (key3.pubkey(), account_data), ], + program_indices: vec![1, 1], fee_details: FeeDetails::default(), - rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), loaded_accounts_data_size, } @@ -2062,9 +1227,6 @@ mod tests { #[test] fn test_load_accounts_error() { let mock_bank = TestCallbacks::default(); - let mut account_loader = (&mock_bank).into(); - let rent = Rent::default(); - let message = Message { account_keys: vec![Pubkey::new_from_array([0; 32])], header: MessageHeader::default(), @@ -2083,261 +1245,18 @@ mod tests { false, ); - let validation_result = Ok(ValidatedTransactionDetails::default()); - let load_result = load_transaction( - &mut account_loader, - &sanitized_transaction, - validation_result, - &mut TransactionErrorMetrics::default(), - &rent, - ); - - assert!(matches!( - load_result, - TransactionLoadResult::FeesOnly(FeesOnlyTransaction { - load_error: TransactionError::ProgramAccountNotFound, - .. - }), - )); - - let validation_result = Err(TransactionError::InvalidWritableAccount); - let load_result = load_transaction( - &mut account_loader, + &mock_bank, &sanitized_transaction, - validation_result, - &mut TransactionErrorMetrics::default(), - &rent, + ValidatedTransactionDetails::default(), ); assert!(matches!( load_result, - TransactionLoadResult::NotLoaded(TransactionError::InvalidWritableAccount), + TransactionLoadResult::NotLoaded(TransactionError::ProgramAccountNotFound), )); } - #[test] - fn test_update_rent_exempt_status_for_account() { - let rent = Rent::default(); - - let min_exempt_balance = rent.minimum_balance(0); - let mut account = AccountSharedData::from(Account { - lamports: min_exempt_balance, - ..Account::default() - }); - - update_rent_exempt_status_for_account(&rent, &mut account); - assert_eq!(account.rent_epoch(), RENT_EXEMPT_RENT_EPOCH); - } - - #[test] - fn test_update_rent_exempt_status_for_rent_paying_account() { - let rent = Rent::default(); - - let mut account = AccountSharedData::from(Account { - lamports: 1, - ..Account::default() - }); - - update_rent_exempt_status_for_account(&rent, &mut account); - assert_eq!(account.rent_epoch(), 0); - assert_eq!(account.lamports(), 1); - } - - // Ensure `TransactionProcessingCallback::inspect_account()` is called when - // loading accounts for transaction processing. - #[test] - fn test_inspect_account_non_fee_payer() { - let mut mock_bank = TestCallbacks::default(); - - let address0 = Pubkey::new_unique(); // <-- fee payer - let address1 = Pubkey::new_unique(); // <-- initially alive - let address2 = Pubkey::new_unique(); // <-- initially dead - let address3 = Pubkey::new_unique(); // <-- program - - let mut account0 = AccountSharedData::default(); - account0.set_lamports(1_000_000_000); - mock_bank - .accounts_map - .insert(address0, (account0.clone(), 1)); - - let mut account1 = AccountSharedData::default(); - account1.set_lamports(2_000_000_000); - mock_bank - .accounts_map - .insert(address1, (account1.clone(), 1)); - - // account2 *not* added to the bank's accounts_map - - let mut account3 = AccountSharedData::default(); - account3.set_lamports(4_000_000_000); - account3.set_executable(true); - account3.set_owner(bpf_loader::id()); - mock_bank - .accounts_map - .insert(address3, (account3.clone(), 0)); - let mut account_loader = (&mock_bank).into(); - - let message = Message { - account_keys: vec![address0, address1, address2, address3], - header: MessageHeader::default(), - instructions: vec![ - CompiledInstruction { - program_id_index: 3, - accounts: vec![0], - data: vec![], - }, - CompiledInstruction { - program_id_index: 3, - accounts: vec![1, 2], - data: vec![], - }, - CompiledInstruction { - program_id_index: 3, - accounts: vec![1], - data: vec![], - }, - ], - recent_blockhash: Hash::new_unique(), - }; - let sanitized_message = new_unchecked_sanitized_message(message); - let sanitized_transaction = SanitizedTransaction::new_for_tests( - sanitized_message, - vec![Signature::new_unique()], - false, - ); - let validation_result = Ok(ValidatedTransactionDetails { - loaded_fee_payer_account: LoadedTransactionAccount { - account: account0.clone(), - ..LoadedTransactionAccount::default() - }, - ..ValidatedTransactionDetails::default() - }); - let _load_results = load_transaction( - &mut account_loader, - &sanitized_transaction, - validation_result, - &mut TransactionErrorMetrics::default(), - &Rent::default(), - ); - - // ensure the loaded accounts are inspected - let mut actual_inspected_accounts: Vec<_> = mock_bank - .inspected_accounts - .borrow() - .iter() - .map(|(k, v)| (*k, v.clone())) - .collect(); - actual_inspected_accounts.sort_unstable_by_key(|a| a.0); - - let mut expected_inspected_accounts = vec![ - // *not* key0, since it is loaded during fee payer validation - (address1, vec![(Some(account1), true)]), - (address2, vec![(None, true)]), - (address3, vec![(Some(account3), false)]), - ]; - expected_inspected_accounts.sort_unstable_by_key(|a| a.0); - - assert_eq!(actual_inspected_accounts, expected_inspected_accounts,); - } - - #[test] - fn test_account_loader_wrappers() { - let fee_payer = Pubkey::new_unique(); - let mut fee_payer_account = AccountSharedData::default(); - fee_payer_account.set_rent_epoch(u64::MAX); - fee_payer_account.set_lamports(5000); - - let mut mock_bank = TestCallbacks::default(); - mock_bank - .accounts_map - .insert(fee_payer, (fee_payer_account.clone(), 1)); - - // test without stored account - let mut account_loader: AccountLoader<_> = (&mock_bank).into(); - assert_eq!( - account_loader - .load_transaction_account(&fee_payer, false) - .unwrap() - .account, - fee_payer_account - ); - - let mut account_loader: AccountLoader<_> = (&mock_bank).into(); - assert_eq!( - account_loader - .load_transaction_account(&fee_payer, true) - .unwrap() - .account, - fee_payer_account - ); - - let mut account_loader: AccountLoader<_> = (&mock_bank).into(); - assert_eq!( - account_loader.load_account(&fee_payer).unwrap(), - fee_payer_account - ); - - let account_loader: AccountLoader<_> = (&mock_bank).into(); - assert_eq!( - account_loader - .get_account_shared_data(&fee_payer) - .unwrap() - .0, - fee_payer_account - ); - - // test with stored account - let mut account_loader: AccountLoader<_> = (&mock_bank).into(); - account_loader.load_account(&fee_payer).unwrap(); - - assert_eq!( - account_loader - .load_transaction_account(&fee_payer, false) - .unwrap() - .account, - fee_payer_account - ); - assert_eq!( - account_loader - .load_transaction_account(&fee_payer, true) - .unwrap() - .account, - fee_payer_account - ); - assert_eq!( - account_loader.load_account(&fee_payer).unwrap(), - fee_payer_account - ); - assert_eq!( - account_loader - .get_account_shared_data(&fee_payer) - .unwrap() - .0, - fee_payer_account - ); - - // drop the account and ensure all deliver the updated state - fee_payer_account.set_lamports(0); - account_loader.update_accounts_for_failed_tx( - &RollbackAccounts::FeePayerOnly { - fee_payer: (fee_payer, fee_payer_account), - }, - 0, - ); - - assert_eq!( - account_loader.load_transaction_account(&fee_payer, false), - None - ); - assert_eq!( - account_loader.load_transaction_account(&fee_payer, true), - None - ); - assert_eq!(account_loader.load_account(&fee_payer), None); - assert_eq!(account_loader.get_account_shared_data(&fee_payer), None); - } - // 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] @@ -2354,9 +1273,7 @@ mod tests { rng.random(), u64::MAX, ); - mock_bank - .accounts_map - .insert(Pubkey::new_unique(), (account, 1)); + mock_bank.accounts_map.insert(Pubkey::new_unique(), (account, 1)); } // fee-payers @@ -2418,9 +1335,7 @@ mod tests { if has_programdata || rng.random() { account - .set_state(&UpgradeableLoaderState::Program { - programdata_address, - }) + .set_state(&UpgradeableLoaderState::Program { programdata_address }) .unwrap(); } } @@ -2432,14 +1347,12 @@ mod tests { let mut all_accounts = mock_bank.accounts_map.keys().copied().collect::>(); - // append some to-be-created accounts - // this is to test that their size is 0 rather than 64 + // 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()); } - let mut account_loader = (&mock_bank).into(); - // 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 @@ -2476,116 +1389,33 @@ mod tests { fee_payers.shuffle(&mut rng); let fee_payer = fee_payers[0]; - let fee_payer_account = mock_bank.accounts_map.get(&fee_payer).cloned().unwrap().0; - let transaction = SanitizedTransaction::from_transaction_for_tests( Transaction::new_with_payer(&instructions, Some(&fee_payer)), ); let mut expected_size = 0; - let mut counted_programdatas = transaction - .account_keys() - .iter() - .copied() - .collect::>(); - for pubkey in transaction.account_keys().iter() { - if let Some((account, _last_modification_slot)) = mock_bank.accounts_map.get(pubkey) - { - expected_size += TRANSACTION_ACCOUNT_BASE_SIZE + account.data().len(); - }; - - if let Some((programdata_address, programdata_size)) = - programdata_tracker.get(pubkey) - && counted_programdatas.get(programdata_address).is_none() - { - expected_size += TRANSACTION_ACCOUNT_BASE_SIZE + programdata_size; - counted_programdatas.insert(*programdata_address); - } + 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 mut loaded_transaction_data_size = - LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); - - load_transaction_accounts( - &mut account_loader, + let loaded_transaction_accounts = load_transaction_accounts( + &mock_bank, &transaction, - LoadedTransactionAccount { - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE + fee_payer_account.data().len(), - account: fee_payer_account, - }, - &mut loaded_transaction_data_size, - &mut TransactionErrorMetrics::default(), - &Rent::default(), + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), ) .unwrap(); assert_eq!( - loaded_transaction_data_size.loaded_accounts_data_size, + loaded_transaction_accounts.loaded_accounts_data_size, expected_size as u32, ); } } - - #[test] - fn test_loader_aliasing() { - let mut mock_bank = TestCallbacks::default(); - - let hit_address = Pubkey::new_unique(); - let miss_address = Pubkey::new_unique(); - - let expected_hit_account = AccountSharedData::default(); - mock_bank - .accounts_map - .insert(hit_address, (expected_hit_account.clone(), 1)); - - let mut account_loader: AccountLoader<_> = (&mock_bank).into(); - - // load hits accounts-db, same account is stored - account_loader.load_account(&hit_address); - let actual_hit_account = account_loader.loaded_accounts.get(&hit_address); - - assert_eq!(actual_hit_account.as_ref().unwrap().0, expected_hit_account); - assert_eq!(actual_hit_account.as_ref().unwrap().1, 1); - assert!(Arc::ptr_eq( - &actual_hit_account.unwrap().0.data_clone(), - &expected_hit_account.data_clone() - )); - - // reload doesn't affect this - account_loader.load_account(&hit_address); - let actual_hit_account = account_loader.loaded_accounts.get(&hit_address); - - assert_eq!(actual_hit_account.as_ref().unwrap().0, expected_hit_account); - assert_eq!(actual_hit_account.as_ref().unwrap().1, 1); - assert!(Arc::ptr_eq( - &actual_hit_account.unwrap().0.data_clone(), - &expected_hit_account.data_clone() - )); - - // load misses accounts-db, placeholder is inserted - account_loader.load_account(&miss_address); - let expected_miss_account = account_loader - .loaded_accounts - .get(&miss_address) - .unwrap() - .clone(); - - assert!(!Arc::ptr_eq( - &expected_miss_account.0.data_clone(), - &expected_hit_account.data_clone() - )); - - // reload keeps the same placeholder - account_loader.load_account(&miss_address); - let actual_miss_account = account_loader.loaded_accounts.get(&miss_address); - - assert_eq!(actual_miss_account, Some(&expected_miss_account)); - assert!(Arc::ptr_eq( - &actual_miss_account.unwrap().0.data_clone(), - &expected_miss_account.0.data_clone() - )); - } } diff --git a/solana/svm/src/account_overrides.rs b/solana/svm/src/account_overrides.rs deleted file mode 100644 index f548c1c..0000000 --- a/solana/svm/src/account_overrides.rs +++ /dev/null @@ -1,65 +0,0 @@ -use { - solana_account::AccountSharedData, solana_pubkey::Pubkey, solana_sdk_ids::sysvar, - std::collections::HashMap, -}; - -/// Encapsulates overridden accounts, typically used for transaction -/// simulations. Account overrides are currently not used when loading the -/// durable nonce account or when constructing the instructions sysvar account. -#[derive(Default)] -pub struct AccountOverrides { - accounts: HashMap, -} - -impl AccountOverrides { - /// Insert or remove an account with a given pubkey to/from the list of overrides. - fn set_account(&mut self, pubkey: &Pubkey, account: Option) { - match account { - Some(account) => self.accounts.insert(*pubkey, account), - None => self.accounts.remove(pubkey), - }; - } - - /// Sets in the slot history - /// - /// Note: no checks are performed on the correctness of the contained data - pub fn set_slot_history(&mut self, slot_history: Option) { - self.set_account(&sysvar::slot_history::id(), slot_history); - } - - /// Gets the account if it's found in the list of overrides - pub(crate) fn get(&self, pubkey: &Pubkey) -> Option<&AccountSharedData> { - self.accounts.get(pubkey) - } -} - -#[cfg(test)] -mod test { - use { - crate::account_overrides::AccountOverrides, solana_account::AccountSharedData, - solana_pubkey::Pubkey, solana_sdk_ids::sysvar, - }; - - #[test] - fn test_set_account() { - let mut accounts = AccountOverrides::default(); - let data = AccountSharedData::default(); - let key = Pubkey::new_unique(); - accounts.set_account(&key, Some(data.clone())); - assert_eq!(accounts.get(&key), Some(&data)); - - accounts.set_account(&key, None); - assert!(accounts.get(&key).is_none()); - } - - #[test] - fn test_slot_history() { - let mut accounts = AccountOverrides::default(); - let data = AccountSharedData::default(); - - assert_eq!(accounts.get(&sysvar::slot_history::id()), None); - accounts.set_slot_history(Some(data.clone())); - - assert_eq!(accounts.get(&sysvar::slot_history::id()), Some(&data)); - } -} diff --git a/solana/svm/src/lib.rs b/solana/svm/src/lib.rs index d837383..e54b7a8 100644 --- a/solana/svm/src/lib.rs +++ b/solana/svm/src/lib.rs @@ -1,22 +1,16 @@ -#![cfg(feature = "agave-unstable-api")] #![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 account_overrides; pub mod message_processor; -pub mod nonce_info; pub mod program_loader; pub mod rent_calculator; -pub mod rollback_accounts; pub mod transaction_account_state_info; pub mod transaction_balances; -pub mod transaction_commit_result; -pub mod transaction_error_metrics; pub mod transaction_execution_result; pub mod transaction_processing_callback; pub mod transaction_processing_result; pub mod transaction_processor; - -#[cfg_attr(feature = "frozen-abi", macro_use)] -#[cfg(feature = "frozen-abi")] -extern crate solana_frozen_abi_macro; diff --git a/solana/svm/src/message_processor.rs b/solana/svm/src/message_processor.rs index bef5d92..301963e 100644 --- a/solana/svm/src/message_processor.rs +++ b/solana/svm/src/message_processor.rs @@ -1,62 +1,48 @@ use { solana_program_runtime::invoke_context::InvokeContext, - solana_svm_measure::measure_us, - solana_svm_timings::{ExecuteDetailsTimings, ExecuteTimings}, - solana_svm_transaction::svm_message::SVMMessage, + solana_svm_transaction::svm_message::SVMMessage, solana_transaction_context::IndexOfAccount, solana_transaction_error::TransactionError, }; -/// Process a message. -/// This method calls each instruction in the message over the set of loaded accounts. -/// For each instruction it calls the program entrypoint method and verifies that the result of -/// the call does not violate the bank's accounting rules. -/// The accounts are committed back to the bank only if every instruction succeeds. +/// 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>, - execute_timings: &mut ExecuteTimings, accumulated_consumed_units: &mut u64, ) -> Result<(), TransactionError> { - invoke_context - .prepare_top_level_instructions(message) - .map_err(|(ix_idx, err)| TransactionError::InstructionError(ix_idx, err))?; - - for (top_level_instruction_index, (program_id, instruction)) in - message.program_instructions_iter().enumerate() + 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, process_instruction_us) = measure_us!({ - 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, execute_timings) - } - }); + 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); - // The per_program_timings are only used for metrics reporting at the trace - // level, so they should only be accumulated when trace level is enabled. - if log::log_enabled!(log::Level::Trace) { - execute_timings.details.accumulate_program( - program_id, - process_instruction_us, - compute_units_consumed, - result.is_err(), - ); - } - invoke_context.timings = { - execute_timings.details.accumulate(&invoke_context.timings); - ExecuteDetailsTimings::default() - }; - execute_timings - .execute_accessories - .process_instructions - .total_us += process_instruction_us; result.map_err(|err| { TransactionError::InstructionError(top_level_instruction_index as u8, err) @@ -69,44 +55,33 @@ pub(crate) fn process_message<'ix_data>( mod tests { use { super::*, - openssl::{ - ec::{EcGroup, EcKey}, - nid::Nid, - }, solana_account::{ Account, AccountSharedData, DUMMY_INHERITABLE_ACCOUNT_FIELDS, ReadableAccount, - WritableAccount, }, solana_ed25519_program::new_ed25519_instruction_with_signature, solana_hash::Hash, solana_instruction::{AccountMeta, Instruction, error::InstructionError}, - solana_keypair::{Address, Keypair}, + 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::{ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, - program_cache_entry::ProgramCacheEntry, + loaded_programs::{ + ProgramCacheEntry, ProgramCacheForTxBatch, ProgramRuntimeEnvironments, + }, + solana_sbpf::program::BuiltinFunctionDefinition, sysvar_cache::SysvarCache, }, solana_pubkey::Pubkey, solana_rent::Rent, - solana_sbpf::program::BuiltinFunctionDefinition, - solana_sdk_ids::{ed25519_program, native_loader, secp256k1_program, system_program}, - solana_secp256k1_program::{ - eth_address_from_pubkey, new_secp256k1_instruction_with_signature, - }, - solana_secp256r1_program::{new_secp256r1_instruction_with_signature, sign_message}, + 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::{HashMap, HashSet}, - sync::Arc, - }, + std::{collections::HashSet, sync::Arc}, }; struct MockCallback {} @@ -127,6 +102,13 @@ mod tests { 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)] @@ -184,17 +166,17 @@ mod tests { ]; 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(0, 0, MockBuiltin::register)), + 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() - }) + .map(|index| *transaction_context.get_key_of_account_at_index(index).unwrap()) .collect::>(); let account_metas = vec![ AccountMeta::new(writable_pubkey, true), @@ -217,11 +199,10 @@ mod tests { )); let sysvar_cache = SysvarCache::default(); let feature_set = SVMFeatureSet::all_enabled(); - let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); let environment_config = EnvironmentConfig::new( Hash::default(), 0, - false, &MockCallback {}, &feature_set, &program_runtime_environments, @@ -235,27 +216,14 @@ mod tests { SVMTransactionExecutionBudget::default(), SVMTransactionExecutionCost::default(), ); - let result = process_message( - &message, - &mut invoke_context, - &mut ExecuteTimings::default(), - &mut 0, - ); + 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(), + transaction_context.accounts().try_borrow(0).unwrap().lamports(), 100 ); assert_eq!( - transaction_context - .accounts() - .try_borrow(1) - .unwrap() - .lamports(), + transaction_context.accounts().try_borrow(1).unwrap().lamports(), 0 ); @@ -273,11 +241,10 @@ mod tests { ), ]), )); - let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); let environment_config = EnvironmentConfig::new( Hash::default(), 0, - false, &MockCallback {}, &feature_set, &program_runtime_environments, @@ -293,12 +260,7 @@ mod tests { SVMTransactionExecutionBudget::default(), SVMTransactionExecutionCost::default(), ); - let result = process_message( - &message, - &mut invoke_context, - &mut ExecuteTimings::default(), - &mut 0, - ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); assert_eq!( result, Err(TransactionError::InstructionError( @@ -321,11 +283,10 @@ mod tests { ), ]), )); - let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); let environment_config = EnvironmentConfig::new( Hash::default(), 0, - false, &MockCallback {}, &feature_set, &program_runtime_environments, @@ -340,12 +301,7 @@ mod tests { SVMTransactionExecutionBudget::default(), SVMTransactionExecutionCost::default(), ); - let result = process_message( - &message, - &mut invoke_context, - &mut ExecuteTimings::default(), - &mut 0, - ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); assert_eq!( result, Err(TransactionError::InstructionError( @@ -380,12 +336,10 @@ mod tests { 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(); + 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); } @@ -426,10 +380,14 @@ mod tests { ]; 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(0, 0, MockBuiltin::register)), + Arc::new(ProgramCacheEntry::new_builtin(( + MockBuiltin::vm, + MockBuiltin::codegen, + ))), ); let account_metas = vec![ AccountMeta::new( @@ -457,11 +415,10 @@ mod tests { )); let sysvar_cache = SysvarCache::default(); let feature_set = SVMFeatureSet::all_enabled(); - let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); let environment_config = EnvironmentConfig::new( Hash::default(), 0, - false, &MockCallback {}, &feature_set, &program_runtime_environments, @@ -475,12 +432,7 @@ mod tests { SVMTransactionExecutionBudget::default(), SVMTransactionExecutionCost::default(), ); - let result = process_message( - &message, - &mut invoke_context, - &mut ExecuteTimings::default(), - &mut 0, - ); + let result = process_message(&message, &program_indices, &mut invoke_context, &mut 0); assert_eq!( result, Err(TransactionError::InstructionError( @@ -498,11 +450,10 @@ mod tests { )], Some(transaction_context.get_key_of_account_at_index(0).unwrap()), )); - let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); let environment_config = EnvironmentConfig::new( Hash::default(), 0, - false, &MockCallback {}, &feature_set, &program_runtime_environments, @@ -518,31 +469,22 @@ mod tests { SVMTransactionExecutionBudget::default(), SVMTransactionExecutionCost::default(), ); - let result = process_message( - &message, - &mut invoke_context, - &mut ExecuteTimings::default(), - &mut 0, - ); + 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, - }, + &MockSystemInstruction::DoWork { lamports: 10, data: 42 }, account_metas, )], Some(transaction_context.get_key_of_account_at_index(0).unwrap()), )); - let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); let environment_config = EnvironmentConfig::new( Hash::default(), 0, - false, &MockCallback {}, &feature_set, &program_runtime_environments, @@ -557,27 +499,14 @@ mod tests { SVMTransactionExecutionBudget::default(), SVMTransactionExecutionCost::default(), ); - let result = process_message( - &message, - &mut invoke_context, - &mut ExecuteTimings::default(), - &mut 0, - ); + 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(), + transaction_context.accounts().try_borrow(0).unwrap().lamports(), 80 ); assert_eq!( - transaction_context - .accounts() - .try_borrow(1) - .unwrap() - .lamports(), + transaction_context.accounts().try_borrow(1).unwrap().lamports(), 20 ); assert_eq!( @@ -586,45 +515,6 @@ mod tests { ); } - fn secp256k1_instruction_for_test() -> Instruction { - let message = b"hello"; - let bytes: [u8; 32] = rand::random(); - let secret_key = libsecp256k1::SecretKey::parse(&bytes).unwrap(); - let pubkey = libsecp256k1::PublicKey::from_secret_key(&secret_key); - let eth_address = eth_address_from_pubkey(&pubkey.serialize()[1..].try_into().unwrap()); - let (signature, recovery_id) = - solana_secp256k1_program::sign_message(&secret_key.serialize(), &message[..]).unwrap(); - new_secp256k1_instruction_with_signature( - &message[..], - &signature, - recovery_id, - ð_address, - ) - } - - 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) - } - - fn secp256r1_instruction_for_test() -> Instruction { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let secret_key = EcKey::generate(&group).unwrap(); - let signature = sign_message(b"hello", &secret_key.private_key_to_der().unwrap()).unwrap(); - let mut ctx = openssl::bn::BigNumContext::new().unwrap(); - let pubkey = secret_key - .public_key() - .to_bytes( - &group, - openssl::ec::PointConversionForm::COMPRESSED, - &mut ctx, - ) - .unwrap(); - new_secp256r1_instruction_with_signature(b"hello", &signature, &pubkey.try_into().unwrap()) - } - #[test] fn test_precompile() { let mock_program_id = Pubkey::new_unique(); @@ -632,57 +522,50 @@ mod tests { Err(InstructionError::Custom(0xbabb1e)) }); - let mut secp256k1_account = AccountSharedData::new(1, 0, &native_loader::id()); - secp256k1_account.set_executable(true); - let mut ed25519_account = AccountSharedData::new(1, 0, &native_loader::id()); - ed25519_account.set_executable(true); - let mut secp256r1_account = AccountSharedData::new(1, 0, &native_loader::id()); - secp256r1_account.set_executable(true); - let mut mock_program_account = AccountSharedData::new(1, 0, &native_loader::id()); - mock_program_account.set_executable(true); - - let fee_payer = Pubkey::new_unique(); - let accounts_map: HashMap = HashMap::from([ + let payer = Pubkey::new_unique(); + let accounts = vec![ + (payer, AccountSharedData::new(1, 0, &native_loader::id())), ( - fee_payer, - AccountSharedData::new(1, 0, &system_program::id()), + ed25519_program::id(), + create_loadable_account_for_test("ed25519_program"), + ), + ( + mock_program_id, + create_loadable_account_for_test("mock_program"), ), - (secp256k1_program::id(), secp256k1_account), - (ed25519_program::id(), ed25519_account), - (solana_secp256r1_program::id(), secp256r1_account), - (mock_program_id, mock_program_account), - ]); + ]; + let mut transaction_context = TransactionContext::new(accounts, Rent::default(), 1, 2, 2); - let message = new_sanitized_message(Message::new( - &[ - secp256k1_instruction_for_test(), - ed25519_instruction_for_test(), - secp256r1_instruction_for_test(), - Instruction::new_with_bytes(mock_program_id, &[], vec![]), - ], - Some(&fee_payer), + 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 accounts = message - .account_keys() - .iter() - .map(|key| (*key, accounts_map.get(key).unwrap().clone())) - .collect(); - let mut transaction_context = TransactionContext::new(accounts, Rent::default(), 1, 4, 4); - 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(0, 0, MockBuiltin::register)), + 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 == &secp256k1_program::id() - || program_id == &ed25519_program::id() - || program_id == &solana_secp256r1_program::id() + program_id == &ed25519_program::id() } fn process_precompile( @@ -698,12 +581,12 @@ mod tests { } } } + let feature_set = SVMFeatureSet::all_enabled(); - let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let program_runtime_environments = ProgramRuntimeEnvironments::default(); let environment_config = EnvironmentConfig::new( Hash::default(), 0, - false, &MockCallback {}, &feature_set, &program_runtime_environments, @@ -717,23 +600,15 @@ mod tests { SVMTransactionExecutionBudget::default(), SVMTransactionExecutionCost::default(), ); - let result = process_message( - &message, - &mut invoke_context, - &mut ExecuteTimings::default(), - &mut 0, - ); + let result = process_message(&message, &[1, 2], &mut invoke_context, &mut 0); assert_eq!( result, Err(TransactionError::InstructionError( - 3, + 1, InstructionError::Custom(0xbabb1e) )) ); - assert_eq!( - transaction_context.number_of_called_instructions_in_trace(), - 4 - ); + assert_eq!(transaction_context.get_instruction_trace_length(), 2); } } diff --git a/solana/svm/src/nonce_info.rs b/solana/svm/src/nonce_info.rs deleted file mode 100644 index 3f0a788..0000000 --- a/solana/svm/src/nonce_info.rs +++ /dev/null @@ -1,151 +0,0 @@ -#[cfg(feature = "dev-context-only-utils")] -use { - qualifier_attr::qualifiers, - solana_account::state_traits::StateMut, - solana_nonce::{ - state::{DurableNonce, State as NonceState}, - versions::Versions as NonceVersions, - }, - thiserror::Error, -}; -use {solana_account::AccountSharedData, solana_pubkey::Pubkey}; - -/// Holds limited nonce info available during transaction checks -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct NonceInfo { - pub address: Pubkey, - pub account: AccountSharedData, -} - -#[derive(Error, Debug, PartialEq)] -#[cfg(feature = "dev-context-only-utils")] -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -enum AdvanceNonceError { - #[error("Invalid account")] - Invalid, - #[error("Uninitialized nonce")] - Uninitialized, -} - -impl NonceInfo { - pub fn new(address: Pubkey, account: AccountSharedData) -> Self { - Self { address, account } - } - - // Advance the stored blockhash to prevent fee theft by someone - // replaying nonce transactions that have failed with an - // `InstructionError`. - #[cfg(feature = "dev-context-only-utils")] - #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] - fn try_advance_nonce( - &mut self, - durable_nonce: DurableNonce, - lamports_per_signature: u64, - ) -> Result<(), AdvanceNonceError> { - let nonce_versions = StateMut::::state(&self.account) - .map_err(|_| AdvanceNonceError::Invalid)?; - if let NonceState::Initialized(data) = nonce_versions.state() { - let nonce_state = - NonceState::new_initialized(&data.authority, durable_nonce, lamports_per_signature); - let nonce_versions = NonceVersions::new(nonce_state); - self.account.set_state(&nonce_versions).unwrap(); - Ok(()) - } else { - Err(AdvanceNonceError::Uninitialized) - } - } - - pub fn address(&self) -> &Pubkey { - &self.address - } - - pub fn account(&self) -> &AccountSharedData { - &self.account - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - solana_hash::Hash, - solana_nonce::{ - state::{Data as NonceData, DurableNonce, State as NonceState}, - versions::Versions as NonceVersions, - }, - solana_sdk_ids::system_program, - }; - - fn create_nonce_account(state: NonceState) -> AccountSharedData { - AccountSharedData::new_data(1_000_000, &NonceVersions::new(state), &system_program::id()) - .unwrap() - } - - #[test] - fn test_nonce_info() { - let nonce_address = Pubkey::new_unique(); - let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); - let lamports_per_signature = 42; - let nonce_account = create_nonce_account(NonceState::Initialized(NonceData::new( - Pubkey::default(), - durable_nonce, - lamports_per_signature, - ))); - - let nonce_info = NonceInfo::new(nonce_address, nonce_account.clone()); - assert_eq!(*nonce_info.address(), nonce_address); - assert_eq!(*nonce_info.account(), nonce_account); - } - - #[test] - fn test_try_advance_nonce_success() { - let authority = Pubkey::new_unique(); - let mut nonce_info = NonceInfo::new( - Pubkey::new_unique(), - create_nonce_account(NonceState::Initialized(NonceData::new( - authority, - DurableNonce::from_blockhash(&Hash::new_unique()), - 42, - ))), - ); - - let new_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); - let new_lamports_per_signature = 100; - let result = nonce_info.try_advance_nonce(new_nonce, new_lamports_per_signature); - assert_eq!(result, Ok(())); - - let nonce_versions = StateMut::::state(&nonce_info.account).unwrap(); - assert_eq!( - &NonceState::Initialized(NonceData::new( - authority, - new_nonce, - new_lamports_per_signature - )), - nonce_versions.state() - ); - } - - #[test] - fn test_try_advance_nonce_invalid() { - let mut nonce_info = NonceInfo::new( - Pubkey::new_unique(), - AccountSharedData::new(1_000_000, 0, &Pubkey::default()), - ); - - let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); - let result = nonce_info.try_advance_nonce(durable_nonce, 5000); - assert_eq!(result, Err(AdvanceNonceError::Invalid)); - } - - #[test] - fn test_try_advance_nonce_uninitialized() { - let mut nonce_info = NonceInfo::new( - Pubkey::new_unique(), - create_nonce_account(NonceState::Uninitialized), - ); - - let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); - let result = nonce_info.try_advance_nonce(durable_nonce, 5000); - assert_eq!(result, Err(AdvanceNonceError::Uninitialized)); - } -} diff --git a/solana/svm/src/program_loader.rs b/solana/svm/src/program_loader.rs index edbba40..78bebe9 100644 --- a/solana/svm/src/program_loader.rs +++ b/solana/svm/src/program_loader.rs @@ -1,763 +1,26 @@ -#[cfg(feature = "metrics")] -use solana_program_runtime::program_metrics::LoadProgramMetrics; use { - solana_account::{AccountSharedData, ReadableAccount, state_traits::StateMut}, - solana_clock::Slot, - solana_instruction::error::InstructionError, - solana_loader_v3_interface::state::UpgradeableLoaderState, - solana_loader_v4_interface::state::{LoaderV4State, LoaderV4Status}, - solana_program_runtime::{ - loaded_programs::ProgramRuntimeEnvironment, - program_cache_entry::{ - DELAY_VISIBILITY_SLOT_OFFSET, ProgramCacheEntry, ProgramCacheEntryOwner, - ProgramCacheEntryType, - }, + solana_account::{AccountSharedData, ReadableAccount}, + solana_program_runtime::loaded_programs::{ + ProgramCacheEntry, ProgramCacheEntryType, ProgramRuntimeEnvironments, }, - solana_pubkey::Pubkey, - solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4}, - solana_svm_callback::TransactionProcessingCallback, - solana_svm_timings::ExecuteTimings, solana_svm_type_overrides::sync::Arc, - solana_transaction_error::{TransactionError, TransactionResult}, }; -#[derive(Debug)] -pub(crate) enum ProgramAccountLoadResult { - InvalidAccountData(ProgramCacheEntryOwner), - ProgramOfLoaderV1(AccountSharedData), - ProgramOfLoaderV2(AccountSharedData), - ProgramOfLoaderV3(AccountSharedData, AccountSharedData, Slot), - ProgramOfLoaderV4(AccountSharedData, Slot), -} - -pub(crate) fn load_program_accounts( - callbacks: &CB, - pubkey: &Pubkey, -) -> Option<(ProgramAccountLoadResult, Slot)> { - let (program_account, last_modification_slot) = callbacks.get_account_shared_data(pubkey)?; - - let load_result = if loader_v4::check_id(program_account.owner()) { - loader_v4_get_state(program_account.data()) - .ok() - .and_then(|state| { - (!matches!(state.status, LoaderV4Status::Retracted)).then_some(state.slot) - }) - .map(|slot| ProgramAccountLoadResult::ProgramOfLoaderV4(program_account, slot)) - .unwrap_or(ProgramAccountLoadResult::InvalidAccountData( - ProgramCacheEntryOwner::LoaderV4, - )) - } else if bpf_loader_upgradeable::check_id(program_account.owner()) { - if let Ok(UpgradeableLoaderState::Program { - programdata_address, - }) = program_account.state() - { - if let Some((programdata_account, _slot)) = - callbacks.get_account_shared_data(&programdata_address) - { - if bpf_loader_upgradeable::check_id(programdata_account.owner()) { - if let Ok(UpgradeableLoaderState::ProgramData { - slot, - upgrade_authority_address: _, - }) = programdata_account.state() - { - ProgramAccountLoadResult::ProgramOfLoaderV3( - program_account, - programdata_account, - slot, - ) - } else { - ProgramAccountLoadResult::InvalidAccountData( - ProgramCacheEntryOwner::LoaderV3, - ) - } - } else { - ProgramAccountLoadResult::InvalidAccountData(ProgramCacheEntryOwner::LoaderV3) - } - } else { - ProgramAccountLoadResult::InvalidAccountData(ProgramCacheEntryOwner::LoaderV3) - } - } else { - ProgramAccountLoadResult::InvalidAccountData(ProgramCacheEntryOwner::LoaderV3) - } - } else if bpf_loader::check_id(program_account.owner()) { - ProgramAccountLoadResult::ProgramOfLoaderV2(program_account) - } else if bpf_loader_deprecated::check_id(program_account.owner()) { - ProgramAccountLoadResult::ProgramOfLoaderV1(program_account) - } else { - return None; - }; - - Some((load_result, last_modification_slot)) -} - -/// Loads the program with the given pubkey. +/// Builds a program-cache entry from an executable account. /// -/// If the account doesn't exist it returns `None`. If the account does exist, it must be a program -/// account (belong to one of the program loaders). Returns `Some(InvalidAccountData)` if the program -/// account is `Closed`, contains invalid data or any of the programdata accounts are invalid. -pub fn load_program_with_pubkey( - callbacks: &CB, - program_runtime_environment: &ProgramRuntimeEnvironment, - pubkey: &Pubkey, - current_slot: Slot, - execute_timings: &mut ExecuteTimings, -) -> Option<(Arc, Slot)> { - #[cfg(feature = "metrics")] - let mut load_program_metrics = LoadProgramMetrics { - program_id: pubkey.to_string(), - ..LoadProgramMetrics::default() - }; - #[cfg(not(feature = "metrics"))] - let _ = execute_timings; - - let (load_result, last_modification_slot) = load_program_accounts(callbacks, pubkey)?; - let loaded_program = match load_result { - ProgramAccountLoadResult::InvalidAccountData(owner) => Ok( - ProgramCacheEntry::new_tombstone(current_slot, owner, ProgramCacheEntryType::Closed), - ), - - ProgramAccountLoadResult::ProgramOfLoaderV1(program_account) => ProgramCacheEntry::new( - program_account.owner(), - ProgramRuntimeEnvironment::clone(program_runtime_environment), - 0, - DELAY_VISIBILITY_SLOT_OFFSET, - program_account.data(), - program_account.data().len(), - #[cfg(feature = "metrics")] - &mut load_program_metrics, - ) - .map_err(|_| (0, ProgramCacheEntryOwner::LoaderV1)), - - ProgramAccountLoadResult::ProgramOfLoaderV2(program_account) => ProgramCacheEntry::new( - program_account.owner(), - ProgramRuntimeEnvironment::clone(program_runtime_environment), - 0, - DELAY_VISIBILITY_SLOT_OFFSET, - program_account.data(), - program_account.data().len(), - #[cfg(feature = "metrics")] - &mut load_program_metrics, - ) - .map_err(|_| (0, ProgramCacheEntryOwner::LoaderV2)), - - ProgramAccountLoadResult::ProgramOfLoaderV3( - program_account, - programdata_account, - deployment_slot, - ) => programdata_account - .data() - .get(UpgradeableLoaderState::size_of_programdata_metadata()..) - .ok_or(()) - .and_then(|programdata| { - ProgramCacheEntry::new( - program_account.owner(), - ProgramRuntimeEnvironment::clone(program_runtime_environment), - deployment_slot, - deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), - programdata, - program_account - .data() - .len() - .saturating_add(programdata_account.data().len()), - #[cfg(feature = "metrics")] - &mut load_program_metrics, - ) - .map_err(|_| ()) - }) - .map_err(|_| (deployment_slot, ProgramCacheEntryOwner::LoaderV3)), - - ProgramAccountLoadResult::ProgramOfLoaderV4(program_account, deployment_slot) => { - program_account - .data() - .get(LoaderV4State::program_data_offset()..) - .ok_or(()) - .and_then(|elf_bytes| { - ProgramCacheEntry::new( - &loader_v4::id(), - ProgramRuntimeEnvironment::clone(program_runtime_environment), - deployment_slot, - deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), - elf_bytes, - program_account.data().len(), - #[cfg(feature = "metrics")] - &mut load_program_metrics, - ) - .map_err(|_| ()) - }) - .map_err(|_| (deployment_slot, ProgramCacheEntryOwner::LoaderV4)) - } - } - .unwrap_or_else(|(deployment_slot, owner)| { - let env = ProgramRuntimeEnvironment::clone(program_runtime_environment); - ProgramCacheEntry::new_tombstone( - deployment_slot, - owner, - ProgramCacheEntryType::FailedVerification(env), - ) - }); - - #[cfg(feature = "metrics")] - load_program_metrics.submit_datapoint(&mut execute_timings.details); - loaded_program.update_access_slot(current_slot); - Some((Arc::new(loaded_program), last_modification_slot)) -} - -/// Find the slot in which the program was most recently re-/deployed. -/// Returns slot 0 for programs deployed with v1/v2 loaders, since programs deployed -/// with those loaders do not retain deployment slot information. -/// Returns an error if the program's account state can not be found or parsed. -pub(crate) fn get_program_deployment_slot( - callbacks: &CB, - pubkey: &Pubkey, -) -> TransactionResult { - let (program, _slot) = callbacks - .get_account_shared_data(pubkey) - .ok_or(TransactionError::ProgramAccountNotFound)?; - if bpf_loader_upgradeable::check_id(program.owner()) { - if let Ok(UpgradeableLoaderState::Program { - programdata_address, - }) = program.state() - { - let (programdata, _slot) = callbacks - .get_account_shared_data(&programdata_address) - .ok_or(TransactionError::ProgramAccountNotFound)?; - if let Ok(UpgradeableLoaderState::ProgramData { - slot, - upgrade_authority_address: _, - }) = programdata.state() - { - return Ok(slot); +/// 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), } - } - Err(TransactionError::ProgramAccountNotFound) - } else if loader_v4::check_id(program.owner()) { - let state = loader_v4_get_state(program.data()) - .map_err(|_| TransactionError::ProgramAccountNotFound)?; - Ok(state.slot) - } else { - Ok(0) - } -} - -// Plucked from the now-removed Loader V4 program library. -fn loader_v4_get_state(data: &[u8]) -> Result<&LoaderV4State, InstructionError> { - unsafe { - let data = data - .get(0..LoaderV4State::program_data_offset()) - .ok_or(InstructionError::AccountDataTooSmall)? - .try_into() - .unwrap(); - Ok(std::mem::transmute::< - &[u8; LoaderV4State::program_data_offset()], - &LoaderV4State, - >(data)) - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::transaction_processor::TransactionBatchProcessor, - solana_account::WritableAccount, - solana_program_runtime::{ - loaded_programs::{ - BlockRelation, ForkGraph, ProgramRuntimeEnvironment, - get_mock_program_runtime_environment, - }, - solana_sbpf::program::BuiltinProgram, - }, - solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable}, - solana_svm_callback::InvokeContextCallback, - std::{ - cell::RefCell, - collections::HashMap, - env, - fs::{self, File}, - io::Read, - }, - }; - - struct TestForkGraph {} - - impl ForkGraph for TestForkGraph { - fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation { - BlockRelation::Unknown - } - } - - #[derive(Default, Clone)] - pub(crate) struct MockBankCallback { - pub(crate) account_shared_data: RefCell>, - } - - impl InvokeContextCallback for MockBankCallback {} - - impl TransactionProcessingCallback for MockBankCallback { - fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { - self.account_shared_data.borrow().get(pubkey).cloned() - } - } - - #[test] - fn test_load_program_accounts_account_not_found() { - let mock_bank = MockBankCallback::default(); - let key = Pubkey::new_unique(); - - let result = load_program_accounts(&mock_bank, &key); - assert!(result.is_none()); - - let mut account_data = AccountSharedData::default(); - account_data.set_owner(bpf_loader_upgradeable::id()); - let state = UpgradeableLoaderState::Program { - programdata_address: Pubkey::new_unique(), - }; - account_data.set_data(bincode::serialize(&state).unwrap()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data.clone(), 0)); - - let result = load_program_accounts(&mock_bank, &key); - assert!(matches!( - result, - Some((ProgramAccountLoadResult::InvalidAccountData(_), _)) - )); - - account_data.set_data(Vec::new()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data, 0)); - - let result = load_program_accounts(&mock_bank, &key); - - assert!(matches!( - result, - Some((ProgramAccountLoadResult::InvalidAccountData(_), _)) - )); - } - - #[test] - fn test_load_program_accounts_loader_v1_or_v2() { - let key = Pubkey::new_unique(); - let mock_bank = MockBankCallback::default(); - let mut account_data = AccountSharedData::default(); - account_data.set_owner(bpf_loader::id()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data.clone(), 0)); - - let result = load_program_accounts(&mock_bank, &key); - match result { - Some((ProgramAccountLoadResult::ProgramOfLoaderV1(data), last_modification_slot)) - | Some((ProgramAccountLoadResult::ProgramOfLoaderV2(data), last_modification_slot)) => { - assert_eq!(data, account_data); - assert_eq!(last_modification_slot, 0); - } - _ => panic!("Invalid result"), - } - } - - #[test] - fn test_load_program_accounts_success() { - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let mock_bank = MockBankCallback::default(); - - let mut account_data = AccountSharedData::default(); - account_data.set_owner(bpf_loader_upgradeable::id()); - - let state = UpgradeableLoaderState::Program { - programdata_address: key2, - }; - account_data.set_data(bincode::serialize(&state).unwrap()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key1, (account_data.clone(), 25)); - - let state = UpgradeableLoaderState::ProgramData { - slot: 25, - upgrade_authority_address: None, - }; - let mut account_data2 = AccountSharedData::default(); - account_data2.set_owner(bpf_loader_upgradeable::id()); - account_data2.set_data(bincode::serialize(&state).unwrap()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key2, (account_data2.clone(), 25)); - - let result = load_program_accounts(&mock_bank, &key1); - - match result { - Some(( - ProgramAccountLoadResult::ProgramOfLoaderV3(data1, data2, deployment_slot), - last_modification_slot, - )) => { - assert_eq!(data1, account_data); - assert_eq!(data2, account_data2); - assert_eq!(deployment_slot, 25); - assert_eq!(last_modification_slot, 25); - } - - _ => panic!("Invalid result"), - } - } - - fn load_test_program() -> Vec { - let mut dir = env::current_dir().unwrap(); - dir.push("tests"); - dir.push("example-programs"); - dir.push("hello-solana"); - dir.push("hello_solana_program.so"); - let mut file = File::open(dir.clone()).expect("file not found"); - let metadata = fs::metadata(dir).expect("Unable to read metadata"); - let mut buffer = vec![0; metadata.len() as usize]; - file.read_exact(&mut buffer).expect("Buffer overflow"); - buffer - } - - #[test] - fn test_load_program_from_bytes() { - let buffer = load_test_program(); - - #[cfg(feature = "metrics")] - let mut metrics = LoadProgramMetrics::default(); - let loader = bpf_loader_upgradeable::id(); - let size = buffer.len(); - let slot: Slot = 2; - let environment = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); - - let result = ProgramCacheEntry::new( - &loader, - ProgramRuntimeEnvironment::clone(&environment), - slot, - slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), - &buffer, - size, - #[cfg(feature = "metrics")] - &mut metrics, - ); - - assert!(result.is_ok()); - } - - #[test] - fn test_load_program_not_found() { - let mock_bank = MockBankCallback::default(); - let key = Pubkey::new_unique(); - let batch_processor = TransactionBatchProcessor::::default(); - - let result = load_program_with_pubkey( - &mock_bank, - &batch_processor.program_runtime_environment_for_epoch(50), - &key, - 500, - &mut ExecuteTimings::default(), - ); - assert!(result.is_none()); - } - - #[test] - fn test_load_program_invalid_account_data() { - let key = Pubkey::new_unique(); - let mock_bank = MockBankCallback::default(); - let mut account_data = AccountSharedData::default(); - account_data.set_owner(bpf_loader_upgradeable::id()); - let batch_processor = TransactionBatchProcessor::::default(); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data.clone(), 0)); - - let result = load_program_with_pubkey( - &mock_bank, - &batch_processor.program_runtime_environment_for_epoch(20), - &key, - 0, // Slot 0 - &mut ExecuteTimings::default(), - ); - - let loaded_program = ProgramCacheEntry::new_tombstone( - 0, // Slot 0 - ProgramCacheEntryOwner::LoaderV3, - ProgramCacheEntryType::FailedVerification( - batch_processor.program_runtime_environment_for_epoch(20), - ), - ); - assert_eq!(result.unwrap(), (Arc::new(loaded_program), 0)); - } - - #[test] - fn test_load_program_program_loader_v1_or_v2() { - let key = Pubkey::new_unique(); - let mock_bank = MockBankCallback::default(); - let mut account_data = AccountSharedData::default(); - account_data.set_owner(bpf_loader::id()); - let batch_processor = TransactionBatchProcessor::::default(); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data.clone(), 0)); - - // This should return an error - let result = load_program_with_pubkey( - &mock_bank, - &batch_processor.program_runtime_environment_for_epoch(20), - &key, - 200, - &mut ExecuteTimings::default(), - ); - let loaded_program = ProgramCacheEntry::new_tombstone( - 0, - ProgramCacheEntryOwner::LoaderV2, - ProgramCacheEntryType::FailedVerification( - batch_processor.program_runtime_environment_for_epoch(20), - ), - ); - assert_eq!(result.unwrap(), (Arc::new(loaded_program), 0)); - - let buffer = load_test_program(); - account_data.set_data(buffer); - - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data.clone(), 0)); - - let result = load_program_with_pubkey( - &mock_bank, - &batch_processor.program_runtime_environment_for_epoch(20), - &key, - 200, - &mut ExecuteTimings::default(), - ); - - let program_runtime_environment = get_mock_program_runtime_environment(); - let expected = ProgramCacheEntry::new( - account_data.owner(), - ProgramRuntimeEnvironment::clone(&program_runtime_environment), - 0, - DELAY_VISIBILITY_SLOT_OFFSET, - account_data.data(), - account_data.data().len(), - #[cfg(feature = "metrics")] - &mut LoadProgramMetrics::default(), - ); - - assert_eq!(result.unwrap(), (Arc::new(expected.unwrap()), 0)); - } - - #[test] - fn test_load_program_program_loader_v3() { - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let mock_bank = MockBankCallback::default(); - let batch_processor = TransactionBatchProcessor::::default(); - - let mut account_data = AccountSharedData::default(); - account_data.set_owner(bpf_loader_upgradeable::id()); - - let state = UpgradeableLoaderState::Program { - programdata_address: key2, - }; - account_data.set_data(bincode::serialize(&state).unwrap()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key1, (account_data.clone(), 0)); - - let state = UpgradeableLoaderState::ProgramData { - slot: 0, - upgrade_authority_address: None, - }; - let mut account_data2 = AccountSharedData::default(); - account_data2.set_data(bincode::serialize(&state).unwrap()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key2, (account_data2.clone(), 0)); - - // This should return an error - let result = load_program_with_pubkey( - &mock_bank, - &batch_processor.program_runtime_environment_for_epoch(0), - &key1, - 0, - &mut ExecuteTimings::default(), - ); - let loaded_program = ProgramCacheEntry::new_tombstone( - 0, - ProgramCacheEntryOwner::LoaderV3, - ProgramCacheEntryType::FailedVerification( - batch_processor.program_runtime_environment_for_epoch(0), - ), - ); - assert_eq!(result.unwrap(), (Arc::new(loaded_program), 0)); - - let mut buffer = load_test_program(); - let mut header = bincode::serialize(&state).unwrap(); - let mut complement = vec![ - 0; - std::cmp::max( - 0, - UpgradeableLoaderState::size_of_programdata_metadata() - header.len() - ) - ]; - header.append(&mut complement); - header.append(&mut buffer); - account_data.set_data(header); - - mock_bank - .account_shared_data - .borrow_mut() - .insert(key2, (account_data.clone(), 0)); - - let result = load_program_with_pubkey( - &mock_bank, - &batch_processor.program_runtime_environment_for_epoch(20), - &key1, - 200, - &mut ExecuteTimings::default(), - ); - - let data = account_data.data(); - account_data - .set_data(data[UpgradeableLoaderState::size_of_programdata_metadata()..].to_vec()); - - let program_runtime_environment = get_mock_program_runtime_environment(); - let expected = ProgramCacheEntry::new( - account_data.owner(), - ProgramRuntimeEnvironment::clone(&program_runtime_environment), - 0, - DELAY_VISIBILITY_SLOT_OFFSET, - account_data.data(), - account_data.data().len(), - #[cfg(feature = "metrics")] - &mut LoadProgramMetrics::default(), - ); - assert_eq!(result.unwrap(), (Arc::new(expected.unwrap()), 0)); - } - - #[test] - fn test_load_program_environment() { - let key = Pubkey::new_unique(); - let mock_bank = MockBankCallback::default(); - let mut account_data = AccountSharedData::default(); - account_data.set_owner(bpf_loader::id()); - let batch_processor = TransactionBatchProcessor::::default(); - let upcoming_environment = get_mock_program_runtime_environment(); - let current_environment = - ProgramRuntimeEnvironment::clone(&batch_processor.program_runtime_environment); - { - let mut epoch_boundary_preparation = - batch_processor.epoch_boundary_preparation.write().unwrap(); - epoch_boundary_preparation.upcoming_epoch = 1; - epoch_boundary_preparation.upcoming_environment = Some(upcoming_environment.clone()); - } - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data.clone(), 0)); - - for is_upcoming_env in [false, true] { - let (result, _last_modification_slot) = load_program_with_pubkey( - &mock_bank, - &batch_processor.program_runtime_environment_for_epoch(is_upcoming_env as u64), - &key, - 200, - &mut ExecuteTimings::default(), - ) - .unwrap(); - assert_ne!( - is_upcoming_env, - result.program.get_environment().unwrap() == ¤t_environment, - ); - assert_eq!( - is_upcoming_env, - result.program.get_environment().unwrap() == &upcoming_environment, - ); - } - } - - #[test] - fn test_program_modification_slot_account_not_found() { - let mock_bank = MockBankCallback::default(); - - let key = Pubkey::new_unique(); - - let result = get_program_deployment_slot(&mock_bank, &key); - assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); - - let mut account_data = AccountSharedData::new(100, 100, &bpf_loader_upgradeable::id()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data.clone(), 0)); - - let result = get_program_deployment_slot(&mock_bank, &key); - assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); - - let state = UpgradeableLoaderState::Program { - programdata_address: Pubkey::new_unique(), - }; - account_data.set_data(bincode::serialize(&state).unwrap()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key, (account_data.clone(), 0)); - - let result = get_program_deployment_slot(&mock_bank, &key); - assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); - } - - #[test] - fn test_program_deployment_slot_success() { - let mock_bank = MockBankCallback::default(); - - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - - let account_data = AccountSharedData::new_data( - 100, - &UpgradeableLoaderState::Program { - programdata_address: key2, - }, - &bpf_loader_upgradeable::id(), - ) - .unwrap(); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key1, (account_data, 0)); - - let mut account_data = AccountSharedData::new_data( - 100, - &UpgradeableLoaderState::ProgramData { - slot: 77, - upgrade_authority_address: None, - }, - &bpf_loader_upgradeable::id(), - ) - .unwrap(); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key2, (account_data.clone(), 0)); - - let result = get_program_deployment_slot(&mock_bank, &key1); - assert_eq!(result.unwrap(), 77); - - account_data.set_owner(Pubkey::new_unique()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key2, (account_data, 0)); - - let result = get_program_deployment_slot(&mock_bank, &key2); - assert_eq!(result.unwrap(), 0); - } + .into() + }) } diff --git a/solana/svm/src/rent_calculator.rs b/solana/svm/src/rent_calculator.rs index 48bfb6b..b235630 100644 --- a/solana/svm/src/rent_calculator.rs +++ b/solana/svm/src/rent_calculator.rs @@ -3,6 +3,7 @@ //! Rent management for SVM. use { + solana_account::{AccountMode, AccountSharedData, ReadableAccount}, solana_clock::Epoch, solana_pubkey::Pubkey, solana_rent::Rent, @@ -29,33 +30,28 @@ pub enum RentState { RentExempt, } -/// Check rent state transition for an account in a transaction. -/// -/// This method has a default implementation that calls into -/// `check_rent_state_with_account`. +/// Checks a writable account rent-state transition inside a transaction. pub fn check_rent_state( - pre_rent_state: &RentState, - post_rent_state: &RentState, + pre_rent_state: Option<&RentState>, + post_rent_state: Option<&RentState>, transaction_context: &TransactionContext, index: IndexOfAccount, ) -> TransactionResult<()> { - let expect_msg = "account must exist at TransactionContext index"; - check_rent_state_with_account( - pre_rent_state, - post_rent_state, - transaction_context - .get_key_of_account_at_index(index) - .expect(expect_msg), - index, - )?; + 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(()) } -/// Check rent state transition for an account directly. +/// Checks a rent-state transition for a known account address. /// -/// This method has a default implementation that checks whether the -/// transition is allowed and returns an error if it is not. It also -/// verifies that the account is not the incinerator. +/// The incinerator is exempt from this check. pub fn check_rent_state_with_account( pre_rent_state: &RentState, post_rent_state: &RentState, @@ -72,36 +68,23 @@ pub fn check_rent_state_with_account( } } -/// Determine the rent state of an account. -/// -/// This method has a default implementation that treats accounts with zero -/// lamports as uninitialized and uses the implemented `get_rent` to -/// determine whether an account is rent-exempt. -pub fn get_account_rent_state( - rent: &Rent, - account_lamports: u64, - account_size: usize, -) -> RentState { - if account_lamports == 0 { +/// 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(account_lamports, account_size) { + } else if rent.is_exempt(lamports, len) || acc.is(AccountMode::Ephemeral) { RentState::RentExempt } else { - RentState::RentPaying { - data_size: account_size, - lamports: account_lamports, - } + RentState::RentPaying { data_size: len, lamports } } } -/// Check whether a transition from the pre_rent_state to the -/// post_rent_state is valid. +/// Returns whether a pre/post rent-state transition is valid. /// -/// This method has a default implementation that allows transitions from -/// any state to `RentState::Uninitialized` or `RentState::RentExempt`. -/// Pre-state `RentState::RentPaying` can only transition to -/// `RentState::RentPaying` if the data size remains the same and the -/// account is not credited. +/// 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, diff --git a/solana/svm/src/rollback_accounts.rs b/solana/svm/src/rollback_accounts.rs deleted file mode 100644 index 5e3418c..0000000 --- a/solana/svm/src/rollback_accounts.rs +++ /dev/null @@ -1,271 +0,0 @@ -use { - crate::nonce_info::NonceInfo, - solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, - solana_clock::Epoch, - solana_pubkey::Pubkey, - solana_transaction_context::transaction_accounts::KeyedAccountSharedData, -}; - -/// Captured account state used to rollback account state for nonce and fee -/// payer accounts after a failed executed transaction. -#[derive(PartialEq, Eq, Debug, Clone)] -pub enum RollbackAccounts { - FeePayerOnly { - fee_payer: KeyedAccountSharedData, - }, - SameNonceAndFeePayer { - nonce: KeyedAccountSharedData, - }, - SeparateNonceAndFeePayer { - nonce: KeyedAccountSharedData, - fee_payer: KeyedAccountSharedData, - }, -} - -#[cfg(feature = "dev-context-only-utils")] -impl Default for RollbackAccounts { - fn default() -> Self { - Self::FeePayerOnly { - fee_payer: KeyedAccountSharedData::default(), - } - } -} - -/// Rollback accounts iterator. -/// This struct is created by the `RollbackAccounts::iter`. -pub struct RollbackAccountsIter<'a> { - fee_payer: Option<&'a KeyedAccountSharedData>, - nonce: Option<&'a KeyedAccountSharedData>, -} - -impl<'a> Iterator for RollbackAccountsIter<'a> { - type Item = &'a KeyedAccountSharedData; - - fn next(&mut self) -> Option { - if let Some(fee_payer) = self.fee_payer.take() { - return Some(fee_payer); - } - if let Some(nonce) = self.nonce.take() { - return Some(nonce); - } - None - } -} - -impl<'a> IntoIterator for &'a RollbackAccounts { - type Item = &'a KeyedAccountSharedData; - type IntoIter = RollbackAccountsIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl RollbackAccounts { - pub(crate) fn new( - nonce: Option, - fee_payer_address: Pubkey, - mut fee_payer_account: AccountSharedData, - fee_payer_loaded_rent_epoch: Epoch, - ) -> Self { - if let Some(nonce) = nonce { - if &fee_payer_address == nonce.address() { - // `nonce` contains an AccountSharedData which has already been advanced to the current DurableNonce - // `fee_payer_account` is an AccountSharedData as it currently exists on-chain - // thus if the nonce account is being used as the fee payer, we need to update that data here - // so we capture both the data change for the nonce and the lamports/rent epoch change for the fee payer - fee_payer_account.set_data_from_slice(nonce.account().data()); - - RollbackAccounts::SameNonceAndFeePayer { - nonce: (fee_payer_address, fee_payer_account), - } - } else { - RollbackAccounts::SeparateNonceAndFeePayer { - nonce: (nonce.address, nonce.account), - fee_payer: (fee_payer_address, fee_payer_account), - } - } - } else { - // When rolling back failed transactions which don't use nonces, the - // runtime should not update the fee payer's rent epoch so reset the - // rollback fee payer account's rent epoch to its originally loaded - // rent epoch value. In the future, a feature gate could be used to - // alter this behavior such that rent epoch updates are handled the - // same for both nonce and non-nonce failed transactions. - fee_payer_account.set_rent_epoch(fee_payer_loaded_rent_epoch); - RollbackAccounts::FeePayerOnly { - fee_payer: (fee_payer_address, fee_payer_account), - } - } - } - - /// Return a reference to the fee payer account. - pub fn fee_payer(&self) -> &KeyedAccountSharedData { - match self { - Self::FeePayerOnly { fee_payer } => fee_payer, - Self::SameNonceAndFeePayer { nonce } => nonce, - Self::SeparateNonceAndFeePayer { fee_payer, .. } => fee_payer, - } - } - - /// Number of accounts tracked for rollback - pub fn count(&self) -> usize { - match self { - Self::FeePayerOnly { .. } | Self::SameNonceAndFeePayer { .. } => 1, - Self::SeparateNonceAndFeePayer { .. } => 2, - } - } - - /// Iterator over accounts tracked for rollback. - pub fn iter(&self) -> RollbackAccountsIter<'_> { - match self { - Self::FeePayerOnly { fee_payer } => RollbackAccountsIter { - fee_payer: Some(fee_payer), - nonce: None, - }, - Self::SameNonceAndFeePayer { nonce } => RollbackAccountsIter { - fee_payer: None, - nonce: Some(nonce), - }, - Self::SeparateNonceAndFeePayer { nonce, fee_payer } => RollbackAccountsIter { - fee_payer: Some(fee_payer), - nonce: Some(nonce), - }, - } - } - - // Size of accounts tracked for rollback, used internally when calculating the actual - // loaded transaction data size for the cost model. This function will be removed by - // the fee-payer data size amendment to SIMD-186. - pub(crate) fn data_size(&self) -> usize { - let mut total_size: usize = 0; - for (_, account) in self.iter() { - total_size = total_size.saturating_add(account.data().len()); - } - total_size - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - solana_account::{ReadableAccount, WritableAccount}, - solana_hash::Hash, - solana_nonce::{ - state::{Data as NonceData, DurableNonce, State as NonceState}, - versions::Versions as NonceVersions, - }, - solana_sdk_ids::system_program, - }; - - #[test] - fn test_new_fee_payer_only() { - let fee_payer_address = Pubkey::new_unique(); - let fee_payer_account = AccountSharedData::new(100, 0, &Pubkey::default()); - let fee_payer_rent_epoch = fee_payer_account.rent_epoch(); - - let rent_epoch_updated_fee_payer_account = { - let mut account = fee_payer_account.clone(); - account.set_lamports(fee_payer_account.lamports()); - account.set_rent_epoch(fee_payer_rent_epoch + 1); - account - }; - - let rollback_accounts = RollbackAccounts::new( - None, - fee_payer_address, - rent_epoch_updated_fee_payer_account, - fee_payer_rent_epoch, - ); - - let expected_fee_payer = (fee_payer_address, fee_payer_account); - match rollback_accounts { - RollbackAccounts::FeePayerOnly { fee_payer } => { - assert_eq!(expected_fee_payer, fee_payer); - } - _ => panic!("Expected FeePayerOnly variant"), - } - } - - #[test] - fn test_new_same_nonce_and_fee_payer() { - let nonce_address = Pubkey::new_unique(); - let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); - let lamports_per_signature = 42; - let nonce_account = AccountSharedData::new_data( - 43, - &NonceVersions::new(NonceState::Initialized(NonceData::new( - Pubkey::default(), - durable_nonce, - lamports_per_signature, - ))), - &system_program::id(), - ) - .unwrap(); - - let rent_epoch_updated_fee_payer_account = { - let mut account = nonce_account.clone(); - account.set_lamports(nonce_account.lamports()); - account - }; - - let nonce = NonceInfo::new(nonce_address, rent_epoch_updated_fee_payer_account.clone()); - let rollback_accounts = RollbackAccounts::new( - Some(nonce), - nonce_address, - rent_epoch_updated_fee_payer_account, - u64::MAX, // ignored - ); - - let expected_rollback_accounts = RollbackAccounts::SameNonceAndFeePayer { - nonce: (nonce_address, nonce_account), - }; - - assert_eq!(expected_rollback_accounts, rollback_accounts); - } - - #[test] - fn test_separate_nonce_and_fee_payer() { - let nonce_address = Pubkey::new_unique(); - let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); - let lamports_per_signature = 42; - let nonce_account = AccountSharedData::new_data( - 43, - &NonceVersions::new(NonceState::Initialized(NonceData::new( - Pubkey::default(), - durable_nonce, - lamports_per_signature, - ))), - &system_program::id(), - ) - .unwrap(); - - let fee_payer_address = Pubkey::new_unique(); - let fee_payer_account = AccountSharedData::new(44, 0, &Pubkey::default()); - - let rent_epoch_updated_fee_payer_account = { - let mut account = fee_payer_account.clone(); - account.set_lamports(fee_payer_account.lamports()); - account - }; - - let nonce = NonceInfo::new(nonce_address, nonce_account.clone()); - let rollback_accounts = RollbackAccounts::new( - Some(nonce), - fee_payer_address, - rent_epoch_updated_fee_payer_account, - u64::MAX, // ignored - ); - - let expected_nonce = (nonce_address, nonce_account); - let expected_fee_payer = (fee_payer_address, fee_payer_account); - match rollback_accounts { - RollbackAccounts::SeparateNonceAndFeePayer { nonce, fee_payer } => { - assert_eq!(expected_nonce, nonce); - assert_eq!(expected_fee_payer, fee_payer); - } - _ => panic!("Expected SeparateNonceAndFeePayer variant"), - } - } -} diff --git a/solana/svm/src/transaction_account_state_info.rs b/solana/svm/src/transaction_account_state_info.rs index bd8e4ef..06e20ed 100644 --- a/solana/svm/src/transaction_account_state_info.rs +++ b/solana/svm/src/transaction_account_state_info.rs @@ -1,6 +1,5 @@ use { crate::rent_calculator::{RentState, check_rent_state, get_account_rent_state}, - solana_account::ReadableAccount, solana_rent::Rent, solana_svm_transaction::svm_message::SVMMessage, solana_transaction_context::{IndexOfAccount, transaction::TransactionContext}, @@ -9,7 +8,7 @@ use { #[derive(PartialEq, Debug)] pub(crate) struct TransactionAccountStateInfo { - info: Option, // None: readonly account + rent_state: Option, // None: readonly account } impl TransactionAccountStateInfo { @@ -20,21 +19,12 @@ impl TransactionAccountStateInfo { ) -> Vec { (0..message.account_keys().len()) .map(|i| { - let info = if message.is_writable(i) { - let state = if let Ok(account) = transaction_context + let rent_state = if message.is_writable(i) { + let state = transaction_context .accounts() .try_borrow(i as IndexOfAccount) - { - let balance = account.lamports(); - let data_size = account.data().len(); - let rent_state = get_account_rent_state(rent, balance, data_size); - Some(WritableTransactionAccountStateInfo { - rent_state, - data_size, - }) - } else { - None - }; + .map(|acc| get_account_rent_state(rent, &acc)) + .ok(); debug_assert!( state.is_some(), "message and transaction context out of sync, fatal" @@ -43,7 +33,7 @@ impl TransactionAccountStateInfo { } else { None }; - Self { info } + Self { rent_state } }) .collect() } @@ -56,37 +46,17 @@ impl TransactionAccountStateInfo { for (i, (pre_state_info, post_state_info)) in pre_state_infos.iter().zip(post_state_infos).enumerate() { - if let (Some(pre_state_info), Some(post_state_info)) = - (pre_state_info.info.as_ref(), post_state_info.info.as_ref()) - { - check_rent_state( - &pre_state_info.rent_state, - &post_state_info.rent_state, - transaction_context, - i as IndexOfAccount, - )?; - } + check_rent_state( + pre_state_info.rent_state.as_ref(), + post_state_info.rent_state.as_ref(), + transaction_context, + i as IndexOfAccount, + )?; } Ok(()) } } -#[derive(PartialEq, Debug)] -struct WritableTransactionAccountStateInfo { - rent_state: RentState, - data_size: usize, -} - -// Returns the cumulative size of all post-exec uninitialized accounts -pub(crate) fn get_uninitialized_accounts_size(post: &[TransactionAccountStateInfo]) -> u64 { - post.iter() - .filter_map(|post_info| post_info.info.as_ref()) - .filter_map(|post| { - matches!(&post.rent_state, RentState::Uninitialized).then_some(post.data_size as u64) - }) - .sum() -} - #[cfg(test)] mod test { use { @@ -146,17 +116,11 @@ mod test { result, vec![ TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 0, - }) + rent_state: Some(RentState::Uninitialized) }, - TransactionAccountStateInfo { info: None }, + TransactionAccountStateInfo { rent_state: None }, TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 0, - }) + rent_state: Some(RentState::Uninitialized) } ] ); @@ -208,23 +172,14 @@ mod test { let key2 = Keypair::new(); let pre_rent_state = vec![ TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 0, - }), + rent_state: Some(RentState::Uninitialized), }, TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 0, - }), + rent_state: Some(RentState::Uninitialized), }, ]; let post_rent_state = vec![TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 0, - }), + rent_state: Some(RentState::Uninitialized), }]; let transaction_accounts = vec![ @@ -242,19 +197,10 @@ mod test { assert!(result.is_ok()); let pre_rent_state = vec![TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 0, - }), + rent_state: Some(RentState::Uninitialized), }]; let post_rent_state = vec![TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::RentPaying { - data_size: 2, - lamports: 5, - }, - data_size: 2, - }), + rent_state: Some(RentState::RentPaying { data_size: 2, lamports: 5 }), }]; let transaction_accounts = vec![ @@ -273,37 +219,4 @@ mod test { Some(TransactionError::InsufficientFundsForRent { account_index: 0 }) ); } - - #[test] - fn test_get_uninitialized_accounts_size_with_deleted_accounts() { - let post_state_infos = vec![ - TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 50, - }), - }, - TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 50, - }), - }, - TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::Uninitialized, - data_size: 50, - }), - }, - TransactionAccountStateInfo { - info: Some(WritableTransactionAccountStateInfo { - rent_state: RentState::RentExempt, - data_size: 50, - }), - }, - ]; - - // 3 deleted accounts should contribute 3 * (50) = 150 to the count - assert_eq!(get_uninitialized_accounts_size(&post_state_infos), 150); - } } diff --git a/solana/svm/src/transaction_balances.rs b/solana/svm/src/transaction_balances.rs index 97a54a9..28a817c 100644 --- a/solana/svm/src/transaction_balances.rs +++ b/solana/svm/src/transaction_balances.rs @@ -1,204 +1,60 @@ #[cfg(feature = "dev-context-only-utils")] use qualifier_attr::field_qualifiers; -use { - crate::{ - account_loader::AccountLoader, - transaction_processing_callback::TransactionProcessingCallback, - }, - solana_account::{AccountSharedData, ReadableAccount}, - solana_pubkey::Pubkey, - solana_svm_transaction::svm_transaction::SVMTransaction, - spl_generic_token::{generic_token, is_known_spl_token_id}, -}; +use solana_account::ReadableAccount; +use solana_transaction_context::transaction_accounts::KeyedAccountSharedData; -// we use internal aliases for clarity, the external type aliases are often confusing +// Use an internal alias so this stays tied to native lamport balances. type TxNativeBalances = Vec; -type TxTokenBalances = Vec; -type BatchNativeBalances = Vec; -type BatchTokenBalances = Vec; -// to operate cleanly over Option we use a trait impled on the outer and inner type +// Implemented for Option to keep call sites branch-free. pub(crate) trait BalanceCollectionRoutines { - fn collect_pre_balances( - &mut self, - account_loader: &mut AccountLoader, - transaction: &impl SVMTransaction, - ); + fn collect_pre_balances(&mut self, accounts: &[KeyedAccountSharedData]); - fn collect_post_balances( - &mut self, - account_loader: &mut AccountLoader, - transaction: &impl SVMTransaction, - ); + fn collect_post_balances(&mut self, accounts: &[KeyedAccountSharedData]); } -#[derive(Debug, Default)] +/// 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), token_pre(pub), token_post(pub),) + field_qualifiers(native_pre(pub), native_post(pub)) )] pub struct BalanceCollector { - native_pre: BatchNativeBalances, - native_post: BatchNativeBalances, - token_pre: BatchTokenBalances, - token_post: BatchTokenBalances, + native_pre: TxNativeBalances, + native_post: TxNativeBalances, } impl BalanceCollector { - // we always provide one vec for every transaction, even if the vecs are empty - pub(crate) fn new_with_transaction_count(transaction_count: usize) -> Self { - Self { - native_pre: Vec::with_capacity(transaction_count), - native_post: Vec::with_capacity(transaction_count), - token_pre: Vec::with_capacity(transaction_count), - token_post: Vec::with_capacity(transaction_count), - } - } - - // we use this pattern to prevent anything outside svm mutating BalanceCollector internals - // with no public constructor, and only private fields, non-svm code can only disassemble the struct - pub fn into_vecs( - self, - ) -> ( - BatchNativeBalances, - BatchNativeBalances, - BatchTokenBalances, - BatchTokenBalances, - ) { - ( - self.native_pre, - self.native_post, - self.token_pre, - self.token_post, - ) - } - - // gather native lamport balances for all accounts - // and token balances for valid, initialized token accounts with valid, initialized mints - fn collect_balances( - &mut self, - account_loader: &mut AccountLoader, - transaction: &impl SVMTransaction, - ) -> (TxNativeBalances, TxTokenBalances) { - let mut native_balances = Vec::with_capacity(transaction.account_keys().len()); - let mut token_balances = vec![]; - - let has_token_program = transaction.account_keys().iter().any(is_known_spl_token_id); - - for (index, key) in transaction.account_keys().iter().enumerate() { - let Some(account) = account_loader.load_account(key) else { - native_balances.push(0); - continue; - }; - - native_balances.push(account.lamports()); - - if has_token_program - && !transaction.is_invoked(index) - && !is_known_spl_token_id(key) - && is_known_spl_token_id(account.owner()) - && let Some(token_info) = - SvmTokenInfo::unpack_token_account(account_loader, &account, index) - { - token_balances.push(token_info); - } - } - - (native_balances, token_balances) + /// Returns recorded pre- and post-execution lamport balances. + pub fn into_vecs(self) -> (TxNativeBalances, TxNativeBalances) { + (self.native_pre, self.native_post) } - pub(crate) fn lengths_match_expected(&self, expected_len: usize) -> bool { - self.native_pre.len() == expected_len - && self.native_post.len() == expected_len - && self.token_pre.len() == expected_len - && self.token_post.len() == expected_len + 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, - account_loader: &mut AccountLoader, - transaction: &impl SVMTransaction, - ) { - let (native_balances, token_balances) = self.collect_balances(account_loader, transaction); - self.native_pre.push(native_balances); - self.token_pre.push(token_balances); + fn collect_pre_balances(&mut self, accounts: &[KeyedAccountSharedData]) { + self.native_pre = self.collect_balances(accounts); } - fn collect_post_balances( - &mut self, - account_loader: &mut AccountLoader, - transaction: &impl SVMTransaction, - ) { - let (native_balances, token_balances) = self.collect_balances(account_loader, transaction); - self.native_post.push(native_balances); - self.token_post.push(token_balances); + 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, - account_loader: &mut AccountLoader, - transaction: &impl SVMTransaction, - ) { + fn collect_pre_balances(&mut self, accounts: &[KeyedAccountSharedData]) { if let Some(inner) = self { - inner.collect_pre_balances(account_loader, transaction) + inner.collect_pre_balances(accounts) } } - fn collect_post_balances( - &mut self, - account_loader: &mut AccountLoader, - transaction: &impl SVMTransaction, - ) { + fn collect_post_balances(&mut self, accounts: &[KeyedAccountSharedData]) { if let Some(inner) = self { - inner.collect_post_balances(account_loader, transaction) - } - } -} - -// this contains all the information we can provide to construct TransactionTokenBalance -// that type, in ledger, depends on UiTokenAmount from account-decoder, so we cannot build it here -#[derive(Debug, Clone, PartialEq)] -pub struct SvmTokenInfo { - pub account_index: u8, - pub mint: Pubkey, - pub amount: u64, - pub owner: Pubkey, - pub program_id: Pubkey, - pub decimals: u8, -} - -impl SvmTokenInfo { - fn unpack_token_account( - account_loader: &mut AccountLoader, - account: &AccountSharedData, - index: usize, - ) -> Option { - let program_id = *account.owner(); - let generic_token::Account { - mint, - owner, - amount, - } = generic_token::Account::unpack(account.data(), &program_id)?; - - let mint_account = account_loader.load_account(&mint)?; - if *mint_account.owner() != program_id { - return None; + inner.collect_post_balances(accounts) } - - let generic_token::Mint { decimals, .. } = - generic_token::Mint::unpack(mint_account.data(), &program_id)?; - - Some(Self { - account_index: index.try_into().ok()?, - mint, - amount, - owner, - program_id, - decimals, - }) } } diff --git a/solana/svm/src/transaction_commit_result.rs b/solana/svm/src/transaction_commit_result.rs deleted file mode 100644 index 7fdc51a..0000000 --- a/solana/svm/src/transaction_commit_result.rs +++ /dev/null @@ -1,39 +0,0 @@ -use { - crate::transaction_execution_result::TransactionLoadedAccountsStats, - solana_fee_structure::FeeDetails, solana_message::inner_instruction::InnerInstructionsList, - solana_transaction_context::transaction::TransactionReturnData, - solana_transaction_error::TransactionResult, -}; - -pub type TransactionCommitResult = TransactionResult; - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))] -pub struct CommittedTransaction { - pub status: TransactionResult<()>, - pub log_messages: Option>, - pub inner_instructions: Option, - pub return_data: Option, - pub executed_units: u64, - pub fee_details: FeeDetails, - pub loaded_account_stats: TransactionLoadedAccountsStats, - pub fee_payer_post_balance: u64, -} - -pub trait TransactionCommitResultExtensions { - fn was_committed(&self) -> bool; - fn was_executed_successfully(&self) -> bool; -} - -impl TransactionCommitResultExtensions for TransactionCommitResult { - fn was_committed(&self) -> bool { - self.is_ok() - } - - fn was_executed_successfully(&self) -> bool { - match self { - Ok(committed_tx) => committed_tx.status.is_ok(), - Err(_) => false, - } - } -} diff --git a/solana/svm/src/transaction_error_metrics.rs b/solana/svm/src/transaction_error_metrics.rs deleted file mode 100644 index 8f34539..0000000 --- a/solana/svm/src/transaction_error_metrics.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::num::Saturating; - -#[derive(Debug, Default)] -pub struct TransactionErrorMetrics { - pub total: Saturating, - pub account_in_use: Saturating, - pub too_many_account_locks: Saturating, - pub account_loaded_twice: Saturating, - pub account_not_found: Saturating, - pub blockhash_not_found: Saturating, - pub blockhash_too_old: Saturating, - pub call_chain_too_deep: Saturating, - pub already_processed: Saturating, - pub instruction_error: Saturating, - pub insufficient_funds: Saturating, - pub invalid_account_for_fee: Saturating, - pub invalid_account_index: Saturating, - pub invalid_program_for_execution: Saturating, - pub invalid_compute_budget: Saturating, - pub not_allowed_during_cluster_maintenance: Saturating, - pub invalid_writable_account: Saturating, - pub invalid_rent_paying_account: Saturating, - pub would_exceed_max_block_cost_limit: Saturating, - pub would_exceed_max_account_cost_limit: Saturating, - pub would_exceed_max_vote_cost_limit: Saturating, - pub would_exceed_account_data_block_limit: Saturating, - pub max_loaded_accounts_data_size_exceeded: Saturating, - pub program_execution_temporarily_restricted: Saturating, -} - -impl TransactionErrorMetrics { - pub fn new() -> Self { - Self::default() - } - - pub fn accumulate(&mut self, other: &TransactionErrorMetrics) { - self.total += other.total; - self.account_in_use += other.account_in_use; - self.too_many_account_locks += other.too_many_account_locks; - self.account_loaded_twice += other.account_loaded_twice; - self.account_not_found += other.account_not_found; - self.blockhash_not_found += other.blockhash_not_found; - self.blockhash_too_old += other.blockhash_too_old; - self.call_chain_too_deep += other.call_chain_too_deep; - self.already_processed += other.already_processed; - self.instruction_error += other.instruction_error; - self.insufficient_funds += other.insufficient_funds; - self.invalid_account_for_fee += other.invalid_account_for_fee; - self.invalid_account_index += other.invalid_account_index; - self.invalid_program_for_execution += other.invalid_program_for_execution; - self.invalid_compute_budget += other.invalid_compute_budget; - self.not_allowed_during_cluster_maintenance += other.not_allowed_during_cluster_maintenance; - self.invalid_writable_account += other.invalid_writable_account; - self.invalid_rent_paying_account += other.invalid_rent_paying_account; - self.would_exceed_max_block_cost_limit += other.would_exceed_max_block_cost_limit; - self.would_exceed_max_account_cost_limit += other.would_exceed_max_account_cost_limit; - self.would_exceed_max_vote_cost_limit += other.would_exceed_max_vote_cost_limit; - self.would_exceed_account_data_block_limit += other.would_exceed_account_data_block_limit; - self.max_loaded_accounts_data_size_exceeded += other.max_loaded_accounts_data_size_exceeded; - self.program_execution_temporarily_restricted += - other.program_execution_temporarily_restricted; - } -} diff --git a/solana/svm/src/transaction_execution_result.rs b/solana/svm/src/transaction_execution_result.rs index dd4dd88..954c1f0 100644 --- a/solana/svm/src/transaction_execution_result.rs +++ b/solana/svm/src/transaction_execution_result.rs @@ -1,54 +1,56 @@ use { crate::account_loader::LoadedTransaction, solana_message::inner_instruction::InnerInstructionsList, - solana_program_runtime::program_cache_entry::ProgramCacheEntry, - solana_pubkey::Pubkey, solana_transaction_context::transaction::TransactionReturnData, - solana_transaction_error::TransactionResult, - std::{collections::HashMap, sync::Arc}, + 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, - pub programs_modified_by_tx: HashMap>, } 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<()>, - pub log_messages: Option>, + /// 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, - /// deltas related to total account data size changes for this transaction. - /// NOTE: set to None IFF `status` is not `Ok`. - pub accounts_deltas: Option, + /// 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() } } - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AccountsDeltas { - /// aggregate resize delta across all accounts touched by the transaction - pub accounts_resize_delta: i64, - /// aggregate size of all accounts that were uninitialized by this transaction - pub accounts_uninitialized_size: u64, -} diff --git a/solana/svm/src/transaction_processing_callback.rs b/solana/svm/src/transaction_processing_callback.rs index ef1d7ca..cf53bea 100644 --- a/solana/svm/src/transaction_processing_callback.rs +++ b/solana/svm/src/transaction_processing_callback.rs @@ -1 +1 @@ -pub use solana_svm_callback::{AccountState, TransactionProcessingCallback}; +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 index 4805f48..6e1316c 100644 --- a/solana/svm/src/transaction_processing_result.rs +++ b/solana/svm/src/transaction_processing_result.rs @@ -1,31 +1,27 @@ use { - crate::{ - account_loader::FeesOnlyTransaction, - transaction_execution_result::{ExecutedTransaction, TransactionExecutionDetails}, - }, - solana_fee_structure::FeeDetails, - solana_transaction_error::{TransactionError, TransactionResult}, + crate::transaction_execution_result::ExecutedTransaction, + solana_transaction_error::TransactionResult, }; -pub type TransactionProcessingResult = 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; - fn processed_transaction(&self) -> Option<&ProcessedTransaction>; + /// 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<()>; } -#[derive(Debug)] -pub enum ProcessedTransaction { - /// Transaction was executed, but if execution failed, all account state changes - /// will be rolled back except deducted fees and any advanced nonces - Executed(Box), - /// Transaction was not able to be executed but fees are able to be - /// collected and any nonces are advanceable - FeesOnly(Box), -} - impl TransactionProcessingResultExtensions for TransactionProcessingResult { fn was_processed(&self) -> bool { self.is_ok() @@ -33,68 +29,18 @@ impl TransactionProcessingResultExtensions for TransactionProcessingResult { fn was_processed_with_successful_result(&self) -> bool { match self { - Ok(processed_tx) => processed_tx.was_processed_with_successful_result(), + Ok(processed_tx) => processed_tx.was_successful(), Err(_) => false, } } - fn processed_transaction(&self) -> Option<&ProcessedTransaction> { - self.as_ref().ok() + 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.status()) - } -} - -impl ProcessedTransaction { - fn was_processed_with_successful_result(&self) -> bool { - match self { - Self::Executed(executed_tx) => executed_tx.execution_details.status.is_ok(), - Self::FeesOnly(_) => false, - } - } - - pub fn status(&self) -> TransactionResult<()> { - match self { - Self::Executed(executed_tx) => executed_tx.execution_details.status.clone(), - Self::FeesOnly(details) => Err(TransactionError::clone(&details.load_error)), - } - } - - pub fn fee_details(&self) -> FeeDetails { - match self { - Self::Executed(executed_tx) => executed_tx.loaded_transaction.fee_details, - Self::FeesOnly(details) => details.fee_details, - } - } - - pub fn executed_transaction(&self) -> Option<&ExecutedTransaction> { - match self { - Self::Executed(context) => Some(context), - Self::FeesOnly { .. } => None, - } - } - - pub fn execution_details(&self) -> Option<&TransactionExecutionDetails> { - match self { - Self::Executed(context) => Some(&context.execution_details), - Self::FeesOnly { .. } => None, - } - } - - pub fn executed_units(&self) -> u64 { - self.execution_details() - .map(|detail| detail.executed_units) - .unwrap_or_default() - } - - pub fn loaded_accounts_data_size(&self) -> u32 { - match self { - Self::Executed(context) => context.loaded_transaction.loaded_accounts_data_size, - Self::FeesOnly(details) => details.loaded_accounts_data_size, - } + .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 index 235d6b9..682f338 100644 --- a/solana/svm/src/transaction_processor.rs +++ b/solana/svm/src/transaction_processor.rs @@ -1,52 +1,34 @@ +#[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::{ - AccountLoader, CheckedTransactionDetails, LoadedTransaction, PROGRAM_OWNERS, - TransactionCheckResult, TransactionLoadResult, ValidatedTransactionDetails, - load_transaction, update_rent_exempt_status_for_account, validate_fee_payer, + CheckedTransactionDetails, LoadedTransaction, TransactionLoadResult, + ValidatedTransactionDetails, load_transaction, }, - account_overrides::AccountOverrides, message_processor::process_message, - nonce_info::NonceInfo, - program_loader::{get_program_deployment_slot, load_program_with_pubkey}, - rollback_accounts::RollbackAccounts, - transaction_account_state_info::{ - TransactionAccountStateInfo, get_uninitialized_accounts_size, - }, + program_loader::load_program, + transaction_account_state_info::TransactionAccountStateInfo, transaction_balances::{BalanceCollectionRoutines, BalanceCollector}, - transaction_error_metrics::TransactionErrorMetrics, - transaction_execution_result::{ - AccountsDeltas, ExecutedTransaction, TransactionExecutionDetails, - }, - transaction_processing_result::{ProcessedTransaction, TransactionProcessingResult}, + transaction_execution_result::{ExecutedTransaction, TransactionExecutionDetails}, + transaction_processing_result::TransactionProcessingResult, }, - log::debug, - percentage::Percentage, - solana_account::{AccountSharedData, ReadableAccount, state_traits::StateMut}, - solana_clock::{Epoch, Slot}, + 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_nonce::{ - NONCED_TX_MARKER_IX_INDEX, - state::{DurableNonce, State as NonceState}, - versions::Versions as NonceVersions, - }, - solana_nonce_account::{SystemAccountKind, get_system_account_kind, verify_nonce_account}, solana_program_runtime::{ - execution_budget::{ - SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionCost, - }, + execution_budget::SVMTransactionExecutionCost, invoke_context::{EnvironmentConfig, InvokeContext}, - loaded_programs::{ - EpochBoundaryPreparation, ForkGraph, ProgramCache, ProgramCacheForTxBatch, - ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, ProgramRuntimeEnvironments, - }, - program_cache_entry::ProgramCacheEntry, - solana_sbpf::{program::BuiltinProgram, vm::Config as VmConfig}, + loaded_programs::{ProgramCache, ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, sysvar_cache::SysvarCache, }, solana_pubkey::Pubkey, @@ -54,53 +36,45 @@ use { solana_svm_callback::TransactionProcessingCallback, solana_svm_feature_set::SVMFeatureSet, solana_svm_log_collector::LogCollector, - solana_svm_measure::{measure::Measure, measure_us}, - solana_svm_timings::{ExecuteTimingType, ExecuteTimings}, - solana_svm_transaction::{svm_message::SVMMessage, svm_transaction::SVMTransaction}, - solana_svm_type_overrides::sync::{Arc, RwLock, RwLockReadGuard, atomic::Ordering}, + solana_svm_transaction::svm_transaction::SVMTransaction, + solana_svm_type_overrides::sync::Arc, solana_transaction_context::transaction::{ExecutionRecord, TransactionContext}, - solana_transaction_error::{TransactionError, TransactionResult}, + solana_transaction_error::TransactionError, std::{ - collections::{HashMap, HashSet}, fmt::{Debug, Formatter}, rc::Rc, }, }; -#[cfg(feature = "dev-context-only-utils")] -use { - qualifier_attr::{field_qualifiers, qualifiers}, - std::sync::Weak, -}; -/// A list of log messages emitted during a transaction +/// Log messages emitted during a transaction. pub type TransactionLogMessages = Vec; -/// The output of the transaction batch processor's -/// `load_and_execute_sanitized_transactions` method. -pub struct LoadAndExecuteSanitizedTransactionsOutput { - /// Error metrics for transactions that were processed. - pub error_metrics: TransactionErrorMetrics, - /// Timings for transaction batch execution. - pub execute_timings: ExecuteTimings, - /// Vector of results indicating whether a transaction was processed or - /// could not be processed. Note processed transactions can still have a - /// failure result meaning that the transaction will be rolled back. - pub processing_results: Vec, - /// Balances accumulated for TransactionStatusSender when - /// transaction balance recording is enabled. +/// 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, } -/// Configuration of the recording capabilities for transaction execution +/// 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, @@ -111,841 +85,193 @@ impl ExecutionRecordingConfig { } } -/// Configurations for processing transactions. +/// Transaction execution options. #[derive(Default)] -pub struct TransactionProcessingConfig<'a> { - /// Encapsulates overridden accounts, typically used for transaction - /// simulation. - pub account_overrides: Option<&'a AccountOverrides>, - /// Whether or not to check a program's deployment slot when replenishing - /// a program cache instance. - pub check_program_deployment_slot: bool, +pub struct TransactionProcessingConfig { /// The maximum number of bytes that log messages can consume. pub log_messages_bytes_limit: Option, - /// Whether to limit the number of programs loaded for the transaction - /// batch. - pub limit_to_load_programs: bool, /// Recording capabilities for transaction execution. pub recording_config: ExecutionRecordingConfig, - /// Should failing transactions within the batch be dropped (no fee charged - /// & not committed). - pub drop_on_failure: bool, - /// If any transaction in the batch is not committed then the entire batch - /// should not be committed. - /// - /// # Note - /// - /// Without `drop_on_failure` this flag will still allow processed but - /// failing transactions to be committed. If both flags are set then any - /// failing transaction will cause all transactions to be aborted. - pub all_or_nothing: bool, - /// Strictly require durable nonce accounts to have the canonical nonce account size. - /// - /// This is a leader-side filtering policy. It must not be enabled for replay. - pub strict_nonce_size_check: bool, } -/// Runtime environment for transaction batch processing. +/// Runtime inputs that are shared across a transaction execution. +#[derive(Default)] pub struct TransactionProcessingEnvironment { - /// The blockhash to use for the transaction batch. + /// Blockhash exposed to programs through the invocation environment. pub blockhash: Hash, - /// Lamports per signature that corresponds to this blockhash. - /// - /// Note: This value is primarily used for nonce accounts. If set to zero, - /// it will disable transaction fees. However, any non-zero value will not - /// change transaction fees. For this reason, it is recommended to use the - /// `fee_per_signature` field to adjust transaction fees. + /// Lamports per signature associated with `blockhash`. pub blockhash_lamports_per_signature: u64, - /// Whether the alpenglow migration has completed for this bank context. - pub alpenglow_migration_succeeded: bool, - /// The total stake for the current epoch. + /// Retained for API compatibility; the current execution path does not use + /// stake weighting. pub epoch_total_stake: u64, - /// Runtime feature set to use for the transaction batch. + /// Runtime feature set used during execution. pub feature_set: SVMFeatureSet, - /// Program runtime environments for execution and deployment. - pub program_runtime_environments: ProgramRuntimeEnvironments, - /// Rent calculator to use for the transaction batch. + /// 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(feature = "dev-context-only-utils")] -pub fn get_mock_transaction_processing_environment() -> TransactionProcessingEnvironment { - TransactionProcessingEnvironment { - blockhash: Hash::default(), - blockhash_lamports_per_signature: 0, - alpenglow_migration_succeeded: false, - epoch_total_stake: 0, - feature_set: SVMFeatureSet::default(), - program_runtime_environments: ProgramRuntimeEnvironments::mock(), - rent: Rent::default(), - } -} - -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[cfg_attr( feature = "dev-context-only-utils", - field_qualifiers(slot(pub), epoch(pub), sysvar_cache(pub)) + field_qualifiers(slot(pub), sysvar_cache(pub)) )] -pub struct TransactionBatchProcessor { - /// Bank slot (i.e. block) - slot: Slot, - - /// Bank epoch - epoch: Epoch, - - /// SysvarCache is a collection of system variables that are - /// accessible from on chain programs. It is passed to SVM from - /// client code (e.g. Bank) and forwarded to process_message. - sysvar_cache: RwLock, - - /// Anticipates the environments of the upcoming epoch - pub epoch_boundary_preparation: Arc>, +pub struct TransactionBatchProcessor { + /// Slot associated with this processor. + pub slot: Slot, - /// Programs required for transaction batch processing - pub global_program_cache: Arc>>, + /// Sysvars exposed to programs during execution. + sysvar_cache: SysvarCache, - /// ProgramRuntimeEnvironment of the current epoch - pub program_runtime_environment: ProgramRuntimeEnvironment, - - /// Builtin program ids - pub builtin_program_ids: RwLock>, - - /// Cached ProgramCacheForTxBatch pre-populated with builtin entries. - /// Populated once per block in `new_from()` from the global program cache, - /// avoiding re-acquiring the lock and re-running extract() on every batch. - builtin_program_cache: RwLock, + /// Shared cache of loaded programs. + pub program_cache: Arc, execution_cost: SVMTransactionExecutionCost, } -impl Debug for TransactionBatchProcessor { +impl Debug for TransactionBatchProcessor { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("TransactionBatchProcessor") .field("slot", &self.slot) - .field("epoch", &self.epoch) .field("sysvar_cache", &self.sysvar_cache) - .field("global_program_cache", &self.global_program_cache) + .field("program_cache", &self.program_cache) .finish() } } -impl Default for TransactionBatchProcessor { +impl Default for TransactionBatchProcessor { fn default() -> Self { Self { slot: Slot::default(), - epoch: Epoch::default(), - sysvar_cache: RwLock::::default(), - epoch_boundary_preparation: Arc::new(RwLock::new(EpochBoundaryPreparation::default())), - global_program_cache: Arc::new(RwLock::new(ProgramCache::new(Slot::default()))), - program_runtime_environment: ProgramRuntimeEnvironment::from( - BuiltinProgram::new_loader(VmConfig::default()), - ), - builtin_program_ids: RwLock::new(HashSet::new()), - builtin_program_cache: RwLock::new(ProgramCacheForTxBatch::new(Slot::default())), + sysvar_cache: Default::default(), + program_cache: Arc::new(ProgramCache::default()), execution_cost: SVMTransactionExecutionCost::default(), } } } -impl TransactionBatchProcessor { +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) and has not been configured with a valid - /// fork graph. - /// - /// When using this method, it's advisable to call `set_fork_graph_in_program_cache` - /// as well as `add_builtin` to configure the cache before using the processor. - pub fn new_uninitialized(slot: Slot, epoch: Epoch) -> Self { - let epoch_boundary_preparation = - Arc::new(RwLock::new(EpochBoundaryPreparation::new(epoch))); + /// programs, including builtins. + pub fn new_uninitialized(slot: Slot, cache: Arc) -> Self { Self { slot, - epoch, - epoch_boundary_preparation, - global_program_cache: Arc::new(RwLock::new(ProgramCache::new(slot))), - builtin_program_cache: RwLock::new(ProgramCacheForTxBatch::new(slot)), + program_cache: cache, ..Self::default() } } /// Create a new `TransactionBatchProcessor`. /// - /// The created processor's program cache is initialized with the provided - /// fork graph and loaders. If any loaders are omitted, a default "empty" - /// loader (no syscalls) will be used. - /// - /// The cache will still not contain any builtin programs. It's advisable to - /// call `add_builtin` to add the required builtins before using the processor. - #[cfg(feature = "dev-context-only-utils")] - pub fn new( - slot: Slot, - epoch: Epoch, - fork_graph: Weak>, - program_runtime_environment: Option, - ) -> Self { - let mut processor = Self::new_uninitialized(slot, epoch); - processor - .global_program_cache - .write() - .unwrap() - .set_fork_graph(fork_graph); - let empty_loader = || ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); - processor - .global_program_cache - .write() - .unwrap() - .latest_root_slot = processor.slot; - processor - .epoch_boundary_preparation - .write() - .unwrap() - .upcoming_epoch = processor.epoch; - processor.program_runtime_environment = - program_runtime_environment.unwrap_or(empty_loader()); - processor - } - - /// Create a new `TransactionBatchProcessor` from the current instance, but - /// with the provided slot and epoch. - /// - /// * Inherits the program cache and builtin program ids from the current - /// instance. - /// * Resets the sysvar cache. - pub fn new_from(&self, slot: Slot, epoch: Epoch) -> Self { - let builtin_program_ids = self.builtin_program_ids.read().unwrap().clone(); - let environments = self.program_runtime_environment.clone(); - - // Pre-populate the builtin program cache from the global cache. - // This is done once per block rather than once per batch. - let mut builtin_program_cache = ProgramCacheForTxBatch::new(slot); - let mut search_for: Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> = builtin_program_ids - .iter() - .map(|key| (*key, ProgramCacheMatchCriteria::NoCriteria, 0)) - .collect(); - self.global_program_cache.read().unwrap().extract( - &mut search_for, - &mut builtin_program_cache, - &environments, - false, - false, - ); - - Self { - slot, - epoch, - sysvar_cache: RwLock::::default(), - epoch_boundary_preparation: self.epoch_boundary_preparation.clone(), - global_program_cache: self.global_program_cache.clone(), - program_runtime_environment: environments, - builtin_program_ids: RwLock::new(builtin_program_ids), - builtin_program_cache: RwLock::new(builtin_program_cache), - execution_cost: self.execution_cost, - } + /// 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 for the transactions that this instance of transaction processor - /// will execute. + /// Sets the base execution cost charged by this processor. pub fn set_execution_cost(&mut self, cost: SVMTransactionExecutionCost) { self.execution_cost = cost; } - /// Updates the environments when entering a new Epoch. - pub fn set_program_runtime_environment(&mut self, new_environment: ProgramRuntimeEnvironment) { - // First update the environment only if it is different - if *self.program_runtime_environment != *new_environment { - self.program_runtime_environment = new_environment; - } - // Then try to consolidate with the upcoming environment (to reuse the address) - if let Some(upcoming_environment) = &self - .epoch_boundary_preparation - .read() - .unwrap() - .upcoming_environment - { - let upcoming_environment = ProgramRuntimeEnvironment::clone(upcoming_environment); - if self.program_runtime_environment != upcoming_environment - && *self.program_runtime_environment == *upcoming_environment - { - // Use the prediction if equal but not identical - self.program_runtime_environment = upcoming_environment; - } - } + /// Returns mutable access to cached sysvars for the current processor slot. + pub fn sysvar_cache_mut(&mut self) -> &mut SysvarCache { + &mut self.sysvar_cache } - /// Returns the current environments depending on the given epoch - /// Returns None if the call could result in a deadlock - pub fn program_runtime_environment_for_epoch(&self, epoch: Epoch) -> ProgramRuntimeEnvironment { - self.epoch_boundary_preparation - .read() - .unwrap() - .get_upcoming_environment_for_epoch(epoch) - .unwrap_or_else(|| ProgramRuntimeEnvironment::clone(&self.program_runtime_environment)) - } - - pub fn sysvar_cache(&self) -> RwLockReadGuard<'_, SysvarCache> { - self.sysvar_cache.read().unwrap() - } - - /// Main entrypoint to the SVM. - pub fn load_and_execute_sanitized_transactions( + /// Loads accounts, prepares programs, and executes one sanitized transaction. + pub fn load_and_execute_sanitized_transaction( &self, callbacks: &CB, - sanitized_txs: &[impl SVMTransaction], - check_results: Vec, + tx: &impl SVMTransaction, + details: CheckedTransactionDetails, environment: &TransactionProcessingEnvironment, config: &TransactionProcessingConfig, - ) -> LoadAndExecuteSanitizedTransactionsOutput { - // If `check_results` does not have the same length as `sanitized_txs`, - // transactions could be truncated as a result of `.iter().zip()` in - // many of the below methods. - // See . - debug_assert_eq!( - sanitized_txs.len(), - check_results.len(), - "Length of check_results does not match length of sanitized_txs" - ); - - // Initialize metrics. - let mut error_metrics = TransactionErrorMetrics::default(); - let mut execute_timings = ExecuteTimings::default(); - let mut processing_results = Vec::with_capacity(sanitized_txs.len()); - - // Determine a capacity for the internal account cache. This - // over-allocates but avoids ever reallocating, and spares us from - // deduplicating the account keys lists. - let account_keys_in_batch = sanitized_txs.iter().map(|tx| tx.account_keys().len()).sum(); - - // Create the account loader, which wraps all external account fetching. - let mut account_loader = AccountLoader::new_with_loaded_accounts_capacity( - config.account_overrides, - callbacks, - &environment.feature_set, - account_keys_in_batch, - ); - + ) -> LoadAndExecuteSanitizedTransactionOutput { // Create the transaction balance collector if recording is enabled. let mut balance_collector = config .recording_config .enable_transaction_balance_recording - .then(|| BalanceCollector::new_with_transaction_count(sanitized_txs.len())); - - // Clone the batch-local program cache (builtins already populated in new_from()). - // User-deployed programs are loaded per-transaction via replenish_program_cache - // in the transaction loop below. - let mut program_cache_for_tx_batch = self.builtin_program_cache.read().unwrap().clone(); - - if program_cache_for_tx_batch.hit_max_limit { - return LoadAndExecuteSanitizedTransactionsOutput { - error_metrics, - execute_timings, - processing_results: (0..sanitized_txs.len()) - .map(|_| Err(TransactionError::ProgramCacheHitMaxLimit)) - .collect(), - // If we abort the batch and balance recording is enabled, no balances should be - // collected. If this is a leader thread, no batch will be committed. - balance_collector: None, - }; - } - - let (mut load_us, mut execution_us): (u64, u64) = (0, 0); - - // Validate, execute, and collect results from each transaction in order. - // With SIMD83, transactions must be executed in order, because transactions - // in the same batch may modify the same accounts. Transaction order is - // preserved within entries written to the ledger. - for (tx, check_result) in sanitized_txs.iter().zip(check_results) { - let (validate_result, validate_fees_us) = - measure_us!(check_result.and_then(|tx_details| { - Self::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - tx, - tx_details, - &environment.blockhash, - environment.blockhash_lamports_per_signature, - &environment.rent, - config.strict_nonce_size_check, - &mut error_metrics, - ) - })); - execute_timings - .saturating_add_in_place(ExecuteTimingType::ValidateFeesUs, validate_fees_us); - - let (load_result, single_load_us) = measure_us!(load_transaction( - &mut account_loader, - tx, - validate_result, - &mut error_metrics, - &environment.rent, - )); - load_us = load_us.saturating_add(single_load_us); - - let ((), collect_balances_us) = - measure_us!(balance_collector.collect_pre_balances(&mut account_loader, tx)); - execute_timings - .saturating_add_in_place(ExecuteTimingType::CollectBalancesUs, collect_balances_us); - - let (processing_result, single_execution_us) = measure_us!(match load_result { - TransactionLoadResult::NotLoaded(err) => Err(err), - TransactionLoadResult::FeesOnly(fees_only_tx) => match config.drop_on_failure { - true => Err(fees_only_tx.load_error), - false => { - // Update loaded accounts cache with nonce and fee-payer - account_loader.update_accounts_for_failed_tx( - &fees_only_tx.rollback_accounts, - self.slot, - ); - - Ok(ProcessedTransaction::FeesOnly(Box::new(fees_only_tx))) - } - }, - TransactionLoadResult::Loaded(loaded_transaction) => { - let (program_accounts_set, filter_executable_us) = - measure_us!(self.filter_executable_program_accounts( - &account_loader, - &mut program_cache_for_tx_batch, - tx, - )); - execute_timings.saturating_add_in_place( - ExecuteTimingType::FilterExecutableUs, - filter_executable_us, - ); - - let ((), program_cache_us) = measure_us!({ - self.replenish_program_cache( - &account_loader, - &program_accounts_set, - environment - .program_runtime_environments - .get_env_for_execution(), - &mut program_cache_for_tx_batch, - &mut execute_timings, - config.check_program_deployment_slot, - config.limit_to_load_programs, - true, // increment_usage_counter - ); - }); - execute_timings.saturating_add_in_place( - ExecuteTimingType::ProgramCacheUs, - program_cache_us, - ); - - if program_cache_for_tx_batch.hit_max_limit { - return LoadAndExecuteSanitizedTransactionsOutput { - error_metrics, - execute_timings, - processing_results: (0..sanitized_txs.len()) - .map(|_| Err(TransactionError::ProgramCacheHitMaxLimit)) - .collect(), - // If we abort the batch and balance recording is enabled, no balances should be - // collected. If this is a leader thread, no batch will be committed. - balance_collector: None, - }; - } - - let executed_tx = self.execute_loaded_transaction( - callbacks, - tx, - loaded_transaction, - &mut execute_timings, - &mut error_metrics, - &mut program_cache_for_tx_batch, - environment, - config, - ); - - match ( - &executed_tx.execution_details.status, - config.drop_on_failure, - ) { - // Successful transactions need to update the account loader cache as future - // transactions in the batch may depend on them. - (Ok(_), _) => { - account_loader.update_accounts_for_successful_tx( - tx, - &executed_tx.loaded_transaction.accounts, - self.slot, - ); - // Also update local program cache with modifications made by the - // transaction, if it executed successfully. - program_cache_for_tx_batch.merge(&executed_tx.programs_modified_by_tx); - - Ok(ProcessedTransaction::Executed(Box::new(executed_tx))) - } - // If the transaction failed & drop on failure is set then we don't want to - // update the accounts as this transaction will be dropped from the batch. - (Err(err), true) => Err(err.clone()), - // Unsuccessful transactions will still update rollback accounts (fee payer, - // nonce, etc). - (Err(_), false) => { - account_loader.update_accounts_for_failed_tx( - &executed_tx.loaded_transaction.rollback_accounts, - self.slot, - ); - - Ok(ProcessedTransaction::Executed(Box::new(executed_tx))) - } - } - } - }); - execution_us = execution_us.saturating_add(single_execution_us); - - let ((), collect_balances_us) = - measure_us!(balance_collector.collect_post_balances(&mut account_loader, tx)); - execute_timings - .saturating_add_in_place(ExecuteTimingType::CollectBalancesUs, collect_balances_us); - - // If this is an all or nothing batch and we failed to process this transaction then we - // must abort all prior/remaining transactions. - if config.all_or_nothing && processing_result.is_err() { - // Abort prior transactions. - for res in processing_results.iter_mut() { - *res = Err(TransactionError::CommitCancelled); - } + .then(BalanceCollector::default); - // Preserve the failure that triggered the batch to abort. - processing_results.push(processing_result); + // Create the batch-local program cache. + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(self.slot); - // Abort remaining transactions. - processing_results.extend( - (0..sanitized_txs.len() - processing_results.len()) - .map(|_| Err(TransactionError::CommitCancelled)), + 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, ); - return LoadAndExecuteSanitizedTransactionsOutput { - error_metrics, - execute_timings, - processing_results, - // If we abort the batch and balance recording is enabled, no balances should be - // collected. If this is a leader thread, no batch will be committed. - balance_collector: None, - }; - } - - processing_results.push(processing_result); - } - - // Skip eviction when there's no chance this particular tx batch has increased the size of - // ProgramCache entries. Note that loaded_missing is deliberately defined, so that there's - // still at least one other batch, which will evict the program cache, even after the - // occurrences of cooperative loading. - if program_cache_for_tx_batch.loaded_missing || program_cache_for_tx_batch.merged_modified { - const SHRINK_LOADED_PROGRAMS_TO_PERCENTAGE: u8 = 90; - self.global_program_cache - .write() - .unwrap() - .evict_using_random_selection( - Percentage::from(SHRINK_LOADED_PROGRAMS_TO_PERCENTAGE), - self.slot, + let mut executed_tx = self.execute_loaded_transaction( + callbacks, + tx, + loaded_transaction, + &mut program_cache_for_tx_batch, + environment, + config, ); - } - - debug!( - "load: {}us execute: {}us txs_len={}", - load_us, - execution_us, - sanitized_txs.len(), - ); - execute_timings.saturating_add_in_place(ExecuteTimingType::LoadUs, load_us); - execute_timings.saturating_add_in_place(ExecuteTimingType::ExecuteUs, execution_us); - - if let Some(ref balance_collector) = balance_collector { - debug_assert!(balance_collector.lengths_match_expected(sanitized_txs.len())); - } - - LoadAndExecuteSanitizedTransactionsOutput { - error_metrics, - execute_timings, - processing_results, - balance_collector, - } - } - - fn validate_transaction_nonce_and_fee_payer( - account_loader: &mut AccountLoader, - message: &impl SVMMessage, - checked_details: CheckedTransactionDetails, - environment_blockhash: &Hash, - next_lamports_per_signature: u64, - rent: &Rent, - strict_nonce_size_check: bool, - error_counters: &mut TransactionErrorMetrics, - ) -> TransactionResult { - let CheckedTransactionDetails { - nonce_address, - compute_budget_and_limits, - } = checked_details; - - // If this is a nonce transaction, validate the nonce info. - // This must be done for every transaction to support SIMD83 because - // it may have changed due to use, authorization, or deallocation. - let nonce_info = if let Some(ref nonce_address) = nonce_address { - let next_durable_nonce = DurableNonce::from_blockhash(environment_blockhash); - Some(Self::validate_transaction_nonce( - account_loader, - message, - nonce_address, - &next_durable_nonce, - next_lamports_per_signature, - strict_nonce_size_check, - error_counters, - )?) - } else { - None - }; - - // Now validate the fee-payer for the transaction unconditionally. - Self::validate_transaction_fee_payer( - account_loader, - message, - nonce_info, - compute_budget_and_limits, - rent, - error_counters, - ) - } - - // Loads transaction fee payer, collects rent if necessary, then calculates - // transaction fees, and deducts them from the fee payer balance. If the - // account is not found or has insufficient funds, an error is returned. - fn validate_transaction_fee_payer( - account_loader: &mut AccountLoader, - message: &impl SVMMessage, - nonce_info: Option, - compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits, - rent: &Rent, - error_counters: &mut TransactionErrorMetrics, - ) -> TransactionResult { - let fee_payer_address = message.fee_payer(); - - // We *must* use load_transaction_account() here because *this* is when the fee-payer - // is loaded for the transaction. Transaction loading skips the first account and - // loads (and thus inspects) all others normally. - let Some(mut loaded_fee_payer) = - account_loader.load_transaction_account(fee_payer_address, true) - else { - error_counters.account_not_found += 1; - return Err(TransactionError::AccountNotFound); - }; - - let fee_payer_loaded_rent_epoch = loaded_fee_payer.account.rent_epoch(); - update_rent_exempt_status_for_account(rent, &mut loaded_fee_payer.account); - - let fee_payer_index = 0; - validate_fee_payer( - fee_payer_address, - &mut loaded_fee_payer.account, - fee_payer_index, - error_counters, - rent, - compute_budget_and_limits.fee_details.total_fee(), - )?; - - // Capture fee-subtracted fee payer account and next nonce account state - // to commit if transaction execution fails. - let rollback_accounts = RollbackAccounts::new( - nonce_info, - *fee_payer_address, - loaded_fee_payer.account.clone(), - fee_payer_loaded_rent_epoch, - ); - - Ok(ValidatedTransactionDetails { - fee_details: compute_budget_and_limits.fee_details, - rollback_accounts, - loaded_accounts_bytes_limit: compute_budget_and_limits.loaded_accounts_data_size_limit, - compute_budget: compute_budget_and_limits.budget, - loaded_fee_payer_account: loaded_fee_payer, - }) - } - - fn validate_transaction_nonce( - account_loader: &mut AccountLoader, - message: &impl SVMMessage, - nonce_address: &Pubkey, - next_durable_nonce: &DurableNonce, - next_lamports_per_signature: u64, - strict_nonce_size_check: bool, - error_counters: &mut TransactionErrorMetrics, - ) -> TransactionResult { - // When SIMD83 is enabled, if the nonce has been used in this batch already, we must drop - // the transaction. This is the same as if it was used in different batches in the same slot. - // It is possible that the nonce account was used, closed, closed and reopened, closed and - // spoofed by a non-system program, or had its authority changed. Such a transaction cannot - // be processed, even as fee-only. - - let Some(mut nonce_account) = account_loader - .load_transaction_account(nonce_address, true) - .map(|loaded| loaded.account) - else { - error_counters.account_not_found += 1; - return Err(TransactionError::AccountNotFound); - }; - - if strict_nonce_size_check - && get_system_account_kind(&nonce_account) != Some(SystemAccountKind::Nonce) - { - error_counters.blockhash_not_found += 1; - return Err(TransactionError::BlockhashNotFound); - } - - // This function verifies: - // * Nonce account owner is SystemProgram - // * Nonce account parses as State::Initialized - // * Stored durable nonce matches the message blockhash - let Some(nonce_data) = verify_nonce_account(&nonce_account, message.recent_blockhash()) - else { - error_counters.blockhash_not_found += 1; - return Err(TransactionError::BlockhashNotFound); + if executed_tx.access_is_valid(tx) { + balance_collector + .collect_post_balances(&executed_tx.loaded_transaction.accounts); + let cache = program_cache_for_tx_batch.drain_modified_entries(); + self.program_cache.merge(&cache); + } + Ok(Box::new(executed_tx)) + } }; - // We must still check that the nonce account is usable and that its authority has signed. - let nonce_can_be_advanced = &nonce_data.durable_nonce != next_durable_nonce; - let nonce_authority_is_valid = message - .get_ix_signers(NONCED_TX_MARKER_IX_INDEX as usize) - .any(|signer| signer == &nonce_data.authority); - - if nonce_can_be_advanced && nonce_authority_is_valid { - let next_nonce_state = NonceState::new_initialized( - &nonce_data.authority, - *next_durable_nonce, - next_lamports_per_signature, - ); - nonce_account - .set_state(&NonceVersions::new(next_nonce_state)) - .expect("Serializing into a validated nonce account cannot fail"); - - Ok(NonceInfo::new(*nonce_address, nonce_account)) - } else { - error_counters.blockhash_not_found += 1; - Err(TransactionError::BlockhashNotFound) - } - } - - /// Appends to a set of executable program accounts (all accounts owned by any loader) - /// for transactions with a valid blockhash or nonce. - fn filter_executable_program_accounts( - &self, - account_loader: &AccountLoader, - program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, - tx: &impl SVMMessage, - ) -> HashMap { - let mut program_accounts_set = HashMap::default(); - for account_key in tx.account_keys().iter() { - if let Some(cache_entry) = program_cache_for_tx_batch.find(account_key) { - cache_entry.stats.uses.fetch_add(1, Ordering::Relaxed); - } else if let Some((account, last_modification_slot)) = - account_loader.get_account_shared_data(account_key) - && PROGRAM_OWNERS.contains(account.owner()) - { - program_accounts_set.insert(*account_key, last_modification_slot); - } + LoadAndExecuteSanitizedTransactionOutput { + processing_result, + balance_collector, } - program_accounts_set } #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] - fn replenish_program_cache( + fn replenish_program_cache( &self, - account_loader: &AccountLoader, - program_accounts_set: &HashMap, - program_runtime_environment_for_execution: &ProgramRuntimeEnvironment, + environments: &ProgramRuntimeEnvironments, program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, - execute_timings: &mut ExecuteTimings, - check_program_deployment_slot: bool, - limit_to_load_programs: bool, - increment_usage_counter: bool, + accounts: &[KeyedAccountSharedData], ) { - let mut missing_programs: Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> = - program_accounts_set - .iter() - .map(|(pubkey, last_modification_slot)| { - let match_criteria = if check_program_deployment_slot { - get_program_deployment_slot(account_loader, pubkey) - .map_or(ProgramCacheMatchCriteria::Tombstone, |slot| { - ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) - }) - } else { - ProgramCacheMatchCriteria::NoCriteria - }; - (*pubkey, match_criteria, *last_modification_slot) - }) - .collect(); - - let mut count_hits_and_misses = true; - loop { - // Lock the global cache. - let global_program_cache = self.global_program_cache.read().unwrap(); - // Figure out which program needs to be loaded next. - let program_to_load = global_program_cache.extract( - &mut missing_programs, - program_cache_for_tx_batch, - program_runtime_environment_for_execution, - increment_usage_counter, - count_hits_and_misses, - ); - count_hits_and_misses = false; - let task_waiter = Arc::clone(&global_program_cache.loading_task_waiter); - let task_cookie = task_waiter.cookie(); - // Unlock the global cache again. - drop(global_program_cache); - - let program_to_store = program_to_load.map(|key| { - // Load, verify and compile one program. - let (program, last_modification_slot) = load_program_with_pubkey( - account_loader, - program_runtime_environment_for_execution, - &key, - self.slot, - execute_timings, - ) - .expect("called load_program_with_pubkey() with nonexistent account"); - (key, program, last_modification_slot) - }); - - if let Some((key, program, last_modification_slot)) = program_to_store { - program_cache_for_tx_batch.loaded_missing = true; - let mut global_program_cache = self.global_program_cache.write().unwrap(); - // Submit our last completed loading task. - if global_program_cache.finish_cooperative_loading_task( - program_runtime_environment_for_execution, - self.slot, - key, - last_modification_slot, - program, - ) && limit_to_load_programs - { - // This branch is taken when there is an error in assigning a program to a - // cache slot. It is not possible to mock this error for SVM unit - // tests purposes. - *program_cache_for_tx_batch = ProgramCacheForTxBatch::new(self.slot); - program_cache_for_tx_batch.hit_max_limit = true; - return; - } - } else if missing_programs.is_empty() { - break; - } else { - // Remember: there are multiple transaction processor threads running concurrently - // and those other threads may be loading this or other programs. - // - // So, sleep until some other thread submits a program with their - // `finish_cooperative_loading_task` call. We'll then wake up and try to load the - // missing programs inside the tx batch again. - let _new_cookie = task_waiter.wait(task_cookie); + 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); } } - /// Execute a transaction using the provided loaded accounts and update - /// the executors cache if the transaction was successful. + /// 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, - execute_timings: &mut ExecuteTimings, - error_metrics: &mut TransactionErrorMetrics, program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, environment: &TransactionProcessingEnvironment, config: &TransactionProcessingConfig, @@ -993,7 +319,6 @@ impl TransactionBatchProcessor { }; let mut executed_units = 0u64; - let sysvar_cache = &self.sysvar_cache.read().unwrap(); let mut invoke_context = InvokeContext::new( &mut transaction_context, @@ -1001,56 +326,35 @@ impl TransactionBatchProcessor { EnvironmentConfig::new( environment.blockhash, environment.blockhash_lamports_per_signature, - environment.alpenglow_migration_succeeded, callback, &environment.feature_set, - &environment.program_runtime_environments, - sysvar_cache, + &environment.program_runtime_environments_for_execution, + &self.sysvar_cache, ), log_collector.clone(), compute_budget, self.execution_cost, ); - let mut process_message_time = Measure::start("process_message_time"); let process_result = process_message( tx, + &loaded_transaction.program_indices, &mut invoke_context, - execute_timings, &mut executed_units, ); - process_message_time.stop(); drop(invoke_context); - execute_timings.execute_accessories.process_message_us += process_message_time.as_us(); - - let mut post_account_state_info_result = process_result - .and_then(|_| { - 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(|_| post_account_state_info) - }) - .map_err(|err| { - match err { - TransactionError::InvalidRentPayingAccount - | TransactionError::InsufficientFundsForRent { .. } => { - error_metrics.invalid_rent_paying_account += 1; - } - TransactionError::InvalidAccountIndex => { - error_metrics.invalid_account_index += 1; - } - _ => { - error_metrics.instruction_error += 1; - } - } - err - }); + 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| { @@ -1067,37 +371,20 @@ impl TransactionBatchProcessor { let ExecutionRecord { accounts, return_data, - touched_account_count, - accounts_resize_delta, + accounts_resize_delta: accounts_data_len_delta, + .. } = execution_record; - if post_account_state_info_result.is_ok() + if status.is_ok() && transaction_accounts_lamports_sum(&accounts) .filter(|lamports_after_tx| lamports_before_tx == *lamports_after_tx) .is_none() { - post_account_state_info_result = Err(TransactionError::UnbalancedTransaction); + status = Err(TransactionError::UnbalancedTransaction); } - - // accounts_resize_delta and accounts_uninitialized_size must be set to None - // in the result if status is an error - let (status, accounts_deltas) = post_account_state_info_result - .map(|post_state_info| { - ( - Ok(()), - Some(AccountsDeltas { - accounts_resize_delta, - accounts_uninitialized_size: get_uninitialized_accounts_size( - &post_state_info, - ), - }), - ) - }) - .unwrap_or_else(|err| (Err(err), None)); + let status = status.map(|_| ()); loaded_transaction.accounts = accounts; - execute_timings.details.total_account_count += loaded_transaction.accounts.len() as u64; - execute_timings.details.changed_account_count += touched_account_count; let return_data = if config.recording_config.enable_return_data_recording && !return_data.data.is_empty() @@ -1110,14 +397,13 @@ impl TransactionBatchProcessor { ExecutedTransaction { execution_details: TransactionExecutionDetails { status, - log_messages, + log_messages: log_messages.map(Arc::new), inner_instructions, return_data, executed_units, - accounts_deltas, + accounts_data_len_delta, }, loaded_transaction, - programs_modified_by_tx: program_cache_for_tx_batch.drain_modified_entries(), } } @@ -1135,66 +421,21 @@ impl TransactionBatchProcessor { .unwrap_or(true) ); - let top_level_ixs_num = transaction_context - .get_instruction_trace_length() - .saturating_sub(transaction_context.number_of_cpis_in_trace()); - // This vector is a map between CPI number in trace (not counting top level - // instructions) and the top level caller index. - // In TransactionContext, caller instructions always precede callee instructions, so - // we can use it to avoid backtracking on instructions callers to - // find the top level instruction that started the call chain. - let mut parent_positions: Vec = - vec![usize::MAX; transaction_context.number_of_cpis_in_trace()]; let (ix_trace, accounts, ix_data_trace) = transaction_context.take_instruction_trace(); - let mut outer_instructions: Vec> = - vec![Vec::new(); top_level_ixs_num]; - for (cpi_num, ((ix_in_trace, ix_data), ix_accounts)) in ix_trace - .into_iter() - .zip(ix_data_trace) - .zip(accounts) - .skip(top_level_ixs_num) - .enumerate() + let mut outer_instructions = Vec::new(); + for ((ix_in_trace, ix_data), ix_accounts) in + ix_trace.into_iter().zip(ix_data_trace).zip(accounts) { - let caller_ix = ix_in_trace.index_of_caller_instruction; - debug_assert_ne!(caller_ix, u16::MAX, "Instruction is not a CPI"); - - // If the caller index is less than the number of top level instructions, - // it directly represents a top level instruction index. - // Top level instructions precede all CPIs in the instruction trace. - let outer_index = if (caller_ix as usize) < top_level_ixs_num { - *parent_positions.get_mut(cpi_num).unwrap() = caller_ix as usize; - caller_ix as usize - // If the above condition was false, we are dealing with a nested CPI. - // The caller_ix represents the CPI index in the instruction trace. - // To calculate its cpi_number (i.e. the index in `parent_positions)` - // we subtract is from the number of top level instructions. - } else if let Some(caller_index) = parent_positions - .get((caller_ix as usize).saturating_sub(top_level_ixs_num)) - .copied() - && caller_index != usize::MAX - { - *parent_positions.get_mut(cpi_num).unwrap() = caller_index; - caller_index - } else { - // This case shall never happen. Program runtime always executes caller before - // callees, so the if-statement can only be broken into two different cases: - // 1. Top-level instructions doing a CPI - // 2. A nested CPI. - debug_assert!(false); - usize::MAX - }; - - if let Some(inner_instructions) = outer_instructions.get_mut(outer_index) { - let stack_height = ix_in_trace.nesting_level.saturating_add(1); + 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(), + ix_accounts.iter().map(|acc| acc.index_in_transaction as u8).collect(), ), stack_height, }); @@ -1214,124 +455,58 @@ impl TransactionBatchProcessor { } pub fn fill_missing_sysvar_cache_entries( - &self, + &mut self, callbacks: &CB, ) { - let mut sysvar_cache = self.sysvar_cache.write().unwrap(); - sysvar_cache.fill_missing_entries(|pubkey, set_sysvar| { + 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(&self) { - let mut sysvar_cache = self.sysvar_cache.write().unwrap(); - sysvar_cache.reset(); - } - - pub fn get_sysvar_cache_for_tests(&self) -> SysvarCache { - self.sysvar_cache.read().unwrap().clone() - } - - /// Add a built-in program - pub fn add_builtin(&self, program_id: Pubkey, builtin: ProgramCacheEntry) { - self.builtin_program_ids.write().unwrap().insert(program_id); - let entry = Arc::new(builtin); - self.global_program_cache.write().unwrap().assign_program( - &self.program_runtime_environment, - program_id, - 0, - Arc::clone(&entry), - ); - self.builtin_program_cache - .write() - .unwrap() - .replenish(program_id, entry); - } - - #[cfg(feature = "dev-context-only-utils")] - #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] - fn writable_sysvar_cache(&self) -> &RwLock { - &self.sysvar_cache + 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::*, - crate::{ - account_loader::{ - LoadedTransactionAccount, TRANSACTION_ACCOUNT_BASE_SIZE, - ValidatedTransactionDetails, - }, - nonce_info::NonceInfo, - rent_calculator::RENT_EXEMPT_RENT_EPOCH, - rollback_accounts::RollbackAccounts, - }, solana_account::{WritableAccount, create_account_shared_data_for_test}, solana_clock::Clock, - solana_compute_budget_interface::ComputeBudgetInstruction, solana_epoch_schedule::EpochSchedule, solana_fee_calculator::FeeCalculator, solana_fee_structure::FeeDetails, solana_hash::Hash, - solana_keypair::Keypair, solana_message::{LegacyMessage, Message, MessageHeader, SanitizedMessage}, - solana_nonce as nonce, solana_program_runtime::{ - execution_budget::{ - SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionBudget, - }, - invoke_context::BuiltinFunctionRegisterer, - loaded_programs::BlockRelation, - program_cache_entry::ProgramCacheEntryType, + execution_budget::SVMTransactionExecutionBudget, loaded_programs::ProgramCacheEntryType, }, solana_rent::Rent, - solana_sbpf::vm, - solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable, system_program, sysvar}, + solana_sdk_ids::{bpf_loader, sysvar}, solana_signature::Signature, solana_svm_callback::{AccountState, InvokeContextCallback}, - solana_system_interface::instruction as system_instruction, - solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction::sanitized::SanitizedTransaction, solana_transaction_context::transaction::TransactionContext, - solana_transaction_error::TransactionError, - std::{borrow::Cow, collections::HashMap}, - test_case::test_case, + std::collections::HashMap, }; fn new_unchecked_sanitized_message(message: Message) -> SanitizedMessage { SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())) } - struct TestForkGraph {} - - impl ForkGraph for TestForkGraph { - fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation { - BlockRelation::Unknown - } - } - - #[derive(Clone)] + #[derive(Clone, Default)] struct MockBankCallback { account_shared_data: Arc>>, #[allow(clippy::type_complexity)] inspected_accounts: Arc, /* is_writable */ bool)>>>>, - feature_set: SVMFeatureSet, - } - - impl Default for MockBankCallback { - fn default() -> Self { - Self { - account_shared_data: Arc::default(), - inspected_accounts: Arc::default(), - feature_set: SVMFeatureSet::all_enabled(), - } - } } impl InvokeContextCallback for MockBankCallback {} @@ -1364,78 +539,6 @@ mod tests { } } - impl MockBankCallback { - pub fn calculate_fee_details( - message: &impl SVMMessage, - lamports_per_signature: u64, - prioritization_fee: u64, - ) -> FeeDetails { - let signature_count = message - .num_transaction_signatures() - .saturating_add(message.num_ed25519_signatures()) - .saturating_add(message.num_secp256k1_signatures()) - .saturating_add(message.num_secp256r1_signatures()); - - FeeDetails::new( - signature_count.saturating_mul(lamports_per_signature), - prioritization_fee, - ) - } - } - - impl<'a> From<&'a MockBankCallback> for AccountLoader<'a, MockBankCallback> { - fn from(callbacks: &'a MockBankCallback) -> AccountLoader<'a, MockBankCallback> { - AccountLoader::new_with_loaded_accounts_capacity( - None, - callbacks, - &callbacks.feature_set, - 0, - ) - } - } - - #[test_case(1; "Check results too small")] - #[test_case(3; "Check results too large")] - #[should_panic(expected = "Length of check_results does not match length of sanitized_txs")] - fn test_check_results_txs_length_mismatch(check_results_len: usize) { - let sanitized_message = new_unchecked_sanitized_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(), - }); - - // Transactions, length 2. - let sanitized_txs = vec![ - SanitizedTransaction::new_for_tests( - sanitized_message, - vec![Signature::new_unique()], - false, - ); - 2 - ]; - - let check_results = vec![ - TransactionCheckResult::Ok(CheckedTransactionDetails::default()); - check_results_len - ]; - - let batch_processor = TransactionBatchProcessor::::default(); - let callback = MockBankCallback::default(); - - batch_processor.load_and_execute_sanitized_transactions( - &callback, - &sanitized_txs, - check_results, - &get_mock_transaction_processing_environment(), - &TransactionProcessingConfig::default(), - ); - } - #[test] fn test_inner_instructions_list_from_instruction_trace() { let mut transaction_context = TransactionContext::new( @@ -1449,73 +552,72 @@ mod tests { 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(); - } + // 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.push().unwrap(); - // ix #0 does a CPI transaction_context - .configure_next_cpi_for_tests(0, vec![], vec![0, 0]) + .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.push().unwrap(); - // ix #2 does a CPI transaction_context - .configure_next_cpi_for_tests(0, vec![], vec![2, 0]) + .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.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.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.push().unwrap(); - // ix #3 does a CPI transaction_context - .configure_next_cpi_for_tests(0, vec![], vec![3, 0]) + .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.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.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(); @@ -1524,12 +626,9 @@ mod tests { transaction_context.pop().unwrap(); let inner_instructions = - TransactionBatchProcessor::::deconstruct_transaction( - transaction_context, - true, - ) - .1 - .unwrap(); + TransactionBatchProcessor::deconstruct_transaction(transaction_context, true) + .1 + .unwrap(); assert_eq!( inner_instructions, @@ -1589,7 +688,7 @@ mod tests { let sanitized_message = new_unchecked_sanitized_message(message); let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); - let batch_processor = TransactionBatchProcessor::::default(); + let batch_processor = TransactionBatchProcessor::default(); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1599,13 +698,13 @@ mod tests { let loaded_transaction = LoadedTransaction { accounts: vec![(Pubkey::new_unique(), AccountSharedData::default())], + program_indices: vec![0], fee_details: FeeDetails::default(), - rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), loaded_accounts_data_size: 32, }; - let processing_environment = get_mock_transaction_processing_environment(); + let processing_environment = TransactionProcessingEnvironment::default(); let mut processing_config = TransactionProcessingConfig::default(); processing_config.recording_config.enable_log_recording = true; @@ -1616,8 +715,6 @@ mod tests { &mock_bank, &sanitized_transaction, loaded_transaction.clone(), - &mut ExecuteTimings::default(), - &mut TransactionErrorMetrics::default(), &mut program_cache_for_tx_batch, &processing_environment, &processing_config, @@ -1630,8 +727,6 @@ mod tests { &mock_bank, &sanitized_transaction, loaded_transaction.clone(), - &mut ExecuteTimings::default(), - &mut TransactionErrorMetrics::default(), &mut program_cache_for_tx_batch, &processing_environment, &processing_config, @@ -1647,8 +742,6 @@ mod tests { &mock_bank, &sanitized_transaction, loaded_transaction, - &mut ExecuteTimings::default(), - &mut TransactionErrorMetrics::default(), &mut program_cache_for_tx_batch, &processing_environment, &processing_config, @@ -1659,302 +752,29 @@ mod tests { } #[test] - fn test_execute_loaded_transaction_error_metrics() { - // 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 key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let message = Message { - account_keys: vec![key1, key2], - header: MessageHeader::default(), - instructions: vec![CompiledInstruction { - program_id_index: 0, - accounts: vec![2], - 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, - ); + 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()); - let loaded_transaction = LoadedTransaction { - accounts: vec![ - (key1, AccountSharedData::default()), - (key2, AccountSharedData::default()), - ], - fee_details: FeeDetails::default(), - rollback_accounts: RollbackAccounts::default(), - compute_budget: SVMTransactionExecutionBudget::default(), - loaded_accounts_data_size: 0, - }; - - let processing_config = TransactionProcessingConfig { - recording_config: ExecutionRecordingConfig::new_single_setting(false), - ..Default::default() - }; - let mut error_metrics = TransactionErrorMetrics::new(); - let mock_bank = MockBankCallback::default(); - - let _ = batch_processor.execute_loaded_transaction( - &mock_bank, - &sanitized_transaction, - loaded_transaction, - &mut ExecuteTimings::default(), - &mut error_metrics, - &mut program_cache_for_tx_batch, - &get_mock_transaction_processing_environment(), - &processing_config, - ); - - assert_eq!(error_metrics.instruction_error.0, 1); - } - - #[test] - #[should_panic = "called load_program_with_pubkey() with nonexistent account"] - fn test_replenish_program_cache_with_nonexistent_accounts() { - let mock_bank = MockBankCallback::default(); - let account_loader = (&mock_bank).into(); - let fork_graph = Arc::new(RwLock::new(TestForkGraph {})); - let batch_processor = - TransactionBatchProcessor::new(0, 0, Arc::downgrade(&fork_graph), None); - let program_runtime_environment_for_execution = - batch_processor.program_runtime_environment_for_epoch(0); - let key = Pubkey::new_unique(); - - let mut account_set = HashMap::new(); - account_set.insert(key, 0); + 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( - &account_loader, - &account_set, - &program_runtime_environment_for_execution, + &Default::default(), &mut program_cache_for_tx_batch, - &mut ExecuteTimings::default(), - false, - true, - true, - ); - } - - #[test] - fn test_replenish_program_cache() { - let mock_bank = MockBankCallback::default(); - let fork_graph = Arc::new(RwLock::new(TestForkGraph {})); - let batch_processor = - TransactionBatchProcessor::new(0, 0, Arc::downgrade(&fork_graph), None); - let program_runtime_environment_for_execution = - batch_processor.program_runtime_environment_for_epoch(0); - let key = Pubkey::new_unique(); - - let mut account_data = AccountSharedData::default(); - account_data.set_owner(bpf_loader::id()); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(key, account_data); - let account_loader = (&mock_bank).into(); - - let mut account_set = HashMap::new(); - account_set.insert(key, 0); - let mut loaded_missing = 0; - - for limit_to_load_programs in [false, true] { - let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(batch_processor.slot); - - batch_processor.replenish_program_cache( - &account_loader, - &account_set, - &program_runtime_environment_for_execution, - &mut program_cache_for_tx_batch, - &mut ExecuteTimings::default(), - false, - limit_to_load_programs, - true, - ); - assert!(!program_cache_for_tx_batch.hit_max_limit); - if program_cache_for_tx_batch.loaded_missing { - loaded_missing += 1; - } - - let program = program_cache_for_tx_batch.find(&key).unwrap(); - assert!(matches!( - program.program, - ProgramCacheEntryType::FailedVerification(_) - )); - } - assert!(loaded_missing > 0); - } - - #[test] - fn test_filter_executable_program_accounts() { - let mock_bank = MockBankCallback::default(); - let key1 = Pubkey::new_unique(); - let owner1 = bpf_loader::id(); - let key2 = Pubkey::new_unique(); - let owner2 = bpf_loader_upgradeable::id(); - - let mut data1 = AccountSharedData::default(); - data1.set_owner(owner1); - data1.set_lamports(93); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(key1, data1); - - let mut data2 = AccountSharedData::default(); - data2.set_owner(owner2); - data2.set_lamports(90); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(key2, data2); - let account_loader = (&mock_bank).into(); - - let message = Message { - account_keys: vec![key1, key2], - 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 batch_processor = TransactionBatchProcessor::::default(); - let program_accounts_set = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_transaction, - ); - - assert_eq!(program_accounts_set.len(), 2); - assert!(program_accounts_set.contains_key(&key1)); - assert!(program_accounts_set.contains_key(&key2)); - } - - #[test] - fn test_filter_executable_program_accounts_no_errors() { - let keypair1 = Keypair::new(); - let keypair2 = Keypair::new(); - - let non_program_pubkey1 = Pubkey::new_unique(); - let non_program_pubkey2 = Pubkey::new_unique(); - let program1_pubkey = bpf_loader::id(); - let program2_pubkey = bpf_loader_upgradeable::id(); - let account1_pubkey = Pubkey::new_unique(); - let account2_pubkey = Pubkey::new_unique(); - let account3_pubkey = Pubkey::new_unique(); - let account4_pubkey = Pubkey::new_unique(); - - let account5_pubkey = Pubkey::new_unique(); - - let bank = MockBankCallback::default(); - bank.account_shared_data.write().unwrap().insert( - non_program_pubkey1, - AccountSharedData::new(1, 10, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - non_program_pubkey2, - AccountSharedData::new(1, 10, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - program1_pubkey, - AccountSharedData::new(40, 1, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - program2_pubkey, - AccountSharedData::new(40, 1, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - account1_pubkey, - AccountSharedData::new(1, 10, &non_program_pubkey1), - ); - bank.account_shared_data.write().unwrap().insert( - account2_pubkey, - AccountSharedData::new(1, 10, &non_program_pubkey2), - ); - bank.account_shared_data.write().unwrap().insert( - account3_pubkey, - AccountSharedData::new(40, 1, &program1_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - account4_pubkey, - AccountSharedData::new(40, 1, &program2_pubkey), - ); - let account_loader = (&bank).into(); - - let tx1 = Transaction::new_with_compiled_instructions( - &[&keypair1], - &[non_program_pubkey1], - Hash::new_unique(), - vec![account1_pubkey, account2_pubkey, account3_pubkey], - vec![CompiledInstruction::new(1, &(), vec![0])], - ); - let sanitized_tx1 = SanitizedTransaction::from_transaction_for_tests(tx1); - - let tx2 = Transaction::new_with_compiled_instructions( - &[&keypair2], - &[non_program_pubkey2], - Hash::new_unique(), - vec![account4_pubkey, account3_pubkey, account2_pubkey], - vec![CompiledInstruction::new(1, &(), vec![0])], - ); - let sanitized_tx2 = SanitizedTransaction::from_transaction_for_tests(tx2); - - let batch_processor = TransactionBatchProcessor::::default(); - - let tx1_programs = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_tx1, + &accounts, ); - assert_eq!(tx1_programs.len(), 1); - assert!( - tx1_programs.contains_key(&account3_pubkey), - "failed to find the program account", - ); - - let tx2_programs = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_tx2, - ); - - assert_eq!(tx2_programs.len(), 2); - assert!( - tx2_programs.contains_key(&account3_pubkey), - "failed to find the program account", - ); - assert!( - tx2_programs.contains_key(&account4_pubkey), - "failed to find the program account", - ); + 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] @@ -1985,9 +805,7 @@ mod tests { .insert(sysvar::epoch_schedule::id(), epoch_schedule_account); let fees = Fees { - fee_calculator: FeeCalculator { - lamports_per_signature: 123, - }, + fee_calculator: FeeCalculator { lamports_per_signature: 123 }, }; let fees_account = create_account_shared_data_for_test(&fees); mock_bank @@ -2004,33 +822,22 @@ mod tests { .unwrap() .insert(sysvar::rent::id(), rent_account); - let transaction_processor = TransactionBatchProcessor::::default(); + let mut transaction_processor = TransactionBatchProcessor::default(); transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); - let sysvar_cache = transaction_processor.sysvar_cache.read().unwrap(); + let sysvar_cache = &transaction_processor.sysvar_cache; let cached_clock = sysvar_cache.get_clock(); - let cached_epoch_schedule = sysvar_cache.get_epoch_schedule(); - let cached_fees = sysvar_cache.get_fees(); let cached_rent = sysvar_cache.get_rent(); assert_eq!( cached_clock.expect("clock sysvar missing in cache"), clock.into() ); - assert_eq!( - cached_epoch_schedule.expect("epoch_schedule sysvar missing in cache"), - epoch_schedule.into() - ); - assert_eq!( - cached_fees.expect("fees sysvar missing in cache"), - fees.into() - ); assert_eq!( cached_rent.expect("rent sysvar missing in cache"), rent.into() ); assert!(sysvar_cache.get_slot_hashes().is_err()); - assert!(sysvar_cache.get_epoch_rewards().is_err()); } #[test] @@ -2061,9 +868,7 @@ mod tests { .insert(sysvar::epoch_schedule::id(), epoch_schedule_account); let fees = Fees { - fee_calculator: FeeCalculator { - lamports_per_signature: 123, - }, + fee_calculator: FeeCalculator { lamports_per_signature: 123 }, }; let fees_account = create_account_shared_data_for_test(&fees); mock_bank @@ -2080,695 +885,34 @@ mod tests { .unwrap() .insert(sysvar::rent::id(), rent_account); - let transaction_processor = TransactionBatchProcessor::::default(); + 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.read().unwrap(); + 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_epoch_schedule().is_err()); - assert!(sysvar_cache.get_fees().is_err()); - assert!(sysvar_cache.get_epoch_rewards().is_err()); assert!(sysvar_cache.get_rent().is_err()); - assert!(sysvar_cache.get_epoch_rewards().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.read().unwrap(); + let sysvar_cache = &transaction_processor.sysvar_cache; let cached_clock = sysvar_cache.get_clock(); - let cached_epoch_schedule = sysvar_cache.get_epoch_schedule(); - let cached_fees = sysvar_cache.get_fees(); let cached_rent = sysvar_cache.get_rent(); assert_eq!( cached_clock.expect("clock sysvar missing in cache"), clock.into() ); - assert_eq!( - cached_epoch_schedule.expect("epoch_schedule sysvar missing in cache"), - epoch_schedule.into() - ); - assert_eq!( - cached_fees.expect("fees sysvar missing in cache"), - fees.into() - ); assert_eq!( cached_rent.expect("rent sysvar missing in cache"), rent.into() ); assert!(sysvar_cache.get_slot_hashes().is_err()); - assert!(sysvar_cache.get_epoch_rewards().is_err()); - } - - #[test] - fn test_add_builtin() { - let fork_graph = Arc::new(RwLock::new(TestForkGraph {})); - let batch_processor = - TransactionBatchProcessor::new(0, 0, Arc::downgrade(&fork_graph), None); - - let key = Pubkey::new_unique(); - let name = "a_builtin_name"; - let register_fn: BuiltinFunctionRegisterer = |p, n| { - p.register_function( - n, - ( - |_invoke_context, _param0, _param1, _param2, _param3, _param4| {}, - |_| {}, - ), - ) - }; - let program = ProgramCacheEntry::new_builtin(0, name.len(), register_fn); - batch_processor.add_builtin(key, program); - - let mut loaded_programs_for_tx_batch = ProgramCacheForTxBatch::new(0); - let program_runtime_environment = - batch_processor.program_runtime_environment_for_epoch(batch_processor.epoch); - batch_processor - .global_program_cache - .write() - .unwrap() - .extract( - &mut vec![(key, ProgramCacheMatchCriteria::NoCriteria, 0)], - &mut loaded_programs_for_tx_batch, - &program_runtime_environment, - true, - true, - ); - let entry = loaded_programs_for_tx_batch.find(&key).unwrap(); - - // Repeating code because ProgramCacheEntry does not implement clone. - let program = ProgramCacheEntry::new_builtin(0, name.len(), register_fn); - assert_eq!(entry, Arc::new(program)); - } - - #[test] - fn test_validate_transaction_fee_payer_exact_balance() { - let lamports_per_signature = 5000; - let message = new_unchecked_sanitized_message(Message::new_with_blockhash( - &[ - ComputeBudgetInstruction::set_compute_unit_limit(2000u32), - ComputeBudgetInstruction::set_compute_unit_price(1_000_000_000), - ], - Some(&Pubkey::new_unique()), - &Hash::new_unique(), - )); - let fee_payer_address = message.fee_payer(); - let current_epoch = 42; - let rent = Rent::default(); - let min_balance = rent.minimum_balance(nonce::state::State::size()); - let transaction_fee = lamports_per_signature; - let priority_fee = 2_000_000u64; - let starting_balance = transaction_fee + priority_fee; - assert!( - starting_balance > min_balance, - "we're testing that a rent exempt fee payer can be fully drained, so ensure that the \ - starting balance is more than the min balance" - ); - - let fee_payer_rent_epoch = current_epoch; - let fee_payer_account = AccountSharedData::new_rent_epoch( - starting_balance, - 0, - &Pubkey::default(), - fee_payer_rent_epoch, - ); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; - let mut account_loader = (&mock_bank).into(); - - let mut error_counters = TransactionErrorMetrics::default(); - let compute_budget_and_limits = SVMTransactionExecutionAndFeeBudgetLimits { - budget: SVMTransactionExecutionBudget { - compute_unit_limit: 2000, - ..SVMTransactionExecutionBudget::default() - }, - fee_details: FeeDetails::new(transaction_fee, priority_fee), - ..SVMTransactionExecutionAndFeeBudgetLimits::default() - }; - let result = - TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - CheckedTransactionDetails::new(None, compute_budget_and_limits), - &Hash::default(), - lamports_per_signature, - &rent, - false, - &mut error_counters, - ); - - let post_validation_fee_payer_account = { - let mut account = fee_payer_account.clone(); - account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); - account.set_lamports(0); - account - }; - - assert_eq!( - result, - Ok(ValidatedTransactionDetails { - rollback_accounts: RollbackAccounts::new( - None, // nonce - *fee_payer_address, - post_validation_fee_payer_account.clone(), - fee_payer_rent_epoch - ), - compute_budget: compute_budget_and_limits.budget, - loaded_accounts_bytes_limit: compute_budget_and_limits - .loaded_accounts_data_size_limit, - fee_details: FeeDetails::new(transaction_fee, priority_fee), - loaded_fee_payer_account: LoadedTransactionAccount { - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE + fee_payer_account.data().len(), - account: post_validation_fee_payer_account, - }, - }) - ); - } - - #[test] - fn test_validate_transaction_fee_payer_not_found() { - let lamports_per_signature = 5000; - let message = - new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); - - let mock_bank = MockBankCallback::default(); - let mut account_loader = (&mock_bank).into(); - let mut error_counters = TransactionErrorMetrics::default(); - let result = - TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - CheckedTransactionDetails::new( - None, - SVMTransactionExecutionAndFeeBudgetLimits::default(), - ), - &Hash::default(), - lamports_per_signature, - &Rent::default(), - false, - &mut error_counters, - ); - - assert_eq!(error_counters.account_not_found.0, 1); - assert_eq!(result, Err(TransactionError::AccountNotFound)); - } - - #[test] - fn test_validate_transaction_fee_payer_insufficient_funds() { - let lamports_per_signature = 5000; - let message = - new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); - let fee_payer_address = message.fee_payer(); - let fee_payer_account = AccountSharedData::new(1, 0, &Pubkey::default()); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; - let mut account_loader = (&mock_bank).into(); - - let mut error_counters = TransactionErrorMetrics::default(); - let result = - TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - CheckedTransactionDetails::new( - None, - SVMTransactionExecutionAndFeeBudgetLimits::with_fee( - MockBankCallback::calculate_fee_details( - &message, - lamports_per_signature, - 0, - ), - ), - ), - &Hash::default(), - lamports_per_signature, - &Rent::default(), - false, - &mut error_counters, - ); - - assert_eq!(error_counters.insufficient_funds.0, 1); - assert_eq!(result, Err(TransactionError::InsufficientFundsForFee)); - } - - #[test] - fn test_validate_transaction_fee_payer_insufficient_rent() { - let lamports_per_signature = 5000; - let message = - new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); - let fee_payer_address = message.fee_payer(); - let transaction_fee = lamports_per_signature; - let rent = Rent::default(); - let min_balance = rent.minimum_balance(0); - let starting_balance = min_balance + transaction_fee - 1; - let fee_payer_account = AccountSharedData::new(starting_balance, 0, &Pubkey::default()); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; - let mut account_loader = (&mock_bank).into(); - - let mut error_counters = TransactionErrorMetrics::default(); - let result = - TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - CheckedTransactionDetails::new( - None, - SVMTransactionExecutionAndFeeBudgetLimits::with_fee( - MockBankCallback::calculate_fee_details( - &message, - lamports_per_signature, - 0, - ), - ), - ), - &Hash::default(), - lamports_per_signature, - &rent, - false, - &mut error_counters, - ); - - assert_eq!( - result, - Err(TransactionError::InsufficientFundsForRent { account_index: 0 }) - ); - } - - #[test] - fn test_validate_transaction_fee_payer_invalid() { - let lamports_per_signature = 5000; - let message = - new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); - let fee_payer_address = message.fee_payer(); - let fee_payer_account = AccountSharedData::new(1_000_000, 0, &Pubkey::new_unique()); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; - let mut account_loader = (&mock_bank).into(); - - let mut error_counters = TransactionErrorMetrics::default(); - let result = - TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - CheckedTransactionDetails::new( - None, - SVMTransactionExecutionAndFeeBudgetLimits::with_fee( - MockBankCallback::calculate_fee_details( - &message, - lamports_per_signature, - 0, - ), - ), - ), - &Hash::default(), - lamports_per_signature, - &Rent::default(), - false, - &mut error_counters, - ); - - assert_eq!(error_counters.invalid_account_for_fee.0, 1); - assert_eq!(result, Err(TransactionError::InvalidAccountForFee)); - } - - #[derive(Debug, PartialEq, Eq)] - enum ValidateNonce { - Success, - NoAccount, - BadOwner, - BlockhashMismatch, - AlreadyUsed, - BadSigner, - } - - #[test_case(ValidateNonce::Success)] - #[test_case(ValidateNonce::NoAccount)] - #[test_case(ValidateNonce::BadOwner)] - #[test_case(ValidateNonce::BlockhashMismatch)] - #[test_case(ValidateNonce::AlreadyUsed)] - #[test_case(ValidateNonce::BadSigner)] - fn test_validate_transaction_nonce(case: ValidateNonce) { - let lamports_per_signature = 5000; - let previous_durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); - let nonce_address = Pubkey::new_unique(); - let authority_address = Pubkey::new_unique(); - - let message_blockhash = if case == ValidateNonce::BlockhashMismatch { - Hash::new_unique() - } else { - *previous_durable_nonce.as_hash() - }; - - let message_authority = if case == ValidateNonce::BadSigner { - Pubkey::new_unique() - } else { - authority_address - }; - - let message = new_unchecked_sanitized_message(Message::new_with_blockhash( - &[system_instruction::advance_nonce_account( - &nonce_address, - &message_authority, - )], - Some(&Pubkey::new_unique()), - &message_blockhash, - )); - - let environment_blockhash = Hash::new_unique(); - let next_durable_nonce = DurableNonce::from_blockhash(&environment_blockhash); - - let stored_durable_nonce = if case == ValidateNonce::AlreadyUsed { - next_durable_nonce - } else { - previous_durable_nonce - }; - - let nonce_versions = nonce::versions::Versions::new(nonce::state::State::Initialized( - nonce::state::Data::new( - authority_address, - stored_durable_nonce, - lamports_per_signature, - ), - )); - - let nonce_owner = if case == ValidateNonce::BadOwner { - Pubkey::new_unique() - } else { - system_program::id() - }; - - let nonce_account = AccountSharedData::new_data(1, &nonce_versions, &nonce_owner).unwrap(); - - let mut mock_accounts = HashMap::new(); - - if case != ValidateNonce::NoAccount { - mock_accounts.insert(nonce_address, nonce_account.clone()); - } - - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; - let mut account_loader = (&mock_bank).into(); - - let mut error_counters = TransactionErrorMetrics::default(); - let result = TransactionBatchProcessor::::validate_transaction_nonce( - &mut account_loader, - &message, - &nonce_address, - &next_durable_nonce, - lamports_per_signature, - false, - &mut error_counters, - ); - - match case { - ValidateNonce::Success => { - let mut future_nonce_info = NonceInfo::new(nonce_address, nonce_account); - future_nonce_info - .try_advance_nonce(next_durable_nonce, lamports_per_signature) - .unwrap(); - - assert_eq!(result, Ok(future_nonce_info)); - } - ValidateNonce::NoAccount => { - assert_eq!(error_counters.account_not_found.0, 1); - assert_eq!(result, Err(TransactionError::AccountNotFound)); - } - _ => { - assert_eq!(error_counters.blockhash_not_found.0, 1); - assert_eq!(result, Err(TransactionError::BlockhashNotFound)); - } - } - } - - #[test] - fn test_validate_transaction_fee_payer_is_nonce() { - let lamports_per_signature = 5000; - let rent = Rent::default(); - let compute_unit_limit = 1000u64; - let previous_durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); - let fee_payer_address = &Pubkey::new_unique(); - let message = new_unchecked_sanitized_message(Message::new_with_blockhash( - &[ - system_instruction::advance_nonce_account(fee_payer_address, fee_payer_address), - ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit as u32), - ComputeBudgetInstruction::set_compute_unit_price(1_000_000), - ], - Some(fee_payer_address), - previous_durable_nonce.as_hash(), - )); - let transaction_fee = lamports_per_signature; - let compute_budget_and_limits = SVMTransactionExecutionAndFeeBudgetLimits { - fee_details: FeeDetails::new(transaction_fee, compute_unit_limit), - ..SVMTransactionExecutionAndFeeBudgetLimits::default() - }; - let min_balance = Rent::default().minimum_balance(nonce::state::State::size()); - let priority_fee = compute_unit_limit; - - let nonce_versions = nonce::versions::Versions::new(nonce::state::State::Initialized( - nonce::state::Data::new( - *fee_payer_address, - previous_durable_nonce, - lamports_per_signature, - ), - )); - - let environment_blockhash = Hash::new_unique(); - let next_durable_nonce = DurableNonce::from_blockhash(&environment_blockhash); - - // Sufficient Fees - { - let fee_payer_account = AccountSharedData::new_data( - min_balance + transaction_fee + priority_fee, - &nonce_versions, - &system_program::id(), - ) - .unwrap(); - - let mut future_nonce = NonceInfo::new(*fee_payer_address, fee_payer_account.clone()); - future_nonce - .try_advance_nonce(next_durable_nonce, lamports_per_signature) - .unwrap(); - - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; - let mut account_loader = (&mock_bank).into(); - - let mut error_counters = TransactionErrorMetrics::default(); - - let tx_details = - CheckedTransactionDetails::new(Some(*fee_payer_address), compute_budget_and_limits); - - let result = TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - tx_details, - &environment_blockhash, - lamports_per_signature, - &rent, - false, - &mut error_counters, - ); - - let post_validation_fee_payer_account = { - let mut account = fee_payer_account.clone(); - account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); - account.set_lamports(min_balance); - account - }; - - assert_eq!( - result, - Ok(ValidatedTransactionDetails { - rollback_accounts: RollbackAccounts::new( - Some(future_nonce), - *fee_payer_address, - post_validation_fee_payer_account.clone(), - 0, // fee_payer_rent_epoch - ), - compute_budget: compute_budget_and_limits.budget, - loaded_accounts_bytes_limit: compute_budget_and_limits - .loaded_accounts_data_size_limit, - fee_details: FeeDetails::new(transaction_fee, priority_fee), - loaded_fee_payer_account: LoadedTransactionAccount { - loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE + fee_payer_account.data().len(), - account: post_validation_fee_payer_account, - } - }) - ); - } - - // Insufficient Fees - { - let fee_payer_account = AccountSharedData::new_data( - transaction_fee + priority_fee, // no min_balance this time - &nonce_versions, - &system_program::id(), - ) - .unwrap(); - - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; - let mut account_loader = (&mock_bank).into(); - - let mut error_counters = TransactionErrorMetrics::default(); - - let tx_details = - CheckedTransactionDetails::new(Some(*fee_payer_address), compute_budget_and_limits); - - let result = TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - tx_details, - &environment_blockhash, - lamports_per_signature, - &rent, - false, - &mut error_counters, - ); - - assert_eq!(error_counters.insufficient_funds.0, 1); - assert_eq!(result, Err(TransactionError::InsufficientFundsForFee)); - } - } - - // Ensure `TransactionProcessingCallback::inspect_account()` is called when - // validating the fee payer, since that's when the fee payer account is loaded. - #[test] - fn test_inspect_account_fee_payer() { - let lamports_per_signature = 5000; - let fee_payer_address = Pubkey::new_unique(); - let fee_payer_account = AccountSharedData::new_rent_epoch( - 123_000_000_000, - 0, - &Pubkey::default(), - RENT_EXEMPT_RENT_EPOCH, - ); - let mock_bank = MockBankCallback::default(); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(fee_payer_address, fee_payer_account.clone()); - let mut account_loader = (&mock_bank).into(); - - let message = new_unchecked_sanitized_message(Message::new_with_blockhash( - &[ - ComputeBudgetInstruction::set_compute_unit_limit(2000u32), - ComputeBudgetInstruction::set_compute_unit_price(1_000_000_000), - ], - Some(&fee_payer_address), - &Hash::new_unique(), - )); - TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - CheckedTransactionDetails::new( - None, - SVMTransactionExecutionAndFeeBudgetLimits::with_fee( - MockBankCallback::calculate_fee_details(&message, 5000, 0), - ), - ), - &Hash::default(), - lamports_per_signature, - &Rent::default(), - false, - &mut TransactionErrorMetrics::default(), - ) - .unwrap(); - - // ensure the fee payer is an inspected account - let actual_inspected_accounts: Vec<_> = mock_bank - .inspected_accounts - .read() - .unwrap() - .iter() - .map(|(k, v)| (*k, v.clone())) - .collect(); - assert_eq!( - actual_inspected_accounts.as_slice(), - &[(fee_payer_address, vec![(Some(fee_payer_account), true)])], - ); - } - - #[test] - fn test_set_program_runtime_environment() { - let mut transaction_processor = TransactionBatchProcessor::::default(); - let current_environment = - ProgramRuntimeEnvironment::clone(&transaction_processor.program_runtime_environment); - let new_environment = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); - let config = vm::Config { - enable_symbol_and_section_labels: true, - ..vm::Config::default() - }; - let new_environment2 = ProgramRuntimeEnvironment::from(BuiltinProgram::new_loader(config)); - assert_ne!(current_environment, new_environment); - assert_ne!(current_environment, new_environment2); - assert_ne!(new_environment, new_environment2); - // Assign an equal and identical environment: No changes - transaction_processor.set_program_runtime_environment(ProgramRuntimeEnvironment::clone( - ¤t_environment, - )); - assert_eq!( - transaction_processor.program_runtime_environment, - current_environment, - ); - // Assign an equal but not identical environment: No changes - transaction_processor - .set_program_runtime_environment(ProgramRuntimeEnvironment::clone(&new_environment)); - assert_eq!( - transaction_processor.program_runtime_environment, - current_environment, - ); - // Assign a different and not identical environment: Overwritten - transaction_processor - .set_program_runtime_environment(ProgramRuntimeEnvironment::clone(&new_environment2)); - assert_eq!( - transaction_processor.program_runtime_environment, - new_environment2, - ); - // Assign an environment which is equal to the upcoming_environment: Overwritten - transaction_processor - .epoch_boundary_preparation - .write() - .unwrap() - .upcoming_environment = Some(ProgramRuntimeEnvironment::clone(&new_environment)); - transaction_processor.set_program_runtime_environment(ProgramRuntimeEnvironment::clone( - ¤t_environment, - )); - assert_eq!( - transaction_processor.program_runtime_environment, - new_environment, - ); } } diff --git a/solana/svm/tests/concurrent_tests.rs b/solana/svm/tests/concurrent_tests.rs deleted file mode 100644 index ac4b1c4..0000000 --- a/solana/svm/tests/concurrent_tests.rs +++ /dev/null @@ -1,311 +0,0 @@ -#![cfg(feature = "shuttle-test")] - -use { - crate::mock_bank::{MockForkGraph, create_custom_loader, deploy_program, register_builtins}, - assert_matches::assert_matches, - mock_bank::MockBankCallback, - shuttle::{ - Runner, - sync::{Arc, RwLock}, - thread, - }, - solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, - solana_clock::Slot, - solana_instruction::{AccountMeta, Instruction}, - solana_program_runtime::{ - execution_budget::SVMTransactionExecutionAndFeeBudgetLimits, - loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, - program_cache_entry::ProgramCacheEntryType, - }, - solana_pubkey::Pubkey, - solana_svm::{ - account_loader::{AccountLoader, CheckedTransactionDetails, TransactionCheckResult}, - transaction_processing_result::{ - ProcessedTransaction, TransactionProcessingResultExtensions, - }, - transaction_processor::{ - ExecutionRecordingConfig, TransactionBatchProcessor, TransactionProcessingConfig, - TransactionProcessingEnvironment, get_mock_transaction_processing_environment, - }, - }, - solana_svm_feature_set::SVMFeatureSet, - solana_svm_timings::ExecuteTimings, - solana_transaction::{Transaction, sanitized::SanitizedTransaction}, - std::collections::{HashMap, HashSet}, -}; - -mod mock_bank; - -const MAX_ITERATIONS: usize = 10_000; - -fn program_cache_execution(threads: usize) { - let mut mock_bank = MockBankCallback::default(); - let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); - let batch_processor = TransactionBatchProcessor::new(5, 5, Arc::downgrade(&fork_graph), None); - - let programs = vec![ - deploy_program("hello-solana".to_string(), 0, &mut mock_bank), - deploy_program("simple-transfer".to_string(), 0, &mut mock_bank), - deploy_program("clock-sysvar".to_string(), 0, &mut mock_bank), - ]; - - let account_maps: HashMap = programs.iter().map(|key| (*key, 0)).collect(); - - let ths: Vec<_> = (0..threads) - .map(|_| { - let local_bank = mock_bank.clone(); - let processor = TransactionBatchProcessor::new_from( - &batch_processor, - batch_processor.slot, - batch_processor.epoch, - ); - let maps = account_maps.clone(); - let programs = programs.clone(); - thread::spawn(move || { - let feature_set = SVMFeatureSet::all_enabled(); - let account_loader = AccountLoader::new_with_loaded_accounts_capacity( - None, - &local_bank, - &feature_set, - 0, - ); - let mut result = ProgramCacheForTxBatch::new(processor.slot); - let program_runtime_environment_for_execution = - processor.program_runtime_environment_for_epoch(processor.epoch); - processor.replenish_program_cache( - &account_loader, - &maps, - &program_runtime_environment_for_execution, - &mut result, - &mut ExecuteTimings::default(), - false, - true, - true, - ); - for key in &programs { - let cache_entry = result.find(key); - assert!(matches!( - cache_entry.unwrap().program, - ProgramCacheEntryType::Loaded(_) - )); - } - }) - }) - .collect(); - - for th in ths { - th.join().unwrap(); - } -} - -// Shuttle has its own internal scheduler and the following tests change the way it operates to -// increase the efficiency in finding problems in the program cache's concurrent code. - -// This test leverages the probabilistic concurrency testing algorithm -// (https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/asplos277-pct.pdf). -// It bounds the numbers of preemptions to explore (five in this test) for the four -// threads we use. We run it for 300 iterations. -#[test] -fn test_program_cache_with_probabilistic_scheduler() { - shuttle::check_pct( - move || { - program_cache_execution(4); - }, - MAX_ITERATIONS, - 5, - ); -} - -// In this case, the scheduler is random and may preempt threads at any point and any time. -#[test] -fn test_program_cache_with_random_scheduler() { - shuttle::check_random(move || program_cache_execution(4), MAX_ITERATIONS); -} - -// This test explores all the possible thread scheduling patterns that might affect the program -// cache. There is a limitation to run only 500 iterations to avoid consuming too much CI time. -#[test] -fn test_program_cache_with_exhaustive_scheduler() { - // The DFS (shuttle::check_dfs) test is only complete when we do not generate random - // values in a thread. - // Since this is not the case for the execution of jitted program, we can still run the test - // but with decreased accuracy. - let scheduler = shuttle::scheduler::DfsScheduler::new(Some(MAX_ITERATIONS), true); - let runner = Runner::new(scheduler, Default::default()); - runner.run(move || program_cache_execution(4)); -} - -// This test executes multiple transactions in parallel where all read from the same data account, -// but write to different accounts. Given that there are no locks in this case, SVM must behave -// correctly. -fn svm_concurrent() { - let mock_bank = Arc::new(MockBankCallback::default()); - let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); - let batch_processor = Arc::new(TransactionBatchProcessor::new( - 5, - 2, - Arc::downgrade(&fork_graph), - Some(create_custom_loader()), - )); - - mock_bank.configure_sysvars(); - batch_processor.fill_missing_sysvar_cache_entries(&*mock_bank); - register_builtins(&mock_bank, &batch_processor); - - let program_id = deploy_program("transfer-from-account".to_string(), 0, &mock_bank); - - const THREADS: usize = 4; - const TRANSACTIONS_PER_THREAD: usize = 3; - const AMOUNT: u64 = 50; - const CAPACITY: usize = THREADS * TRANSACTIONS_PER_THREAD; - const BALANCE: u64 = 10_000_000; - - let mut transactions = vec![Vec::new(); THREADS]; - let mut check_data = vec![Vec::new(); THREADS]; - let read_account = Pubkey::new_unique(); - let mut account_data = AccountSharedData::default(); - account_data.set_data(AMOUNT.to_le_bytes().to_vec()); - account_data.set_rent_epoch(u64::MAX); - account_data.set_lamports(1); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(read_account, account_data); - - #[derive(Clone)] - struct CheckTxData { - sender: Pubkey, - recipient: Pubkey, - fee_payer: Pubkey, - } - - for idx in 0..CAPACITY { - let sender = Pubkey::new_unique(); - let recipient = Pubkey::new_unique(); - let fee_payer = Pubkey::new_unique(); - let system_account = Pubkey::from([0u8; 32]); - - let mut account_data = AccountSharedData::default(); - account_data.set_lamports(BALANCE); - - { - let shared_data = &mut mock_bank.account_shared_data.write().unwrap(); - shared_data.insert(sender, account_data.clone()); - shared_data.insert(recipient, account_data.clone()); - shared_data.insert(fee_payer, account_data); - } - - let accounts = vec![ - AccountMeta { - pubkey: sender, - is_signer: true, - is_writable: true, - }, - AccountMeta { - pubkey: recipient, - is_signer: false, - is_writable: true, - }, - AccountMeta { - pubkey: read_account, - is_signer: false, - is_writable: false, - }, - AccountMeta { - pubkey: system_account, - is_signer: false, - is_writable: false, - }, - ]; - - let instruction = Instruction::new_with_bytes(program_id, &[0], accounts); - let legacy_transaction = Transaction::new_with_payer(&[instruction], Some(&fee_payer)); - - let sanitized_transaction = - SanitizedTransaction::try_from_legacy_transaction(legacy_transaction, &HashSet::new()); - transactions[idx % THREADS].push(sanitized_transaction.unwrap()); - check_data[idx % THREADS].push(CheckTxData { - fee_payer, - recipient, - sender, - }); - } - - let ths: Vec<_> = (0..THREADS) - .map(|idx| { - let local_batch = batch_processor.clone(); - let local_bank = mock_bank.clone(); - let th_txs = std::mem::take(&mut transactions[idx]); - let check_results = th_txs - .iter() - .map(|tx| { - Ok(CheckedTransactionDetails::new( - None, - SVMTransactionExecutionAndFeeBudgetLimits::with_fee( - MockBankCallback::calculate_fee_details(tx, 0), - ), - )) as TransactionCheckResult - }) - .collect(); - let processing_config = TransactionProcessingConfig { - recording_config: ExecutionRecordingConfig { - enable_log_recording: true, - enable_return_data_recording: false, - enable_cpi_recording: false, - enable_transaction_balance_recording: false, - }, - ..Default::default() - }; - let check_tx_data = std::mem::take(&mut check_data[idx]); - - thread::spawn(move || { - let result = local_batch.load_and_execute_sanitized_transactions( - &*local_bank, - &th_txs, - check_results, - &TransactionProcessingEnvironment { - program_runtime_environments: ProgramRuntimeEnvironments::new( - local_batch.program_runtime_environment.clone(), - local_batch.program_runtime_environment.clone(), - ), - ..get_mock_transaction_processing_environment() - }, - &processing_config, - ); - - for (idx, processing_result) in result.processing_results.iter().enumerate() { - assert!(processing_result.was_processed()); - let processed_tx = processing_result.processed_transaction().unwrap(); - assert_matches!(processed_tx, &ProcessedTransaction::Executed(_)); - let executed_tx = processed_tx.executed_transaction().unwrap(); - let inserted_accounts = &check_tx_data[idx]; - for (key, account_data) in &executed_tx.loaded_transaction.accounts { - if *key == inserted_accounts.fee_payer { - assert_eq!(account_data.lamports(), BALANCE - 10000); - } else if *key == inserted_accounts.sender { - assert_eq!(account_data.lamports(), BALANCE - AMOUNT); - } else if *key == inserted_accounts.recipient { - assert_eq!(account_data.lamports(), BALANCE + AMOUNT); - } - } - } - }) - }) - .collect(); - - for th in ths { - th.join().unwrap(); - } -} - -#[test] -fn test_svm_with_probabilistic_scheduler() { - shuttle::check_pct( - move || { - svm_concurrent(); - }, - MAX_ITERATIONS, - 5, - ); -} diff --git a/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml b/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml index be59164..3dacc69 100644 --- a/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml +++ b/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "clock-sysvar-program" -version = "4.1.1" edition = "2021" +name = "clock-sysvar-program" +version = "4.0.0-rc.1" [dependencies] 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 index 4c744a2..dee43a1 100755 Binary files a/solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so and b/solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so differ diff --git a/solana/svm/tests/example-programs/hello-solana/Cargo.toml b/solana/svm/tests/example-programs/hello-solana/Cargo.toml index b7c55eb..4f08a1b 100644 --- a/solana/svm/tests/example-programs/hello-solana/Cargo.toml +++ b/solana/svm/tests/example-programs/hello-solana/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "hello-solana-program" -version = "4.1.1" edition = "2021" +name = "hello-solana-program" +version = "4.0.0-rc.1" [dependencies] 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 index f79f4f1..a9da4ff 100755 Binary files a/solana/svm/tests/example-programs/hello-solana/hello_solana_program.so and b/solana/svm/tests/example-programs/hello-solana/hello_solana_program.so differ diff --git a/solana/svm/tests/example-programs/simple-transfer/Cargo.toml b/solana/svm/tests/example-programs/simple-transfer/Cargo.toml index 6767871..8d93d9d 100644 --- a/solana/svm/tests/example-programs/simple-transfer/Cargo.toml +++ b/solana/svm/tests/example-programs/simple-transfer/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "simple-transfer-program" -version = "4.1.1" edition = "2021" +name = "simple-transfer-program" +version = "4.0.0-rc.1" [dependencies] 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 index b9041ec..5132b38 100755 Binary files a/solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so and b/solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so differ diff --git a/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml b/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml index 2484b5a..c431ebe 100644 --- a/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml +++ b/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "transfer-from-account" -version = "4.1.1" edition = "2021" +name = "transfer-from-account" +version = "4.0.0-rc.1" [dependencies] 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 index 86e6859..a3ef926 100755 Binary files a/solana/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so 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 index 1875f3d..122f321 100644 --- a/solana/svm/tests/example-programs/write-to-account/Cargo.toml +++ b/solana/svm/tests/example-programs/write-to-account/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "write-to-account" -version = "4.1.1" edition = "2021" +name = "write-to-account" +version = "4.0.0-rc.1" [dependencies] 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 index a6a1472..e436426 100755 Binary files a/solana/svm/tests/example-programs/write-to-account/write_to_account_program.so and b/solana/svm/tests/example-programs/write-to-account/write_to_account_program.so differ diff --git a/solana/svm/tests/integration_test.rs b/solana/svm/tests/integration_test.rs deleted file mode 100644 index 810a01d..0000000 --- a/solana/svm/tests/integration_test.rs +++ /dev/null @@ -1,4011 +0,0 @@ -#![cfg(test)] -#![allow(clippy::arithmetic_side_effects)] - -use { - crate::mock_bank::{ - EXECUTION_EPOCH, EXECUTION_SLOT, MockBankCallback, MockForkGraph, WALLCLOCK_TIME, - create_custom_loader, deploy_program_with_upgrade_authority, load_program, program_address, - program_data_size, register_builtins, - }, - solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, - solana_clock::Slot, - solana_compute_budget::compute_budget_limits::ComputeBudgetLimits, - solana_compute_budget_interface::ComputeBudgetInstruction, - solana_fee_structure::FeeDetails, - solana_hash::Hash, - solana_instruction::{AccountMeta, Instruction}, - solana_keypair::Keypair, - solana_loader_v3_interface::{ - get_program_data_address, instruction as loaderv3_instruction, - state::UpgradeableLoaderState, - }, - solana_native_token::LAMPORTS_PER_SOL, - solana_nonce::{self as nonce, state::DurableNonce}, - solana_program_entrypoint::MAX_PERMITTED_DATA_INCREASE, - solana_program_runtime::{ - execution_budget::{ - MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, SVMTransactionExecutionAndFeeBudgetLimits, - }, - loaded_programs::ProgramRuntimeEnvironments, - }, - solana_pubkey::Pubkey, - solana_sdk_ids::{ - bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, compute_budget, native_loader, - }, - solana_signer::Signer, - solana_svm::{ - account_loader::{ - CheckedTransactionDetails, TRANSACTION_ACCOUNT_BASE_SIZE, TransactionCheckResult, - }, - nonce_info::NonceInfo, - transaction_execution_result::TransactionExecutionDetails, - transaction_processing_result::{ - ProcessedTransaction, TransactionProcessingResult, - TransactionProcessingResultExtensions, - }, - transaction_processor::{ - ExecutionRecordingConfig, LoadAndExecuteSanitizedTransactionsOutput, - TransactionBatchProcessor, TransactionProcessingConfig, - TransactionProcessingEnvironment, - }, - }, - solana_svm_feature_set::SVMFeatureSet, - solana_svm_transaction::{ - instruction::SVMInstruction, - svm_message::{SVMMessage, SVMStaticMessage}, - }, - solana_svm_type_overrides::sync::{Arc, RwLock}, - solana_system_interface::{instruction as system_instruction, program as system_program}, - solana_system_transaction as system_transaction, - solana_sysvar::rent::Rent, - solana_transaction::{Transaction, sanitized::SanitizedTransaction}, - solana_transaction_context::transaction::TransactionReturnData, - solana_transaction_error::TransactionError, - std::{collections::HashMap, num::NonZeroU32, slice, sync::atomic::Ordering}, - test_case::test_case, -}; - -// This module contains the implementation of TransactionProcessingCallback -mod mock_bank; - -// Local implementation of compute budget processing for tests. -fn process_test_compute_budget_instructions<'a>( - instructions: impl Iterator)> + Clone, -) -> Result { - let mut loaded_accounts_data_size_limit = None; - - // Scan for compute budget instructions. - // Only key on `SetLoadedAccountsDataSizeLimit`. - for (program_id, instruction) in instructions { - if *program_id == compute_budget::id() - && instruction.data.len() >= 5 - && instruction.data[0] == 4 - { - let size = u32::from_le_bytes([ - instruction.data[1], - instruction.data[2], - instruction.data[3], - instruction.data[4], - ]); - loaded_accounts_data_size_limit = Some(size); - } - } - - let loaded_accounts_bytes = - if let Some(requested_loaded_accounts_data_size_limit) = loaded_accounts_data_size_limit { - NonZeroU32::new(requested_loaded_accounts_data_size_limit) - .ok_or(TransactionError::InvalidLoadedAccountsDataSizeLimit)? - } else { - MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES - } - .min(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES); - - Ok(ComputeBudgetLimits { - loaded_accounts_bytes, - ..Default::default() - }) -} - -const DEPLOYMENT_SLOT: u64 = 0; -const LAMPORTS_PER_SIGNATURE: u64 = 5000; -const LAST_BLOCKHASH: Hash = Hash::new_from_array([7; 32]); // Arbitrary constant hash for advancing nonces - -pub type AccountsMap = HashMap; - -// container for everything needed to execute a test entry -// care should be taken if reused, because we update bank account states, but otherwise leave it as-is -// the environment is made available for tests that check it after processing -pub struct SvmTestEnvironment<'a> { - pub mock_bank: MockBankCallback, - pub fork_graph: Arc>, - pub batch_processor: TransactionBatchProcessor, - pub processing_config: TransactionProcessingConfig<'a>, - pub processing_environment: TransactionProcessingEnvironment, - pub test_entry: SvmTestEntry, -} - -impl SvmTestEnvironment<'_> { - pub fn create(test_entry: SvmTestEntry) -> Self { - let mock_bank = MockBankCallback::default(); - - for (name, slot, authority) in &test_entry.initial_programs { - deploy_program_with_upgrade_authority(name.to_string(), *slot, &mock_bank, *authority); - } - - for (pubkey, account) in &test_entry.initial_accounts { - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(*pubkey, account.clone()); - } - - let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); - let batch_processor = TransactionBatchProcessor::new( - EXECUTION_SLOT, - EXECUTION_EPOCH, - Arc::downgrade(&fork_graph), - Some(create_custom_loader()), - ); - - // The sysvars must be put in the cache - mock_bank.configure_sysvars(); - batch_processor.fill_missing_sysvar_cache_entries(&mock_bank); - register_builtins(&mock_bank, &batch_processor); - - let processing_config = TransactionProcessingConfig { - recording_config: ExecutionRecordingConfig { - enable_log_recording: true, - enable_return_data_recording: true, - enable_cpi_recording: false, - enable_transaction_balance_recording: false, - }, - drop_on_failure: test_entry.drop_on_failure, - all_or_nothing: test_entry.all_or_nothing, - ..Default::default() - }; - - let processing_environment = TransactionProcessingEnvironment { - blockhash: LAST_BLOCKHASH, - blockhash_lamports_per_signature: LAMPORTS_PER_SIGNATURE, - alpenglow_migration_succeeded: false, - epoch_total_stake: 0, - feature_set: test_entry.feature_set, - program_runtime_environments: ProgramRuntimeEnvironments::new( - batch_processor.program_runtime_environment_for_epoch(EXECUTION_EPOCH), - batch_processor.program_runtime_environment_for_epoch(EXECUTION_EPOCH), - ), - rent: test_entry.rent.clone(), - }; - - Self { - mock_bank, - fork_graph, - batch_processor, - processing_config, - processing_environment, - test_entry, - } - } - - pub fn execute(&self) -> LoadAndExecuteSanitizedTransactionsOutput { - let (transactions, check_results) = self.test_entry.prepare_transactions(); - let batch_output = self - .batch_processor - .load_and_execute_sanitized_transactions( - &self.mock_bank, - &transactions, - check_results, - &self.processing_environment, - &self.processing_config, - ); - - // build a hashmap of final account states incrementally - // starting with all initial states, updating to all final states - // with SIMD83, an account might change multiple times in the same batch - // but it might not exist on all transactions - let mut final_accounts_actual = self.test_entry.initial_accounts.clone(); - let update_or_dealloc_account = - |final_accounts: &mut AccountsMap, pubkey, account: AccountSharedData| { - if account.lamports() == 0 { - final_accounts.insert(pubkey, AccountSharedData::default()); - } else { - final_accounts.insert(pubkey, account); - } - }; - - for (tx_index, processed_transaction) in batch_output.processing_results.iter().enumerate() - { - let sanitized_transaction = &transactions[tx_index]; - - match processed_transaction { - Ok(ProcessedTransaction::Executed(executed_transaction)) => { - if executed_transaction.was_successful() { - for (index, (pubkey, account_data)) in executed_transaction - .loaded_transaction - .accounts - .iter() - .enumerate() - { - if sanitized_transaction.is_writable(index) { - update_or_dealloc_account( - &mut final_accounts_actual, - *pubkey, - account_data.clone(), - ); - } - } - } else { - for (pubkey, account_data) in - &executed_transaction.loaded_transaction.rollback_accounts - { - update_or_dealloc_account( - &mut final_accounts_actual, - *pubkey, - account_data.clone(), - ); - } - } - } - Ok(ProcessedTransaction::FeesOnly(fees_only_transaction)) => { - for (pubkey, account_data) in &fees_only_transaction.rollback_accounts { - update_or_dealloc_account( - &mut final_accounts_actual, - *pubkey, - account_data.clone(), - ); - } - } - Err(_) => {} - } - } - - // first assert all transaction states together, it makes test-driven development much less of a headache - let (actual_statuses, expected_statuses): (Vec<_>, Vec<_>) = batch_output - .processing_results - .iter() - .zip(self.test_entry.asserts()) - .map(|(processing_result, test_item_assert)| { - ( - ExecutionStatus::from(processing_result), - test_item_assert.status, - ) - }) - .unzip(); - assert_eq!( - expected_statuses, - actual_statuses, - "mismatch between expected and actual statuses. execution details:\n{}", - batch_output - .processing_results - .iter() - .enumerate() - .map(|(i, tx)| match tx { - Ok(ProcessedTransaction::Executed(executed)) => { - format!("{} (executed): {:#?}", i, executed.execution_details) - } - Ok(ProcessedTransaction::FeesOnly(fee_only)) => { - format!("{} (fee-only): {:?}", i, fee_only.load_error) - } - Err(e) => format!("{i} (discarded): {e:?}"), - }) - .collect::>() - .join("\n"), - ); - - // check that all the account states we care about are present and correct - for (pubkey, expected_account_data) in self.test_entry.final_accounts.iter() { - let actual_account_data = final_accounts_actual.get(pubkey); - assert_eq!( - Some(expected_account_data), - actual_account_data, - "mismatch on account {pubkey}" - ); - } - - // now run our transaction-by-transaction checks - for (processing_result, test_item_asserts) in batch_output - .processing_results - .iter() - .zip(self.test_entry.asserts()) - { - match processing_result { - Ok(ProcessedTransaction::Executed(executed_transaction)) => test_item_asserts - .check_executed_transaction(&executed_transaction.execution_details), - Ok(ProcessedTransaction::FeesOnly(_)) => { - assert!(test_item_asserts.processed()); - assert!(!test_item_asserts.executed()); - } - Err(_) => assert!(test_item_asserts.discarded()), - } - } - - // merge new account states into the bank for multi-batch tests - let mut mock_bank_accounts = self.mock_bank.account_shared_data.write().unwrap(); - mock_bank_accounts.extend(final_accounts_actual); - - // update global program cache - for processing_result in batch_output.processing_results.iter() { - if let Some(ProcessedTransaction::Executed(executed_tx)) = - processing_result.processed_transaction() - { - let programs_modified_by_tx = &executed_tx.programs_modified_by_tx; - if executed_tx.was_successful() && !programs_modified_by_tx.is_empty() { - self.batch_processor - .global_program_cache - .write() - .unwrap() - .merge( - &self.batch_processor.program_runtime_environment, - self.batch_processor.slot, - programs_modified_by_tx, - ); - } - } - } - - batch_output - } - - pub fn is_program_blocked(&self, program_id: &Pubkey) -> bool { - let (_, program_cache_entry) = self - .batch_processor - .global_program_cache - .read() - .unwrap() - .get_flattened_entries_for_tests() - .into_iter() - .rev() - .find(|(key, _)| key == program_id) - .unwrap(); - - // in the same batch, a new valid loaderv3 program may have a Loaded entry with a later execution slot - // in a later batch, the same loaderv3 program will have a DelayedVisibility tombstone - // a new loaderv1/v2 account will have a FailedVerification tombstone - // and a closed loaderv3 program or any loaderv3 buffer will have a Closed tombstone - program_cache_entry.effective_slot > EXECUTION_SLOT || program_cache_entry.is_tombstone() - } -} - -// container for a transaction batch and all data needed to run and verify it against svm -#[derive(Clone)] -pub struct SvmTestEntry { - // features configuration for this test - pub feature_set: SVMFeatureSet, - - // enables drop on failure processing (transactions without Ok status have no state effect) - pub drop_on_failure: bool, - - // enables all or nothing processing (if not all transactions can be committed then none are) - pub all_or_nothing: bool, - - // programs to deploy to the new svm - pub initial_programs: Vec<(String, Slot, Option)>, - - // accounts to deploy to the new svm before transaction execution - pub initial_accounts: AccountsMap, - - // transactions to execute and transaction-specific checks to perform on the results from svm - pub transaction_batch: Vec, - - // expected final account states, checked after transaction execution - pub final_accounts: AccountsMap, - - // rent parameters for the test - pub rent: Rent, -} - -impl Default for SvmTestEntry { - fn default() -> Self { - Self { - feature_set: SVMFeatureSet::all_enabled(), - all_or_nothing: false, - drop_on_failure: false, - initial_programs: Vec::new(), - initial_accounts: HashMap::new(), - transaction_batch: Vec::new(), - final_accounts: HashMap::new(), - rent: Rent::default(), - } - } -} - -impl SvmTestEntry { - pub fn set_rent_params(&mut self, rent: Rent) { - self.rent = rent; - } - - // add a new rent-exempt account that exists before the batch - // inserts it into both account maps, assuming it lives unchanged (except for svm fixing rent epoch) - // rent-paying accounts must be added by hand because svm will not set rent epoch to u64::MAX - pub fn add_initial_account(&mut self, pubkey: Pubkey, account: &AccountSharedData) { - assert!( - self.initial_accounts - .insert(pubkey, account.clone()) - .is_none() - ); - - self.create_expected_account(pubkey, account); - } - - // add an immutable program that will have been deployed before the slot we execute transactions in - pub fn add_initial_program(&mut self, program_name: &str) { - self.initial_programs - .push((program_name.to_string(), DEPLOYMENT_SLOT, None)); - } - - // add a new rent-exempt account that is created by the transaction - // inserts it only into the post account map - pub fn create_expected_account(&mut self, pubkey: Pubkey, account: &AccountSharedData) { - let mut account = account.clone(); - account.set_rent_epoch(u64::MAX); - - assert!(self.final_accounts.insert(pubkey, account).is_none()); - } - - // edit an existing account to reflect changes you expect the transaction to make to it - pub fn update_expected_account_data(&mut self, pubkey: Pubkey, account: &AccountSharedData) { - let mut account = account.clone(); - account.set_rent_epoch(u64::MAX); - - assert!(self.final_accounts.insert(pubkey, account).is_some()); - } - - // indicate that an existing account is expected to be deallocated - pub fn drop_expected_account(&mut self, pubkey: Pubkey) { - assert!( - self.final_accounts - .insert(pubkey, AccountSharedData::default()) - .is_some() - ); - } - - // add lamports to an existing expected final account state - pub fn increase_expected_lamports(&mut self, pubkey: &Pubkey, lamports: u64) { - self.final_accounts - .get_mut(pubkey) - .unwrap() - .checked_add_lamports(lamports) - .unwrap(); - } - - // subtract lamports from an existing expected final account state - pub fn decrease_expected_lamports(&mut self, pubkey: &Pubkey, lamports: u64) { - self.final_accounts - .get_mut(pubkey) - .unwrap() - .checked_sub_lamports(lamports) - .unwrap(); - } - - // convenience function that adds a transaction that is expected to succeed - pub fn push_transaction(&mut self, transaction: Transaction) { - self.push_transaction_with_status(transaction, ExecutionStatus::Succeeded) - } - - // convenience function that adds a transaction with an expected execution status - pub fn push_transaction_with_status( - &mut self, - transaction: Transaction, - status: ExecutionStatus, - ) { - self.transaction_batch.push(TransactionBatchItem { - transaction, - asserts: TransactionBatchItemAsserts { - status, - ..TransactionBatchItemAsserts::default() - }, - ..TransactionBatchItem::default() - }); - } - - // convenience function that adds a nonce transaction that is expected to succeed - pub fn push_nonce_transaction(&mut self, transaction: Transaction, nonce_address: Pubkey) { - self.push_nonce_transaction_with_status( - transaction, - nonce_address, - ExecutionStatus::Succeeded, - ) - } - - // convenience function that adds a nonce transaction with an expected execution status - pub fn push_nonce_transaction_with_status( - &mut self, - transaction: Transaction, - nonce_address: Pubkey, - status: ExecutionStatus, - ) { - self.transaction_batch.push(TransactionBatchItem { - transaction, - asserts: TransactionBatchItemAsserts { - status, - ..TransactionBatchItemAsserts::default() - }, - ..TransactionBatchItem::with_nonce(nonce_address) - }); - } - - // internal helper to gather SanitizedTransaction objects for execution - fn prepare_transactions(&self) -> (Vec, Vec) { - self.transaction_batch - .iter() - .cloned() - .map(|item| { - let message = SanitizedTransaction::from_transaction_for_tests(item.transaction); - let check_result = item.check_result.map(|tx_details| { - let compute_budget_limits = process_test_compute_budget_instructions( - SVMStaticMessage::program_instructions_iter(&message), - ); - let signature_count = message - .num_transaction_signatures() - .saturating_add(message.num_ed25519_signatures()) - .saturating_add(message.num_secp256k1_signatures()) - .saturating_add(message.num_secp256r1_signatures()); - - let compute_budget = compute_budget_limits - .map(|v| { - v.get_compute_budget_and_limits( - v.loaded_accounts_bytes, - FeeDetails::new( - signature_count.saturating_mul(LAMPORTS_PER_SIGNATURE), - v.get_prioritization_fee(), - ), - self.feature_set.raise_cpi_nesting_limit_to_8, - ) - }) - .unwrap(); - CheckedTransactionDetails::new(tx_details.nonce_address, compute_budget) - }); - - (message, check_result) - }) - .unzip() - } - - // internal helper to gather test items for post-execution checks - fn asserts(&self) -> Vec { - self.transaction_batch - .iter() - .cloned() - .map(|item| item.asserts) - .collect() - } -} - -// one transaction in a batch plus check results for svm and asserts for tests -#[derive(Clone, Debug)] -pub struct TransactionBatchItem { - pub transaction: Transaction, - pub check_result: TransactionCheckResult, - pub asserts: TransactionBatchItemAsserts, -} - -impl TransactionBatchItem { - fn with_nonce(nonce_address: Pubkey) -> Self { - Self { - check_result: Ok(CheckedTransactionDetails::new( - Some(nonce_address), - SVMTransactionExecutionAndFeeBudgetLimits::default(), - )), - ..Self::default() - } - } -} - -impl Default for TransactionBatchItem { - fn default() -> Self { - Self { - transaction: Transaction::default(), - check_result: Ok(CheckedTransactionDetails::new( - None, - SVMTransactionExecutionAndFeeBudgetLimits::default(), - )), - asserts: TransactionBatchItemAsserts::default(), - } - } -} - -// asserts for a given transaction in a batch -// we can automatically check whether it executed, whether it succeeded -// log items we expect to see (exact match only), and rodata -#[derive(Clone, Debug, Default)] -pub struct TransactionBatchItemAsserts { - pub status: ExecutionStatus, - pub logs: Vec, - pub return_data: ReturnDataAssert, -} - -impl TransactionBatchItemAsserts { - pub fn succeeded(&self) -> bool { - self.status.succeeded() - } - - pub fn executed(&self) -> bool { - self.status.executed() - } - - pub fn processed(&self) -> bool { - self.status.processed() - } - - pub fn discarded(&self) -> bool { - self.status.discarded() - } - - pub fn check_executed_transaction(&self, execution_details: &TransactionExecutionDetails) { - assert!(self.executed()); - assert_eq!(self.succeeded(), execution_details.status.is_ok()); - - if !self.logs.is_empty() { - let actual_logs = execution_details.log_messages.as_ref().unwrap(); - for expected_log in &self.logs { - assert!(actual_logs.contains(expected_log)); - } - } - - if self.return_data != ReturnDataAssert::Skip { - assert_eq!( - self.return_data, - execution_details.return_data.clone().into() - ); - } - } -} - -impl From for TransactionBatchItemAsserts { - fn from(status: ExecutionStatus) -> Self { - Self { - status, - ..Self::default() - } - } -} - -// states a transaction can end in after a trip through the batch processor: -// * discarded: no-op. not even processed. a flawed transaction excluded from the entry -// * processed-failed: aka fee (and nonce) only. charged and added to an entry but not executed, would have failed invariably -// * executed-failed: failed during execution. as above, fees charged and nonce advanced -// * succeeded: what we all aspire to be in our transaction processing lifecycles -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -pub enum ExecutionStatus { - Discarded, - ProcessedFailed, - ExecutedFailed, - #[default] - Succeeded, -} - -// note we avoid the word "failed" because it is confusing -// the batch processor uses it to mean "executed and not succeeded" -// but intuitively (and from the point of a user) it could just as likely mean "any state other than succeeded" -impl ExecutionStatus { - pub fn succeeded(self) -> bool { - self == Self::Succeeded - } - - pub fn executed(self) -> bool { - self > Self::ProcessedFailed - } - - pub fn processed(self) -> bool { - self != Self::Discarded - } - - pub fn discarded(self) -> bool { - self == Self::Discarded - } -} - -impl From<&TransactionProcessingResult> for ExecutionStatus { - fn from(processing_result: &TransactionProcessingResult) -> Self { - match processing_result { - Ok(ProcessedTransaction::Executed(executed_transaction)) => { - if executed_transaction.execution_details.status.is_ok() { - ExecutionStatus::Succeeded - } else { - ExecutionStatus::ExecutedFailed - } - } - Ok(ProcessedTransaction::FeesOnly(_)) => ExecutionStatus::ProcessedFailed, - Err(_) => ExecutionStatus::Discarded, - } - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub enum ReturnDataAssert { - Some(TransactionReturnData), - None, - #[default] - Skip, -} - -impl From> for ReturnDataAssert { - fn from(option_ro_data: Option) -> Self { - match option_ro_data { - Some(ro_data) => Self::Some(ro_data), - None => Self::None, - } - } -} - -fn program_medley(drop_on_failure: bool) -> Vec { - let mut test_entry = SvmTestEntry { - drop_on_failure, - ..Default::default() - }; - - // 0: A transaction that works without any account - { - let program_name = "hello-solana"; - let program_id = program_address(program_name); - test_entry.add_initial_program(program_name); - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - - let instruction = Instruction::new_with_bytes(program_id, &[], vec![]); - test_entry.push_transaction(Transaction::new_signed_with_payer( - &[instruction], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - )); - - test_entry.transaction_batch[0] - .asserts - .logs - .push("Program log: Hello, Solana!".to_string()); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - } - - // 1: A simple funds transfer between accounts - { - let program_name = "simple-transfer"; - let program_id = program_address(program_name); - test_entry.add_initial_program(program_name); - - let fee_payer_keypair = Keypair::new(); - let sender_keypair = Keypair::new(); - - let fee_payer = fee_payer_keypair.pubkey(); - let sender = sender_keypair.pubkey(); - let recipient = Pubkey::new_unique(); - - let transfer_amount = 10; - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - - let mut sender_data = AccountSharedData::default(); - sender_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(sender, &sender_data); - - let mut recipient_data = AccountSharedData::default(); - recipient_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(recipient, &recipient_data); - - let instruction = Instruction::new_with_bytes( - program_id, - &u64::to_be_bytes(transfer_amount), - vec![ - AccountMeta::new(sender, true), - AccountMeta::new(recipient, false), - AccountMeta::new_readonly(system_program::id(), false), - ], - ); - - test_entry.push_transaction(Transaction::new_signed_with_payer( - &[instruction], - Some(&fee_payer), - &[&fee_payer_keypair, &sender_keypair], - Hash::default(), - )); - - test_entry.increase_expected_lamports(&recipient, transfer_amount); - test_entry.decrease_expected_lamports(&sender, transfer_amount); - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - } - - // 2: A program that utilizes a Sysvar - { - let program_name = "clock-sysvar"; - let program_id = program_address(program_name); - test_entry.add_initial_program(program_name); - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - - let instruction = Instruction::new_with_bytes(program_id, &[], vec![]); - test_entry.push_transaction(Transaction::new_signed_with_payer( - &[instruction], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - )); - - let ro_data = TransactionReturnData { - program_id, - data: i64::to_be_bytes(WALLCLOCK_TIME).to_vec(), - }; - test_entry.transaction_batch[2].asserts.return_data = ReturnDataAssert::Some(ro_data); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - } - - // 3: A transaction that fails - { - let program_id = program_address("simple-transfer"); - - let fee_payer_keypair = Keypair::new(); - let sender_keypair = Keypair::new(); - - let fee_payer = fee_payer_keypair.pubkey(); - let sender = sender_keypair.pubkey(); - let recipient = Pubkey::new_unique(); - - let base_amount = 900_000; - let transfer_amount = base_amount + 50; - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - fee_payer_data.set_rent_epoch(u64::MAX); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - if drop_on_failure { - test_entry.final_accounts.insert(fee_payer, fee_payer_data); - } - - let mut sender_data = AccountSharedData::default(); - sender_data.set_lamports(base_amount); - sender_data.set_rent_epoch(u64::MAX); - test_entry.add_initial_account(sender, &sender_data); - if drop_on_failure { - test_entry.final_accounts.insert(sender, sender_data); - } - - let mut recipient_data = AccountSharedData::default(); - recipient_data.set_lamports(base_amount); - recipient_data.set_rent_epoch(u64::MAX); - test_entry.add_initial_account(recipient, &recipient_data); - if drop_on_failure { - test_entry.final_accounts.insert(recipient, recipient_data); - } - - let instruction = Instruction::new_with_bytes( - program_id, - &u64::to_be_bytes(transfer_amount), - vec![ - AccountMeta::new(sender, true), - AccountMeta::new(recipient, false), - AccountMeta::new_readonly(system_program::id(), false), - ], - ); - - test_entry.push_transaction_with_status( - Transaction::new_signed_with_payer( - &[instruction], - Some(&fee_payer), - &[&fee_payer_keypair, &sender_keypair], - Hash::default(), - ), - match drop_on_failure { - true => ExecutionStatus::Discarded, - false => ExecutionStatus::ExecutedFailed, - }, - ); - - if !drop_on_failure { - test_entry.transaction_batch[3] - .asserts - .logs - .push("Transfer: insufficient lamports 900000, need 900050".to_string()); - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - } - } - - // 4: A transaction whose verification has already failed - { - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - test_entry.transaction_batch.push(TransactionBatchItem { - transaction: Transaction::new_signed_with_payer( - &[], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ), - check_result: Err(TransactionError::BlockhashNotFound), - asserts: ExecutionStatus::Discarded.into(), - }); - } - - vec![test_entry] -} - -fn simple_transfer(drop_on_failure: bool) -> Vec { - let mut test_entry = SvmTestEntry { - drop_on_failure, - ..Default::default() - }; - let transfer_amount = LAMPORTS_PER_SOL; - let drop_on_failure_status = |status: ExecutionStatus| match (drop_on_failure, status) { - (true, ExecutionStatus::Succeeded) => ExecutionStatus::Succeeded, - (true, _) => ExecutionStatus::Discarded, - (false, status) => status, - }; - - // 0: a transfer that succeeds - { - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - let destination = Pubkey::new_unique(); - - let mut source_data = AccountSharedData::default(); - let mut destination_data = AccountSharedData::default(); - - source_data.set_lamports(LAMPORTS_PER_SOL * 10); - test_entry.add_initial_account(source, &source_data); - - test_entry.push_transaction(system_transaction::transfer( - &source_keypair, - &destination, - transfer_amount, - Hash::default(), - )); - - destination_data - .checked_add_lamports(transfer_amount) - .unwrap(); - test_entry.create_expected_account(destination, &destination_data); - - test_entry.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); - } - - // 1: an executable transfer that fails - { - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - - let mut source_data = AccountSharedData::default(); - - source_data.set_lamports(transfer_amount - 1); - source_data.set_rent_epoch(u64::MAX); - test_entry.add_initial_account(source, &source_data); - if drop_on_failure { - test_entry.final_accounts.insert(source, source_data); - } - - test_entry.push_transaction_with_status( - system_transaction::transfer( - &source_keypair, - &Pubkey::new_unique(), - transfer_amount, - Hash::default(), - ), - drop_on_failure_status(ExecutionStatus::ExecutedFailed), - ); - - if !drop_on_failure { - test_entry.decrease_expected_lamports(&source, LAMPORTS_PER_SIGNATURE); - } - } - - // 2: a non-processable transfer that fails before loading - { - test_entry.transaction_batch.push(TransactionBatchItem { - transaction: system_transaction::transfer( - &Keypair::new(), - &Pubkey::new_unique(), - transfer_amount, - Hash::default(), - ), - check_result: Err(TransactionError::BlockhashNotFound), - asserts: ExecutionStatus::Discarded.into(), - }); - } - - // 3: a non-processable transfer that fails loading the fee-payer - { - test_entry.push_transaction_with_status( - system_transaction::transfer( - &Keypair::new(), - &Pubkey::new_unique(), - transfer_amount, - Hash::default(), - ), - ExecutionStatus::Discarded, - ); - } - - // 4: a processable non-executable transfer that fails loading the program - { - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - - let mut source_data = AccountSharedData::default(); - - source_data.set_lamports(transfer_amount * 10); - test_entry - .initial_accounts - .insert(source, source_data.clone()); - test_entry.final_accounts.insert(source, source_data); - - let mut instruction = - system_instruction::transfer(&source, &Pubkey::new_unique(), transfer_amount); - instruction.program_id = Pubkey::new_unique(); - - if !drop_on_failure { - test_entry.decrease_expected_lamports(&source, LAMPORTS_PER_SIGNATURE); - } - - test_entry.push_transaction_with_status( - Transaction::new_signed_with_payer( - &[instruction], - Some(&source), - &[&source_keypair], - Hash::default(), - ), - drop_on_failure_status(ExecutionStatus::ProcessedFailed), - ); - } - - vec![test_entry] -} - -fn simple_nonce(fee_paying_nonce: bool) -> Vec { - let mut test_entry = SvmTestEntry::default(); - - let program_name = "hello-solana"; - let real_program_id = program_address(program_name); - test_entry.add_initial_program(program_name); - - // create and return a transaction, fee payer, and nonce info - // sets up initial account states but not final ones - // there are four cases of fee_paying_nonce and fake_fee_payer: - // * false/false: normal nonce account with rent minimum, normal fee payer account with 1sol - // * true/false: normal nonce account used to pay fees with rent minimum plus 1sol - // * false/true: normal nonce account with rent minimum, fee payer doesn't exist - // * true/true: same account for both which does not exist - // we also provide a side door to bring a fee-paying nonce account below rent-exemption - let mk_nonce_transaction = |test_entry: &mut SvmTestEntry, - program_id, - fake_fee_payer: bool, - rent_paying_nonce: bool| { - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - let nonce_pubkey = if fee_paying_nonce { - fee_payer - } else { - Pubkey::new_unique() - }; - - let nonce_size = nonce::state::State::size(); - let mut nonce_balance = Rent::default().minimum_balance(nonce_size); - - if !fake_fee_payer && !fee_paying_nonce { - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - fee_payer_data.set_rent_epoch(u64::MAX); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - } else if rent_paying_nonce { - assert!(fee_paying_nonce); - nonce_balance += LAMPORTS_PER_SIGNATURE; - nonce_balance -= 1; - } else if fee_paying_nonce { - nonce_balance += LAMPORTS_PER_SOL; - } - - let nonce_initial_hash = DurableNonce::from_blockhash(&Hash::new_unique()); - let nonce_data = - nonce::state::Data::new(fee_payer, nonce_initial_hash, LAMPORTS_PER_SIGNATURE); - let mut nonce_account = AccountSharedData::new_data( - nonce_balance, - &nonce::versions::Versions::new(nonce::state::State::Initialized(nonce_data.clone())), - &system_program::id(), - ) - .unwrap(); - nonce_account.set_rent_epoch(u64::MAX); - let nonce_info = NonceInfo::new(nonce_pubkey, nonce_account.clone()); - - if !(fake_fee_payer && fee_paying_nonce) { - test_entry.add_initial_account(nonce_pubkey, &nonce_account); - } - - let instructions = vec![ - system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer), - Instruction::new_with_bytes(program_id, &[], vec![]), - ]; - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&fee_payer), - &[&fee_payer_keypair], - nonce_data.blockhash(), - ); - - (transaction, fee_payer, nonce_info) - }; - - // 0: successful nonce transaction, regardless of features - { - let (transaction, fee_payer, mut nonce_info) = - mk_nonce_transaction(&mut test_entry, real_program_id, false, false); - - test_entry.push_nonce_transaction(transaction, *nonce_info.address()); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - nonce_info - .try_advance_nonce( - DurableNonce::from_blockhash(&LAST_BLOCKHASH), - LAMPORTS_PER_SIGNATURE, - ) - .unwrap(); - - test_entry - .final_accounts - .get_mut(nonce_info.address()) - .unwrap() - .data_as_mut_slice() - .copy_from_slice(nonce_info.account().data()); - } - - // 1: non-executing nonce transaction (fee payer doesn't exist) regardless of features - { - let (transaction, _fee_payer, nonce_info) = - mk_nonce_transaction(&mut test_entry, real_program_id, true, false); - - test_entry.push_nonce_transaction_with_status( - transaction, - *nonce_info.address(), - ExecutionStatus::Discarded, - ); - } - - // 2: failing nonce transaction (bad system instruction) regardless of features - { - let (transaction, fee_payer, mut nonce_info) = - mk_nonce_transaction(&mut test_entry, system_program::id(), false, false); - - test_entry.push_nonce_transaction_with_status( - transaction, - *nonce_info.address(), - ExecutionStatus::ExecutedFailed, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - nonce_info - .try_advance_nonce( - DurableNonce::from_blockhash(&LAST_BLOCKHASH), - LAMPORTS_PER_SIGNATURE, - ) - .unwrap(); - - test_entry - .final_accounts - .get_mut(nonce_info.address()) - .unwrap() - .data_as_mut_slice() - .copy_from_slice(nonce_info.account().data()); - } - - // 3: processable non-executable nonce transaction with fee-only enabled, otherwise discarded - { - let (transaction, fee_payer, mut nonce_info) = - mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false, false); - - test_entry.push_nonce_transaction_with_status( - transaction, - *nonce_info.address(), - ExecutionStatus::ProcessedFailed, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - nonce_info - .try_advance_nonce( - DurableNonce::from_blockhash(&LAST_BLOCKHASH), - LAMPORTS_PER_SIGNATURE, - ) - .unwrap(); - - test_entry - .final_accounts - .get_mut(nonce_info.address()) - .unwrap() - .data_as_mut_slice() - .copy_from_slice(nonce_info.account().data()); - } - - // 4: safety check that nonce fee-payers are required to be rent-exempt (blockhash fee-payers may be below rent-exemption) - // if this situation is ever allowed in the future, the nonce account MUST be hidden for fee-only transactions - // as an aside, nonce accounts closed by WithdrawNonceAccount are safe because they are ordinary executed transactions - // we also dont care whether a non-fee nonce (or any account) pays rent because rent is charged on executed transactions - if fee_paying_nonce { - let (transaction, _, nonce_info) = - mk_nonce_transaction(&mut test_entry, real_program_id, false, true); - - test_entry.push_nonce_transaction_with_status( - transaction, - *nonce_info.address(), - ExecutionStatus::Discarded, - ); - } - - // 5: rent-paying nonce fee-payers are also not charged for fee-only transactions - if fee_paying_nonce { - let (transaction, _, nonce_info) = - mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false, true); - - test_entry.push_nonce_transaction_with_status( - transaction, - *nonce_info.address(), - ExecutionStatus::Discarded, - ); - } - - vec![test_entry] -} - -fn simd83_intrabatch_account_reuse() -> Vec { - let mut test_entries = vec![]; - let transfer_amount = LAMPORTS_PER_SOL; - let wallet_rent = Rent::default().minimum_balance(0); - - // batch 0: two successful transfers from the same source - { - let mut test_entry = SvmTestEntry::default(); - - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - let destination1 = Pubkey::new_unique(); - let destination2 = Pubkey::new_unique(); - - let mut source_data = AccountSharedData::default(); - let destination1_data = AccountSharedData::default(); - let destination2_data = AccountSharedData::default(); - - source_data.set_lamports(LAMPORTS_PER_SOL * 10); - test_entry.add_initial_account(source, &source_data); - - for (destination, mut destination_data) in [ - (destination1, destination1_data), - (destination2, destination2_data), - ] { - test_entry.push_transaction(system_transaction::transfer( - &source_keypair, - &destination, - transfer_amount, - Hash::default(), - )); - - destination_data - .checked_add_lamports(transfer_amount) - .unwrap(); - test_entry.create_expected_account(destination, &destination_data); - - test_entry - .decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); - } - - test_entries.push(test_entry); - } - - // batch 1: - // * successful transfer, source left with rent-exempt minimum - // * non-processable transfer due to underfunded fee-payer - { - let mut test_entry = SvmTestEntry::default(); - - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - let destination = Pubkey::new_unique(); - - let mut source_data = AccountSharedData::default(); - let mut destination_data = AccountSharedData::default(); - - source_data.set_lamports(transfer_amount + LAMPORTS_PER_SIGNATURE + wallet_rent); - test_entry.add_initial_account(source, &source_data); - - test_entry.push_transaction(system_transaction::transfer( - &source_keypair, - &destination, - transfer_amount, - Hash::default(), - )); - - destination_data - .checked_add_lamports(transfer_amount) - .unwrap(); - test_entry.create_expected_account(destination, &destination_data); - - test_entry.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); - - test_entry.push_transaction_with_status( - system_transaction::transfer( - &source_keypair, - &destination, - transfer_amount, - Hash::default(), - ), - ExecutionStatus::Discarded, - ); - - test_entries.push(test_entry); - } - - // batch 2: - // * successful transfer to a previously unfunded account - // * successful transfer using the new account as a fee-payer in the same batch - { - let mut test_entry = SvmTestEntry::default(); - let first_transfer_amount = transfer_amount + LAMPORTS_PER_SIGNATURE + wallet_rent; - let second_transfer_amount = transfer_amount; - - let grandparent_keypair = Keypair::new(); - let grandparent = grandparent_keypair.pubkey(); - let parent_keypair = Keypair::new(); - let parent = parent_keypair.pubkey(); - let child = Pubkey::new_unique(); - - let mut grandparent_data = AccountSharedData::default(); - let mut parent_data = AccountSharedData::default(); - let mut child_data = AccountSharedData::default(); - - grandparent_data.set_lamports(LAMPORTS_PER_SOL * 10); - test_entry.add_initial_account(grandparent, &grandparent_data); - - test_entry.push_transaction(system_transaction::transfer( - &grandparent_keypair, - &parent, - first_transfer_amount, - Hash::default(), - )); - - parent_data - .checked_add_lamports(first_transfer_amount) - .unwrap(); - test_entry.create_expected_account(parent, &parent_data); - - test_entry.decrease_expected_lamports( - &grandparent, - first_transfer_amount + LAMPORTS_PER_SIGNATURE, - ); - - test_entry.push_transaction(system_transaction::transfer( - &parent_keypair, - &child, - second_transfer_amount, - Hash::default(), - )); - - child_data - .checked_add_lamports(second_transfer_amount) - .unwrap(); - test_entry.create_expected_account(child, &child_data); - - test_entry - .decrease_expected_lamports(&parent, second_transfer_amount + LAMPORTS_PER_SIGNATURE); - - test_entries.push(test_entry); - } - - // batch 3: - // * non-processable transfer due to underfunded fee-payer (two signatures) - // * successful transfer with the same fee-payer (one signature) - { - let mut test_entry = SvmTestEntry::default(); - - let feepayer_keypair = Keypair::new(); - let feepayer = feepayer_keypair.pubkey(); - let separate_source_keypair = Keypair::new(); - let separate_source = separate_source_keypair.pubkey(); - let destination = Pubkey::new_unique(); - - let mut feepayer_data = AccountSharedData::default(); - let mut separate_source_data = AccountSharedData::default(); - let mut destination_data = AccountSharedData::default(); - - feepayer_data.set_lamports(1 + LAMPORTS_PER_SIGNATURE + wallet_rent); - test_entry.add_initial_account(feepayer, &feepayer_data); - - separate_source_data.set_lamports(LAMPORTS_PER_SOL * 10); - test_entry.add_initial_account(separate_source, &separate_source_data); - - test_entry.push_transaction_with_status( - Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &separate_source, - &destination, - 1, - )], - Some(&feepayer), - &[&feepayer_keypair, &separate_source_keypair], - Hash::default(), - ), - ExecutionStatus::Discarded, - ); - - test_entry.push_transaction(system_transaction::transfer( - &feepayer_keypair, - &destination, - 1, - Hash::default(), - )); - - destination_data.checked_add_lamports(1).unwrap(); - test_entry.create_expected_account(destination, &destination_data); - - test_entry.decrease_expected_lamports(&feepayer, 1 + LAMPORTS_PER_SIGNATURE); - } - - // batch 4: - // * processable non-executable transaction - // * successful transfer - // this confirms we update the AccountsMap from RollbackAccounts intrabatch - { - let mut test_entry = SvmTestEntry::default(); - - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - let destination = Pubkey::new_unique(); - - let mut source_data = AccountSharedData::default(); - let mut destination_data = AccountSharedData::default(); - - source_data.set_lamports(LAMPORTS_PER_SOL * 10); - test_entry.add_initial_account(source, &source_data); - - let mut load_program_fail_instruction = - system_instruction::transfer(&source, &Pubkey::new_unique(), transfer_amount); - load_program_fail_instruction.program_id = Pubkey::new_unique(); - - test_entry.push_transaction_with_status( - Transaction::new_signed_with_payer( - &[load_program_fail_instruction], - Some(&source), - &[&source_keypair], - Hash::default(), - ), - ExecutionStatus::ProcessedFailed, - ); - - test_entry.push_transaction(system_transaction::transfer( - &source_keypair, - &destination, - transfer_amount, - Hash::default(), - )); - - destination_data - .checked_add_lamports(transfer_amount) - .unwrap(); - test_entry.create_expected_account(destination, &destination_data); - - test_entry - .decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE * 2); - - test_entries.push(test_entry); - } - - test_entries -} - -fn simd83_nonce_reuse(fee_paying_nonce: bool) -> Vec { - let mut test_entries = vec![]; - - let program_name = "hello-solana"; - let program_id = program_address(program_name); - - let fee_payer_keypair = Keypair::new(); - let non_fee_nonce_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - let nonce_pubkey = if fee_paying_nonce { - fee_payer - } else { - non_fee_nonce_keypair.pubkey() - }; - - let nonce_size = nonce::state::State::size(); - let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique()); - let initial_nonce_data = - nonce::state::Data::new(fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE); - let mut initial_nonce_account = AccountSharedData::new_data( - LAMPORTS_PER_SOL, - &nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)), - &system_program::id(), - ) - .unwrap(); - initial_nonce_account.set_rent_epoch(u64::MAX); - let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone()); - - let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH); - let mut advanced_nonce_info = initial_nonce_info; - advanced_nonce_info - .try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE) - .unwrap(); - - let advance_instruction = system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer); - let withdraw_instruction = system_instruction::withdraw_nonce_account( - &nonce_pubkey, - &fee_payer, - &fee_payer, - LAMPORTS_PER_SOL, - ); - - let successful_noop_instruction = Instruction::new_with_bytes(program_id, &[], vec![]); - let failing_noop_instruction = Instruction::new_with_bytes(system_program::id(), &[], vec![]); - let fee_only_noop_instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); - - let second_transaction = Transaction::new_signed_with_payer( - &[ - advance_instruction.clone(), - successful_noop_instruction.clone(), - ], - Some(&fee_payer), - &[&fee_payer_keypair], - *advanced_durable.as_hash(), - ); - - let mut common_test_entry = SvmTestEntry::default(); - - common_test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account); - - if !fee_paying_nonce { - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - common_test_entry.add_initial_account(fee_payer, &fee_payer_data); - } - - common_test_entry - .final_accounts - .get_mut(&nonce_pubkey) - .unwrap() - .data_as_mut_slice() - .copy_from_slice(advanced_nonce_info.account().data()); - - common_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - let common_test_entry = common_test_entry; - - // batch 0: one transaction that advances the nonce twice - { - let mut test_entry = common_test_entry.clone(); - - let transaction = Transaction::new_signed_with_payer( - &[advance_instruction.clone(), advance_instruction.clone()], - Some(&fee_payer), - &[&fee_payer_keypair], - *initial_durable.as_hash(), - ); - - test_entry.push_nonce_transaction_with_status( - transaction, - nonce_pubkey, - ExecutionStatus::ExecutedFailed, - ); - - test_entries.push(test_entry); - } - - // batch 1: - // * a successful nonce transaction - // * a nonce transaction that reuses the same nonce; this transaction must be dropped - { - let mut test_entry = common_test_entry.clone(); - - let first_transaction = Transaction::new_signed_with_payer( - &[ - advance_instruction.clone(), - successful_noop_instruction.clone(), - ], - Some(&fee_payer), - &[&fee_payer_keypair], - *initial_durable.as_hash(), - ); - - test_entry.push_nonce_transaction(first_transaction, nonce_pubkey); - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entries.push(test_entry); - } - - // batch 2: - // * an executable failed nonce transaction - // * a nonce transaction that reuses the same nonce; this transaction must be dropped - { - let mut test_entry = common_test_entry.clone(); - - let first_transaction = Transaction::new_signed_with_payer( - &[advance_instruction.clone(), failing_noop_instruction], - Some(&fee_payer), - &[&fee_payer_keypair], - *initial_durable.as_hash(), - ); - - test_entry.push_nonce_transaction_with_status( - first_transaction, - nonce_pubkey, - ExecutionStatus::ExecutedFailed, - ); - - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entries.push(test_entry); - } - - // batch 3: - // * a processable non-executable nonce transaction, if fee-only transactions are enabled - // * a nonce transaction that reuses the same nonce; this transaction must be dropped - { - let mut test_entry = common_test_entry.clone(); - - let first_transaction = Transaction::new_signed_with_payer( - &[advance_instruction.clone(), fee_only_noop_instruction], - Some(&fee_payer), - &[&fee_payer_keypair], - *initial_durable.as_hash(), - ); - - test_entry.push_nonce_transaction_with_status( - first_transaction, - nonce_pubkey, - ExecutionStatus::ProcessedFailed, - ); - - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entries.push(test_entry); - } - - // batch 4: - // * a successful blockhash transaction that also advances the nonce - // * a nonce transaction that reuses the same nonce; this transaction must be dropped - { - let mut test_entry = common_test_entry.clone(); - - let first_transaction = Transaction::new_signed_with_payer( - &[successful_noop_instruction.clone(), advance_instruction], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - test_entry.push_transaction(first_transaction); - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entries.push(test_entry); - } - - // batch 5: - // * a successful blockhash transaction that closes the nonce - // * a nonce transaction that uses the nonce; this transaction must be dropped - if !fee_paying_nonce { - let mut test_entry = common_test_entry.clone(); - - let first_transaction = Transaction::new_signed_with_payer( - slice::from_ref(&withdraw_instruction), - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - test_entry.push_transaction(first_transaction); - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entry.increase_expected_lamports(&fee_payer, LAMPORTS_PER_SOL); - - test_entry.drop_expected_account(nonce_pubkey); - - test_entries.push(test_entry); - } - - // batch 6: - // * a successful blockhash transaction that closes the nonce - // * a successful blockhash transaction that funds the closed account - // * a nonce transaction that uses the account; this transaction must be dropped - if !fee_paying_nonce { - let mut test_entry = common_test_entry.clone(); - - let first_transaction = Transaction::new_signed_with_payer( - slice::from_ref(&withdraw_instruction), - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - let middle_transaction = system_transaction::transfer( - &fee_payer_keypair, - &nonce_pubkey, - LAMPORTS_PER_SOL, - Hash::default(), - ); - - test_entry.push_transaction(first_transaction); - test_entry.push_transaction(middle_transaction); - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - let mut new_nonce_state = AccountSharedData::default(); - new_nonce_state.set_lamports(LAMPORTS_PER_SOL); - - test_entry.update_expected_account_data(nonce_pubkey, &new_nonce_state); - - test_entries.push(test_entry); - } - - // batch 7: - // * a successful blockhash transaction that closes the nonce - // * a successful blockhash transaction that reopens the account with proper nonce size - // * a nonce transaction that uses the account; this transaction must be dropped - if !fee_paying_nonce { - let mut test_entry = common_test_entry.clone(); - - let first_transaction = Transaction::new_signed_with_payer( - slice::from_ref(&withdraw_instruction), - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - let middle_transaction = system_transaction::create_account( - &fee_payer_keypair, - &non_fee_nonce_keypair, - Hash::default(), - LAMPORTS_PER_SOL, - nonce_size as u64, - &system_program::id(), - ); - - test_entry.push_transaction(first_transaction); - test_entry.push_transaction(middle_transaction); - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - - let new_nonce_state = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(vec![0; nonce_size]), - system_program::id(), - false, - u64::MAX, - ); - - test_entry.update_expected_account_data(nonce_pubkey, &new_nonce_state); - - test_entries.push(test_entry); - } - - // batch 8: - // * a successful blockhash transaction that closes the nonce - // * a successful blockhash transaction that reopens the nonce - // * a nonce transaction that uses the nonce; this transaction must be dropped - if !fee_paying_nonce { - let mut test_entry = common_test_entry.clone(); - - let first_transaction = Transaction::new_signed_with_payer( - slice::from_ref(&withdraw_instruction), - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - let create_instructions = system_instruction::create_nonce_account( - &fee_payer, - &nonce_pubkey, - &fee_payer, - LAMPORTS_PER_SOL, - ); - - let middle_transaction = Transaction::new_signed_with_payer( - &create_instructions, - Some(&fee_payer), - &[&fee_payer_keypair, &non_fee_nonce_keypair], - Hash::default(), - ); - - test_entry.push_transaction(first_transaction); - test_entry.push_transaction(middle_transaction); - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - - test_entries.push(test_entry); - } - - // batch 9: - // * a successful blockhash noop transaction - // * a nonce transaction that uses a spoofed nonce account; this transaction must be dropped - // check_age would never let such a transaction through validation - // this simulates the case where someone closes a nonce account, then reuses the address in the same batch - // but as a non-system account that parses as an initialized nonce account - if !fee_paying_nonce { - let mut test_entry = common_test_entry.clone(); - test_entry.initial_accounts.remove(&nonce_pubkey); - test_entry.final_accounts.remove(&nonce_pubkey); - - let mut fake_nonce_account = initial_nonce_account.clone(); - fake_nonce_account.set_rent_epoch(u64::MAX); - fake_nonce_account.set_owner(Pubkey::new_unique()); - test_entry.add_initial_account(nonce_pubkey, &fake_nonce_account); - - let first_transaction = Transaction::new_signed_with_payer( - slice::from_ref(&successful_noop_instruction), - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - test_entry.push_transaction(first_transaction); - test_entry.push_nonce_transaction_with_status( - second_transaction.clone(), - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - test_entries.push(test_entry); - } - - // batch 10: - // * a successful blockhash transaction that changes the nonce authority - // * a nonce transaction that uses the nonce with the old authority; this transaction must be dropped - if !fee_paying_nonce { - let mut test_entry = common_test_entry.clone(); - - let new_authority = Pubkey::new_unique(); - - let first_transaction = Transaction::new_signed_with_payer( - &[system_instruction::authorize_nonce_account( - &nonce_pubkey, - &fee_payer, - &new_authority, - )], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - test_entry.push_transaction(first_transaction); - test_entry.push_nonce_transaction_with_status( - second_transaction, - nonce_pubkey, - ExecutionStatus::Discarded, - ); - - let final_nonce_data = - nonce::state::Data::new(new_authority, initial_durable, LAMPORTS_PER_SIGNATURE); - let final_nonce_account = AccountSharedData::new_data( - LAMPORTS_PER_SOL, - &nonce::versions::Versions::new(nonce::state::State::Initialized(final_nonce_data)), - &system_program::id(), - ) - .unwrap(); - - test_entry.update_expected_account_data(nonce_pubkey, &final_nonce_account); - - test_entries.push(test_entry); - } - - // batch 11: - // * a successful blockhash transaction that changes the nonce authority - // * a nonce transaction that uses the nonce with the new authority; this transaction succeeds - if !fee_paying_nonce { - let mut test_entry = common_test_entry; - - let new_authority_keypair = Keypair::new(); - let new_authority = new_authority_keypair.pubkey(); - - let first_transaction = Transaction::new_signed_with_payer( - &[system_instruction::authorize_nonce_account( - &nonce_pubkey, - &fee_payer, - &new_authority, - )], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - let second_transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::advance_nonce_account(&nonce_pubkey, &new_authority), - successful_noop_instruction, - ], - Some(&fee_payer), - &[&fee_payer_keypair, &new_authority_keypair], - *initial_durable.as_hash(), - ); - - test_entry.push_transaction(first_transaction); - test_entry.push_nonce_transaction(second_transaction, nonce_pubkey); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - - let final_nonce_data = - nonce::state::Data::new(new_authority, advanced_durable, LAMPORTS_PER_SIGNATURE); - let final_nonce_account = AccountSharedData::new_data( - LAMPORTS_PER_SOL, - &nonce::versions::Versions::new(nonce::state::State::Initialized(final_nonce_data)), - &system_program::id(), - ) - .unwrap(); - - test_entry.update_expected_account_data(nonce_pubkey, &final_nonce_account); - - test_entries.push(test_entry); - } - - for test_entry in &mut test_entries { - test_entry.add_initial_program(program_name); - } - - test_entries -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum WriteProgramInstruction { - Print, - Set, - Dealloc, - Realloc(usize), -} -impl WriteProgramInstruction { - fn create_transaction( - self, - program_id: Pubkey, - fee_payer: &Keypair, - target: Pubkey, - clamp_data_size: Option, - ) -> Transaction { - let (instruction_data, account_metas) = match self { - Self::Print => (vec![0], vec![AccountMeta::new_readonly(target, false)]), - Self::Set => (vec![1], vec![AccountMeta::new(target, false)]), - Self::Dealloc => ( - vec![2], - vec![ - AccountMeta::new(target, false), - AccountMeta::new(solana_sdk_ids::incinerator::id(), false), - ], - ), - Self::Realloc(new_size) => { - let mut instruction_data = vec![3]; - instruction_data.extend_from_slice(&new_size.to_le_bytes()); - (instruction_data, vec![AccountMeta::new(target, false)]) - } - }; - - let mut instructions = vec![]; - - if let Some(size) = clamp_data_size { - instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(size)); - } - - instructions.push(Instruction::new_with_bytes( - program_id, - &instruction_data, - account_metas, - )); - - Transaction::new_signed_with_payer( - &instructions, - Some(&fee_payer.pubkey()), - &[fee_payer], - Hash::default(), - ) - } -} - -fn simd83_account_deallocate() -> Vec { - let mut test_entries = vec![]; - - // batch 0: sanity check, the program actually sets data - // batch 1: removing lamports from account hides it from subsequent in-batch transactions - for remove_lamports in [false, true] { - let mut test_entry = SvmTestEntry::default(); - - let program_name = "write-to-account"; - let program_id = program_address(program_name); - test_entry.add_initial_program(program_name); - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - - let target = Pubkey::new_unique(); - - let mut target_data = AccountSharedData::create_from_existing_shared_data( - Rent::default().minimum_balance(1), - Arc::new(vec![0]), - program_id, - false, - u64::MAX, - ); - test_entry.add_initial_account(target, &target_data); - - let set_data_transaction = WriteProgramInstruction::Set.create_transaction( - program_id, - &fee_payer_keypair, - target, - None, - ); - test_entry.push_transaction(set_data_transaction); - - target_data.data_as_mut_slice()[0] = 100; - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - test_entry.update_expected_account_data(target, &target_data); - - if remove_lamports { - let dealloc_transaction = WriteProgramInstruction::Dealloc.create_transaction( - program_id, - &fee_payer_keypair, - target, - None, - ); - test_entry.push_transaction(dealloc_transaction); - - let print_transaction = WriteProgramInstruction::Print.create_transaction( - program_id, - &fee_payer_keypair, - target, - None, - ); - test_entry.push_transaction(print_transaction); - test_entry.transaction_batch[2] - .asserts - .logs - .push("Program log: account size 0".to_string()); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - - test_entry.drop_expected_account(target); - } - - test_entries.push(test_entry); - } - - test_entries -} - -fn simd83_fee_payer_deallocate() -> Vec { - let mut test_entry = SvmTestEntry::default(); - - let program_name = "hello-solana"; - let real_program_id = program_address(program_name); - test_entry.add_initial_program(program_name); - - // rent minimum needs to be adjusted so fee payer can be deallocated - let rent = Rent { - lamports_per_byte: LAMPORTS_PER_SIGNATURE / solana_rent::ACCOUNT_STORAGE_OVERHEAD, - ..Rent::default() - }; - test_entry.set_rent_params(rent); - - // 0/1: a fee-payer balance goes to zero lamports on an executed transaction, the batch sees it as deallocated - // 2/3: the same, except if fee-only transactions are enabled, it goes to zero lamports from a fee-only transaction - for do_fee_only_transaction in [false, true] { - let dealloc_fee_payer_keypair = Keypair::new(); - let dealloc_fee_payer = dealloc_fee_payer_keypair.pubkey(); - - let mut dealloc_fee_payer_data = AccountSharedData::default(); - dealloc_fee_payer_data.set_lamports(LAMPORTS_PER_SIGNATURE); - test_entry.add_initial_account(dealloc_fee_payer, &dealloc_fee_payer_data); - - let stable_fee_payer_keypair = Keypair::new(); - let stable_fee_payer = stable_fee_payer_keypair.pubkey(); - - let mut stable_fee_payer_data = AccountSharedData::default(); - stable_fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(stable_fee_payer, &stable_fee_payer_data); - - // transaction which drains a fee-payer - let instruction = Instruction::new_with_bytes( - if do_fee_only_transaction { - Pubkey::new_unique() - } else { - real_program_id - }, - &[], - vec![], - ); - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&dealloc_fee_payer), - &[&dealloc_fee_payer_keypair], - Hash::default(), - ); - - test_entry.push_transaction_with_status( - transaction, - if do_fee_only_transaction { - ExecutionStatus::ProcessedFailed - } else { - ExecutionStatus::Succeeded - }, - ); - - test_entry.decrease_expected_lamports(&dealloc_fee_payer, LAMPORTS_PER_SIGNATURE); - - // as noted in `account_deallocate()` we must touch the account to see if anything actually happened - let instruction = Instruction::new_with_bytes( - real_program_id, - &[], - vec![AccountMeta::new_readonly(dealloc_fee_payer, false)], - ); - test_entry.push_transaction(Transaction::new_signed_with_payer( - &[instruction], - Some(&stable_fee_payer), - &[&stable_fee_payer_keypair], - Hash::default(), - )); - - test_entry.decrease_expected_lamports(&stable_fee_payer, LAMPORTS_PER_SIGNATURE); - - test_entry.drop_expected_account(dealloc_fee_payer); - } - - // 4: a non-nonce fee-payer balance goes to zero on a fee-only nonce transaction, the batch sees it as deallocated - // we test in `simple_nonce()` that nonce fee-payers cannot as a rule be brought below rent-exemption - { - let dealloc_fee_payer_keypair = Keypair::new(); - let dealloc_fee_payer = dealloc_fee_payer_keypair.pubkey(); - - let mut dealloc_fee_payer_data = AccountSharedData::default(); - dealloc_fee_payer_data.set_lamports(LAMPORTS_PER_SIGNATURE); - dealloc_fee_payer_data.set_rent_epoch(u64::MAX - 1); - test_entry.add_initial_account(dealloc_fee_payer, &dealloc_fee_payer_data); - - let stable_fee_payer_keypair = Keypair::new(); - let stable_fee_payer = stable_fee_payer_keypair.pubkey(); - - let mut stable_fee_payer_data = AccountSharedData::default(); - stable_fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(stable_fee_payer, &stable_fee_payer_data); - - let nonce_pubkey = Pubkey::new_unique(); - let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique()); - let initial_nonce_data = - nonce::state::Data::new(dealloc_fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE); - let mut initial_nonce_account = AccountSharedData::new_data( - LAMPORTS_PER_SOL, - &nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)), - &system_program::id(), - ) - .unwrap(); - initial_nonce_account.set_rent_epoch(u64::MAX); - let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone()); - - let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH); - let mut advanced_nonce_info = initial_nonce_info; - advanced_nonce_info - .try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE) - .unwrap(); - - test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account); - - let advance_instruction = - system_instruction::advance_nonce_account(&nonce_pubkey, &dealloc_fee_payer); - let fee_only_noop_instruction = - Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); - - // fee-only nonce transaction which drains a fee-payer - let transaction = Transaction::new_signed_with_payer( - &[advance_instruction, fee_only_noop_instruction], - Some(&dealloc_fee_payer), - &[&dealloc_fee_payer_keypair], - *initial_durable.as_hash(), - ); - test_entry.push_nonce_transaction_with_status( - transaction, - nonce_pubkey, - ExecutionStatus::ProcessedFailed, - ); - - test_entry - .final_accounts - .get_mut(&nonce_pubkey) - .unwrap() - .data_as_mut_slice() - .copy_from_slice(advanced_nonce_info.account().data()); - - test_entry.decrease_expected_lamports(&dealloc_fee_payer, LAMPORTS_PER_SIGNATURE); - - // as noted in `account_deallocate()` we must touch the account to see if anything actually happened - let instruction = Instruction::new_with_bytes( - real_program_id, - &[], - vec![AccountMeta::new_readonly(dealloc_fee_payer, false)], - ); - test_entry.push_transaction(Transaction::new_signed_with_payer( - &[instruction], - Some(&stable_fee_payer), - &[&stable_fee_payer_keypair], - Hash::default(), - )); - - test_entry.decrease_expected_lamports(&stable_fee_payer, LAMPORTS_PER_SIGNATURE); - - test_entry.drop_expected_account(dealloc_fee_payer); - } - - vec![test_entry] -} - -fn simd83_account_reallocate() -> Vec { - let mut test_entries = vec![]; - - let program_name = "write-to-account"; - let program_id = program_address(program_name); - let program_size = program_data_size(program_name); - - let mut common_test_entry = SvmTestEntry::default(); - common_test_entry.add_initial_program(program_name); - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - common_test_entry.add_initial_account(fee_payer, &fee_payer_data); - - let mk_target = |size| { - AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL * 10, - Arc::new(vec![0; size]), - program_id, - false, - u64::MAX, - ) - }; - - let target = Pubkey::new_unique(); - let target_start_size = 100; - common_test_entry.add_initial_account(target, &mk_target(target_start_size)); - - // we set a budget that is enough pre-large-realloc but not enough post-large-realloc - // we must add program size because programdata buffers are counted - let size_budget = Some((program_size + MAX_PERMITTED_DATA_INCREASE) as u32); - - let print_transaction = WriteProgramInstruction::Print.create_transaction( - program_id, - &fee_payer_keypair, - target, - size_budget, - ); - - common_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - - let common_test_entry = common_test_entry; - - // batch 0/1: - // * successful realloc up/down - // * change reflected in same batch - for new_target_size in [target_start_size + 1, target_start_size - 1] { - let mut test_entry = common_test_entry.clone(); - - let realloc_transaction = WriteProgramInstruction::Realloc(new_target_size) - .create_transaction(program_id, &fee_payer_keypair, target, None); - test_entry.push_transaction(realloc_transaction); - - test_entry.push_transaction(print_transaction.clone()); - test_entry.transaction_batch[1] - .asserts - .logs - .push(format!("Program log: account size {new_target_size}")); - - test_entry.update_expected_account_data(target, &mk_target(new_target_size)); - - test_entries.push(test_entry); - } - - // batch 2: - // * successful large realloc up - // * transaction is aborted based on the new transaction data size post-realloc - { - let mut test_entry = common_test_entry; - - let new_target_size = target_start_size + MAX_PERMITTED_DATA_INCREASE; - - let realloc_transaction = WriteProgramInstruction::Realloc(new_target_size) - .create_transaction(program_id, &fee_payer_keypair, target, None); - test_entry.push_transaction(realloc_transaction); - - test_entry - .push_transaction_with_status(print_transaction, ExecutionStatus::ProcessedFailed); - - test_entry.update_expected_account_data(target, &mk_target(new_target_size)); - - test_entries.push(test_entry); - } - - test_entries -} - -enum AbortReason { - None, - Unprocessable, - DropOnFailure, -} - -fn all_or_nothing(abort: AbortReason) -> Vec { - let mut test_entry = SvmTestEntry { - all_or_nothing: true, - drop_on_failure: matches!(abort, AbortReason::DropOnFailure), - ..Default::default() - }; - let transfer_amount = LAMPORTS_PER_SOL; - - // 0: a transfer that succeeds - { - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - let destination = Pubkey::new_unique(); - - let mut source_data = AccountSharedData::default(); - let mut destination_data = AccountSharedData::default(); - - source_data.set_lamports(LAMPORTS_PER_SOL * 10); - test_entry.add_initial_account(source, &source_data); - - let status = match abort { - AbortReason::None => { - destination_data - .checked_add_lamports(transfer_amount) - .unwrap(); - test_entry.create_expected_account(destination, &destination_data); - test_entry - .decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); - - ExecutionStatus::Succeeded - } - AbortReason::Unprocessable | AbortReason::DropOnFailure => { - test_entry.final_accounts.insert(source, source_data); - - ExecutionStatus::Discarded - } - }; - - test_entry.push_transaction_with_status( - system_transaction::transfer( - &source_keypair, - &destination, - transfer_amount, - Hash::default(), - ), - status, - ); - } - - // 1: an executable transfer that fails - if matches!(abort, AbortReason::DropOnFailure) { - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - - let mut source_data = AccountSharedData::default(); - - source_data.set_lamports(transfer_amount - 1); - test_entry.add_initial_account(source, &source_data); - test_entry.final_accounts.insert(source, source_data); - - test_entry.push_transaction_with_status( - system_transaction::transfer( - &source_keypair, - &Pubkey::new_unique(), - transfer_amount, - Hash::default(), - ), - ExecutionStatus::Discarded, - ); - } - - // 2: a non-processable transfer that fails before loading - if matches!(abort, AbortReason::Unprocessable) { - test_entry.transaction_batch.push(TransactionBatchItem { - transaction: system_transaction::transfer( - &Keypair::new(), - &Pubkey::new_unique(), - transfer_amount, - Hash::default(), - ), - check_result: Err(TransactionError::BlockhashNotFound), - asserts: ExecutionStatus::Discarded.into(), - }); - } - - vec![test_entry] -} - -fn drop_on_failure_batch(statuses: &[bool]) -> Vec { - let mut test_entry = SvmTestEntry { - drop_on_failure: true, - ..Default::default() - }; - let transfer_amount = LAMPORTS_PER_SOL; - - // Shared source account to fund all transfers. - let source_keypair = Keypair::new(); - let source = source_keypair.pubkey(); - let mut source_data = AccountSharedData::default(); - source_data.set_lamports(LAMPORTS_PER_SOL * 100); - test_entry.add_initial_account(source, &source_data); - - // Shared destination account to receive all transfers. - let destination = Pubkey::new_unique(); - let mut destination_data = AccountSharedData::default(); - - println!("source: {source}"); - println!("destination: {destination}"); - - for success in statuses { - match success { - true => { - test_entry - .decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); - destination_data - .checked_add_lamports(transfer_amount) - .unwrap(); - destination_data.set_rent_epoch(u64::MAX); - - test_entry.push_transaction_with_status( - system_transaction::transfer( - &source_keypair, - &destination, - transfer_amount, - Hash::default(), - ), - ExecutionStatus::Succeeded, - ); - } - false => test_entry.push_transaction_with_status( - system_transaction::transfer( - &source_keypair, - &destination, - source_data.lamports() + 1, - Hash::default(), - ), - ExecutionStatus::Discarded, - ), - } - } - - // Set the final expected source state. - if statuses.iter().all(|success| !*success) { - test_entry - .final_accounts - .get_mut(&source) - .unwrap() - .set_rent_epoch(0); - } - - // Set the final expected destination state. - if statuses.iter().any(|success| *success) { - assert!( - test_entry - .final_accounts - .insert(destination, destination_data) - .is_none() - ); - } - - vec![test_entry] -} - -#[test_case(program_medley(false))] -#[test_case(program_medley(true))] -#[test_case(simple_transfer(false))] -#[test_case(simple_transfer(true))] -#[test_case(simple_nonce(false))] -#[test_case(simple_nonce(true))] -#[test_case(simd83_intrabatch_account_reuse())] -#[test_case(simd83_nonce_reuse(false))] -#[test_case(simd83_nonce_reuse(true))] -#[test_case(simd83_account_deallocate())] -#[test_case(simd83_fee_payer_deallocate())] -#[test_case(simd83_account_reallocate())] -#[test_case(all_or_nothing(AbortReason::None))] -#[test_case(all_or_nothing(AbortReason::Unprocessable))] -#[test_case(all_or_nothing(AbortReason::DropOnFailure))] -#[test_case(drop_on_failure_batch(&[false]))] -#[test_case(drop_on_failure_batch(&[true]))] -#[test_case(drop_on_failure_batch(&[false, false]))] -#[test_case(drop_on_failure_batch(&[true, true]))] -#[test_case(drop_on_failure_batch(&[false, false, true]))] -#[test_case(drop_on_failure_batch(&[true, true, false]))] -#[test_case(drop_on_failure_batch(&[false, true, false]))] -#[test_case(drop_on_failure_batch(&[true, false, true]))] -fn svm_integration(test_entries: Vec) { - for test_entry in test_entries { - let env = SvmTestEnvironment::create(test_entry); - env.execute(); - } -} - -#[test] -fn program_cache_create_account() { - let supported_loaders = [ - bpf_loader_upgradeable::id(), - bpf_loader::id(), - bpf_loader_deprecated::id(), - ]; - for loader_id in &supported_loaders { - let mut test_entry = SvmTestEntry::default(); - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 10); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - - let new_account_keypair = Keypair::new(); - let program_id = new_account_keypair.pubkey(); - - // create an account owned by a loader - let create_transaction = system_transaction::create_account( - &fee_payer_keypair, - &new_account_keypair, - Hash::default(), - LAMPORTS_PER_SOL, - 0, - loader_id, - ); - - test_entry.push_transaction(create_transaction); - - test_entry - .decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SOL + LAMPORTS_PER_SIGNATURE * 2); - - // attempt to invoke the new account - let invoke_transaction = Transaction::new_signed_with_payer( - &[Instruction::new_with_bytes(program_id, &[], vec![])], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - test_entry.push_transaction_with_status( - invoke_transaction.clone(), - ExecutionStatus::ExecutedFailed, - ); - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - let mut env = SvmTestEnvironment::create(test_entry); - - // test in same entry as account creation - env.execute(); - - let mut test_entry = SvmTestEntry { - initial_accounts: env.test_entry.final_accounts.clone(), - final_accounts: env.test_entry.final_accounts.clone(), - ..SvmTestEntry::default() - }; - - test_entry - .push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed); - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - // test in different entry same slot - env.test_entry = test_entry; - env.execute(); - } -} - -#[test_case(false, false; "close::scan_only")] -#[test_case(false, true; "close::invoke")] -#[test_case(true, false; "upgrade::scan_only")] -#[test_case(true, true; "upgrade::invoke")] -fn program_cache_loaderv3_update_tombstone(upgrade_program: bool, invoke_changed_program: bool) { - let mut test_entry = SvmTestEntry::default(); - - let program_name = "hello-solana"; - let program_id = program_address(program_name); - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - - test_entry - .initial_programs - .push((program_name.to_string(), DEPLOYMENT_SLOT, Some(fee_payer))); - - let buffer_address = Pubkey::new_unique(); - - // upgrade or close a deployed program - let change_instruction = if upgrade_program { - let mut data = bincode::serialize(&UpgradeableLoaderState::Buffer { - authority_address: Some(fee_payer), - }) - .unwrap(); - let mut program_bytecode = load_program(program_name.to_string()); - data.append(&mut program_bytecode); - - let buffer_account = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(data), - bpf_loader_upgradeable::id(), - true, - u64::MAX, - ); - - test_entry.add_initial_account(buffer_address, &buffer_account); - test_entry.drop_expected_account(buffer_address); - - loaderv3_instruction::upgrade( - &program_id, - &buffer_address, - &fee_payer, - &Pubkey::new_unique(), - ) - } else { - loaderv3_instruction::close_any( - &get_program_data_address(&program_id), - &Pubkey::new_unique(), - Some(&fee_payer), - Some(&program_id), - ) - }; - - test_entry.push_transaction(Transaction::new_signed_with_payer( - &[change_instruction], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - )); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - let invoke_transaction = Transaction::new_signed_with_payer( - &[Instruction::new_with_bytes(program_id, &[], vec![])], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - // attempt to invoke the program, which must fail - // this ensures the local program cache reflects the change of state - // we have cases without this so we can assert the cache *before* the invoke contains the tombstone - if invoke_changed_program { - test_entry.push_transaction_with_status( - invoke_transaction.clone(), - ExecutionStatus::ExecutedFailed, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - } - - let mut env = SvmTestEnvironment::create(test_entry); - - // test in same entry as program change - env.execute(); - assert!(env.is_program_blocked(&program_id)); - - let mut test_entry = SvmTestEntry { - initial_accounts: env.test_entry.final_accounts.clone(), - final_accounts: env.test_entry.final_accounts.clone(), - ..SvmTestEntry::default() - }; - - test_entry.push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - // test in different entry same slot - env.test_entry = test_entry; - env.execute(); - assert!(env.is_program_blocked(&program_id)); -} - -#[test_case(false; "upgrade::scan_only")] -#[test_case(true; "upgrade::invoke")] -fn program_cache_loaderv3_buffer_swap(invoke_changed_program: bool) { - let mut test_entry = SvmTestEntry::default(); - - let program_name = "hello-solana"; - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 10); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - - // this account will start as a buffer and then become a program - // buffers make their way into the program cache - // so we test that pathological address reuse is not a problem - let target_keypair = Keypair::new(); - let target = target_keypair.pubkey(); - let programdata_address = get_program_data_address(&target); - - // we have the same buffer ready at a different address to deploy from - let deploy_keypair = Keypair::new(); - let deploy = deploy_keypair.pubkey(); - - let mut buffer_data = bincode::serialize(&UpgradeableLoaderState::Buffer { - authority_address: Some(fee_payer), - }) - .unwrap(); - let mut program_bytecode = load_program(program_name.to_string()); - buffer_data.append(&mut program_bytecode); - - let buffer_account = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(buffer_data.clone()), - bpf_loader_upgradeable::id(), - true, - u64::MAX, - ); - - test_entry.add_initial_account(target, &buffer_account); - test_entry.add_initial_account(deploy, &buffer_account); - - let program_data = bincode::serialize(&UpgradeableLoaderState::Program { - programdata_address, - }) - .unwrap(); - let program_account = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(program_data), - bpf_loader_upgradeable::id(), - true, - u64::MAX, - ); - test_entry.update_expected_account_data(target, &program_account); - test_entry.drop_expected_account(deploy); - - // close the buffer - let close_instruction = - loaderv3_instruction::close_any(&target, &Pubkey::new_unique(), Some(&fee_payer), None); - - // reopen as a program - #[allow(deprecated)] - let deploy_instruction = loaderv3_instruction::deploy_with_max_program_len( - &fee_payer, - &target, - &deploy, - &fee_payer, - LAMPORTS_PER_SOL, - buffer_data.len(), - ) - .unwrap(); - - test_entry.push_transaction(Transaction::new_signed_with_payer( - &[close_instruction], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - )); - - test_entry.push_transaction(Transaction::new_signed_with_payer( - &deploy_instruction, - Some(&fee_payer), - &[&fee_payer_keypair, &target_keypair], - Hash::default(), - )); - - test_entry.decrease_expected_lamports( - &fee_payer, - Rent::default().minimum_balance( - UpgradeableLoaderState::size_of_programdata_metadata() + buffer_data.len(), - ) + LAMPORTS_PER_SIGNATURE * 3, - ); - - let invoke_transaction = Transaction::new_signed_with_payer( - &[Instruction::new_with_bytes(target, &[], vec![])], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - if invoke_changed_program { - test_entry.push_transaction_with_status( - invoke_transaction.clone(), - ExecutionStatus::ExecutedFailed, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - } - - let mut env = SvmTestEnvironment::create(test_entry); - - // test in same entry as program change - env.execute(); - assert!(env.is_program_blocked(&target)); - - let mut test_entry = SvmTestEntry { - initial_accounts: env.test_entry.final_accounts.clone(), - final_accounts: env.test_entry.final_accounts.clone(), - ..SvmTestEntry::default() - }; - - test_entry.push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - // test in different entry same slot - env.test_entry = test_entry; - env.execute(); - assert!(env.is_program_blocked(&target)); -} - -#[test] -fn program_cache_stats() { - let mut test_entry = SvmTestEntry::default(); - - let program_name = "hello-solana"; - let noop_program = program_address(program_name); - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - - let mut fee_payer_data = AccountSharedData::default(); - fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 100); - test_entry.add_initial_account(fee_payer, &fee_payer_data); - - test_entry - .initial_programs - .push((program_name.to_string(), DEPLOYMENT_SLOT, Some(fee_payer))); - - let missing_program = Pubkey::new_unique(); - - // set up a future upgrade after the first batch - let buffer_address = Pubkey::new_unique(); - { - let mut data = bincode::serialize(&UpgradeableLoaderState::Buffer { - authority_address: Some(fee_payer), - }) - .unwrap(); - let mut program_bytecode = load_program(program_name.to_string()); - data.append(&mut program_bytecode); - - let buffer_account = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(data), - bpf_loader_upgradeable::id(), - true, - u64::MAX, - ); - - test_entry.add_initial_account(buffer_address, &buffer_account); - } - - let make_transaction = |instructions: &[Instruction]| { - Transaction::new_signed_with_payer( - instructions, - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ) - }; - - let successful_noop_instruction = Instruction::new_with_bytes(noop_program, &[], vec![]); - let successful_transfer_instruction = - system_instruction::transfer(&fee_payer, &Pubkey::new_unique(), LAMPORTS_PER_SOL); - let failing_transfer_instruction = - system_instruction::transfer(&fee_payer, &Pubkey::new_unique(), LAMPORTS_PER_SOL * 1000); - let fee_only_noop_instruction = Instruction::new_with_bytes(missing_program, &[], vec![]); - - let mut noop_tx_usage = 0; - let mut system_tx_usage = 0; - let mut successful_transfers = 0; - - test_entry.push_transaction(make_transaction(slice::from_ref( - &successful_noop_instruction, - ))); - noop_tx_usage += 1; - - test_entry.push_transaction(make_transaction(slice::from_ref( - &successful_transfer_instruction, - ))); - system_tx_usage += 1; - successful_transfers += 1; - - test_entry.push_transaction_with_status( - make_transaction(slice::from_ref(&failing_transfer_instruction)), - ExecutionStatus::ExecutedFailed, - ); - system_tx_usage += 1; - - test_entry.push_transaction(make_transaction(&[ - successful_noop_instruction.clone(), - successful_noop_instruction.clone(), - successful_transfer_instruction.clone(), - successful_transfer_instruction.clone(), - successful_noop_instruction.clone(), - ])); - noop_tx_usage += 1; - system_tx_usage += 1; - successful_transfers += 2; - - test_entry.push_transaction_with_status( - make_transaction(&[ - failing_transfer_instruction, - successful_noop_instruction.clone(), - successful_transfer_instruction.clone(), - ]), - ExecutionStatus::ExecutedFailed, - ); - noop_tx_usage += 1; - system_tx_usage += 1; - - // load failure/fee-only does not touch the program cache - test_entry.push_transaction_with_status( - make_transaction(&[ - successful_noop_instruction.clone(), - fee_only_noop_instruction, - ]), - ExecutionStatus::ProcessedFailed, - ); - - test_entry.decrease_expected_lamports( - &fee_payer, - LAMPORTS_PER_SIGNATURE * test_entry.transaction_batch.len() as u64 - + LAMPORTS_PER_SOL * successful_transfers, - ); - - // nor does discard - test_entry.transaction_batch.push(TransactionBatchItem { - transaction: make_transaction(slice::from_ref(&successful_transfer_instruction)), - check_result: Err(TransactionError::BlockhashNotFound), - asserts: ExecutionStatus::Discarded.into(), - }); - - let mut env = SvmTestEnvironment::create(test_entry); - env.execute(); - - // check all usage stats are as we expect - let global_program_cache = env - .batch_processor - .global_program_cache - .read() - .unwrap() - .get_flattened_entries_for_tests() - .into_iter() - .rev() - .collect::>(); - - let (_, noop_entry) = global_program_cache - .iter() - .find(|(pubkey, _)| *pubkey == noop_program) - .unwrap(); - - assert_eq!( - noop_entry.stats.uses.load(Ordering::Relaxed), - noop_tx_usage, - "noop_tx_usage matches" - ); - - let (_, system_entry) = global_program_cache - .iter() - .find(|(pubkey, _)| *pubkey == system_program::id()) - .unwrap(); - - assert_eq!( - system_entry.stats.uses.load(Ordering::Relaxed), - system_tx_usage, - "system_tx_usage matches" - ); - - assert!( - !global_program_cache - .iter() - .any(|(pubkey, _)| *pubkey == missing_program), - "missing_program is missing" - ); - - // set up the second batch - let mut test_entry = SvmTestEntry { - initial_accounts: env.test_entry.final_accounts.clone(), - final_accounts: env.test_entry.final_accounts.clone(), - ..SvmTestEntry::default() - }; - - // upgrade the program. this blocks execution but does not create a tombstone - // the main thing we are testing is the tx counter is ported across upgrades - // - // note the upgrade transaction actually counts as a usage, per the existing rules - // the program cache must load the program because it has no idea if it will be used for cpi - test_entry.push_transaction(Transaction::new_signed_with_payer( - &[loaderv3_instruction::upgrade( - &noop_program, - &buffer_address, - &fee_payer, - &Pubkey::new_unique(), - )], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - )); - noop_tx_usage += 1; - - test_entry.drop_expected_account(buffer_address); - - test_entry.push_transaction_with_status( - make_transaction(slice::from_ref(&successful_noop_instruction)), - ExecutionStatus::ExecutedFailed, - ); - noop_tx_usage += 1; - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - - env.test_entry = test_entry; - env.execute(); - - let (_, noop_entry) = env - .batch_processor - .global_program_cache - .read() - .unwrap() - .get_flattened_entries_for_tests() - .into_iter() - .rev() - .find(|(pubkey, _)| *pubkey == noop_program) - .unwrap(); - - assert_eq!( - noop_entry.stats.uses.load(Ordering::Relaxed), - noop_tx_usage, - "noop_tx_usage matches" - ); - - // third batch, this creates a delayed visibility tombstone - let mut test_entry = SvmTestEntry { - initial_accounts: env.test_entry.final_accounts.clone(), - final_accounts: env.test_entry.final_accounts.clone(), - ..SvmTestEntry::default() - }; - - test_entry.push_transaction_with_status( - make_transaction(slice::from_ref(&successful_noop_instruction)), - ExecutionStatus::ExecutedFailed, - ); - noop_tx_usage += 1; - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - env.test_entry = test_entry; - env.execute(); - - let (_, noop_entry) = env - .batch_processor - .global_program_cache - .read() - .unwrap() - .get_flattened_entries_for_tests() - .into_iter() - .rev() - .find(|(pubkey, _)| *pubkey == noop_program) - .unwrap(); - - assert_eq!( - noop_entry.stats.uses.load(Ordering::Relaxed), - noop_tx_usage, - "noop_tx_usage matches" - ); -} - -#[derive(Clone, PartialEq, Eq)] -enum Inspect<'a> { - LiveRead(&'a AccountSharedData), - LiveWrite(&'a AccountSharedData), - #[allow(dead_code)] - DeadRead, - DeadWrite, -} -impl From> for (Option, bool) { - fn from(inspect: Inspect) -> Self { - match inspect { - Inspect::LiveRead(account) => (Some(account.clone()), false), - Inspect::LiveWrite(account) => (Some(account.clone()), true), - Inspect::DeadRead => (None, false), - Inspect::DeadWrite => (None, true), - } - } -} - -#[derive(Clone, Default)] -struct InspectedAccounts(pub HashMap, bool)>>); -impl InspectedAccounts { - fn inspect(&mut self, pubkey: Pubkey, inspect: Inspect) { - self.0.entry(pubkey).or_default().push(inspect.into()) - } -} - -#[test_case(false; "separate_nonce::old")] -#[test_case(true; "fee_paying_nonce::old")] -fn svm_inspect_nonce_load_failure(fee_paying_nonce: bool) { - let mut test_entry = SvmTestEntry::default(); - let mut expected_inspected_accounts = InspectedAccounts::default(); - - let fee_payer_keypair = Keypair::new(); - let separate_nonce_keypair = Keypair::new(); - - let fee_payer = fee_payer_keypair.pubkey(); - let nonce_pubkey = if fee_paying_nonce { - fee_payer - } else { - separate_nonce_keypair.pubkey() - }; - - let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique()); - let initial_nonce_data = - nonce::state::Data::new(fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE); - let mut initial_nonce_account = AccountSharedData::new_data( - LAMPORTS_PER_SOL, - &nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)), - &system_program::id(), - ) - .unwrap(); - initial_nonce_account.set_rent_epoch(u64::MAX); - let initial_nonce_account = initial_nonce_account; - let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone()); - - let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH); - let mut advanced_nonce_info = initial_nonce_info; - advanced_nonce_info - .try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE) - .unwrap(); - - let compute_instruction = ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(1); - let advance_instruction = system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer); - let fee_only_noop_instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); - - test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account); - - let mut separate_fee_payer_account = AccountSharedData::default(); - separate_fee_payer_account.set_lamports(LAMPORTS_PER_SOL); - let separate_fee_payer_account = separate_fee_payer_account; - - // we always inspect the nonce at least once - expected_inspected_accounts.inspect(nonce_pubkey, Inspect::LiveWrite(&initial_nonce_account)); - - // if we have a fee-paying nonce, we happen to inspect it again - // this is an unimportant implementation detail and also means these cases are trivial - // the true test is a separate nonce, to ensure we inspect it in pre-checks - if fee_paying_nonce { - expected_inspected_accounts - .inspect(nonce_pubkey, Inspect::LiveWrite(&initial_nonce_account)); - } else { - test_entry.add_initial_account(fee_payer, &separate_fee_payer_account); - expected_inspected_accounts - .inspect(fee_payer, Inspect::LiveWrite(&separate_fee_payer_account)); - } - - let transaction = Transaction::new_signed_with_payer( - &[ - advance_instruction, - compute_instruction, - fee_only_noop_instruction, - ], - Some(&fee_payer), - &[&fee_payer_keypair], - *initial_durable.as_hash(), - ); - - test_entry.push_nonce_transaction_with_status( - transaction, - nonce_pubkey, - ExecutionStatus::ProcessedFailed, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - test_entry - .final_accounts - .get_mut(&nonce_pubkey) - .unwrap() - .data_as_mut_slice() - .copy_from_slice(advanced_nonce_info.account().data()); - - let env = SvmTestEnvironment::create(test_entry.clone()); - env.execute(); - - let actual_inspected_accounts = env.mock_bank.inspected_accounts.read().unwrap().clone(); - for (expected_pubkey, expected_account) in &expected_inspected_accounts.0 { - let actual_account = actual_inspected_accounts.get(expected_pubkey).unwrap(); - assert_eq!( - expected_account, actual_account, - "pubkey: {expected_pubkey}", - ); - } -} - -#[test] -fn svm_inspect_account() { - let mut initial_test_entry = SvmTestEntry::default(); - let mut expected_inspected_accounts = InspectedAccounts::default(); - - let fee_payer_keypair = Keypair::new(); - let sender_keypair = Keypair::new(); - - let fee_payer = fee_payer_keypair.pubkey(); - let sender = sender_keypair.pubkey(); - let recipient = Pubkey::new_unique(); - - // Setting up the accounts for the transfer - - // fee payer - let mut fee_payer_account = AccountSharedData::default(); - fee_payer_account.set_lamports(10_000_000); - fee_payer_account.set_rent_epoch(u64::MAX); - initial_test_entry.add_initial_account(fee_payer, &fee_payer_account); - expected_inspected_accounts.inspect(fee_payer, Inspect::LiveWrite(&fee_payer_account)); - - // sender - let mut sender_account = AccountSharedData::default(); - sender_account.set_lamports(11_000_000); - sender_account.set_rent_epoch(u64::MAX); - initial_test_entry.add_initial_account(sender, &sender_account); - expected_inspected_accounts.inspect(sender, Inspect::LiveWrite(&sender_account)); - - // recipient -- initially dead - expected_inspected_accounts.inspect(recipient, Inspect::DeadWrite); - - // system program - let system_account = AccountSharedData::create_from_existing_shared_data( - 5000, - Arc::new("system_program".as_bytes().to_vec()), - native_loader::id(), - true, - 0, - ); - expected_inspected_accounts.inspect(system_program::id(), Inspect::LiveRead(&system_account)); - - let transfer_amount = 1_000_000; - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &sender, - &recipient, - transfer_amount, - )], - Some(&fee_payer), - &[&fee_payer_keypair, &sender_keypair], - Hash::default(), - ); - - initial_test_entry.push_transaction(transaction); - - let mut recipient_account = AccountSharedData::default(); - recipient_account.set_lamports(transfer_amount); - - initial_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - initial_test_entry.decrease_expected_lamports(&sender, transfer_amount); - initial_test_entry.create_expected_account(recipient, &recipient_account); - - let initial_test_entry = initial_test_entry; - - // Load and execute the transaction - let mut env = SvmTestEnvironment::create(initial_test_entry.clone()); - env.execute(); - - // do another transfer; recipient should be alive now - - // fee payer - let intermediate_fee_payer_account = initial_test_entry - .final_accounts - .get(&fee_payer) - .cloned() - .unwrap(); - expected_inspected_accounts.inspect( - fee_payer, - Inspect::LiveWrite(&intermediate_fee_payer_account), - ); - - // sender - let intermediate_sender_account = initial_test_entry - .final_accounts - .get(&sender) - .cloned() - .unwrap(); - expected_inspected_accounts.inspect(sender, Inspect::LiveWrite(&intermediate_sender_account)); - - // recipient -- now alive - let intermediate_recipient_account = initial_test_entry - .final_accounts - .get(&recipient) - .cloned() - .unwrap(); - expected_inspected_accounts.inspect( - recipient, - Inspect::LiveWrite(&intermediate_recipient_account), - ); - - // system program - expected_inspected_accounts.inspect(system_program::id(), Inspect::LiveRead(&system_account)); - - let mut final_test_entry = SvmTestEntry { - initial_accounts: initial_test_entry.final_accounts.clone(), - final_accounts: initial_test_entry.final_accounts, - ..SvmTestEntry::default() - }; - - let transfer_amount = 456; - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &sender, - &recipient, - transfer_amount, - )], - Some(&fee_payer), - &[&fee_payer_keypair, &sender_keypair], - Hash::default(), - ); - - final_test_entry.push_transaction(transaction); - - final_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - final_test_entry.decrease_expected_lamports(&sender, transfer_amount); - final_test_entry.increase_expected_lamports(&recipient, transfer_amount); - - // Load and execute the second transaction - env.test_entry = final_test_entry; - env.execute(); - - // Ensure all the expected inspected accounts were inspected - let actual_inspected_accounts = env.mock_bank.inspected_accounts.read().unwrap().clone(); - for (expected_pubkey, expected_account) in &expected_inspected_accounts.0 { - let actual_account = actual_inspected_accounts.get(expected_pubkey).unwrap(); - assert_eq!( - expected_account, actual_account, - "pubkey: {expected_pubkey}", - ); - } - - let num_expected_inspected_accounts: usize = - expected_inspected_accounts.0.values().map(Vec::len).sum(); - let num_actual_inspected_accounts: usize = - actual_inspected_accounts.values().map(Vec::len).sum(); - - assert_eq!( - num_expected_inspected_accounts, - num_actual_inspected_accounts, - ); -} - -#[test_case(false; "old_fee_only")] -#[test_case(true; "simd186_fee_only")] -fn fee_only_loaded_transaction_data_size(define_ltds_fee_only_semantics: bool) { - let mut common_test_entry = SvmTestEntry::default(); - common_test_entry.feature_set.define_ltds_fee_only_semantics = define_ltds_fee_only_semantics; - - let program_name = "hello-solana"; - let program_id = program_address(program_name); - let loaded_program_size = (UpgradeableLoaderState::size_of_program() - + program_data_size(program_name) - + TRANSACTION_ACCOUNT_BASE_SIZE * 2) as u32; - - common_test_entry.add_initial_program(program_name); - - let fee_payer_keypair = Keypair::new(); - let fee_payer = fee_payer_keypair.pubkey(); - let loaded_fee_payer_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32; - - let fee_payer_data = - AccountSharedData::new_rent_epoch(LAMPORTS_PER_SOL, 0, &Pubkey::default(), u64::MAX); - - common_test_entry.add_initial_account(fee_payer, &fee_payer_data); - - let mut loaded_account_sizes = vec![]; - - // make accounts of base size 512..=8192 - for i in 9..=13 { - let base_size = 2_usize.pow(i); - - let pubkey = Pubkey::new_unique(); - let account_data = AccountSharedData::new_rent_epoch( - LAMPORTS_PER_SOL, - base_size, - &Pubkey::default(), - u64::MAX, - ); - - common_test_entry.add_initial_account(pubkey, &account_data); - loaded_account_sizes.push((pubkey, base_size + TRANSACTION_ACCOUNT_BASE_SIZE)); - } - - let common_test_entry = common_test_entry; - - let transaction = |program_id: Pubkey, accounts: &[Pubkey], loaded_data_limit: Option| { - let account_metas = accounts - .iter() - .map(|pubkey| AccountMeta { - pubkey: *pubkey, - ..AccountMeta::default() - }) - .collect::>(); - - let mut instructions = vec![]; - - if let Some(size) = loaded_data_limit { - instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(size)); - } - - instructions.push(Instruction::new_with_bytes(program_id, &[], account_metas)); - - Transaction::new_signed_with_payer( - &instructions, - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ) - }; - - // for increasing sets of accounts, run: - // * success: loaded size is total size - // * fail due to limit: loaded size is limit with feature, 0 without - // * fail due to program id: loaded size is total size with feature, 0 without - for count in 0..loaded_account_sizes.len() { - let mut test_entry = common_test_entry.clone(); - - let (account_keys, other_accounts_size) = - &loaded_account_sizes[..count] - .iter() - .fold((vec![], 0), |mut acc, (pubkey, size)| { - acc.0.push(*pubkey); - acc.1 += *size as u32; - acc - }); - - let success_transaction = transaction(program_id, account_keys, None); - test_entry.push_transaction_with_status(success_transaction, ExecutionStatus::Succeeded); - - let size_limit = (other_accounts_size / 2).max(1); - let fail_limit_transaction = transaction(program_id, account_keys, Some(size_limit)); - test_entry - .push_transaction_with_status(fail_limit_transaction, ExecutionStatus::ProcessedFailed); - - let fail_program_id_transaction = transaction(Pubkey::new_unique(), account_keys, None); - test_entry.push_transaction_with_status( - fail_program_id_transaction, - ExecutionStatus::ProcessedFailed, - ); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 3); - - let env = SvmTestEnvironment::create(test_entry); - let output = env.execute(); - - let success_loaded_size = output.processing_results[0] - .as_ref() - .unwrap() - .loaded_accounts_data_size(); - - // success is always computed size - assert_eq!( - loaded_fee_payer_size + loaded_program_size + other_accounts_size, - success_loaded_size, - ); - - let fail_limit_loaded_size = output.processing_results[1] - .as_ref() - .unwrap() - .loaded_accounts_data_size(); - - // blowing limit with define_ltds_fee_only_semantics sets the size to the limit - // otherwise it is the raw sum of rollback sizes which here is zero - assert_eq!( - if define_ltds_fee_only_semantics { - size_limit - } else { - 0 - }, - fail_limit_loaded_size, - ); - - let fail_program_id_loaded_size = output.processing_results[2] - .as_ref() - .unwrap() - .loaded_accounts_data_size(); - - // violating constraints *after* passing size with define_ltds_fee_only_semantics uses the size - // otherwise as above it is the raw sum of rollback sizes which here is zero - assert_eq!( - if define_ltds_fee_only_semantics { - loaded_fee_payer_size + other_accounts_size - } else { - 0 - }, - fail_program_id_loaded_size, - ); - } -} - -// Tests for proper accumulation of metrics across loaded programs in a batch. -#[test] -fn svm_metrics_accumulation() { - for test_entry in program_medley(false) { - let env = SvmTestEnvironment::create(test_entry); - - let (transactions, check_results) = env.test_entry.prepare_transactions(); - - let result = env.batch_processor.load_and_execute_sanitized_transactions( - &env.mock_bank, - &transactions, - check_results, - &env.processing_environment, - &env.processing_config, - ); - - // jit compilation only happens on non-windows && x86_64 - #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))] - { - assert_ne!( - result - .execute_timings - .details - .create_executor_jit_compile_us - .0, - 0 - ); - } - assert_ne!( - result.execute_timings.details.create_executor_load_elf_us.0, - 0 - ); - assert_ne!( - result - .execute_timings - .details - .create_executor_verify_code_us - .0, - 0 - ); - } -} - -// NOTE this could be moved to its own file in the future, but it requires a total refactor of the test runner -mod balance_collector { - use { - super::*, - rand::prelude::*, - solana_program_pack::Pack, - spl_generic_token::token_2022, - spl_token_interface::state::{ - Account as TokenAccount, AccountState as TokenAccountState, Mint, - }, - test_case::test_case, - }; - - const STARTING_BALANCE: u64 = LAMPORTS_PER_SOL * 100; - - // a helper for constructing a transfer instruction, agnostic over system/token - // it also pulls double duty as a record of what the *result* of a transfer should be - // so we can instantiate a Transfer, gen the instruction, change it to fail, change the record to amount 0 - // and then the final test confirms the pre/post balances are unchanged with no special casing - #[derive(Debug, Default)] - struct Transfer { - from: Pubkey, - to: Pubkey, - amount: u64, - } - - impl Transfer { - // given a set of users, picks two randomly and does a random transfer between them - fn new_rand(users: &[Pubkey]) -> Self { - let mut rng = rand::rng(); - let [from_idx, to_idx] = (0..users.len()).choose_multiple(&mut rng, 2)[..] else { - unreachable!() - }; - let from = users[from_idx]; - let to = users[to_idx]; - let amount = rng.random_range(1..STARTING_BALANCE / 100); - - Self { from, to, amount } - } - - fn to_system_instruction(&self) -> Instruction { - system_instruction::transfer(&self.from, &self.to, self.amount) - } - - fn to_token_instruction(&self, fee_payer: &Pubkey) -> Instruction { - // true tokenkeg connoisseurs will note we shouldnt have to sign the sender - // we use a common account owner, the fee-payer, to conveniently reuse account state - // so why do we sign? to force the sender and receiver to be in a consistent order in account keys - // which means we can grab them by index in our final test instead of searching by key - let mut instruction = spl_token_interface::instruction::transfer( - &spl_token_interface::id(), - &self.from, - &self.to, - fee_payer, - &[], - self.amount, - ) - .unwrap(); - instruction.accounts[0].is_signer = true; - - instruction - } - - fn to_instruction(&self, fee_payer: &Pubkey, use_tokens: bool) -> Instruction { - if use_tokens { - self.to_token_instruction(fee_payer) - } else { - self.to_system_instruction() - } - } - } - - #[test_case(false; "native")] - #[test_case(true; "token")] - fn svm_collect_balances(use_tokens: bool) { - let mut rng = rand::rng(); - - let fee_payer_keypair = Keypair::new(); - let fake_fee_payer_keypair = Keypair::new(); - let alice_keypair = Keypair::new(); - let bob_keypair = Keypair::new(); - let charlie_keypair = Keypair::new(); - - let fee_payer = fee_payer_keypair.pubkey(); - let fake_fee_payer = fake_fee_payer_keypair.pubkey(); - let mint = Pubkey::new_unique(); - let alice = alice_keypair.pubkey(); - let bob = bob_keypair.pubkey(); - let charlie = charlie_keypair.pubkey(); - - let native_state = AccountSharedData::create_from_existing_shared_data( - STARTING_BALANCE, - Arc::new(vec![]), - system_program::id(), - false, - u64::MAX, - ); - - let mut mint_buf = vec![0; Mint::get_packed_len()]; - Mint { - decimals: 9, - is_initialized: true, - ..Mint::default() - } - .pack_into_slice(&mut mint_buf); - - let mint_state = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(mint_buf), - spl_token_interface::id(), - false, - u64::MAX, - ); - - let token_account_for_tests = || TokenAccount { - mint, - owner: fee_payer, - amount: STARTING_BALANCE, - state: TokenAccountState::Initialized, - ..TokenAccount::default() - }; - - let mut token_buf = vec![0; TokenAccount::get_packed_len()]; - token_account_for_tests().pack_into_slice(&mut token_buf); - - let token_state = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(token_buf), - spl_token_interface::id(), - false, - u64::MAX, - ); - - let mut program_accounts = - solana_program_binaries::by_id(&spl_token_interface::id(), &Rent::default()).unwrap(); - - let (_, spl_token) = program_accounts.swap_remove(0); - let (program_data_key, program_data) = program_accounts.swap_remove(0); - - for _ in 0..100 { - let mut test_entry = SvmTestEntry::default(); - test_entry.add_initial_account(fee_payer, &native_state.clone()); - - if use_tokens { - test_entry.add_initial_account(spl_token_interface::id(), &spl_token); - test_entry.add_initial_account(program_data_key, &program_data); - - test_entry.add_initial_account(mint, &mint_state); - test_entry.add_initial_account(alice, &token_state); - test_entry.add_initial_account(bob, &token_state); - test_entry.add_initial_account(charlie, &token_state); - } else { - test_entry.add_initial_account(alice, &native_state); - test_entry.add_initial_account(bob, &native_state); - test_entry.add_initial_account(charlie, &native_state); - } - - // test that fee-payer balances are reported correctly - // all we need to know is whether the transaction is processed or dropped - let mut transaction_discards = vec![]; - - // every time we perform a transfer, we mutate user_balances - // and then clone and push it into user_balance_history - // this lets us go through every svm balance record and confirm correctness - let mut user_balances = HashMap::new(); - user_balances.insert(alice, STARTING_BALANCE); - user_balances.insert(bob, STARTING_BALANCE); - user_balances.insert(charlie, STARTING_BALANCE); - let mut user_balance_history = vec![(Transfer::default(), user_balances.clone())]; - - for _ in 0..50 { - // failures result in no balance changes (note we use a separate fee-payer) - // we mix some in with the successes to test that we never record changes for failures - let expected_status = match rng.random::() { - n if n < 0.85 => ExecutionStatus::Succeeded, - n if n < 0.90 => ExecutionStatus::ExecutedFailed, - n if n < 0.95 => ExecutionStatus::ProcessedFailed, - _ => ExecutionStatus::Discarded, - }; - transaction_discards.push(expected_status == ExecutionStatus::Discarded); - - let mut transfer = Transfer::new_rand(&[alice, bob, charlie]); - let from_signer = vec![&alice_keypair, &bob_keypair, &charlie_keypair] - .into_iter() - .find(|k| k.pubkey() == transfer.from) - .unwrap(); - - let instructions = match expected_status { - // a success results in balance changes and is a normal transaction - ExecutionStatus::Succeeded => { - user_balances - .entry(transfer.from) - .and_modify(|v| *v -= transfer.amount); - user_balances - .entry(transfer.to) - .and_modify(|v| *v += transfer.amount); - - vec![transfer.to_instruction(&fee_payer, use_tokens)] - } - // transfer an unreasonable amount to fail execution - ExecutionStatus::ExecutedFailed => { - transfer.amount = u64::MAX / 2; - let instruction = transfer.to_instruction(&fee_payer, use_tokens); - transfer.amount = 0; - - vec![instruction] - } - // use a non-existent program to fail loading - // token22 is very convenient because its presence ensures token bals are recorded - // if we had to use a random program id we would need to push a token program onto account keys - ExecutionStatus::ProcessedFailed => { - let mut instruction = transfer.to_instruction(&fee_payer, use_tokens); - instruction.program_id = token_2022::id(); - transfer.amount = 0; - - vec![instruction] - } - // use a non-existent fee-payer to trigger a discard - ExecutionStatus::Discarded => { - let mut instruction = transfer.to_instruction(&fee_payer, use_tokens); - if use_tokens { - instruction.accounts[2].pubkey = fake_fee_payer; - } - transfer.amount = 0; - - vec![instruction] - } - }; - - let transaction = if expected_status.discarded() { - Transaction::new_signed_with_payer( - &instructions, - Some(&fake_fee_payer), - &[&fake_fee_payer_keypair, from_signer], - Hash::default(), - ) - } else { - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); - - Transaction::new_signed_with_payer( - &instructions, - Some(&fee_payer), - &[&fee_payer_keypair, from_signer], - Hash::default(), - ) - }; - - test_entry.push_transaction_with_status(transaction, expected_status); - user_balance_history.push((transfer, user_balances.clone())); - } - - // this block just updates the SvmTestEntry final account states to be accurate - // doing this instead of skipping it, we validate that user_balances is definitely correct - // because env.execute() will assert all these states match the final bank state - if use_tokens { - let mut token_account = token_account_for_tests(); - let mut token_buf = vec![0; TokenAccount::get_packed_len()]; - - token_account.amount = *user_balances.get(&alice).unwrap(); - token_account.pack_into_slice(&mut token_buf); - let final_token_state = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(token_buf.clone()), - spl_token_interface::id(), - false, - u64::MAX, - ); - test_entry.update_expected_account_data(alice, &final_token_state); - - token_account.amount = *user_balances.get(&bob).unwrap(); - token_account.pack_into_slice(&mut token_buf); - let final_token_state = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(token_buf.clone()), - spl_token_interface::id(), - false, - u64::MAX, - ); - test_entry.update_expected_account_data(bob, &final_token_state); - - token_account.amount = *user_balances.get(&charlie).unwrap(); - token_account.pack_into_slice(&mut token_buf); - let final_token_state = AccountSharedData::create_from_existing_shared_data( - LAMPORTS_PER_SOL, - Arc::new(token_buf.clone()), - spl_token_interface::id(), - false, - u64::MAX, - ); - test_entry.update_expected_account_data(charlie, &final_token_state); - } else { - let mut alice_final_state = native_state.clone(); - alice_final_state.set_lamports(*user_balances.get(&alice).unwrap()); - test_entry.update_expected_account_data(alice, &alice_final_state); - - let mut bob_final_state = native_state.clone(); - bob_final_state.set_lamports(*user_balances.get(&bob).unwrap()); - test_entry.update_expected_account_data(bob, &bob_final_state); - - let mut charlie_final_state = native_state.clone(); - charlie_final_state.set_lamports(*user_balances.get(&charlie).unwrap()); - test_entry.update_expected_account_data(charlie, &charlie_final_state); - } - - // turn on balance recording and run the batch - let mut env = SvmTestEnvironment::create(test_entry); - env.processing_config - .recording_config - .enable_transaction_balance_recording = true; - - let batch_output = env.execute(); - let (pre_lamport_vecs, post_lamport_vecs, pre_token_vecs, post_token_vecs) = - batch_output.balance_collector.unwrap().into_vecs(); - - // first test the fee-payer balances - let mut running_fee_payer_balance = STARTING_BALANCE; - for (pre_bal, post_bal, was_discarded) in pre_lamport_vecs - .iter() - .zip(post_lamport_vecs.clone()) - .zip(transaction_discards) - .map(|((pres, posts), discard)| (pres[0], posts[0], discard)) - { - // we trigger discards with a non-existent fee-payer - if was_discarded { - assert_eq!(pre_bal, 0); - assert_eq!(post_bal, 0); - continue; - } - - let expected_post_balance = running_fee_payer_balance - LAMPORTS_PER_SIGNATURE * 2; - - assert_eq!(pre_bal, running_fee_payer_balance); - assert_eq!(post_bal, expected_post_balance); - - running_fee_payer_balance = expected_post_balance; - } - - // thanks to execute() we know user_balances is correct - // now we test that every step in user_balance_history matches the svm recorded balances - // in other words, the test effectively has three balance trackers and we can test they *all* agree - // first get the collected balances in a manner that is system/token agnostic - let (batch_pre, batch_post) = if use_tokens { - let pre_tupls: Vec<_> = pre_token_vecs - .iter() - .map(|bals| (bals[0].amount, bals[1].amount)) - .collect(); - - let post_tupls: Vec<_> = post_token_vecs - .iter() - .map(|bals| (bals[0].amount, bals[1].amount)) - .collect(); - - (pre_tupls, post_tupls) - } else { - let pre_tupls: Vec<_> = pre_lamport_vecs - .iter() - .map(|bals| (bals[1], bals[2])) - .collect(); - - let post_tupls: Vec<_> = post_lamport_vecs - .iter() - .map(|bals| (bals[1], bals[2])) - .collect(); - - (pre_tupls, post_tupls) - }; - - // these two asserts are trivially true. we include them just to make it clearer what these vecs are - // for n transactions, we have n pre-balance sets and n post-balance sets from svm - // but we have *n+1* test balance sets: we push initial state, and then push post-tx bals once per tx - // this mismatch is not strange at all. we also only have n+1 distinct svm timesteps despite 2n records - // pre-balances: (0 1 2 3) - // post-balances: (1 2 3 4) - // this does not mean time-overlapping svm records are equal. svm only captures the two accounts used by transfer - // whereas our test balances capture all three accounts at every timestep, so we require no pre/post separation - assert_eq!(user_balance_history.len(), batch_pre.len() + 1); - assert_eq!(user_balance_history.len(), batch_post.len() + 1); - - // these are the real tests - for (i, (svm_pre_balances, svm_post_balances)) in - batch_pre.into_iter().zip(batch_post).enumerate() - { - let (_, ref expected_pre_balances) = user_balance_history[i]; - let (ref transfer, ref expected_post_balances) = user_balance_history[i + 1]; - - assert_eq!( - svm_pre_balances.0, - *expected_pre_balances.get(&transfer.from).unwrap() - ); - assert_eq!( - svm_pre_balances.1, - *expected_pre_balances.get(&transfer.to).unwrap() - ); - - assert_eq!( - svm_post_balances.0, - *expected_post_balances.get(&transfer.from).unwrap() - ); - assert_eq!( - svm_post_balances.1, - *expected_post_balances.get(&transfer.to).unwrap() - ); - } - } - } -} diff --git a/solana/svm/tests/mock_bank.rs b/solana/svm/tests/mock_bank.rs deleted file mode 100644 index ec89a0e..0000000 --- a/solana/svm/tests/mock_bank.rs +++ /dev/null @@ -1,387 +0,0 @@ -#![allow(unused)] - -#[allow(deprecated)] -use solana_sysvar::recent_blockhashes::{Entry as BlockhashesEntry, RecentBlockhashes}; -use { - solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, - solana_clock::{Clock, Slot, UnixTimestamp}, - solana_epoch_schedule::EpochSchedule, - solana_fee_structure::{FeeDetails, FeeStructure}, - solana_loader_v3_interface::{self as bpf_loader_upgradeable, state::UpgradeableLoaderState}, - solana_program_runtime::{ - execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, - invoke_context::InvokeContext, - loaded_programs::{BlockRelation, ForkGraph, ProgramRuntimeEnvironment}, - program_cache_entry::ProgramCacheEntry, - solana_sbpf::{ - program::{BuiltinFunctionDefinition, BuiltinProgram, SBPFVersion}, - vm::Config, - }, - }, - solana_pubkey::Pubkey, - solana_rent::Rent, - solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, compute_budget}, - solana_svm::transaction_processor::TransactionBatchProcessor, - solana_svm_callback::{AccountState, InvokeContextCallback, TransactionProcessingCallback}, - solana_svm_feature_set::SVMFeatureSet, - solana_svm_transaction::svm_message::SVMMessage, - solana_svm_type_overrides::sync::{Arc, RwLock}, - solana_syscalls::{ - SyscallAbort, SyscallGetClockSysvar, SyscallGetEpochScheduleSysvar, SyscallGetRentSysvar, - SyscallGetSysvar, SyscallInvokeSignedRust, SyscallLog, SyscallMemcmp, SyscallMemcpy, - SyscallMemmove, SyscallMemset, SyscallPanic, SyscallSetReturnData, - }, - solana_sysvar_id::SysvarId, - std::{ - cmp::Ordering, - collections::HashMap, - env, - fs::{self, File}, - io::Read, - }, -}; - -pub const EXECUTION_SLOT: u64 = 5; // The execution slot must be greater than the deployment slot -pub const EXECUTION_EPOCH: u64 = 2; // The execution epoch must be greater than the deployment epoch -pub const WALLCLOCK_TIME: i64 = 1704067200; // Arbitrarily Jan 1, 2024 - -pub struct MockForkGraph {} - -impl ForkGraph for MockForkGraph { - fn relationship(&self, a: Slot, b: Slot) -> BlockRelation { - match a.cmp(&b) { - Ordering::Less => BlockRelation::Ancestor, - Ordering::Equal => BlockRelation::Equal, - Ordering::Greater => BlockRelation::Descendant, - } - } -} - -#[derive(Default, Clone)] -pub struct MockBankCallback { - pub feature_set: SVMFeatureSet, - pub account_shared_data: Arc>>, - #[allow(clippy::type_complexity)] - pub 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)); - } -} - -impl MockBankCallback { - pub fn calculate_fee_details(message: &impl SVMMessage, prioritization_fee: u64) -> FeeDetails { - let signature_count = message - .num_transaction_signatures() - .saturating_add(message.num_ed25519_signatures()) - .saturating_add(message.num_secp256k1_signatures()) - .saturating_add(message.num_secp256r1_signatures()); - - FeeDetails::new( - signature_count.saturating_mul(FeeStructure::default().lamports_per_signature), - prioritization_fee, - ) - } - - pub fn add_builtin( - &self, - batch_processor: &TransactionBatchProcessor, - program_id: Pubkey, - name: &str, - builtin: ProgramCacheEntry, - ) { - let account_data = AccountSharedData::from(Account { - lamports: 5000, - data: name.as_bytes().to_vec(), - owner: solana_sdk_ids::native_loader::id(), - executable: true, - rent_epoch: 0, - }); - - self.account_shared_data - .write() - .unwrap() - .insert(program_id, account_data); - - batch_processor.add_builtin(program_id, builtin); - } - - #[allow(unused)] - pub fn override_feature_set(&mut self, new_set: SVMFeatureSet) { - self.feature_set = new_set - } - - pub fn configure_sysvars(&self) { - // We must fill in the sysvar cache entries - - // clock contents are important because we use them for a sysvar loading test - let clock = Clock { - slot: EXECUTION_SLOT, - epoch_start_timestamp: WALLCLOCK_TIME.saturating_sub(10) as UnixTimestamp, - epoch: EXECUTION_EPOCH, - leader_schedule_epoch: EXECUTION_EPOCH, - unix_timestamp: WALLCLOCK_TIME as UnixTimestamp, - }; - - let mut account_data = AccountSharedData::default(); - account_data.set_data(bincode::serialize(&clock).unwrap()); - self.account_shared_data - .write() - .unwrap() - .insert(Clock::id(), account_data); - - // default rent is fine - let rent = Rent::default(); - - let mut account_data = AccountSharedData::default(); - account_data.set_data(bincode::serialize(&rent).unwrap()); - self.account_shared_data - .write() - .unwrap() - .insert(Rent::id(), account_data); - - // SystemInstruction::AdvanceNonceAccount asserts RecentBlockhashes is - // non-empty but then just gets the blockhash from InvokeContext. So, - // the sysvar doesn't need real entries - #[allow(deprecated)] - let recent_blockhashes = vec![BlockhashesEntry::default()]; - - let mut account_data = AccountSharedData::default(); - account_data.set_data(bincode::serialize(&recent_blockhashes).unwrap()); - #[allow(deprecated)] - self.account_shared_data - .write() - .unwrap() - .insert(RecentBlockhashes::id(), account_data); - - // EpochSchedule is required for non-mocked LoaderV3 deploy - let epoch_schedule = EpochSchedule::without_warmup(); - - let mut account_data = AccountSharedData::default(); - account_data.set_data(bincode::serialize(&epoch_schedule).unwrap()); - self.account_shared_data - .write() - .unwrap() - .insert(EpochSchedule::id(), account_data); - } -} - -pub fn load_program(name: String) -> Vec { - // Loading the program file - let mut dir = env::current_dir().unwrap(); - dir.push("tests"); - dir.push("example-programs"); - dir.push(name.as_str()); - let name = name.replace('-', "_"); - dir.push(name + "_program.so"); - let mut file = File::open(dir.clone()).expect("file not found"); - let metadata = fs::metadata(dir).expect("Unable to read metadata"); - let mut buffer = vec![0; metadata.len() as usize]; - file.read_exact(&mut buffer).expect("Buffer overflow"); - buffer -} - -pub fn program_address(program_name: &str) -> Pubkey { - Pubkey::create_with_seed(&Pubkey::default(), program_name, &Pubkey::default()).unwrap() -} - -pub fn program_data_size(program_name: &str) -> usize { - UpgradeableLoaderState::size_of_programdata_metadata() - .saturating_add(load_program(program_name.to_string()).len()) -} - -pub fn deploy_program(name: String, deployment_slot: Slot, mock_bank: &MockBankCallback) -> Pubkey { - deploy_program_with_upgrade_authority(name, deployment_slot, mock_bank, None) -} - -pub fn deploy_program_with_upgrade_authority( - name: String, - deployment_slot: Slot, - mock_bank: &MockBankCallback, - upgrade_authority_address: Option, -) -> Pubkey { - let rent = Rent::default(); - let program_account = program_address(&name); - let program_data_account = bpf_loader_upgradeable::get_program_data_address(&program_account); - - let state = UpgradeableLoaderState::Program { - programdata_address: program_data_account, - }; - - // The program account must have funds and hold the executable binary - let mut account_data = AccountSharedData::default(); - let buffer = bincode::serialize(&state).unwrap(); - account_data.set_lamports(rent.minimum_balance(buffer.len())); - account_data.set_owner(solana_sdk_ids::bpf_loader_upgradeable::id()); - account_data.set_executable(true); - account_data.set_data(buffer); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(program_account, account_data); - - let mut account_data = AccountSharedData::default(); - let state = UpgradeableLoaderState::ProgramData { - slot: deployment_slot, - upgrade_authority_address, - }; - let mut header = bincode::serialize(&state).unwrap(); - let mut complement = vec![ - 0; - std::cmp::max( - 0, - UpgradeableLoaderState::size_of_programdata_metadata().saturating_sub(header.len()) - ) - ]; - let mut buffer = load_program(name); - header.append(&mut complement); - header.append(&mut buffer); - account_data.set_lamports(rent.minimum_balance(header.len())); - account_data.set_owner(solana_sdk_ids::bpf_loader_upgradeable::id()); - account_data.set_data(header); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(program_data_account, account_data); - - program_account -} - -pub fn register_builtins( - mock_bank: &MockBankCallback, - batch_processor: &TransactionBatchProcessor, -) { - const DEPLOYMENT_SLOT: u64 = 0; - // We must register LoaderV3 as a loadable account, otherwise programs won't execute. - let loader_v3_name = "solana_bpf_loader_upgradeable_program"; - mock_bank.add_builtin( - batch_processor, - solana_sdk_ids::bpf_loader_upgradeable::id(), - loader_v3_name, - ProgramCacheEntry::new_builtin( - DEPLOYMENT_SLOT, - loader_v3_name.len(), - solana_bpf_loader_program::Entrypoint::register, - ), - ); - - // Other loaders are needed for testing program cache behavior. - let loader_v1_name = "solana_bpf_loader_deprecated_program"; - mock_bank.add_builtin( - batch_processor, - bpf_loader_deprecated::id(), - loader_v1_name, - ProgramCacheEntry::new_builtin( - DEPLOYMENT_SLOT, - loader_v1_name.len(), - solana_bpf_loader_program::Entrypoint::register, - ), - ); - - let loader_v2_name = "solana_bpf_loader_program"; - mock_bank.add_builtin( - batch_processor, - bpf_loader::id(), - loader_v2_name, - ProgramCacheEntry::new_builtin( - DEPLOYMENT_SLOT, - loader_v2_name.len(), - solana_bpf_loader_program::Entrypoint::register, - ), - ); - - // In order to perform a transference of native tokens using the system instruction, - // the system program builtin must be registered. - let system_program_name = "system_program"; - mock_bank.add_builtin( - batch_processor, - solana_system_program::id(), - system_program_name, - ProgramCacheEntry::new_builtin( - DEPLOYMENT_SLOT, - system_program_name.len(), - solana_system_program::system_processor::Entrypoint::register, - ), - ); - - // For testing realloc, we need the compute budget program - let compute_budget_program_name = "compute_budget_program"; - mock_bank.add_builtin( - batch_processor, - compute_budget::id(), - compute_budget_program_name, - ProgramCacheEntry::new_builtin( - DEPLOYMENT_SLOT, - compute_budget_program_name.len(), - solana_compute_budget_program::Entrypoint::register, - ), - ); -} - -pub fn create_custom_loader() -> ProgramRuntimeEnvironment { - let compute_budget = SVMTransactionExecutionBudget::default(); - let vm_config = Config { - max_call_depth: compute_budget.max_call_depth, - stack_frame_size: compute_budget.stack_frame_size, - enable_address_translation: true, - enable_stack_frame_gaps: true, - instruction_meter_checkpoint_distance: 10000, - enable_instruction_meter: true, - enable_register_tracing: true, - enable_symbol_and_section_labels: false, - reject_broken_elfs: true, - noop_instruction_rate: 256, - sanitize_user_provided_values: true, - enabled_sbpf_versions: SBPFVersion::V0..=SBPFVersion::V3, - optimize_rodata: false, - aligned_memory_mapping: false, - allow_memory_region_zero: true, - }; - - // These functions are system calls the compile contract calls during execution, so they - // need to be registered. - let mut loader = BuiltinProgram::new_loader(vm_config); - SyscallAbort::register(&mut loader, "abort").expect("Registration failed"); - SyscallLog::register(&mut loader, "sol_log_").expect("Registration failed"); - SyscallMemcpy::register(&mut loader, "sol_memcpy_").expect("Registration failed"); - SyscallMemset::register(&mut loader, "sol_memset_").expect("Registration failed"); - SyscallMemcmp::register(&mut loader, "sol_memcmp_").expect("Registration failed"); - SyscallMemmove::register(&mut loader, "sol_memmove_").expect("Registration failed"); - SyscallInvokeSignedRust::register(&mut loader, "sol_invoke_signed_rust") - .expect("Registration failed"); - SyscallSetReturnData::register(&mut loader, "sol_set_return_data") - .expect("Registration failed"); - SyscallGetClockSysvar::register(&mut loader, "sol_get_clock_sysvar") - .expect("Registration failed"); - SyscallGetRentSysvar::register(&mut loader, "sol_get_rent_sysvar") - .expect("Registration failed"); - SyscallGetEpochScheduleSysvar::register(&mut loader, "sol_get_epoch_schedule_sysvar") - .expect("Registration failed"); - SyscallPanic::register(&mut loader, "sol_panic_").expect("Registration failed"); - SyscallGetSysvar::register(&mut loader, "sol_get_sysvar").expect("Registration failed"); - ProgramRuntimeEnvironment::from(loader) -} diff --git a/solana/transaction-context/Cargo.toml b/solana/transaction-context/Cargo.toml index 0209282..5d2126b 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