Skip to content

feat: add svm crate - #21

Open
bmuddha wants to merge 2 commits into
program-runtimefrom
svm
Open

feat: add svm crate#21
bmuddha wants to merge 2 commits into
program-runtimefrom
svm

Conversation

@bmuddha

@bmuddha bmuddha commented May 28, 2026

Copy link
Copy Markdown
Collaborator

What changed

Customized the imported solana-svm baseline into the engine's caller-owned
transaction loader and executor, and patched it into the workspace.

Why

Persistence and commit policy must remain above transaction execution rather
than inside validator-owned bank state.

Closes #8.

Impact

  • Loads accounts through a caller-provided transaction_processing_callback
    and returns the mutated account set after execution.
  • Restricts program loading to transaction programs and validates native-loader
    and PROGRAM_OWNERS ownership.
  • Removes validator-oriented batch, rollback, nonce, and integration-test
    surfaces that are outside this engine boundary.
  • Keeps commit policy and deployment policy outside the crate.

Reviewer notes

Rent-state and lamport-balance checks still surround execution, but this runtime
owns no validator state. The fork constraints are documented in
solana/README.md.

Follow-up

processor schedules transactions through this SVM later in the stack.

@bmuddha

bmuddha commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9acaa743-0091-4ed1-b773-c6691e410021

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid input: expected boolean, received string at "reviews.auto_review.enabled"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

The PR adds a local Solana SVM workspace fork for Engine execution. It replaces batch-oriented loading and processing with callback-based single-transaction execution, simplifies public result and state types, adds account-access validation, and updates workspace dependencies and documentation.

Changes

Engine SVM execution

