Browse Source

SVM: add a sample stand-alone application based on SVM (#2217)

Add an extended example to the SVM crate

- json-rpc-server is a server application that implements several Solana Json RPC commands, in particular simulateTransaction command to run a transaction in minimal Solana run-time environment required to use SVM.
- json-rpc-client is a sample client application that submits simulateTransaction requests to json-rpc server.
- json-rpc-program is source code of an on-chain program executed in the context of the simultateTransaction command submitted by the client program.
dmakarov 1 năm trước cách đây
mục cha
commit
e5a67dfa96

+ 34 - 0
Cargo.lock

@@ -2808,6 +2808,15 @@ dependencies = [
  "hmac 0.8.1",
 ]
 
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "http"
 version = "0.2.12"
@@ -8059,8 +8068,19 @@ name = "solana-svm"
 version = "2.1.0"
 dependencies = [
  "assert_matches",
+ "base64 0.22.1",
  "bincode",
+ "borsh 1.5.1",
+ "bs58",
+ "clap 2.33.3",
+ "crossbeam-channel",
+ "env_logger",
+ "home",
  "itertools 0.12.1",
+ "jsonrpc-core",
+ "jsonrpc-core-client",
+ "jsonrpc-derive",
+ "jsonrpc-http-server",
  "lazy_static",
  "libsecp256k1",
  "log",
@@ -8070,8 +8090,12 @@ dependencies = [
  "rand 0.8.5",
  "serde",
  "serde_derive",
+ "serde_json",
  "shuttle",
+ "solana-account-decoder",
+ "solana-accounts-db",
  "solana-bpf-loader-program",
+ "solana-client",
  "solana-compute-budget",
  "solana-compute-budget-program",
  "solana-feature-set",
@@ -8082,7 +8106,11 @@ dependencies = [
  "solana-log-collector",
  "solana-logger",
  "solana-measure",
+ "solana-perf",
+ "solana-program",
  "solana-program-runtime",
+ "solana-rpc-client",
+ "solana-rpc-client-api",
  "solana-runtime-transaction",
  "solana-sdk",
  "solana-svm",
@@ -8091,10 +8119,16 @@ dependencies = [
  "solana-svm-transaction",
  "solana-system-program",
  "solana-timings",
+ "solana-transaction-status",
  "solana-type-overrides",
+ "solana-version",
  "solana-vote",
+ "spl-token-2022",
  "test-case",
  "thiserror",
+ "tokio",
+ "tokio-util 0.7.12",
+ "yaml-rust",
 ]
 
 [[package]]

+ 4 - 1
Cargo.toml

@@ -164,7 +164,10 @@ members = [
     "zk-token-sdk",
 ]
 
-exclude = ["programs/sbf", "svm/tests/example-programs"]
+exclude = [
+    "programs/sbf",
+    "svm/tests/example-programs",
+]
 
 resolver = "2"
 

+ 1 - 1
accounts-db/src/accounts_db.rs

@@ -3206,7 +3206,7 @@ impl AccountsDb {
                 .map(|dirty_store_chunk| {
                     let mut oldest_dirty_slot = max_slot_inclusive.saturating_add(1);
                     dirty_store_chunk.iter().for_each(|(slot, store)| {
-                        if slot < &oldest_non_ancient_slot {
+                        if *slot < oldest_non_ancient_slot {
                             dirty_ancient_stores.fetch_add(1, Ordering::Relaxed);
                         }
                         oldest_dirty_slot = oldest_dirty_slot.min(*slot);

+ 41 - 0
svm/Cargo.toml

@@ -46,20 +46,46 @@ name = "solana_svm"
 
 [dev-dependencies]
 assert_matches = { workspace = true }
+base64 = { workspace = true }
 bincode = { workspace = true }
+borsh = { version = "1.5.1", features = ["derive"] }
+bs58 = { workspace = true }
+clap = { workspace = true }
+crossbeam-channel = { workspace = true }
+env_logger = { workspace = true }
+home = "0.5"
+jsonrpc-core = { workspace = true }
+jsonrpc-core-client = { workspace = true }
+jsonrpc-derive = { workspace = true }
+jsonrpc-http-server = { workspace = true }
 lazy_static = { workspace = true }
 libsecp256k1 = { workspace = true }
 prost = { workspace = true }
 rand = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 shuttle = { workspace = true }
+solana-account-decoder = { workspace = true }
+solana-accounts-db = { workspace = true }
 solana-bpf-loader-program = { workspace = true }
+solana-client = { workspace = true }
 solana-compute-budget-program = { workspace = true }
 solana-logger = { workspace = true }
+solana-perf = { workspace = true }
+solana-program = { workspace = true }
+solana-rpc-client = { workspace = true }
+solana-rpc-client-api = { workspace = true }
 solana-sdk = { workspace = true, features = ["dev-context-only-utils"] }
 # See order-crates-for-publishing.py for using this unusual `path = "."`
 solana-svm = { path = ".", features = ["dev-context-only-utils"] }
 solana-svm-conformance = { workspace = true }
+solana-transaction-status = { workspace = true }
+solana-version = { workspace = true }
+spl-token-2022 = { workspace = true, features = ["no-entrypoint"] }
 test-case = { workspace = true }
+tokio = { workspace = true, features = ["full"] }
+tokio-util = { workspace = true, features = ["codec", "compat"] }
+yaml-rust = "0.4"
 
 [package.metadata.docs.rs]
 targets = ["x86_64-unknown-linux-gnu"]
@@ -80,5 +106,20 @@ shuttle-test = [
     "solana-loader-v4-program/shuttle-test",
 ]
 
+[[example]]
+name = "json-rpc-server"
+path = "examples/json-rpc/server/src/main.rs"
+crate-type = ["bin"]
+
+[[example]]
+name = "json-rpc-client"
+path = "examples/json-rpc/client/src/main.rs"
+crate-type = ["bin"]
+
+[[example]]
+name = "json-rpc-example-program"
+path = "examples/json-rpc/program/src/lib.rs"
+crate-type = ["cdylib", "lib"]
+
 [lints]
 workspace = true

+ 31 - 0
svm/examples/json-rpc/README.md

@@ -0,0 +1,31 @@
+This is an example application using SVM to implement a tiny subset of
+Solana RPC protocol for the purpose of simulating transaction
+execution without having to use the entire Solana Runtime.
+
+The exmample consists of two host applications
+- json-rpc-server -- the RPC server that accepts incoming RPC requests
+  and performs transaction simulation sending back the results,
+- json-rpc-client -- the RPC client program that sends transactions to
+  json-rpc-server for simulation,
+
+and
+
+- json-rpc-program is the source code of on-chain program that is
+  executed in a transaction sent by json-rpc-client.
+
+To run the example, compile the json-rpc-program with `cargo
+build-sbf` command. Using solana-test-validator create a ledger, or
+use an existing one, and deploy the compiled program to store it in
+the ledger. Using agave-ledger-tool dump ledger accounts to a file,
+e.g. `accounts.out`. Now start the json-rpc-server, e.g.
+```
+cargo run --manifest-path json-rpc-server/Cargo.toml -- -l test-ledger -a accounts.json
+```
+
+Finally, run the client program.
+```
+cargo run --manifest-path json-rpc-client/Cargo.toml -- -C config.yml -k json-rpc-program/target/deploy/helloworld-keypair.json -u localhost
+```
+
+The client will communicate with the server and print the responses it
+recieves from the server.

+ 79 - 0
svm/examples/json-rpc/client/src/client.rs

@@ -0,0 +1,79 @@
+use {
+    crate::utils,
+    solana_client::rpc_client::RpcClient,
+    solana_sdk::{
+        commitment_config::CommitmentConfig,
+        instruction::{AccountMeta, Instruction},
+        message::Message,
+        signature::Signer,
+        signer::keypair::{read_keypair_file, Keypair},
+        transaction::Transaction,
+    },
+};
+
+/// Establishes a RPC connection with the Simulation server.
+/// Information about the server is gleened from the config file `config.yml`.
+pub fn establish_connection(url: &Option<&str>, config: &Option<&str>) -> utils::Result<RpcClient> {
+    let rpc_url = match url {
+        Some(x) => {
+            if *x == "localhost" {
+                "http://localhost:8899".to_string()
+            } else {
+                String::from(*x)
+            }
+        }
+        None => utils::get_rpc_url(config)?,
+    };
+    Ok(RpcClient::new_with_commitment(
+        rpc_url,
+        CommitmentConfig::confirmed(),
+    ))
+}
+
+/// Loads keypair information from the file located at KEYPAIR_PATH
+/// and then verifies that the loaded keypair information corresponds
+/// to an executable account via CONNECTION. Failure to read the
+/// keypair or the loaded keypair corresponding to an executable
+/// account will result in an error being returned.
+pub fn get_program(keypair_path: &str, connection: &RpcClient) -> utils::Result<Keypair> {
+    let program_keypair = read_keypair_file(keypair_path).map_err(|e| {
+        utils::Error::InvalidConfig(format!(
+            "failed to read program keypair file ({}): ({})",
+            keypair_path, e
+        ))
+    })?;
+
+    let program_info = connection.get_account(&program_keypair.pubkey())?;
+    if !program_info.executable {
+        return Err(utils::Error::InvalidConfig(format!(
+            "program with keypair ({}) is not executable",
+            keypair_path
+        )));
+    }
+
+    Ok(program_keypair)
+}
+
+pub fn say_hello(player: &Keypair, program: &Keypair, connection: &RpcClient) -> utils::Result<()> {
+    let greeting_pubkey = utils::get_greeting_public_key(&player.pubkey(), &program.pubkey())?;
+    println!("greeting pubkey {greeting_pubkey:?}");
+
+    // Submit an instruction to the chain which tells the program to
+    // run. We pass the account that we want the results to be stored
+    // in as one of the account arguments which the program will
+    // handle.
+
+    let data = [1u8];
+    let instruction = Instruction::new_with_bytes(
+        program.pubkey(),
+        &data,
+        vec![AccountMeta::new(greeting_pubkey, false)],
+    );
+    let message = Message::new(&[instruction], Some(&player.pubkey()));
+    let transaction = Transaction::new(&[player], message, connection.get_latest_blockhash()?);
+
+    let response = connection.simulate_transaction(&transaction)?;
+    println!("{:?}", response);
+
+    Ok(())
+}

+ 48 - 0
svm/examples/json-rpc/client/src/main.rs

@@ -0,0 +1,48 @@
+use clap::{crate_description, crate_name, crate_version, App, Arg};
+
+mod client;
+mod utils;
+
+fn main() {
+    let version = crate_version!().to_string();
+    let args = std::env::args().collect::<Vec<_>>();
+    let matches = App::new(crate_name!())
+        .about(crate_description!())
+        .version(version.as_str())
+        .arg(
+            Arg::with_name("config")
+                .long("config")
+                .short("C")
+                .takes_value(true)
+                .value_name("CONFIG")
+                .help("Config filepath"),
+        )
+        .arg(
+            Arg::with_name("keypair")
+                .long("keypair")
+                .short("k")
+                .takes_value(true)
+                .value_name("KEYPAIR")
+                .help("Filepath or URL to a keypair"),
+        )
+        .arg(
+            Arg::with_name("url")
+                .long("url")
+                .short("u")
+                .takes_value(true)
+                .value_name("URL_OR_MONIKER")
+                .help("URL for JSON RPC Server"),
+        )
+        .get_matches_from(args);
+    let config = matches.value_of("config");
+    let keypair = matches.value_of("keypair").unwrap();
+    let url = matches.value_of("url");
+    let connection = client::establish_connection(&url, &config).unwrap();
+    println!(
+        "Connected to Simulation server running version ({}).",
+        connection.get_version().unwrap()
+    );
+    let player = utils::get_player(&config).unwrap();
+    let program = client::get_program(keypair, &connection).unwrap();
+    client::say_hello(&player, &program, &connection).unwrap();
+}

+ 106 - 0
svm/examples/json-rpc/client/src/utils.rs

@@ -0,0 +1,106 @@
+use {
+    borsh::{BorshDeserialize, BorshSerialize},
+    solana_sdk::{
+        pubkey::Pubkey,
+        signer::keypair::{read_keypair_file, Keypair},
+    },
+    thiserror::Error,
+    yaml_rust::YamlLoader,
+};
+
+#[derive(Error, Debug)]
+pub enum Error {
+    #[error("failed to read solana config file: ({0})")]
+    ConfigRead(std::io::Error),
+    #[error("failed to parse solana config file: ({0})")]
+    ConfigParse(#[from] yaml_rust::ScanError),
+    #[error("invalid config: ({0})")]
+    InvalidConfig(String),
+
+    #[error("solana client error: ({0})")]
+    Client(#[from] solana_client::client_error::ClientError),
+
+    #[error("error in public key derivation: ({0})")]
+    KeyDerivation(#[from] solana_sdk::pubkey::PubkeyError),
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// The schema for greeting storage in greeting accounts. This is what
+/// is serialized into the account and updated when hellos are sent.
+#[derive(BorshSerialize, BorshDeserialize)]
+struct GreetingSchema {
+    counter: u32,
+}
+
+/// Parses and returns the Solana yaml config on the system.
+pub fn get_config(config: &Option<&str>) -> Result<yaml_rust::Yaml> {
+    let path = match config {
+        Some(path) => std::path::PathBuf::from(path),
+        None => match home::home_dir() {
+            Some(mut path) => {
+                path.push(".config/solana/cli/config.yml");
+                path
+            }
+            None => {
+                return Err(Error::ConfigRead(std::io::Error::new(
+                    std::io::ErrorKind::NotFound,
+                    "failed to locate homedir and thus can not locate solana config",
+                )));
+            }
+        },
+    };
+    let config = std::fs::read_to_string(path).map_err(Error::ConfigRead)?;
+    let mut config = YamlLoader::load_from_str(&config)?;
+    match config.len() {
+        1 => Ok(config.remove(0)),
+        l => Err(Error::InvalidConfig(format!(
+            "expected one yaml document got ({})",
+            l
+        ))),
+    }
+}
+
+/// Gets the RPC url for the cluster that this machine is configured
+/// to communicate with.
+pub fn get_rpc_url(config: &Option<&str>) -> Result<String> {
+    let config = get_config(config)?;
+    match config["json_rpc_url"].as_str() {
+        Some(s) => Ok(s.to_string()),
+        None => Err(Error::InvalidConfig(
+            "missing `json_rpc_url` field".to_string(),
+        )),
+    }
+}
+
+/// Gets the "player" or local solana wallet that has been configured
+/// on the machine.
+pub fn get_player(config: &Option<&str>) -> Result<Keypair> {
+    let config = get_config(config)?;
+    if let Some(path) = config["keypair_path"].as_str() {
+        read_keypair_file(path).map_err(|e| {
+            Error::InvalidConfig(format!("failed to read keypair file ({}): ({})", path, e))
+        })
+    } else {
+        Err(Error::InvalidConfig(
+            "missing `keypair_path` field".to_string(),
+        ))
+    }
+}
+
+/// Gets the seed used to generate greeting accounts. If you'd like to
+/// force this program to generate a new greeting account and thus
+/// restart the counter you can change this value.
+pub fn get_greeting_seed() -> &'static str {
+    "hello"
+}
+
+/// Derives and returns the greeting account public key for a given
+/// PLAYER, PROGRAM combination.
+pub fn get_greeting_public_key(player: &Pubkey, program: &Pubkey) -> Result<Pubkey> {
+    Ok(Pubkey::create_with_seed(
+        player,
+        get_greeting_seed(),
+        program,
+    )?)
+}

+ 7 - 0
svm/examples/json-rpc/config.yml

@@ -0,0 +1,7 @@
+---
+json_rpc_url: http://127.0.0.1:8899
+websocket_url: ''
+keypair_path: svm/examples/test.json
+address_labels:
+  '11111111111111111111111111111111': System Program
+commitment: confirmed

+ 19 - 0
svm/examples/json-rpc/program/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "json-rpc-example-program"
+version = "2.1.0"
+edition = "2021"
+
+[features]
+# This was needed for ci
+dummy-for-ci-check = []
+frozen-abi = []
+
+[dependencies]
+borsh = "0.9"
+solana-program = { path = "../../../../sdk/program", version = "=2.1.0" }
+
+[lib]
+name = "program"
+crate-type = ["cdylib", "lib"]
+
+[workspace]

+ 34 - 0
svm/examples/json-rpc/program/src/lib.rs

@@ -0,0 +1,34 @@
+use {
+    borsh::{BorshDeserialize, BorshSerialize},
+    solana_program::{
+        account_info::{next_account_info, AccountInfo},
+        entrypoint, msg,
+        pubkey::Pubkey,
+    },
+};
+
+/// The type of state managed by this program. The type defined here
+/// must match the `GreetingAccount` type defined by the client.
+#[derive(BorshSerialize, BorshDeserialize, Debug)]
+pub struct GreetingAccount {
+    /// The number of greetings that have been sent to this account.
+    pub counter: u32,
+}
+
+entrypoint!(process_instruction);
+
+pub fn process_instruction(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    _instruction_data: &[u8],
+) -> entrypoint::ProgramResult {
+    // Get the account that stores greeting count information.
+    let accounts_iter = &mut accounts.iter();
+    let account = next_account_info(accounts_iter)?;
+
+    msg!("account.owner");
+    account.owner.log();
+    msg!("program_id");
+    program_id.log();
+    Ok(())
+}

+ 76 - 0
svm/examples/json-rpc/server/src/main.rs

@@ -0,0 +1,76 @@
+#![allow(clippy::arithmetic_side_effects)]
+
+use {
+    clap::{value_t_or_exit, App, Arg},
+    std::{
+        net::{IpAddr, Ipv4Addr, SocketAddr},
+        path::PathBuf,
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc,
+        },
+        thread,
+        time::Duration,
+    },
+};
+
+pub mod rpc_process;
+pub mod rpc_service;
+pub mod svm_bridge;
+
+fn main() {
+    env_logger::init();
+    let matches = App::new("solana-json-rpc")
+        .version("0.1.0")
+        .author("Agave Team <hello@anza.xyz>")
+        .about("JSON-RPC Simulation server")
+        .arg(
+            Arg::with_name("accounts_path")
+                .short("a")
+                .long("accounts")
+                .value_name("FILE")
+                .takes_value(true)
+                .required(true)
+                .default_value("accounts.json")
+                .help("Use FILE as location of accounts.json"),
+        )
+        .arg(
+            Arg::with_name("ledger_path")
+                .short("l")
+                .long("ledger")
+                .value_name("DIR")
+                .takes_value(true)
+                .required(true)
+                .default_value("test-ledger")
+                .help("Use DIR as ledger location"),
+        )
+        .get_matches();
+
+    let accounts_path = PathBuf::from(value_t_or_exit!(matches, "accounts_path", String));
+    let ledger_path = PathBuf::from(value_t_or_exit!(matches, "ledger_path", String));
+    let rpc_addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
+    let rpc_port = 8899u16;
+    let rpc_addr = SocketAddr::new(rpc_addr, rpc_port);
+
+    let config = rpc_process::JsonRpcConfig {
+        accounts_path,
+        ledger_path,
+        rpc_threads: 1,
+        rpc_niceness_adj: 0,
+        max_request_body_size: Some(8192),
+    };
+
+    let exit = Arc::new(AtomicBool::new(false));
+    let validator_exit = rpc_process::create_exit(exit.clone());
+
+    let _rpc_service =
+        rpc_service::JsonRpcService::new(rpc_addr, config, validator_exit, exit.clone());
+
+    let refresh_interval = Duration::from_millis(250);
+    for _i in 0.. {
+        if exit.load(Ordering::Relaxed) {
+            break;
+        }
+        thread::sleep(refresh_interval);
+    }
+}

+ 928 - 0
svm/examples/json-rpc/server/src/rpc_process.rs

@@ -0,0 +1,928 @@
+use {
+    crate::svm_bridge::{
+        create_executable_environment, LoadAndExecuteTransactionsOutput, MockBankCallback,
+        MockForkGraph, TransactionBatch,
+    },
+    base64::{prelude::BASE64_STANDARD, Engine},
+    bincode::config::Options,
+    jsonrpc_core::{types::error, Error, Metadata, Result},
+    jsonrpc_derive::rpc,
+    log::*,
+    serde_json,
+    solana_account_decoder::{
+        parse_account_data::{AccountAdditionalDataV2, SplTokenAdditionalData},
+        parse_token::{get_token_account_mint, is_known_spl_token_id},
+        UiAccount, UiAccountEncoding, UiDataSliceConfig, MAX_BASE58_BYTES,
+    },
+    solana_accounts_db::blockhash_queue::BlockhashQueue,
+    solana_compute_budget::compute_budget::ComputeBudget,
+    solana_perf::packet::PACKET_DATA_SIZE,
+    solana_program_runtime::loaded_programs::ProgramCacheEntry,
+    solana_rpc_client_api::{
+        config::*,
+        response::{Response as RpcResponse, *},
+    },
+    solana_sdk::{
+        account::{from_account, Account, AccountSharedData, ReadableAccount},
+        clock::{Epoch, Slot, MAX_PROCESSING_AGE, MAX_TRANSACTION_FORWARDING_DELAY},
+        commitment_config::CommitmentConfig,
+        exit::Exit,
+        hash::Hash,
+        inner_instruction::InnerInstructions,
+        message::{
+            v0::{LoadedAddresses, MessageAddressTableLookup},
+            AddressLoaderError,
+        },
+        nonce::state::DurableNonce,
+        pubkey::Pubkey,
+        reserved_account_keys::ReservedAccountKeys,
+        signature::Signature,
+        system_instruction, sysvar,
+        transaction::{
+            AddressLoader, MessageHash, SanitizedTransaction, TransactionError,
+            VersionedTransaction,
+        },
+        transaction_context::{TransactionAccount, TransactionReturnData},
+    },
+    solana_svm::{
+        account_loader::{CheckedTransactionDetails, TransactionCheckResult},
+        account_overrides::AccountOverrides,
+        transaction_error_metrics::TransactionErrorMetrics,
+        transaction_processing_result::{
+            ProcessedTransaction, TransactionProcessingResultExtensions,
+        },
+        transaction_processor::{
+            ExecutionRecordingConfig, TransactionBatchProcessor, TransactionLogMessages,
+            TransactionProcessingConfig, TransactionProcessingEnvironment,
+        },
+    },
+    solana_system_program::system_processor,
+    solana_transaction_status::{
+        map_inner_instructions, parse_ui_inner_instructions, TransactionBinaryEncoding,
+        UiTransactionEncoding,
+    },
+    solana_vote::vote_account::VoteAccountsHashMap,
+    spl_token_2022::{
+        extension::{
+            interest_bearing_mint::InterestBearingConfig, BaseStateWithExtensions,
+            StateWithExtensions,
+        },
+        state::Mint,
+    },
+    std::{
+        any::type_name,
+        cmp::min,
+        collections::{HashMap, HashSet},
+        fs,
+        path::PathBuf,
+        str::FromStr,
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc, RwLock,
+        },
+    },
+};
+
+pub const MAX_REQUEST_BODY_SIZE: usize = 50 * (1 << 10); // 50kB
+
+const EXECUTION_SLOT: u64 = 5; // The execution slot must be greater than the deployment slot
+const EXECUTION_EPOCH: u64 = 2; // The execution epoch must be greater than the deployment epoch
+const MAX_BASE58_SIZE: usize = 1683; // Golden, bump if PACKET_DATA_SIZE changes
+const MAX_BASE64_SIZE: usize = 1644; // Golden, bump if PACKET_DATA_SIZE changes
+
+fn new_response<T>(slot: Slot, value: T) -> RpcResponse<T> {
+    RpcResponse {
+        context: RpcResponseContext::new(slot),
+        value,
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct JsonRpcConfig {
+    pub accounts_path: PathBuf,
+    pub ledger_path: PathBuf,
+    pub rpc_threads: usize,
+    pub rpc_niceness_adj: i8,
+    pub max_request_body_size: Option<usize>,
+}
+
+#[derive(Clone)]
+pub struct JsonRpcRequestProcessor {
+    account_map: Vec<(Pubkey, AccountSharedData)>,
+    #[allow(dead_code)]
+    exit: Arc<RwLock<Exit>>,
+    transaction_processor: Arc<RwLock<TransactionBatchProcessor<MockForkGraph>>>,
+}
+
+struct TransactionSimulationResult {
+    pub result: solana_sdk::transaction::Result<()>,
+    pub logs: TransactionLogMessages,
+    pub post_simulation_accounts: Vec<TransactionAccount>,
+    pub units_consumed: u64,
+    pub return_data: Option<TransactionReturnData>,
+    pub inner_instructions: Option<Vec<InnerInstructions>>,
+}
+
+#[derive(Debug, Default, PartialEq)]
+pub struct ProcessedTransactionCounts {
+    pub processed_transactions_count: u64,
+    pub processed_non_vote_transactions_count: u64,
+    pub processed_with_successful_result_count: u64,
+    pub signature_count: u64,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum TransactionLogCollectorFilter {
+    All,
+    AllWithVotes,
+    None,
+    OnlyMentionedAddresses,
+}
+
+impl Default for TransactionLogCollectorFilter {
+    fn default() -> Self {
+        Self::None
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct TransactionLogCollectorConfig {
+    pub mentioned_addresses: HashSet<Pubkey>,
+    pub filter: TransactionLogCollectorFilter,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct TransactionLogInfo {
+    pub signature: Signature,
+    pub result: solana_sdk::transaction::Result<()>,
+    pub is_vote: bool,
+    pub log_messages: TransactionLogMessages,
+}
+
+#[derive(Default, Debug)]
+pub struct TransactionLogCollector {
+    // All the logs collected for from this Bank.  Exact contents depend on the
+    // active `TransactionLogCollectorFilter`
+    pub logs: Vec<TransactionLogInfo>,
+
+    // For each `mentioned_addresses`, maintain a list of indices into `logs` to easily
+    // locate the logs from transactions that included the mentioned addresses.
+    pub mentioned_address_map: HashMap<Pubkey, Vec<usize>>,
+}
+
+impl AddressLoader for JsonRpcRequestProcessor {
+    fn load_addresses(
+        self,
+        _lookups: &[MessageAddressTableLookup],
+    ) -> core::result::Result<LoadedAddresses, AddressLoaderError> {
+        Ok(LoadedAddresses {
+            writable: vec![],
+            readonly: vec![],
+        })
+    }
+}
+
+impl Metadata for JsonRpcRequestProcessor {}
+
+impl JsonRpcRequestProcessor {
+    pub fn new(config: JsonRpcConfig, exit: Arc<RwLock<Exit>>) -> Self {
+        let accounts_json_path = config.accounts_path.clone();
+        let accounts_data: String = fs::read_to_string(accounts_json_path).unwrap();
+        let accounts_data: serde_json::Value = serde_json::from_str(&accounts_data).unwrap();
+        let accounts_slice: Vec<(Pubkey, AccountSharedData)> = accounts_data["accounts"]
+            .as_array()
+            .unwrap()
+            .iter()
+            .map(|acc| {
+                let pubkey = Pubkey::from_str(acc["pubkey"].as_str().unwrap()).unwrap();
+                let account = acc["account"].as_object().unwrap();
+                let owner = account["owner"].as_str().unwrap();
+                let data = account["data"].as_array().unwrap()[0].as_str().unwrap();
+                let acc_data = AccountSharedData::from(Account {
+                    lamports: account["lamports"].as_u64().unwrap(),
+                    data: BASE64_STANDARD.decode(data).unwrap(),
+                    owner: Pubkey::from_str(owner).unwrap(),
+                    executable: account["executable"].as_bool().unwrap(),
+                    rent_epoch: account["rentEpoch"].as_u64().unwrap(),
+                });
+                (pubkey, acc_data)
+            })
+            .collect();
+        let batch_processor = TransactionBatchProcessor::<MockForkGraph>::new(
+            EXECUTION_SLOT,
+            EXECUTION_EPOCH,
+            HashSet::new(),
+        );
+
+        Self {
+            account_map: accounts_slice,
+            exit,
+            transaction_processor: Arc::new(RwLock::new(batch_processor)),
+        }
+    }
+
+    fn get_account_info(
+        &self,
+        pubkey: &Pubkey,
+        config: Option<RpcAccountInfoConfig>,
+    ) -> Result<RpcResponse<Option<UiAccount>>> {
+        let RpcAccountInfoConfig {
+            encoding,
+            data_slice,
+            commitment: _,
+            min_context_slot: _,
+        } = config.unwrap_or_default();
+        let encoding = encoding.unwrap_or(UiAccountEncoding::Binary);
+        Ok(new_response(
+            0,
+            match self.get_account(pubkey) {
+                Some(account) => {
+                    debug!("Found account {pubkey:?}");
+                    Some(encode_account(&account, pubkey, encoding, data_slice)?)
+                }
+                None => {
+                    debug!("Did not find account {pubkey:?}");
+                    None
+                }
+            },
+        ))
+    }
+
+    fn get_latest_blockhash(&self, _config: RpcContextConfig) -> Result<RpcResponse<RpcBlockhash>> {
+        let blockhash = Hash::default();
+        let last_valid_block_height = 0u64;
+        Ok(new_response(
+            0,
+            RpcBlockhash {
+                blockhash: blockhash.to_string(),
+                last_valid_block_height,
+            },
+        ))
+    }
+
+    fn get_minimum_balance_for_rent_exemption(
+        &self,
+        _data_len: usize,
+        _commitment: Option<CommitmentConfig>,
+    ) -> u64 {
+        0u64
+    }
+
+    fn simulate_transaction_unchecked(
+        &self,
+        transaction: &SanitizedTransaction,
+        enable_cpi_recording: bool,
+    ) -> TransactionSimulationResult {
+        let mut mock_bank = MockBankCallback::new(self.account_map.clone());
+        let transaction_processor = self.transaction_processor.read().unwrap();
+
+        let account_keys = transaction.message().account_keys();
+        let number_of_accounts = account_keys.len();
+        let account_overrides = AccountOverrides::default();
+
+        let fork_graph = Arc::new(RwLock::new(MockForkGraph {}));
+
+        create_executable_environment(
+            fork_graph.clone(),
+            &account_keys,
+            &mut mock_bank,
+            &transaction_processor,
+        );
+
+        // Add the system program builtin.
+        transaction_processor.add_builtin(
+            &mock_bank,
+            solana_system_program::id(),
+            "system_program",
+            ProgramCacheEntry::new_builtin(
+                0,
+                b"system_program".len(),
+                system_processor::Entrypoint::vm,
+            ),
+        );
+        // Add the BPF Loader v2 builtin, for the SPL Token program.
+        transaction_processor.add_builtin(
+            &mock_bank,
+            solana_sdk::bpf_loader_upgradeable::id(),
+            "solana_bpf_loader_upgradeable_program",
+            ProgramCacheEntry::new_builtin(
+                0,
+                b"solana_bpf_loader_upgradeable_program".len(),
+                solana_bpf_loader_program::Entrypoint::vm,
+            ),
+        );
+
+        let batch = self.prepare_unlocked_batch_from_single_tx(transaction);
+        let LoadAndExecuteTransactionsOutput {
+            mut processing_results,
+            ..
+        } = self.load_and_execute_transactions(
+            &mock_bank,
+            &batch,
+            // After simulation, transactions will need to be forwarded to the leader
+            // for processing. During forwarding, the transaction could expire if the
+            // delay is not accounted for.
+            MAX_PROCESSING_AGE - MAX_TRANSACTION_FORWARDING_DELAY,
+            TransactionProcessingConfig {
+                account_overrides: Some(&account_overrides),
+                check_program_modification_slot: false,
+                compute_budget: Some(ComputeBudget::default()),
+                log_messages_bytes_limit: None,
+                limit_to_load_programs: true,
+                recording_config: ExecutionRecordingConfig {
+                    enable_cpi_recording,
+                    enable_log_recording: true,
+                    enable_return_data_recording: true,
+                },
+                transaction_account_lock_limit: Some(64),
+            },
+        );
+
+        let processing_result = processing_results
+            .pop()
+            .unwrap_or(Err(TransactionError::InvalidProgramForExecution));
+        let flattened_result = processing_result.flattened_result();
+        let (post_simulation_accounts, logs, return_data, inner_instructions) =
+            match processing_result {
+                Ok(processed_tx) => match processed_tx {
+                    ProcessedTransaction::Executed(executed_tx) => {
+                        let details = executed_tx.execution_details;
+                        let post_simulation_accounts = executed_tx
+                            .loaded_transaction
+                            .accounts
+                            .into_iter()
+                            .take(number_of_accounts)
+                            .collect::<Vec<_>>();
+                        (
+                            post_simulation_accounts,
+                            details.log_messages,
+                            details.return_data,
+                            details.inner_instructions,
+                        )
+                    }
+                    ProcessedTransaction::FeesOnly(_) => (vec![], None, None, None),
+                },
+                Err(_) => (vec![], None, None, None),
+            };
+        let logs = logs.unwrap_or_default();
+        let units_consumed: u64 = 0;
+
+        TransactionSimulationResult {
+            result: flattened_result,
+            logs,
+            post_simulation_accounts,
+            units_consumed,
+            return_data,
+            inner_instructions,
+        }
+    }
+
+    fn prepare_unlocked_batch_from_single_tx<'a>(
+        &'a self,
+        transaction: &'a SanitizedTransaction,
+    ) -> TransactionBatch<'_> {
+        let tx_account_lock_limit = solana_sdk::transaction::MAX_TX_ACCOUNT_LOCKS;
+        let lock_result = transaction
+            .get_account_locks(tx_account_lock_limit)
+            .map(|_| ());
+        let batch = TransactionBatch::new(
+            vec![lock_result],
+            std::borrow::Cow::Borrowed(std::slice::from_ref(transaction)),
+        );
+        batch
+    }
+
+    fn check_age(
+        &self,
+        sanitized_txs: &[impl core::borrow::Borrow<SanitizedTransaction>],
+        lock_results: &[solana_sdk::transaction::Result<()>],
+        max_age: usize,
+        error_counters: &mut TransactionErrorMetrics,
+    ) -> Vec<TransactionCheckResult> {
+        let last_blockhash = Hash::default();
+        let blockhash_queue = BlockhashQueue::default();
+        let next_durable_nonce = DurableNonce::from_blockhash(&last_blockhash);
+
+        sanitized_txs
+            .iter()
+            .zip(lock_results)
+            .map(|(tx, lock_res)| match lock_res {
+                Ok(()) => self.check_transaction_age(
+                    tx.borrow(),
+                    max_age,
+                    &next_durable_nonce,
+                    &blockhash_queue,
+                    error_counters,
+                ),
+                Err(e) => Err(e.clone()),
+            })
+            .collect()
+    }
+
+    fn check_transaction_age(
+        &self,
+        _tx: &SanitizedTransaction,
+        _max_age: usize,
+        _next_durable_nonce: &DurableNonce,
+        _hash_queue: &BlockhashQueue,
+        _error_counters: &mut TransactionErrorMetrics,
+    ) -> TransactionCheckResult {
+        /* for now just return defaults */
+        Ok(CheckedTransactionDetails {
+            nonce: None,
+            lamports_per_signature: u64::default(),
+        })
+    }
+
+    fn clock(&self) -> sysvar::clock::Clock {
+        from_account(&self.get_account(&sysvar::clock::id()).unwrap_or_default())
+            .unwrap_or_default()
+    }
+
+    fn epoch_total_stake(&self, _epoch: Epoch) -> Option<u64> {
+        Some(u64::default())
+    }
+
+    fn epoch_vote_accounts(&self, _epoch: Epoch) -> Option<&VoteAccountsHashMap> {
+        None
+    }
+
+    fn get_account(&self, pubkey: &Pubkey) -> Option<AccountSharedData> {
+        let account_map: HashMap<Pubkey, AccountSharedData> =
+            HashMap::from_iter(self.account_map.clone());
+        account_map.get(pubkey).cloned()
+    }
+
+    fn get_additional_mint_data(&self, data: &[u8]) -> Result<SplTokenAdditionalData> {
+        StateWithExtensions::<Mint>::unpack(data)
+            .map_err(|_| {
+                Error::invalid_params("Invalid param: Token mint could not be unpacked".to_string())
+            })
+            .map(|mint| {
+                let interest_bearing_config = mint
+                    .get_extension::<InterestBearingConfig>()
+                    .map(|x| (*x, self.clock().unix_timestamp))
+                    .ok();
+                SplTokenAdditionalData {
+                    decimals: mint.base.decimals,
+                    interest_bearing_config,
+                }
+            })
+    }
+
+    fn get_encoded_account(
+        &self,
+        pubkey: &Pubkey,
+        encoding: UiAccountEncoding,
+        data_slice: Option<UiDataSliceConfig>,
+        // only used for simulation results
+        overwrite_accounts: Option<&HashMap<Pubkey, AccountSharedData>>,
+    ) -> Result<Option<UiAccount>> {
+        match overwrite_accounts
+            .and_then(|accounts| accounts.get(pubkey).cloned())
+            .or_else(|| self.get_account(pubkey))
+        {
+            Some(account) => {
+                let response = if is_known_spl_token_id(account.owner())
+                    && encoding == UiAccountEncoding::JsonParsed
+                {
+                    self.get_parsed_token_account(pubkey, account, overwrite_accounts)
+                } else {
+                    encode_account(&account, pubkey, encoding, data_slice)?
+                };
+                Ok(Some(response))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn get_parsed_token_account(
+        &self,
+        pubkey: &Pubkey,
+        account: AccountSharedData,
+        // only used for simulation results
+        overwrite_accounts: Option<&HashMap<Pubkey, AccountSharedData>>,
+    ) -> UiAccount {
+        let additional_data = get_token_account_mint(account.data())
+            .and_then(|mint_pubkey| {
+                overwrite_accounts
+                    .and_then(|accounts| accounts.get(&mint_pubkey).cloned())
+                    .or_else(|| self.get_account(&mint_pubkey))
+            })
+            .and_then(|mint_account| self.get_additional_mint_data(mint_account.data()).ok())
+            .map(|data| AccountAdditionalDataV2 {
+                spl_token_additional_data: Some(data),
+            });
+
+        UiAccount::encode(
+            pubkey,
+            &account,
+            UiAccountEncoding::JsonParsed,
+            additional_data,
+            None,
+        )
+    }
+
+    fn last_blockhash_and_lamports_per_signature(&self) -> (Hash, u64) {
+        let last_hash = Hash::default();
+        let last_lamports_per_signature = u64::default();
+        (last_hash, last_lamports_per_signature)
+    }
+
+    fn load_and_execute_transactions(
+        &self,
+        bank: &MockBankCallback,
+        batch: &TransactionBatch,
+        max_age: usize,
+        processing_config: TransactionProcessingConfig,
+    ) -> LoadAndExecuteTransactionsOutput {
+        let sanitized_txs = batch.sanitized_transactions();
+        debug!("processing transactions: {}", sanitized_txs.len());
+        let mut error_counters = TransactionErrorMetrics::default();
+
+        let check_results = self.check_age(
+            sanitized_txs,
+            batch.lock_results(),
+            max_age,
+            &mut error_counters,
+        );
+
+        let (blockhash, lamports_per_signature) = self.last_blockhash_and_lamports_per_signature();
+        let processing_environment = TransactionProcessingEnvironment {
+            blockhash,
+            epoch_total_stake: self.epoch_total_stake(Epoch::default()),
+            epoch_vote_accounts: self.epoch_vote_accounts(Epoch::default()),
+            feature_set: Arc::clone(&bank.feature_set),
+            fee_structure: None,
+            lamports_per_signature,
+            rent_collector: None,
+        };
+
+        let sanitized_output = self
+            .transaction_processor
+            .read()
+            .unwrap()
+            .load_and_execute_sanitized_transactions(
+                bank,
+                sanitized_txs,
+                check_results,
+                &processing_environment,
+                &processing_config,
+            );
+
+        let err_count = &mut error_counters.total;
+
+        let mut processed_counts = ProcessedTransactionCounts::default();
+        for (processing_result, tx) in sanitized_output
+            .processing_results
+            .iter()
+            .zip(sanitized_txs)
+        {
+            if processing_result.was_processed() {
+                // Signature count must be accumulated only if the transaction
+                // is processed, otherwise a mismatched count between banking
+                // and replay could occur
+                processed_counts.signature_count +=
+                    u64::from(tx.message().header().num_required_signatures);
+                processed_counts.processed_transactions_count += 1;
+
+                if !tx.is_simple_vote_transaction() {
+                    processed_counts.processed_non_vote_transactions_count += 1;
+                }
+            }
+
+            match processing_result.flattened_result() {
+                Ok(()) => {
+                    processed_counts.processed_with_successful_result_count += 1;
+                }
+                Err(err) => {
+                    if *err_count == 0 {
+                        debug!("tx error: {:?} {:?}", err, tx);
+                    }
+                    *err_count += 1;
+                }
+            }
+        }
+
+        LoadAndExecuteTransactionsOutput {
+            processing_results: sanitized_output.processing_results,
+        }
+    }
+}
+
+/// RPC interface that an API node is expected to provide
+pub mod rpc {
+    use super::*;
+    #[rpc]
+    pub trait Rpc {
+        type Metadata;
+
+        #[rpc(meta, name = "getAccountInfo")]
+        fn get_account_info(
+            &self,
+            meta: Self::Metadata,
+            pubkey_str: String,
+            config: Option<RpcAccountInfoConfig>,
+        ) -> Result<RpcResponse<Option<UiAccount>>>;
+
+        #[rpc(meta, name = "getLatestBlockhash")]
+        fn get_latest_blockhash(
+            &self,
+            meta: Self::Metadata,
+            config: Option<RpcContextConfig>,
+        ) -> Result<RpcResponse<RpcBlockhash>>;
+
+        #[rpc(meta, name = "getMinimumBalanceForRentExemption")]
+        fn get_minimum_balance_for_rent_exemption(
+            &self,
+            meta: Self::Metadata,
+            data_len: usize,
+            commitment: Option<CommitmentConfig>,
+        ) -> Result<u64>;
+
+        #[rpc(meta, name = "getVersion")]
+        fn get_version(&self, meta: Self::Metadata) -> Result<RpcVersionInfo>;
+
+        #[rpc(meta, name = "simulateTransaction")]
+        fn simulate_transaction(
+            &self,
+            meta: Self::Metadata,
+            data: String,
+            config: Option<RpcSimulateTransactionConfig>,
+        ) -> Result<RpcResponse<RpcSimulateTransactionResult>>;
+    }
+
+    pub struct RpcImpl;
+
+    impl Rpc for RpcImpl {
+        type Metadata = JsonRpcRequestProcessor;
+
+        fn simulate_transaction(
+            &self,
+            meta: Self::Metadata,
+            data: String,
+            config: Option<RpcSimulateTransactionConfig>,
+        ) -> Result<RpcResponse<RpcSimulateTransactionResult>> {
+            debug!("simulate_transaction rpc request received");
+
+            let RpcSimulateTransactionConfig {
+                sig_verify: _,
+                replace_recent_blockhash: _,
+                commitment: _,
+                encoding,
+                accounts: config_accounts,
+                min_context_slot: _,
+                inner_instructions: enable_cpi_recording,
+            } = config.unwrap_or_default();
+            let tx_encoding = encoding.unwrap_or(UiTransactionEncoding::Base58);
+            let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
+                Error::invalid_params(format!(
+                    "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
+                ))
+            })?;
+            let (_, unsanitized_tx) =
+                decode_and_deserialize::<VersionedTransaction>(data, binary_encoding)?;
+            debug!("unsanitized transaction decoded {:?}", unsanitized_tx);
+
+            let transaction = sanitize_transaction(
+                unsanitized_tx,
+                meta.clone(),
+                &ReservedAccountKeys::default().active,
+            )?;
+
+            let TransactionSimulationResult {
+                result,
+                logs,
+                post_simulation_accounts,
+                units_consumed,
+                return_data,
+                inner_instructions,
+            } = meta.simulate_transaction_unchecked(&transaction, enable_cpi_recording);
+
+            let account_keys = transaction.message().account_keys();
+            let number_of_accounts = account_keys.len();
+
+            let accounts = if let Some(config_accounts) = config_accounts {
+                let accounts_encoding = config_accounts
+                    .encoding
+                    .unwrap_or(UiAccountEncoding::Base64);
+
+                if accounts_encoding == UiAccountEncoding::Binary
+                    || accounts_encoding == UiAccountEncoding::Base58
+                {
+                    return Err(Error::invalid_params("base58 encoding not supported"));
+                }
+
+                if config_accounts.addresses.len() > number_of_accounts {
+                    return Err(Error::invalid_params(format!(
+                        "Too many accounts provided; max {number_of_accounts}"
+                    )));
+                }
+
+                if result.is_err() {
+                    Some(vec![None; config_accounts.addresses.len()])
+                } else {
+                    let mut post_simulation_accounts_map = HashMap::new();
+                    for (pubkey, data) in post_simulation_accounts {
+                        post_simulation_accounts_map.insert(pubkey, data);
+                    }
+
+                    Some(
+                        config_accounts
+                            .addresses
+                            .iter()
+                            .map(|address_str| {
+                                let pubkey = verify_pubkey(address_str)?;
+                                meta.get_encoded_account(
+                                    &pubkey,
+                                    accounts_encoding,
+                                    None,
+                                    Some(&post_simulation_accounts_map),
+                                )
+                            })
+                            .collect::<Result<Vec<_>>>()?,
+                    )
+                }
+            } else {
+                None
+            };
+
+            let inner_instructions = inner_instructions.map(|info| {
+                map_inner_instructions(info)
+                    .map(|converted| parse_ui_inner_instructions(converted, &account_keys))
+                    .collect()
+            });
+
+            Ok(new_response(
+                0,
+                RpcSimulateTransactionResult {
+                    err: result.err(),
+                    logs: Some(logs),
+                    accounts,
+                    units_consumed: Some(units_consumed),
+                    return_data: return_data.map(|return_data| return_data.into()),
+                    inner_instructions,
+                    replacement_blockhash: None,
+                },
+            ))
+        }
+
+        fn get_account_info(
+            &self,
+            meta: Self::Metadata,
+            pubkey_str: String,
+            config: Option<RpcAccountInfoConfig>,
+        ) -> Result<RpcResponse<Option<UiAccount>>> {
+            debug!("get_account_info rpc request received: {:?}", pubkey_str);
+            let pubkey = verify_pubkey(&pubkey_str)?;
+            debug!("pubkey {pubkey:?} verified.");
+            meta.get_account_info(&pubkey, config)
+        }
+
+        fn get_latest_blockhash(
+            &self,
+            meta: Self::Metadata,
+            config: Option<RpcContextConfig>,
+        ) -> Result<RpcResponse<RpcBlockhash>> {
+            debug!("get_latest_blockhash rpc request received");
+            meta.get_latest_blockhash(config.unwrap_or_default())
+        }
+
+        fn get_minimum_balance_for_rent_exemption(
+            &self,
+            meta: Self::Metadata,
+            data_len: usize,
+            commitment: Option<CommitmentConfig>,
+        ) -> Result<u64> {
+            debug!(
+                "get_minimum_balance_for_rent_exemption rpc request received: {:?}",
+                data_len
+            );
+            if data_len as u64 > system_instruction::MAX_PERMITTED_DATA_LENGTH {
+                return Err(Error::invalid_request());
+            }
+            Ok(meta.get_minimum_balance_for_rent_exemption(data_len, commitment))
+        }
+
+        fn get_version(&self, _: Self::Metadata) -> Result<RpcVersionInfo> {
+            debug!("get_version rpc request received");
+            let version = solana_version::Version::default();
+            Ok(RpcVersionInfo {
+                solana_core: version.to_string(),
+                feature_set: Some(version.feature_set),
+            })
+        }
+    }
+}
+
+pub fn create_exit(exit: Arc<AtomicBool>) -> Arc<RwLock<Exit>> {
+    let mut exit_handler = Exit::default();
+    exit_handler.register_exit(Box::new(move || exit.store(true, Ordering::Relaxed)));
+    Arc::new(RwLock::new(exit_handler))
+}
+
+fn decode_and_deserialize<T>(
+    encoded: String,
+    encoding: TransactionBinaryEncoding,
+) -> Result<(Vec<u8>, T)>
+where
+    T: serde::de::DeserializeOwned,
+{
+    let wire_output = match encoding {
+        TransactionBinaryEncoding::Base58 => {
+            if encoded.len() > MAX_BASE58_SIZE {
+                return Err(Error::invalid_params(format!(
+                    "base58 encoded {} too large: {} bytes (max: encoded/raw {}/{})",
+                    type_name::<T>(),
+                    encoded.len(),
+                    MAX_BASE58_SIZE,
+                    PACKET_DATA_SIZE,
+                )));
+            }
+            bs58::decode(encoded)
+                .into_vec()
+                .map_err(|e| Error::invalid_params(format!("invalid base58 encoding: {e:?}")))?
+        }
+        TransactionBinaryEncoding::Base64 => {
+            if encoded.len() > MAX_BASE64_SIZE {
+                return Err(Error::invalid_params(format!(
+                    "base64 encoded {} too large: {} bytes (max: encoded/raw {}/{})",
+                    type_name::<T>(),
+                    encoded.len(),
+                    MAX_BASE64_SIZE,
+                    PACKET_DATA_SIZE,
+                )));
+            }
+            BASE64_STANDARD
+                .decode(encoded)
+                .map_err(|e| Error::invalid_params(format!("invalid base64 encoding: {e:?}")))?
+        }
+    };
+    if wire_output.len() > PACKET_DATA_SIZE {
+        return Err(Error::invalid_params(format!(
+            "decoded {} too large: {} bytes (max: {} bytes)",
+            type_name::<T>(),
+            wire_output.len(),
+            PACKET_DATA_SIZE
+        )));
+    }
+    bincode::options()
+        .with_limit(PACKET_DATA_SIZE as u64)
+        .with_fixint_encoding()
+        .allow_trailing_bytes()
+        .deserialize_from(&wire_output[..])
+        .map_err(|err| {
+            Error::invalid_params(format!(
+                "failed to deserialize {}: {}",
+                type_name::<T>(),
+                &err.to_string()
+            ))
+        })
+        .map(|output| (wire_output, output))
+}
+
+fn encode_account<T: ReadableAccount>(
+    account: &T,
+    pubkey: &Pubkey,
+    encoding: UiAccountEncoding,
+    data_slice: Option<UiDataSliceConfig>,
+) -> Result<UiAccount> {
+    if (encoding == UiAccountEncoding::Binary || encoding == UiAccountEncoding::Base58)
+        && data_slice
+            .map(|s| min(s.length, account.data().len().saturating_sub(s.offset)))
+            .unwrap_or(account.data().len())
+            > MAX_BASE58_BYTES
+    {
+        let message = format!("Encoded binary (base 58) data should be less than {MAX_BASE58_BYTES} bytes, please use Base64 encoding.");
+        Err(error::Error {
+            code: error::ErrorCode::InvalidRequest,
+            message,
+            data: None,
+        })
+    } else {
+        Ok(UiAccount::encode(
+            pubkey, account, encoding, None, data_slice,
+        ))
+    }
+}
+
+fn sanitize_transaction(
+    transaction: VersionedTransaction,
+    address_loader: impl AddressLoader,
+    reserved_account_keys: &HashSet<Pubkey>,
+) -> Result<SanitizedTransaction> {
+    SanitizedTransaction::try_create(
+        transaction,
+        MessageHash::Compute,
+        None,
+        address_loader,
+        reserved_account_keys,
+    )
+    .map_err(|err| Error::invalid_params(format!("invalid transaction: {err}")))
+}
+
+fn verify_pubkey(input: &str) -> Result<Pubkey> {
+    input
+        .parse()
+        .map_err(|e| Error::invalid_params(format!("Invalid param: {e:?}")))
+}

+ 111 - 0
svm/examples/json-rpc/server/src/rpc_service.rs

@@ -0,0 +1,111 @@
+use {
+    crate::rpc_process::{rpc::*, *},
+    crossbeam_channel::unbounded,
+    jsonrpc_core::MetaIoHandler,
+    jsonrpc_http_server::{
+        hyper, AccessControlAllowOrigin, CloseHandle, DomainsValidation, ServerBuilder,
+    },
+    log::*,
+    solana_perf::thread::renice_this_thread,
+    solana_sdk::exit::Exit,
+    std::{
+        net::SocketAddr,
+        sync::{atomic::AtomicBool, Arc, RwLock},
+        thread::{self, Builder, JoinHandle},
+    },
+};
+
+pub struct JsonRpcService {
+    thread_hdl: JoinHandle<()>,
+    close_handle: Option<CloseHandle>,
+}
+
+impl JsonRpcService {
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        rpc_addr: SocketAddr,
+        config: JsonRpcConfig,
+        validator_exit: Arc<RwLock<Exit>>,
+        _exit: Arc<AtomicBool>,
+    ) -> Result<Self, String> {
+        info!("rpc bound to {:?}", rpc_addr);
+        info!("rpc configuration: {:?}", config);
+        let rpc_threads = 1.max(config.rpc_threads);
+        let rpc_niceness_adj = config.rpc_niceness_adj;
+
+        let runtime = Arc::new(
+            tokio::runtime::Builder::new_multi_thread()
+                .worker_threads(rpc_threads)
+                .on_thread_start(move || renice_this_thread(rpc_niceness_adj).unwrap())
+                .thread_name("solRpcEl")
+                .enable_all()
+                .build()
+                .expect("Runtime"),
+        );
+
+        let max_request_body_size = config
+            .max_request_body_size
+            .unwrap_or(MAX_REQUEST_BODY_SIZE);
+        let request_processor = JsonRpcRequestProcessor::new(config, validator_exit.clone());
+        let (close_handle_sender, close_handle_receiver) = unbounded();
+        let thread_hdl = Builder::new()
+            .name("solJsonRpcSvc".to_string())
+            .spawn(move || {
+                renice_this_thread(rpc_niceness_adj).unwrap();
+                let mut io = MetaIoHandler::default();
+                io.extend_with(rpc::RpcImpl.to_delegate());
+                let server = ServerBuilder::with_meta_extractor(
+                    io,
+                    move |_req: &hyper::Request<hyper::Body>| request_processor.clone(),
+                )
+                .event_loop_executor(runtime.handle().clone())
+                .threads(1)
+                .cors(DomainsValidation::AllowOnly(vec![
+                    AccessControlAllowOrigin::Any,
+                ]))
+                .cors_max_age(86400)
+                .max_request_body_size(max_request_body_size)
+                .start_http(&rpc_addr);
+
+                if let Err(e) = server {
+                    warn!(
+                        "JSON RPC service unavailable error: {:?}. \n\
+                           Also, check that port {} is not already in use by another application",
+                        e,
+                        rpc_addr.port()
+                    );
+                    close_handle_sender.send(Err(e.to_string())).unwrap();
+                    return;
+                }
+
+                let server = server.unwrap();
+                close_handle_sender.send(Ok(server.close_handle())).unwrap();
+                server.wait();
+            })
+            .unwrap();
+
+        let close_handle = close_handle_receiver.recv().unwrap()?;
+        let close_handle_ = close_handle.clone();
+        validator_exit
+            .write()
+            .unwrap()
+            .register_exit(Box::new(move || {
+                close_handle_.close();
+            }));
+        Ok(Self {
+            thread_hdl,
+            close_handle: Some(close_handle),
+        })
+    }
+
+    pub fn exit(&mut self) {
+        if let Some(c) = self.close_handle.take() {
+            c.close()
+        }
+    }
+
+    pub fn join(mut self) -> thread::Result<()> {
+        self.exit();
+        self.thread_hdl.join()
+    }
+}

+ 272 - 0
svm/examples/json-rpc/server/src/svm_bridge.rs

@@ -0,0 +1,272 @@
+use {
+    log::*,
+    solana_bpf_loader_program::syscalls::{
+        SyscallAbort, SyscallGetClockSysvar, SyscallInvokeSignedRust, SyscallLog,
+        SyscallLogBpfComputeUnits, SyscallLogPubkey, SyscallLogU64, SyscallMemcpy, SyscallMemset,
+        SyscallSetReturnData,
+    },
+    solana_compute_budget::compute_budget::ComputeBudget,
+    solana_program_runtime::{
+        invoke_context::InvokeContext,
+        loaded_programs::{
+            BlockRelation, ForkGraph, LoadProgramMetrics, ProgramCacheEntry,
+            ProgramRuntimeEnvironments,
+        },
+        solana_rbpf::{
+            program::{BuiltinFunction, BuiltinProgram, FunctionRegistry},
+            vm::Config,
+        },
+    },
+    solana_sdk::{
+        account::{AccountSharedData, ReadableAccount},
+        clock::{Clock, Slot, UnixTimestamp},
+        feature_set::FeatureSet,
+        message::AccountKeys,
+        native_loader,
+        pubkey::Pubkey,
+        sysvar::SysvarId,
+        transaction::SanitizedTransaction,
+    },
+    solana_svm::{
+        transaction_processing_callback::TransactionProcessingCallback,
+        transaction_processing_result::TransactionProcessingResult,
+        transaction_processor::TransactionBatchProcessor,
+    },
+    std::{
+        collections::HashMap,
+        sync::{Arc, RwLock},
+        time::{SystemTime, UNIX_EPOCH},
+    },
+};
+
+const DEPLOYMENT_SLOT: u64 = 0;
+const DEPLOYMENT_EPOCH: u64 = 0;
+
+pub struct MockForkGraph {}
+
+impl ForkGraph for MockForkGraph {
+    fn relationship(&self, a: Slot, b: Slot) -> BlockRelation {
+        match a.cmp(&b) {
+            std::cmp::Ordering::Less => BlockRelation::Ancestor,
+            std::cmp::Ordering::Equal => BlockRelation::Equal,
+            std::cmp::Ordering::Greater => BlockRelation::Descendant,
+        }
+    }
+}
+
+pub struct MockBankCallback {
+    pub feature_set: Arc<FeatureSet>,
+    pub account_shared_data: RwLock<HashMap<Pubkey, AccountSharedData>>,
+}
+
+impl TransactionProcessingCallback for MockBankCallback {
+    fn account_matches_owners(&self, account: &Pubkey, owners: &[Pubkey]) -> Option<usize> {
+        if let Some(data) = self.account_shared_data.read().unwrap().get(account) {
+            if data.lamports() == 0 {
+                None
+            } else {
+                owners.iter().position(|entry| data.owner() == entry)
+            }
+        } else {
+            None
+        }
+    }
+
+    fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<AccountSharedData> {
+        debug!(
+            "Get account {pubkey} shared data, thread {:?}",
+            std::thread::current().name()
+        );
+        self.account_shared_data
+            .read()
+            .unwrap()
+            .get(pubkey)
+            .cloned()
+    }
+
+    fn add_builtin_account(&self, name: &str, program_id: &Pubkey) {
+        let account_data = native_loader::create_loadable_account_with_fields(name, (5000, 0));
+
+        self.account_shared_data
+            .write()
+            .unwrap()
+            .insert(*program_id, account_data);
+    }
+}
+
+impl MockBankCallback {
+    pub fn new(account_map: Vec<(Pubkey, AccountSharedData)>) -> Self {
+        Self {
+            feature_set: Arc::new(FeatureSet::default()),
+            account_shared_data: RwLock::new(HashMap::from_iter(account_map)),
+        }
+    }
+
+    #[allow(dead_code)]
+    pub fn override_feature_set(&mut self, new_set: FeatureSet) {
+        self.feature_set = Arc::new(new_set)
+    }
+}
+
+pub struct LoadAndExecuteTransactionsOutput {
+    // Vector of results indicating whether a transaction was executed or could not
+    // be executed. Note executed transactions can still have failed!
+    pub processing_results: Vec<TransactionProcessingResult>,
+}
+
+pub struct TransactionBatch<'a> {
+    lock_results: Vec<solana_sdk::transaction::Result<()>>,
+    sanitized_txs: std::borrow::Cow<'a, [SanitizedTransaction]>,
+}
+
+impl<'a> TransactionBatch<'a> {
+    pub fn new(
+        lock_results: Vec<solana_sdk::transaction::Result<()>>,
+        sanitized_txs: std::borrow::Cow<'a, [SanitizedTransaction]>,
+    ) -> Self {
+        assert_eq!(lock_results.len(), sanitized_txs.len());
+        Self {
+            lock_results,
+            sanitized_txs,
+        }
+    }
+
+    pub fn lock_results(&self) -> &Vec<solana_sdk::transaction::Result<()>> {
+        &self.lock_results
+    }
+
+    pub fn sanitized_transactions(&self) -> &[SanitizedTransaction] {
+        &self.sanitized_txs
+    }
+}
+
+pub fn create_custom_environment<'a>() -> BuiltinProgram<InvokeContext<'a>> {
+    let compute_budget = ComputeBudget::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_instruction_tracing: true,
+        enable_symbol_and_section_labels: true,
+        reject_broken_elfs: true,
+        noop_instruction_rate: 256,
+        sanitize_user_provided_values: true,
+        external_internal_function_hash_collision: false,
+        reject_callx_r10: false,
+        enable_sbpf_v1: true,
+        enable_sbpf_v2: false,
+        optimize_rodata: false,
+        aligned_memory_mapping: true,
+    };
+
+    // Register system calls that the compiled contract calls during execution.
+    let mut function_registry = FunctionRegistry::<BuiltinFunction<InvokeContext>>::default();
+    function_registry
+        .register_function_hashed(*b"abort", SyscallAbort::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_log_", SyscallLog::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_log_64_", SyscallLogU64::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_log_compute_units_", SyscallLogBpfComputeUnits::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_log_pubkey", SyscallLogPubkey::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_memcpy_", SyscallMemcpy::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_memset_", SyscallMemset::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_invoke_signed_rust", SyscallInvokeSignedRust::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_set_return_data", SyscallSetReturnData::vm)
+        .expect("Registration failed");
+    function_registry
+        .register_function_hashed(*b"sol_get_clock_sysvar", SyscallGetClockSysvar::vm)
+        .expect("Registration failed");
+    BuiltinProgram::new_loader(vm_config, function_registry)
+}
+
+pub fn create_executable_environment(
+    fork_graph: Arc<RwLock<MockForkGraph>>,
+    account_keys: &AccountKeys,
+    mock_bank: &mut MockBankCallback,
+    transaction_processor: &TransactionBatchProcessor<MockForkGraph>,
+) {
+    let mut program_cache = transaction_processor.program_cache.write().unwrap();
+
+    program_cache.environments = ProgramRuntimeEnvironments {
+        program_runtime_v1: Arc::new(create_custom_environment()),
+        // We are not using program runtime v2
+        program_runtime_v2: Arc::new(BuiltinProgram::new_loader(
+            Config::default(),
+            FunctionRegistry::default(),
+        )),
+    };
+
+    program_cache.fork_graph = Some(Arc::downgrade(&fork_graph));
+    // add programs to cache
+    for key in account_keys.iter() {
+        if let Some(account) = mock_bank.get_account_shared_data(key) {
+            if account.executable() && *account.owner() == solana_sdk::bpf_loader_upgradeable::id()
+            {
+                let data = account.data();
+                let program_data_account_key = Pubkey::try_from(data[4..].to_vec()).unwrap();
+                let program_data_account = mock_bank
+                    .get_account_shared_data(&program_data_account_key)
+                    .unwrap();
+                let program_data = program_data_account.data();
+                let elf_bytes = program_data[45..].to_vec();
+
+                let program_runtime_environment =
+                    program_cache.environments.program_runtime_v1.clone();
+                program_cache.assign_program(
+                    *key,
+                    Arc::new(
+                        ProgramCacheEntry::new(
+                            &solana_sdk::bpf_loader_upgradeable::id(),
+                            program_runtime_environment,
+                            0,
+                            0,
+                            &elf_bytes,
+                            elf_bytes.len(),
+                            &mut LoadProgramMetrics::default(),
+                        )
+                        .unwrap(),
+                    ),
+                );
+            }
+        }
+    }
+
+    // We must fill in the sysvar cache entries
+    let time_now = SystemTime::now()
+        .duration_since(UNIX_EPOCH)
+        .expect("Time went backwards")
+        .as_secs() as i64;
+    let clock = Clock {
+        slot: DEPLOYMENT_SLOT,
+        epoch_start_timestamp: time_now.saturating_sub(10) as UnixTimestamp,
+        epoch: DEPLOYMENT_EPOCH,
+        leader_schedule_epoch: DEPLOYMENT_EPOCH,
+        unix_timestamp: time_now as UnixTimestamp,
+    };
+
+    let mut account_data = AccountSharedData::default();
+    account_data.set_data_from_slice(bincode::serialize(&clock).unwrap().as_slice());
+    mock_bank
+        .account_shared_data
+        .write()
+        .unwrap()
+        .insert(Clock::id(), account_data);
+}

+ 1 - 0
svm/examples/json-rpc/test.json

@@ -0,0 +1 @@
+[39,82,169,128,159,226,211,180,118,92,132,200,38,92,230,90,221,95,252,83,174,5,205,251,125,219,15,82,119,57,3,125,134,169,60,216,172,10,24,129,71,172,121,154,5,13,100,84,126,135,69,153,3,163,184,126,153,0,99,201,89,63,43,24]