Skip to content

Commit

Permalink
SVM: add a sample stand-alone application based on SVM (#2217)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dmakarov authored Oct 4, 2024
1 parent a07da92 commit e5a67df
Show file tree
Hide file tree
Showing 16 changed files with 1,792 additions and 2 deletions.
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ members = [
"zk-token-sdk",
]

exclude = ["programs/sbf", "svm/tests/example-programs"]
exclude = [
"programs/sbf",
"svm/tests/example-programs",
]

resolver = "2"

Expand Down
2 changes: 1 addition & 1 deletion accounts-db/src/accounts_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
41 changes: 41 additions & 0 deletions svm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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 changes: 31 additions & 0 deletions svm/examples/json-rpc/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 79 additions & 0 deletions svm/examples/json-rpc/client/src/client.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 48 additions & 0 deletions svm/examples/json-rpc/client/src/main.rs
Original file line number Diff line number Diff line change
@@ -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();
}
Loading

0 comments on commit e5a67df

Please sign in to comment.