Skip to content

add simulateTransaction#1

Open
fanatid wants to merge 28 commits into
masterfrom
simtx
Open

add simulateTransaction#1
fanatid wants to merge 28 commits into
masterfrom
simtx

Conversation

@fanatid

@fanatid fanatid commented Mar 12, 2026

Copy link
Copy Markdown
Member

No description provided.

Copilot AI 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.

Pull request overview

This PR adds simulateTransaction RPC support using LiteSVM, along with blockhash tracking infrastructure needed for transaction simulation. It also refactors account reading into a reusable AccountReader abstraction with built-in metrics.

Changes:

  • Add simulateTransaction JSON-RPC endpoint with full support for sig verification, blockhash replacement, CPI recording, token balances, and post-simulation account inspection
  • Track blockhashes through the geyser pipeline (grpc → blocks → rocksdb) with pruning based on MAX_PROCESSING_AGE
  • Refactor account reading into AccountReader struct, extract ReaderState::get_account(), and add configurable max_multiple_accounts limit

Reviewed changes

Copilot reviewed 10 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/storage/rocksdb.rs Core simulation logic via LiteSVM, blockhash storage/loading, AccountReader abstraction, SnapshotAddressLoader for ALTs
src/storage/reader.rs New SimulateTransaction request type, ReaderState with blockhash map and height tracking, get_account() helper
src/storage/blocks.rs Blockhash propagation through block processing, finalized blockhash map management, build_reader_state updates
src/source/grpc.rs Parse blockhash from geyser block meta updates
src/rpc/jsonrpc.rs simulateTransaction RPC handler, transaction decoding, response formatting
src/config.rs ConfigFeatureSet for feature set customization, max_multiple_accounts config
src/bin/socotra.rs Wire feature set into Rocksdb
src/bin/gen-future-set.rs New utility to generate feature set config from mainnet
config.yml Default feature set config with mainnet active/inactive features
build.rs Emit agave-feature-set version
Cargo.toml / Cargo.lock New dependencies (litesvm, solana-runtime-transaction, etc.)
src/metrics.rs New READ_ACCOUNTS_TOTAL metric

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/storage/blocks.rs
match msg {
GeyserMessage::Reset => {
// Keep finalized entries (backed by DB), clear in-memory above finalized height
finalized_blockhashes.split_off(&(latest_stored_slot.height + 1));

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

The finalized_blockhashes map is being reassigned via split_off on reset, but the logic is inverted. split_off(&(latest_stored_slot.height + 1)) returns entries at or above that key, discarding entries at or below — which is the opposite of what the comment says ("Keep finalized entries (backed by DB), clear in-memory above finalized height"). This effectively drops all finalized blockhashes and keeps only the ones above the finalized height. The intended behavior should be to keep entries up to latest_stored_slot.height and discard the rest. You should assign the result of split_off to a throwaway variable and keep the original, or use a different approach like finalized_blockhashes.retain(|h, _| *h <= latest_stored_slot.height).

Suggested change
finalized_blockhashes.split_off(&(latest_stored_slot.height + 1));
finalized_blockhashes.retain(|slot, _| *slot <= latest_stored_slot.height);

Copilot uses AI. Check for mistakes.
Comment thread src/storage/rocksdb.rs
Comment on lines +672 to +674
let num_lookup_tables = address_loader.accounts.len();
let mut loaded_accounts_data_size = (num_lookup_tables * 8248) as u32;
for (pubkey, account) in address_loader.accounts {

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

The loaded_accounts_data_size for ALT accounts is estimated as num_lookup_tables * 8248, which is a hardcoded magic number. This should be documented with a comment explaining where 8248 comes from (presumably the max size of a lookup table account). Also, this estimate doesn't account for the actual sizes of the loaded ALT accounts, which could be smaller.

Suggested change
let num_lookup_tables = address_loader.accounts.len();
let mut loaded_accounts_data_size = (num_lookup_tables * 8248) as u32;
for (pubkey, account) in address_loader.accounts {
// Track the total data size of loaded ALT accounts using their actual sizes,
// rather than an estimated upper bound per lookup table.
let mut loaded_accounts_data_size: u32 = 0;
for (pubkey, account) in address_loader.accounts {
loaded_accounts_data_size += account.data.len() as u32;

Copilot uses AI. Check for mistakes.
Comment thread src/storage/rocksdb.rs
Comment on lines +598 to +599
let age = state.processed_height.saturating_sub(height);
let last_valid_block_height = height + MAX_PROCESSING_AGE as u64 - age;

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

last_valid_block_height calculation looks incorrect. In Solana, last_valid_block_height = blockhash_height + MAX_PROCESSING_AGE. Here, subtracting the current age from that doesn't match the standard semantics. The standard calculation is simply height + MAX_PROCESSING_AGE as u64 (where height is the blockhash's height), without subtracting the current age.

Suggested change
let age = state.processed_height.saturating_sub(height);
let last_valid_block_height = height + MAX_PROCESSING_AGE as u64 - age;
let last_valid_block_height = height + MAX_PROCESSING_AGE as u64;

Copilot uses AI. Check for mistakes.
Comment thread src/storage/blocks.rs
Comment on lines +587 to +588
.chain(confirmed_blockhashes.into_iter())
.chain(processed_blockhashes.into_iter())

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

The blockhash_map is built by chaining finalized, confirmed, and processed blockhashes into a HashMap<Hash, Slot>. If the same hash maps to different heights across these sources (unlikely but theoretically possible with forks), the last one wins silently. More importantly, confirmed_blockhashes and processed_blockhashes are not filtered by min_height, unlike the finalized range query on line 585. This means stale confirmed/processed blockhashes below min_height could be included in the map.

Suggested change
.chain(confirmed_blockhashes.into_iter())
.chain(processed_blockhashes.into_iter())
.chain(
confirmed_blockhashes
.into_iter()
.filter(|(height, _)| *height >= min_height && *height <= processed_height),
)
.chain(
processed_blockhashes
.into_iter()
.filter(|(height, _)| *height >= min_height && *height <= processed_height),
)

Copilot uses AI. Check for mistakes.
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.

2 participants