From e0b478ff0c63871a920df1750f5b5dfe9b438260 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 15 May 2026 21:58:32 +0400 Subject: [PATCH] feat: add svm crate --- Cargo.toml | 42 +- solana/README.md | 165 + solana/program-runtime/Cargo.toml | 4 +- solana/svm/Cargo.toml | 86 +- solana/svm/README.md | 16 + solana/svm/doc/diagrams/context.svg | 88 - solana/svm/doc/diagrams/context.tex | 35 - solana/svm/doc/spec.md | 311 -- solana/svm/src/access_permissions.rs | 281 ++ solana/svm/src/account_loader.rs | 1684 ++----- solana/svm/src/account_overrides.rs | 65 - solana/svm/src/lib.rs | 14 +- solana/svm/src/message_processor.rs | 347 +- solana/svm/src/nonce_info.rs | 151 - solana/svm/src/program_loader.rs | 773 +--- solana/svm/src/rent_calculator.rs | 67 +- solana/svm/src/rollback_accounts.rs | 271 -- .../svm/src/transaction_account_state_info.rs | 127 +- solana/svm/src/transaction_balances.rs | 192 +- solana/svm/src/transaction_commit_result.rs | 39 - solana/svm/src/transaction_error_metrics.rs | 63 - .../svm/src/transaction_execution_result.rs | 36 +- .../src/transaction_processing_callback.rs | 2 +- .../svm/src/transaction_processing_result.rs | 90 +- solana/svm/src/transaction_processor.rs | 2310 +--------- solana/svm/tests/concurrent_tests.rs | 311 -- .../example-programs/clock-sysvar/Cargo.toml | 4 +- .../clock-sysvar/clock_sysvar_program.so | Bin 17304 -> 44168 bytes .../example-programs/hello-solana/Cargo.toml | 4 +- .../hello-solana/hello_solana_program.so | Bin 8368 -> 35408 bytes .../simple-transfer/Cargo.toml | 4 +- .../simple_transfer_program.so | Bin 54232 -> 67320 bytes .../transfer-from-account/Cargo.toml | 4 +- .../transfer_from_account_program.so | Bin 54592 -> 67888 bytes .../write-to-account/Cargo.toml | 4 +- .../write_to_account_program.so | Bin 16816 -> 21992 bytes solana/svm/tests/integration_test.rs | 4011 ----------------- solana/svm/tests/mock_bank.rs | 387 -- solana/transaction-context/Cargo.toml | 2 +- 39 files changed, 1249 insertions(+), 10741 deletions(-) create mode 100644 solana/README.md create mode 100644 solana/svm/README.md delete mode 100644 solana/svm/doc/diagrams/context.svg delete mode 100644 solana/svm/doc/diagrams/context.tex delete mode 100644 solana/svm/doc/spec.md create mode 100644 solana/svm/src/access_permissions.rs delete mode 100644 solana/svm/src/account_overrides.rs delete mode 100644 solana/svm/src/nonce_info.rs delete mode 100644 solana/svm/src/rollback_accounts.rs delete mode 100644 solana/svm/src/transaction_commit_result.rs delete mode 100644 solana/svm/src/transaction_error_metrics.rs delete mode 100644 solana/svm/tests/concurrent_tests.rs delete mode 100644 solana/svm/tests/integration_test.rs delete mode 100644 solana/svm/tests/mock_bank.rs 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 4c744a249f90d7303a70c6cc9745b9c46ffa79de..dee43a1e38b28684312760f0b81203605ae79c6c 100755 GIT binary patch literal 44168 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 XWACW0*4XUM-6^0hw^1L)YApW;kTGOA 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 f79f4f1cd9d950e69311617457f2b77abedb6b29..a9da4ff47e5d84903c9ade357361930953992ec3 100755 GIT binary patch literal 35408 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 literal 8368 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 Tn`1Li_R6DPm+N%sZXEvsNs73B 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 b9041ec41baa22ca45b14b9b3da989f3d732266b..5132b38cdc1b3dcfab8714c9aca50be5a4f08ff6 100755 GIT binary patch literal 67320 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 literal 54232 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 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 86e685930c2cfd1f9ca7df61dbd3e176d169d952..a3ef926d3747ef4a499aa4dd4127fcadbead6948 100755 GIT binary patch literal 67888 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 literal 54592 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 literal 16816 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 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