Rework TestValidator API to be more like ProgramTest

This commit is contained in:
Michael Vines 2020-12-11 20:03:15 -08:00 committed by mergify[bot]
parent 5237da4e01
commit 9f2d154588
6 changed files with 383 additions and 175 deletions

1
Cargo.lock generated
View File

@ -5094,6 +5094,7 @@ dependencies = [
name = "solana-validator" name = "solana-validator"
version = "1.5.0" version = "1.5.0"
dependencies = [ dependencies = [
"base64 0.12.3",
"chrono", "chrono",
"clap", "clap",
"console", "console",

View File

@ -13,35 +13,165 @@ use {
hardened_unpack::MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, hardened_unpack::MAX_GENESIS_ARCHIVE_UNPACKED_SIZE,
}, },
solana_sdk::{ solana_sdk::{
account::Account,
clock::DEFAULT_MS_PER_SLOT, clock::DEFAULT_MS_PER_SLOT,
commitment_config::CommitmentConfig, commitment_config::CommitmentConfig,
fee_calculator::FeeRateGovernor, fee_calculator::{FeeCalculator, FeeRateGovernor},
hash::Hash,
native_token::sol_to_lamports, native_token::sol_to_lamports,
pubkey::Pubkey, pubkey::Pubkey,
rent::Rent, rent::Rent,
signature::{read_keypair_file, write_keypair_file, Keypair, Signer}, signature::{read_keypair_file, write_keypair_file, Keypair, Signer},
}, },
std::{ std::{
fs::remove_dir_all, collections::HashMap, fs::remove_dir_all, net::SocketAddr, path::PathBuf, sync::Arc,
net::SocketAddr, thread::sleep, time::Duration,
path::{Path, PathBuf},
sync::Arc,
thread::sleep,
time::Duration,
}, },
}; };
pub struct TestValidatorGenesisConfig { #[derive(Clone)]
pub fee_rate_governor: FeeRateGovernor, pub struct ProgramInfo {
pub mint_address: Pubkey, pub program_id: Pubkey,
pub rent: Rent, pub loader: Pubkey,
pub program_path: PathBuf,
} }
#[derive(Default)] #[derive(Default)]
pub struct TestValidatorStartConfig { pub struct TestValidatorGenesis {
pub preserve_ledger: bool, fee_rate_governor: FeeRateGovernor,
pub rpc_config: JsonRpcConfig, ledger_path: Option<PathBuf>,
pub rpc_ports: Option<(u16, u16)>, // (JsonRpc, JsonRpcPubSub), None == random ports rent: Rent,
rpc_config: JsonRpcConfig,
rpc_ports: Option<(u16, u16)>, // (JsonRpc, JsonRpcPubSub), None == random ports
accounts: HashMap<Pubkey, Account>,
programs: Vec<ProgramInfo>,
}
impl TestValidatorGenesis {
pub fn ledger_path<P: Into<PathBuf>>(&mut self, ledger_path: P) -> &mut Self {
self.ledger_path = Some(ledger_path.into());
self
}
pub fn fee_rate_governor(&mut self, fee_rate_governor: FeeRateGovernor) -> &mut Self {
self.fee_rate_governor = fee_rate_governor;
self
}
pub fn rent(&mut self, rent: Rent) -> &mut Self {
self.rent = rent;
self
}
pub fn rpc_config(&mut self, rpc_config: JsonRpcConfig) -> &mut Self {
self.rpc_config = rpc_config;
self
}
pub fn rpc_port(&mut self, rpc_port: u16) -> &mut Self {
self.rpc_ports = Some((rpc_port, rpc_port + 1));
self
}
/// Add an account to the test environment
pub fn add_account(&mut self, address: Pubkey, account: Account) -> &mut Self {
self.accounts.insert(address, account);
self
}
/// Add an account to the test environment with the account data in the provided `filename`
pub fn add_account_with_file_data(
&mut self,
address: Pubkey,
lamports: u64,
owner: Pubkey,
filename: &str,
) -> &mut Self {
self.add_account(
address,
Account {
lamports,
data: solana_program_test::read_file(
solana_program_test::find_file(filename).unwrap_or_else(|| {
panic!("Unable to locate {}", filename);
}),
),
owner,
executable: false,
rent_epoch: 0,
},
)
}
/// Add an account to the test environment with the account data in the provided as a base 64
/// string
pub fn add_account_with_base64_data(
&mut self,
address: Pubkey,
lamports: u64,
owner: Pubkey,
data_base64: &str,
) -> &mut Self {
self.add_account(
address,
Account {
lamports,
data: base64::decode(data_base64)
.unwrap_or_else(|err| panic!("Failed to base64 decode: {}", err)),
owner,
executable: false,
rent_epoch: 0,
},
)
}
/// Add a BPF program to the test environment.
///
/// `program_name` will also used to locate the BPF shared object in the current or fixtures
/// directory.
pub fn add_program(&mut self, program_name: &str, program_id: Pubkey) -> &mut Self {
let program_path = solana_program_test::find_file(&format!("{}.so", program_name))
.unwrap_or_else(|| panic!("Unable to locate program {}", program_name));
self.programs.push(ProgramInfo {
program_id,
loader: solana_sdk::bpf_loader::id(),
program_path,
});
self
}
/// Add a list of programs to the test environment.
///pub fn add_programs_with_path<'a>(&'a mut self, programs: &[ProgramInfo]) -> &'a mut Self {
pub fn add_programs_with_path(&mut self, programs: &[ProgramInfo]) -> &mut Self {
for program in programs {
self.programs.push(program.clone());
}
self
}
/// Start a test validator with the address of the mint account that will receive tokens
/// created at genesis.
///
pub fn start_with_mint_address(
&self,
mint_address: Pubkey,
) -> Result<TestValidator, Box<dyn std::error::Error>> {
TestValidator::start(mint_address, self)
}
/// Start a test validator
///
/// Returns a new `TestValidator` as well as the keypair for the mint account that will receive tokens
/// created at genesis.
///
/// This function panics on initialization failure.
pub fn start(&self) -> (TestValidator, Keypair) {
let mint_keypair = Keypair::new();
TestValidator::start(mint_keypair.pubkey(), self)
.map(|test_validator| (test_validator, mint_keypair))
.expect("Test validator failed to start")
}
} }
pub struct TestValidator { pub struct TestValidator {
@ -56,106 +186,92 @@ pub struct TestValidator {
} }
impl TestValidator { impl TestValidator {
/// The default test validator is intended to be generically suitable for unit testing. /// Create and start a `TestValidator` with no transaction fees and minimal rent.
///
/// It uses a unique temporary ledger that is deleted on `close` and randomly assigned ports.
/// All test tokens will be minted into `mint_address`
///
/// This function panics on initialization failure.
pub fn new(mint_address: Pubkey) -> Self {
let ledger_path = Self::initialize_ledger(
None,
TestValidatorGenesisConfig {
fee_rate_governor: FeeRateGovernor::default(),
mint_address,
rent: Rent::default(),
},
)
.unwrap();
Self::start(&ledger_path, TestValidatorStartConfig::default()).unwrap()
}
/// Create a `TestValidator` with no transaction fees and minimal rent.
/// ///
/// This function panics on initialization failure. /// This function panics on initialization failure.
pub fn with_no_fees(mint_address: Pubkey) -> Self { pub fn with_no_fees(mint_address: Pubkey) -> Self {
let ledger_path = Self::initialize_ledger( TestValidatorGenesis::default()
None, .fee_rate_governor(FeeRateGovernor::new(0, 0))
TestValidatorGenesisConfig { .rent(Rent {
fee_rate_governor: FeeRateGovernor::new(0, 0), lamports_per_byte_year: 1,
mint_address, exemption_threshold: 1.0,
rent: Rent { ..Rent::default()
lamports_per_byte_year: 1, })
exemption_threshold: 1.0, .start_with_mint_address(mint_address)
..Rent::default() .expect("validator start failed")
},
},
)
.unwrap();
Self::start(&ledger_path, TestValidatorStartConfig::default()).unwrap()
} }
/// Create a `TestValidator` with custom transaction fees and minimal rent. /// Create and start a `TestValidator` with custom transaction fees and minimal rent.
/// ///
/// This function panics on initialization failure. /// This function panics on initialization failure.
pub fn with_custom_fees(mint_address: Pubkey, target_lamports_per_signature: u64) -> Self { pub fn with_custom_fees(mint_address: Pubkey, target_lamports_per_signature: u64) -> Self {
let ledger_path = Self::initialize_ledger( TestValidatorGenesis::default()
None, .fee_rate_governor(FeeRateGovernor::new(target_lamports_per_signature, 0))
TestValidatorGenesisConfig { .rent(Rent {
fee_rate_governor: FeeRateGovernor::new(target_lamports_per_signature, 0), lamports_per_byte_year: 1,
mint_address, exemption_threshold: 1.0,
rent: Rent { ..Rent::default()
lamports_per_byte_year: 1, })
exemption_threshold: 1.0, .start_with_mint_address(mint_address)
..Rent::default() .expect("validator start failed")
},
},
)
.unwrap();
Self::start(&ledger_path, TestValidatorStartConfig::default()).unwrap()
} }
/// Initialize the test validator's ledger directory /// Initialize the ledger directory
/// ///
/// If `ledger_path` is `None`, a temporary ledger will be created. Otherwise the ledger will /// If `ledger_path` is `None`, a temporary ledger will be created. Otherwise the ledger will
/// be initialized in the provided directory. /// be initialized in the provided directory if it doesn't already exist.
/// ///
/// Returns the path to the ledger directory. /// Returns the path to the ledger directory.
pub fn initialize_ledger( fn initialize_ledger(
ledger_path: Option<&Path>, mint_address: Pubkey,
config: TestValidatorGenesisConfig, config: &TestValidatorGenesis,
) -> Result<PathBuf, Box<dyn std::error::Error>> { ) -> Result<PathBuf, Box<dyn std::error::Error>> {
let TestValidatorGenesisConfig { let validator_identity = Keypair::new();
fee_rate_governor,
mint_address,
rent,
} = config;
let validator_identity_keypair = Keypair::new();
let validator_vote_account = Keypair::new(); let validator_vote_account = Keypair::new();
let validator_stake_account = Keypair::new(); let validator_stake_account = Keypair::new();
let validator_identity_lamports = sol_to_lamports(500.); let validator_identity_lamports = sol_to_lamports(500.);
let validator_stake_lamports = sol_to_lamports(1_000_000.); let validator_stake_lamports = sol_to_lamports(1_000_000.);
let mint_lamports = sol_to_lamports(500_000_000.); let mint_lamports = sol_to_lamports(500_000_000.);
let initial_accounts = solana_program_test::programs::spl_programs(&rent); let mut accounts = config.accounts.clone();
for (address, account) in solana_program_test::programs::spl_programs(&config.rent) {
accounts.entry(address).or_insert(account);
}
for program in &config.programs {
let data = solana_program_test::read_file(&program.program_path);
accounts.insert(
program.program_id,
Account {
lamports: Rent::default().minimum_balance(data.len()).min(1),
data,
owner: program.loader,
executable: true,
rent_epoch: 0,
},
);
}
let genesis_config = create_genesis_config_with_leader_ex( let genesis_config = create_genesis_config_with_leader_ex(
mint_lamports, mint_lamports,
&mint_address, &mint_address,
&validator_identity_keypair.pubkey(), &validator_identity.pubkey(),
&validator_vote_account.pubkey(), &validator_vote_account.pubkey(),
&validator_stake_account.pubkey(), &validator_stake_account.pubkey(),
validator_stake_lamports, validator_stake_lamports,
validator_identity_lamports, validator_identity_lamports,
fee_rate_governor, config.fee_rate_governor.clone(),
rent, config.rent,
solana_sdk::genesis_config::ClusterType::Development, solana_sdk::genesis_config::ClusterType::Development,
initial_accounts, accounts.into_iter().collect(),
); );
let ledger_path = match ledger_path { let ledger_path = match &config.ledger_path {
None => create_new_tmp_ledger!(&genesis_config).0, None => create_new_tmp_ledger!(&genesis_config).0,
Some(ledger_path) => { Some(ledger_path) => {
if ledger_path.join("validator-keypair.json").exists() {
return Ok(ledger_path.to_path_buf());
}
let _ = create_new_ledger( let _ = create_new_ledger(
ledger_path, ledger_path,
&genesis_config, &genesis_config,
@ -174,7 +290,7 @@ impl TestValidator {
}; };
write_keypair_file( write_keypair_file(
&validator_identity_keypair, &validator_identity,
ledger_path.join("validator-keypair.json").to_str().unwrap(), ledger_path.join("validator-keypair.json").to_str().unwrap(),
)?; )?;
write_keypair_file( write_keypair_file(
@ -189,11 +305,14 @@ impl TestValidator {
} }
/// Starts a TestValidator at the provided ledger directory /// Starts a TestValidator at the provided ledger directory
pub fn start( fn start(
ledger_path: &Path, mint_address: Pubkey,
config: TestValidatorStartConfig, config: &TestValidatorGenesis,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
let validator_identity_keypair = let preserve_ledger = config.ledger_path.is_some();
let ledger_path = TestValidator::initialize_ledger(mint_address, config)?;
let validator_identity =
read_keypair_file(ledger_path.join("validator-keypair.json").to_str().unwrap())?; read_keypair_file(ledger_path.join("validator-keypair.json").to_str().unwrap())?;
let validator_vote_account = read_keypair_file( let validator_vote_account = read_keypair_file(
ledger_path ledger_path
@ -202,7 +321,7 @@ impl TestValidator {
.unwrap(), .unwrap(),
)?; )?;
let mut node = Node::new_localhost_with_pubkey(&validator_identity_keypair.pubkey()); let mut node = Node::new_localhost_with_pubkey(&validator_identity.pubkey());
if let Some((rpc, rpc_pubsub)) = config.rpc_ports { if let Some((rpc, rpc_pubsub)) = config.rpc_ports {
node.info.rpc = SocketAddr::new(node.info.gossip.ip(), rpc); node.info.rpc = SocketAddr::new(node.info.gossip.ip(), rpc);
node.info.rpc_pubsub = SocketAddr::new(node.info.gossip.ip(), rpc_pubsub); node.info.rpc_pubsub = SocketAddr::new(node.info.gossip.ip(), rpc_pubsub);
@ -216,7 +335,7 @@ impl TestValidator {
let validator_config = ValidatorConfig { let validator_config = ValidatorConfig {
rpc_addrs: Some((node.info.rpc, node.info.rpc_pubsub)), rpc_addrs: Some((node.info.rpc, node.info.rpc_pubsub)),
rpc_config: config.rpc_config, rpc_config: config.rpc_config.clone(),
accounts_hash_interval_slots: 100, accounts_hash_interval_slots: 100,
account_paths: vec![ledger_path.join("accounts")], account_paths: vec![ledger_path.join("accounts")],
poh_verify: false, // Skip PoH verification of ledger on startup for speed poh_verify: false, // Skip PoH verification of ledger on startup for speed
@ -232,7 +351,7 @@ impl TestValidator {
let validator = Some(Validator::new( let validator = Some(Validator::new(
node, node,
&Arc::new(validator_identity_keypair), &Arc::new(validator_identity),
&ledger_path, &ledger_path,
&validator_vote_account.pubkey(), &validator_vote_account.pubkey(),
vec![Arc::new(validator_vote_account)], vec![Arc::new(validator_vote_account)],
@ -244,12 +363,12 @@ impl TestValidator {
// test validators concurrently... // test validators concurrently...
discover_cluster(&gossip, 1).expect("TestValidator startup failed"); discover_cluster(&gossip, 1).expect("TestValidator startup failed");
// This is a hack to delay until the single gossip commitment fees are non-zero for test // This is a hack to delay until the fees are non-zero for test consistency
// consistency // (fees from genesis are zero until the first block with a transaction in it is completed
// (fees from genesis are zero until the first block with a transaction in it is completed) // due to a bug in the Bank)
{ {
let rpc_client = let rpc_client =
RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::single_gossip()); RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::recent());
let fee_rate_governor = rpc_client let fee_rate_governor = rpc_client
.get_fee_rate_governor() .get_fee_rate_governor()
.expect("get_fee_rate_governor") .expect("get_fee_rate_governor")
@ -268,8 +387,8 @@ impl TestValidator {
} }
Ok(TestValidator { Ok(TestValidator {
ledger_path: ledger_path.to_path_buf(), ledger_path,
preserve_ledger: false, preserve_ledger,
rpc_pubsub_url, rpc_pubsub_url,
rpc_url, rpc_url,
gossip, gossip,
@ -279,30 +398,42 @@ impl TestValidator {
}) })
} }
/// Return the test validator's TPU address /// Return the validator's TPU address
pub fn tpu(&self) -> &SocketAddr { pub fn tpu(&self) -> &SocketAddr {
&self.tpu &self.tpu
} }
/// Return the test validator's Gossip address /// Return the validator's Gossip address
pub fn gossip(&self) -> &SocketAddr { pub fn gossip(&self) -> &SocketAddr {
&self.gossip &self.gossip
} }
/// Return the test validator's JSON RPC URL /// Return the validator's JSON RPC URL
pub fn rpc_url(&self) -> String { pub fn rpc_url(&self) -> String {
self.rpc_url.clone() self.rpc_url.clone()
} }
/// Return the test validator's JSON RPC PubSub URL /// Return the validator's JSON RPC PubSub URL
pub fn rpc_pubsub_url(&self) -> String { pub fn rpc_pubsub_url(&self) -> String {
self.rpc_pubsub_url.clone() self.rpc_pubsub_url.clone()
} }
/// Return the vote account address of the validator /// Return the validator's vote account address
pub fn vote_account_address(&self) -> Pubkey { pub fn vote_account_address(&self) -> Pubkey {
self.vote_account_address self.vote_account_address
} }
/// Return an RpcClient for the validator. As a convenience, also return a recent blockhash and
/// associated fee calculator
pub fn rpc_client(&self) -> (RpcClient, Hash, FeeCalculator) {
let rpc_client =
RpcClient::new_with_commitment(self.rpc_url.clone(), CommitmentConfig::recent());
let (recent_blockhash, fee_calculator) = rpc_client
.get_recent_blockhash()
.expect("get_recent_blockhash");
(rpc_client, recent_blockhash, fee_calculator)
}
} }
impl Drop for TestValidator { impl Drop for TestValidator {

View File

@ -256,14 +256,14 @@ pub fn request_airdrop_transaction(
Ok(transaction) Ok(transaction)
} }
// For integration tests. Listens on random open port and reports port to Sender. pub fn run_local_faucet_with_port(
pub fn run_local_faucet(
faucet_keypair: Keypair, faucet_keypair: Keypair,
sender: Sender<SocketAddr>, sender: Sender<SocketAddr>,
per_time_cap: Option<u64>, per_time_cap: Option<u64>,
port: u16,
) { ) {
thread::spawn(move || { thread::spawn(move || {
let faucet_addr = socketaddr!(0, 0); let faucet_addr = socketaddr!(0, port);
let faucet = Arc::new(Mutex::new(Faucet::new( let faucet = Arc::new(Mutex::new(Faucet::new(
faucet_keypair, faucet_keypair,
None, None,
@ -274,6 +274,15 @@ pub fn run_local_faucet(
}); });
} }
// For integration tests. Listens on random open port and reports port to Sender.
pub fn run_local_faucet(
faucet_keypair: Keypair,
sender: Sender<SocketAddr>,
per_time_cap: Option<u64>,
) {
run_local_faucet_with_port(faucet_keypair, sender, per_time_cap, 0)
}
pub fn run_faucet( pub fn run_faucet(
faucet: Arc<Mutex<Faucet>>, faucet: Arc<Mutex<Faucet>>,
faucet_addr: SocketAddr, faucet_addr: SocketAddr,

View File

@ -338,7 +338,17 @@ impl program_stubs::SyscallStubs for SyscallStubs {
} }
} }
fn find_file(filename: &str, search_path: &[PathBuf]) -> Option<PathBuf> { pub fn find_file(filename: &str) -> Option<PathBuf> {
let mut search_path = vec![];
if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
search_path.push(PathBuf::from(bpf_out_dir));
}
search_path.push(PathBuf::from("tests/fixtures"));
if let Ok(dir) = std::env::current_dir() {
search_path.push(dir);
}
trace!("search path: {:?}", search_path);
for path in search_path { for path in search_path {
let candidate = path.join(&filename); let candidate = path.join(&filename);
if candidate.exists() { if candidate.exists() {
@ -348,7 +358,7 @@ fn find_file(filename: &str, search_path: &[PathBuf]) -> Option<PathBuf> {
None None
} }
fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> { pub fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
let path = path.as_ref(); let path = path.as_ref();
let mut file = File::open(path) let mut file = File::open(path)
.unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err)); .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err));
@ -364,7 +374,6 @@ pub struct ProgramTest {
builtins: Vec<Builtin>, builtins: Vec<Builtin>,
bpf_compute_max_units: Option<u64>, bpf_compute_max_units: Option<u64>,
prefer_bpf: bool, prefer_bpf: bool,
search_path: Vec<PathBuf>,
} }
impl Default for ProgramTest { impl Default for ProgramTest {
@ -388,25 +397,13 @@ impl Default for ProgramTest {
solana_runtime::system_instruction_processor=trace,\ solana_runtime::system_instruction_processor=trace,\
solana_program_test=info", solana_program_test=info",
); );
let mut prefer_bpf = false; let prefer_bpf = std::env::var("BPF_OUT_DIR").is_ok();
let mut search_path = vec![];
if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
prefer_bpf = true;
search_path.push(PathBuf::from(bpf_out_dir));
}
search_path.push(PathBuf::from("tests/fixtures"));
if let Ok(dir) = std::env::current_dir() {
search_path.push(dir);
}
debug!("search path: {:?}", search_path);
Self { Self {
accounts: vec![], accounts: vec![],
builtins: vec![], builtins: vec![],
bpf_compute_max_units: None, bpf_compute_max_units: None,
prefer_bpf, prefer_bpf,
search_path,
} }
} }
} }
@ -449,7 +446,7 @@ impl ProgramTest {
address, address,
Account { Account {
lamports, lamports,
data: read_file(find_file(filename, &self.search_path).unwrap_or_else(|| { data: read_file(find_file(filename).unwrap_or_else(|| {
panic!("Unable to locate {}", filename); panic!("Unable to locate {}", filename);
})), })),
owner, owner,
@ -495,7 +492,7 @@ impl ProgramTest {
process_instruction: Option<ProcessInstructionWithContext>, process_instruction: Option<ProcessInstructionWithContext>,
) { ) {
let loader = solana_program::bpf_loader::id(); let loader = solana_program::bpf_loader::id();
let program_file = find_file(&format!("{}.so", program_name), &self.search_path); let program_file = find_file(&format!("{}.so", program_name));
if process_instruction.is_none() && program_file.is_none() { if process_instruction.is_none() && program_file.is_none() {
panic!("Unable to add program {} ({})", program_name, program_id); panic!("Unable to add program {} ({})", program_name, program_id);

View File

@ -10,6 +10,7 @@ homepage = "https://solana.com/"
default-run = "solana-validator" default-run = "solana-validator"
[dependencies] [dependencies]
base64 = "0.12.3"
clap = "2.33.1" clap = "2.33.1"
chrono = { version = "0.4.11", features = ["serde"] } chrono = { version = "0.4.11", features = ["serde"] }
console = "0.11.3" console = "0.11.3"

View File

@ -5,19 +5,25 @@ use {
solana_clap_utils::{input_parsers::pubkey_of, input_validators::is_pubkey}, solana_clap_utils::{input_parsers::pubkey_of, input_validators::is_pubkey},
solana_client::{client_error, rpc_client::RpcClient}, solana_client::{client_error, rpc_client::RpcClient},
solana_core::rpc::JsonRpcConfig, solana_core::rpc::JsonRpcConfig,
solana_faucet::faucet::{run_local_faucet_with_port, FAUCET_PORT},
solana_sdk::{ solana_sdk::{
account::Account,
clock::{Slot, DEFAULT_TICKS_PER_SLOT, MS_PER_TICK}, clock::{Slot, DEFAULT_TICKS_PER_SLOT, MS_PER_TICK},
commitment_config::CommitmentConfig, commitment_config::CommitmentConfig,
fee_calculator::FeeRateGovernor, native_token::sol_to_lamports,
rent::Rent, pubkey::Pubkey,
rpc_port, rpc_port,
signature::{read_keypair_file, Signer}, signature::{read_keypair_file, write_keypair_file, Keypair, Signer},
system_program,
}, },
solana_validator::{start_logger, test_validator::*}, solana_validator::{start_logger, test_validator::*},
std::{ std::{
fs,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf, path::PathBuf,
process::exit, process::exit,
thread::sleep, sync::mpsc::channel,
thread,
time::{Duration, SystemTime, UNIX_EPOCH}, time::{Duration, SystemTime, UNIX_EPOCH},
}, },
}; };
@ -47,7 +53,8 @@ fn println_name_value(name: &str, value: &str) {
fn main() { fn main() {
let default_rpc_port = rpc_port::DEFAULT_RPC_PORT.to_string(); let default_rpc_port = rpc_port::DEFAULT_RPC_PORT.to_string();
let matches = App::new("solana-test-validator").about("Test Validator") let matches = App::new("solana-test-validator")
.about("Test Validator")
.version(solana_version::version!()) .version(solana_version::version!())
.arg({ .arg({
let arg = Arg::with_name("config_file") let arg = Arg::with_name("config_file")
@ -68,7 +75,10 @@ fn main() {
.value_name("PUBKEY") .value_name("PUBKEY")
.validator(is_pubkey) .validator(is_pubkey)
.takes_value(true) .takes_value(true)
.help("Address of the mint account that will receive all the initial tokens [default: client keypair]"), .help(
"Address of the mint account that will receive tokens \
created at genesis [default: client keypair]",
),
) )
.arg( .arg(
Arg::with_name("ledger_path") Arg::with_name("ledger_path")
@ -86,14 +96,14 @@ fn main() {
.long("quiet") .long("quiet")
.takes_value(false) .takes_value(false)
.conflicts_with("log") .conflicts_with("log")
.help("Quiet mode: suppress normal output") .help("Quiet mode: suppress normal output"),
) )
.arg( .arg(
Arg::with_name("log") Arg::with_name("log")
.long("log") .long("log")
.takes_value(false) .takes_value(false)
.conflicts_with("quiet") .conflicts_with("quiet")
.help("Log mode: stream the validator log") .help("Log mode: stream the validator log"),
) )
.arg( .arg(
Arg::with_name("rpc_port") Arg::with_name("rpc_port")
@ -104,6 +114,15 @@ fn main() {
.validator(solana_validator::port_validator) .validator(solana_validator::port_validator)
.help("Use this port for JSON RPC and the next port for the RPC websocket"), .help("Use this port for JSON RPC and the next port for the RPC websocket"),
) )
.arg(
Arg::with_name("bpf_program")
.long("bpf-program")
.value_name("ADDRESS BPF_PROGRAM.SO")
.takes_value(true)
.number_of_values(2)
.multiple(true)
.help("Add a BPF program to the genesis configuration"),
)
.get_matches(); .get_matches();
let cli_config = if let Some(config_file) = matches.value_of("config_file") { let cli_config = if let Some(config_file) = matches.value_of("config_file") {
@ -132,42 +151,56 @@ fn main() {
} else { } else {
Output::Dashboard Output::Dashboard
}; };
let rpc_port = value_t_or_exit!(matches, "rpc_port", u16);
let faucet_addr = Some(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
FAUCET_PORT,
));
let rpc_ports = { let mut programs = vec![];
let rpc_port = value_t_or_exit!(matches, "rpc_port", u16); if let Some(values) = matches.values_of("bpf_program") {
(rpc_port, rpc_port + 1) let values: Vec<&str> = values.collect::<Vec<_>>();
}; for address_program in values.chunks(2) {
match address_program {
[address, program] => {
let address = address.parse::<Pubkey>().unwrap_or_else(|err| {
eprintln!("Error: invalid address {}: {}", address, err);
exit(1);
});
if !ledger_path.exists() { let program_path = PathBuf::from(program);
let _progress_bar = if output == Output::Dashboard { if !program_path.exists() {
println_name_value("Mint address:", &mint_address.to_string()); eprintln!(
let progress_bar = new_spinner_progress_bar(); "Error: program file does not exist: {}",
progress_bar.set_message("Creating ledger..."); program_path.display()
Some(progress_bar) );
} else { exit(1);
None }
};
TestValidator::initialize_ledger( programs.push(ProgramInfo {
Some(&ledger_path), program_id: address,
TestValidatorGenesisConfig { loader: solana_sdk::bpf_loader::id(),
mint_address, program_path,
fee_rate_governor: FeeRateGovernor::default(), });
rent: Rent::default(), }
}, _ => unreachable!(),
) }
.unwrap_or_else(|err| { }
eprintln!(
"Error: failed to initialize ledger at {}: {}",
ledger_path.display(),
err
);
exit(1);
});
} }
let validator_log_symlink = ledger_path.join("validator.log"); let validator_log_symlink = ledger_path.join("validator.log");
let logfile = if output != Output::Log { let logfile = if output != Output::Log {
if !ledger_path.exists() {
fs::create_dir(&ledger_path).unwrap_or_else(|err| {
eprintln!(
"Error: Unable to create directory {}: {}",
ledger_path.display(),
err
);
exit(1);
})
}
let validator_log_with_timestamp = format!( let validator_log_with_timestamp = format!(
"validator-{}.log", "validator-{}.log",
SystemTime::now() SystemTime::now()
@ -176,7 +209,7 @@ fn main() {
.as_millis() .as_millis()
); );
let _ = std::fs::remove_file(&validator_log_symlink); let _ = fs::remove_file(&validator_log_symlink);
symlink::symlink_file(&validator_log_with_timestamp, &validator_log_symlink).unwrap(); symlink::symlink_file(&validator_log_with_timestamp, &validator_log_symlink).unwrap();
Some( Some(
@ -189,11 +222,35 @@ fn main() {
} else { } else {
None None
}; };
let _logger_thread = start_logger(logfile); let _logger_thread = start_logger(logfile);
let faucet_lamports = sol_to_lamports(1_000_000.);
let faucet_keypair_file = ledger_path.join("faucet-keypair.json");
if !faucet_keypair_file.exists() {
write_keypair_file(&Keypair::new(), faucet_keypair_file.to_str().unwrap()).unwrap_or_else(
|err| {
eprintln!(
"Error: Failed to write {}: {}",
faucet_keypair_file.display(),
err
);
exit(1);
},
);
}
let faucet_keypair =
read_keypair_file(faucet_keypair_file.to_str().unwrap()).unwrap_or_else(|err| {
eprintln!(
"Error: Failed to read {}: {}",
faucet_keypair_file.display(),
err
);
exit(1);
});
let test_validator = { let test_validator = {
let _progress_bar = if output == Output::Dashboard { let _progress_bar = if output == Output::Dashboard {
println_name_value("Mint address:", &mint_address.to_string());
println_name_value("Ledger location:", &format!("{}", ledger_path.display())); println_name_value("Ledger location:", &format!("{}", ledger_path.display()));
println_name_value("Log:", &format!("{}", validator_log_symlink.display())); println_name_value("Log:", &format!("{}", validator_log_symlink.display()));
let progress_bar = new_spinner_progress_bar(); let progress_bar = new_spinner_progress_bar();
@ -203,24 +260,33 @@ fn main() {
None None
}; };
TestValidator::start( TestValidatorGenesis::default()
&ledger_path, .ledger_path(ledger_path)
TestValidatorStartConfig { .add_account(
preserve_ledger: true, faucet_keypair.pubkey(),
rpc_config: JsonRpcConfig { Account::new(faucet_lamports, 0, &system_program::id()),
enable_validator_exit: true, )
enable_rpc_transaction_history: true, .rpc_config(JsonRpcConfig {
..JsonRpcConfig::default() enable_validator_exit: true,
}, enable_rpc_transaction_history: true,
rpc_ports: Some(rpc_ports), faucet_addr,
}, ..JsonRpcConfig::default()
) })
.rpc_port(rpc_port)
.add_programs_with_path(&programs)
.start_with_mint_address(mint_address)
} }
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
eprintln!("Error: failed to start validator: {}", err); eprintln!("Error: failed to start validator: {}", err);
exit(1); exit(1);
}); });
if let Some(faucet_addr) = &faucet_addr {
let (sender, receiver) = channel();
run_local_faucet_with_port(faucet_keypair, sender, None, faucet_addr.port());
receiver.recv().expect("run faucet");
}
if output == Output::Dashboard { if output == Output::Dashboard {
println_name_value("JSON RPC URL:", &test_validator.rpc_url()); println_name_value("JSON RPC URL:", &test_validator.rpc_url());
println_name_value( println_name_value(
@ -229,9 +295,12 @@ fn main() {
); );
println_name_value("Gossip Address:", &test_validator.gossip().to_string()); println_name_value("Gossip Address:", &test_validator.gossip().to_string());
println_name_value("TPU Address:", &test_validator.tpu().to_string()); println_name_value("TPU Address:", &test_validator.tpu().to_string());
if let Some(faucet_addr) = &faucet_addr {
println_name_value("Faucet Address:", &faucet_addr.to_string());
}
let progress_bar = new_spinner_progress_bar(); let progress_bar = new_spinner_progress_bar();
let rpc_client = RpcClient::new(test_validator.rpc_url()); let rpc_client = test_validator.rpc_client().0;
fn get_validator_stats(rpc_client: &RpcClient) -> client_error::Result<(Slot, Slot, u64)> { fn get_validator_stats(rpc_client: &RpcClient) -> client_error::Result<(Slot, Slot, u64)> {
let max_slot = rpc_client.get_slot_with_commitment(CommitmentConfig::max())?; let max_slot = rpc_client.get_slot_with_commitment(CommitmentConfig::max())?;
@ -253,7 +322,7 @@ fn main() {
progress_bar.set_message(&format!("{}", err)); progress_bar.set_message(&format!("{}", err));
} }
} }
sleep(Duration::from_millis( thread::sleep(Duration::from_millis(
MS_PER_TICK * DEFAULT_TICKS_PER_SLOT / 2, MS_PER_TICK * DEFAULT_TICKS_PER_SLOT / 2,
)); ));
} }