Layer / File(s) Summary
Workspace and crate baseline
Cargo.toml, solana/README.md, solana/svm/Cargo.toml, solana/svm/README.md, solana/program-runtime/Cargo.toml
Adds the local SVM workspace member, updates Solana dependencies, reduces SVM features and dependencies, and documents Engine runtime boundaries.
Callback account and program loading
solana/svm/src/account_loader.rs, solana/svm/src/program_loader.rs, solana/svm/src/lib.rs
Replaces stateful account loading with callback access, records program indices and loaded sizes, simplifies program loading, and removes obsolete override and rollback exports.
Single-transaction execution path
solana/svm/src/transaction_processor.rs, solana/svm/src/message_processor.rs, solana/svm/src/access_permissions.rs
Replaces batch processing with single-transaction execution, passes program indices through instruction processing, validates account mutations, checks lamport conservation, and updates CPI grouping.
Transaction state and result contracts
solana/svm/src/rent_calculator.rs, solana/svm/src/transaction_account_state_info.rs, solana/svm/src/transaction_balances.rs, solana/svm/src/transaction_execution_result.rs, solana/svm/src/transaction_processing_result.rs, solana/svm/src/transaction_processing_callback.rs
Uses optional rent states and direct keyed-account balances, removes token and rollback result structures, and stores boxed executed transactions with simplified execution details.
Example crate metadata alignment
solana/svm/tests/example-programs/*/Cargo.toml, solana/transaction-context/Cargo.toml
Updates explicit package versions and metadata ordering for example programs and transaction-context.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title identifies the primary change: adding the customized SVM crate to the workspace.
Description check ✅ Passed The description directly explains the customized solana-svm fork, engine execution model, and removed validator-oriented behavior.
Linked Issues check ✅ Passed For [#8], the PR forks solana-svm, adapts account loading and execution to callbacks, and removes validator-oriented surfaces.
Out of Scope Changes check ✅ Passed The documented runtime differences, API changes, dependency updates, and removed validator surfaces support the linked issue scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch svm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
solana/svm/src/account_loader.rs (1)

212-225: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Reuse the already loaded program account for the owner check.

The loop at Line 207 loads every account key through the callback and stores the result in loaded_transaction_accounts.accounts. The loop at Line 212 calls get_account_shared_data again for each instruction program id. This repeats one callback lookup per instruction, and the owner check can read a different snapshot than the account that is later executed.

Index the already collected account by instruction.program_id_index instead.

♻️ Proposed refactor
-    for (program_id, instruction) in message.program_instructions_iter() {
-        let Some(program_account) = account_loader.get_account_shared_data(program_id) else {
-            return Err(TransactionError::ProgramAccountNotFound);
-        };
-
-        let owner_id = program_account.0.owner();
-        if !PROGRAM_OWNERS.contains(owner_id) {
-            return Err(TransactionError::InvalidProgramForExecution);
-        }
-
-        loaded_transaction_accounts
-            .program_indices
-            .push(instruction.program_id_index as IndexOfAccount);
-    }
+    for (_program_id, instruction) in message.program_instructions_iter() {
+        let index = instruction.program_id_index as IndexOfAccount;
+        let Some((_, program_account)) = loaded_transaction_accounts
+            .accounts
+            .get(index as usize)
+        else {
+            return Err(TransactionError::ProgramAccountNotFound);
+        };
+
+        if !PROGRAM_OWNERS.contains(program_account.owner()) {
+            return Err(TransactionError::InvalidProgramForExecution);
+        }
+
+        loaded_transaction_accounts.program_indices.push(index);
+    }

Note: this changes the error for a missing program account. A missing account is materialized as a default account at Line 246, so the owner check reports InvalidProgramForExecution instead of ProgramAccountNotFound. Several tests assert ProgramAccountNotFound. Keep the current behavior if that error mapping is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana/svm/src/account_loader.rs` around lines 212 - 225, Update the
program-instruction loop to retrieve the program account from
loaded_transaction_accounts.accounts using instruction.program_id_index instead
of calling account_loader.get_account_shared_data. Perform the existing owner
validation against that already loaded account and preserve the current
missing-account error mapping if required by existing tests.
solana/svm/src/access_permissions.rs (1)

227-235: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the privileged fee-payer case.

The comment states that a dirty immutable fee payer is rejected "unless privileged". Every row sets privileged to false, so the exemption is never exercised. Add one row with a privileged transaction, or remove the clause from the comment.

💚 Proposed addition
         let cases = [
             // (payer, privileged, accepted)
             (account(AccountMode::ReadOnly), false, false),
             (account(AccountMode::Transient), false, false),
             (account(AccountMode::Delegated), false, true),
+            (account(AccountMode::ReadOnly), true, true),
         ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana/svm/src/access_permissions.rs` around lines 227 - 235, Add a case to
the `fee_payer_guard` test table where the fee payer is immutable, `privileged`
is true, and the expected result is accepted, so the test covers the documented
privileged exemption while preserving the existing cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@solana/svm/src/access_permissions.rs`:
- Around line 81-86: Update the rustdoc comment for the account function to
describe that it builds a dirtied account in the requested AccountMode, rather
than referring specifically to Delegated mode. Keep the implementation
unchanged.

In `@solana/svm/src/program_loader.rs`:
- Around line 13-26: Update load_program so executable accounts owned by
bpf_loader_upgradeable::id() are not passed to ProgramCacheEntry::new or
inserted into the program cache; filter that owner out of PROGRAM_OWNERS or
bypass caching for it, while preserving caching for ELF-backed owners.

In `@solana/svm/src/rent_calculator.rs`:
- Around line 71-81: Update the rustdoc for get_account_rent_state to state that
ephemeral accounts are classified as RentExempt regardless of their lamport
balance, alongside the lamports and data-size inputs. Keep the implementation
unchanged and identify this as the fork-specific exemption affecting rent-state
transitions.

In `@solana/svm/src/transaction_processor.rs`:
- Around line 154-173: Update the rustdoc for new_uninitialized and new to
reflect that the caller-provided ProgramCache is preserved and may already
contain programs, and remove the outdated runtime-environment statement. Since
new and new_uninitialized are identical, consolidate them into a single
constructor if the API permits, updating references and documentation
accordingly.
- Around line 247-266: Update replenish_program_cache so failed verification
results from load_program are not stored in self.program_cache; cache and reuse
entries only when verification succeeds, while still replenishing
program_cache_for_tx_batch with the current result. Ensure failed entries can be
retried after runtime-environment changes or program redeployments.
- Around line 230-238: Ensure balance_collector.collect_post_balances runs
regardless of the result of executed_tx.access_is_valid(tx), so native_pre and
native_post remain aligned when the transaction is returned successfully. Keep
the program-cache drain and merge restricted to the valid-access branch, while
preserving the existing Ok(Box::new(executed_tx)) return.

---

Nitpick comments:
In `@solana/svm/src/access_permissions.rs`:
- Around line 227-235: Add a case to the `fee_payer_guard` test table where the
fee payer is immutable, `privileged` is true, and the expected result is
accepted, so the test covers the documented privileged exemption while
preserving the existing cases.

In `@solana/svm/src/account_loader.rs`:
- Around line 212-225: Update the program-instruction loop to retrieve the
program account from loaded_transaction_accounts.accounts using
instruction.program_id_index instead of calling
account_loader.get_account_shared_data. Perform the existing owner validation
against that already loaded account and preserve the current missing-account
error mapping if required by existing tests.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 37df8f36-abd6-4935-b6a8-52f4e4d843ae

📥 Commits

Reviewing files that changed from the base of the PR and between c19d469 and e3bfbd7.

⛔ Files ignored due to path filters (6)
  • solana/svm/doc/diagrams/context.svg is excluded by !**/*.svg
  • solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so is excluded by !**/*.so
  • solana/svm/tests/example-programs/hello-solana/hello_solana_program.so is excluded by !**/*.so
  • solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so is excluded by !**/*.so
  • solana/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so is excluded by !**/*.so
  • solana/svm/tests/example-programs/write-to-account/write_to_account_program.so is excluded by !**/*.so
📒 Files selected for processing (33)
  • Cargo.toml
  • solana/README.md
  • solana/program-runtime/Cargo.toml
  • solana/svm/Cargo.toml
  • solana/svm/README.md
  • solana/svm/doc/diagrams/context.tex
  • solana/svm/doc/spec.md
  • solana/svm/src/access_permissions.rs
  • solana/svm/src/account_loader.rs
  • solana/svm/src/account_overrides.rs
  • solana/svm/src/lib.rs
  • solana/svm/src/message_processor.rs
  • solana/svm/src/nonce_info.rs
  • solana/svm/src/program_loader.rs
  • solana/svm/src/rent_calculator.rs
  • solana/svm/src/rollback_accounts.rs
  • solana/svm/src/transaction_account_state_info.rs
  • solana/svm/src/transaction_balances.rs
  • solana/svm/src/transaction_commit_result.rs
  • solana/svm/src/transaction_error_metrics.rs
  • solana/svm/src/transaction_execution_result.rs
  • solana/svm/src/transaction_processing_callback.rs
  • solana/svm/src/transaction_processing_result.rs
  • solana/svm/src/transaction_processor.rs
  • solana/svm/tests/concurrent_tests.rs
  • solana/svm/tests/example-programs/clock-sysvar/Cargo.toml
  • solana/svm/tests/example-programs/hello-solana/Cargo.toml
  • solana/svm/tests/example-programs/simple-transfer/Cargo.toml
  • solana/svm/tests/example-programs/transfer-from-account/Cargo.toml
  • solana/svm/tests/example-programs/write-to-account/Cargo.toml
  • solana/svm/tests/integration_test.rs
  • solana/svm/tests/mock_bank.rs
  • solana/transaction-context/Cargo.toml
💤 Files with no reviewable changes (9)
  • solana/svm/src/transaction_commit_result.rs
  • solana/svm/doc/spec.md
  • solana/svm/src/transaction_error_metrics.rs
  • solana/svm/src/nonce_info.rs
  • solana/svm/tests/concurrent_tests.rs
  • solana/svm/doc/diagrams/context.tex
  • solana/svm/src/rollback_accounts.rs
  • solana/svm/tests/mock_bank.rs
  • solana/svm/src/account_overrides.rs

Comment thread solana/svm/src/access_permissions.rs Outdated
Comment thread solana/svm/src/program_loader.rs
Comment thread solana/svm/src/rent_calculator.rs
Comment thread solana/svm/src/transaction_processor.rs Outdated
Comment on lines 154 to 173
/// 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<ProgramCache>) -> 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<RwLock<FG>>,
program_runtime_environment: Option<ProgramRuntimeEnvironment>,
) -> 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::<SysvarCache>::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<ProgramCache>) -> Self {
Self::new_uninitialized(slot, cache)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the constructor docs; they describe removed behavior.

Two mismatches exist between the docs and the code:

  • Line 169 states "Missing runtime environments are replaced with empty loaders." new no longer touches runtime environments. Runtime environments now arrive through TransactionProcessingEnvironment::program_runtime_environments_for_execution.
  • Line 157 states the processor "has been initialized with an empty program cache." The cache is supplied by the caller and can already contain programs.

new and new_uninitialized are now identical. Consider keeping one constructor.

📝 Proposed doc fix
-    /// 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.
+    /// Create a `TransactionBatchProcessor` that uses the supplied program cache.
+    ///
+    /// The processor does not add builtins to the cache. The caller owns the
+    /// cache contents.
     pub fn new_uninitialized(slot: Slot, cache: Arc<ProgramCache>) -> Self {
         Self {
             slot,
             program_cache: cache,
             ..Self::default()
         }
     }
 
-    /// Create a new `TransactionBatchProcessor`.
-    ///
-    /// Missing runtime environments are replaced with empty loaders. The
-    /// program cache is used as provided and does not receive builtins here.
+    /// Create a new `TransactionBatchProcessor`.
+    ///
+    /// Runtime environments are supplied per execution through
+    /// `TransactionProcessingEnvironment::program_runtime_environments_for_execution`.
     pub fn new(slot: Slot, cache: Arc<ProgramCache>) -> Self {
         Self::new_uninitialized(slot, cache)
     }

As per path instructions: "Check docs and rustdoc for factual consistency with the code. Flag only real mismatches, broken examples, stale comments, or important omissions."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// 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<ProgramCache>) -> 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<RwLock<FG>>,
program_runtime_environment: Option<ProgramRuntimeEnvironment>,
) -> 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::<SysvarCache>::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<ProgramCache>) -> Self {
Self::new_uninitialized(slot, cache)
}
/// Create a `TransactionBatchProcessor` that uses the supplied program cache.
///
/// The processor does not add builtins to the cache. The caller owns the
/// cache contents.
pub fn new_uninitialized(slot: Slot, cache: Arc<ProgramCache>) -> Self {
Self {
slot,
program_cache: cache,
..Self::default()
}
}
/// Create a new `TransactionBatchProcessor`.
///
/// Runtime environments are supplied per execution through
/// `TransactionProcessingEnvironment::program_runtime_environments_for_execution`.
pub fn new(slot: Slot, cache: Arc<ProgramCache>) -> Self {
Self::new_uninitialized(slot, cache)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana/svm/src/transaction_processor.rs` around lines 154 - 173, Update the
rustdoc for new_uninitialized and new to reflect that the caller-provided
ProgramCache is preserved and may already contain programs, and remove the
outdated runtime-environment statement. Since new and new_uninitialized are
identical, consolidate them into a single constructor if the API permits,
updating references and documentation accordingly.

Source: Path instructions

Comment on lines +230 to 238
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))
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Post balances are not collected when access validation fails.

collect_post_balances runs only inside the access_is_valid branch. When access_is_valid returns false, balance_collector keeps a populated native_pre and an empty native_post. BalanceCollector::into_vecs in solana/svm/src/transaction_balances.rs then returns two vectors of different lengths. Any consumer that zips or indexes them by account position will silently drop entries or panic.

The transaction is still returned as Ok(Box<ExecutedTransaction>) at Line 236, so the caller cannot distinguish this case from a normal execution.

Collect post balances on both paths, or document and enforce that native_post may be empty.

🐛 Proposed fix
                 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);
                 }
+                balance_collector.collect_post_balances(&executed_tx.loaded_transaction.accounts);
                 Ok(Box::new(executed_tx))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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))
}
};
if executed_tx.access_is_valid(tx) {
let cache = program_cache_for_tx_batch.drain_modified_entries();
self.program_cache.merge(&cache);
}
balance_collector.collect_post_balances(&executed_tx.loaded_transaction.accounts);
Ok(Box::new(executed_tx))
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana/svm/src/transaction_processor.rs` around lines 230 - 238, Ensure
balance_collector.collect_post_balances runs regardless of the result of
executed_tx.access_is_valid(tx), so native_pre and native_post remain aligned
when the transaction is returned successfully. Keep the program-cache drain and
merge restricted to the valid-access branch, while preserving the existing
Ok(Box::new(executed_tx)) return.

Comment thread solana/svm/src/transaction_processor.rs
@bmuddha

bmuddha commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Regarding the solana/svm/src/account_loader.rs nitpick about reusing the initially loaded program account: I am keeping the second callback lookup. The initial account-loading loop materializes a missing account as AccountSharedData::default(), while the second lookup preserves the existing ProgramAccountNotFound distinction; indexing the collected vector directly would instead turn that case into InvalidProgramForExecution unless we add more bookkeeping. The callback is expected to provide a stable account view for a transaction, and Agave likewise retains a separate program-account lookup through its account-loader boundary. One avoided lookup per instruction is not valuable enough here to change the error contract or add state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fork solana-runtime for engine execution Fork solana-svm for engine execution

1 participant