diff --git a/Cargo.lock b/Cargo.lock index 5eca3401fa..ea98d4ca21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5094,6 +5094,7 @@ dependencies = [ name = "solana-validator" version = "1.5.0" dependencies = [ + "base64 0.12.3", "chrono", "clap", "console", diff --git a/core/src/test_validator.rs b/core/src/test_validator.rs index 1d3eb23671..b942dffa46 100644 --- a/core/src/test_validator.rs +++ b/core/src/test_validator.rs @@ -13,35 +13,165 @@ use { hardened_unpack::MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, }, solana_sdk::{ + account::Account, clock::DEFAULT_MS_PER_SLOT, commitment_config::CommitmentConfig, - fee_calculator::FeeRateGovernor, + fee_calculator::{FeeCalculator, FeeRateGovernor}, + hash::Hash, native_token::sol_to_lamports, pubkey::Pubkey, rent::Rent, signature::{read_keypair_file, write_keypair_file, Keypair, Signer}, }, std::{ - fs::remove_dir_all, - net::SocketAddr, - path::{Path, PathBuf}, - sync::Arc, - thread::sleep, - time::Duration, + collections::HashMap, fs::remove_dir_all, net::SocketAddr, path::PathBuf, sync::Arc, + thread::sleep, time::Duration, }, }; -pub struct TestValidatorGenesisConfig { - pub fee_rate_governor: FeeRateGovernor, - pub mint_address: Pubkey, - pub rent: Rent, +#[derive(Clone)] +pub struct ProgramInfo { + pub program_id: Pubkey, + pub loader: Pubkey, + pub program_path: PathBuf, } #[derive(Default)] -pub struct TestValidatorStartConfig { - pub preserve_ledger: bool, - pub rpc_config: JsonRpcConfig, - pub rpc_ports: Option<(u16, u16)>, // (JsonRpc, JsonRpcPubSub), None == random ports +pub struct TestValidatorGenesis { + fee_rate_governor: FeeRateGovernor, + ledger_path: Option, + rent: Rent, + rpc_config: JsonRpcConfig, + rpc_ports: Option<(u16, u16)>, // (JsonRpc, JsonRpcPubSub), None == random ports + accounts: HashMap, + programs: Vec, +} + +impl TestValidatorGenesis { + pub fn ledger_path>(&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::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 { @@ -56,106 +186,92 @@ pub struct TestValidator { } impl TestValidator { - /// The default test validator is intended to be generically suitable for unit testing. - /// - /// 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. + /// Create and start a `TestValidator` with no transaction fees and minimal rent. /// /// This function panics on initialization failure. pub fn with_no_fees(mint_address: Pubkey) -> Self { - let ledger_path = Self::initialize_ledger( - None, - TestValidatorGenesisConfig { - fee_rate_governor: FeeRateGovernor::new(0, 0), - mint_address, - rent: Rent { - lamports_per_byte_year: 1, - exemption_threshold: 1.0, - ..Rent::default() - }, - }, - ) - .unwrap(); - Self::start(&ledger_path, TestValidatorStartConfig::default()).unwrap() + TestValidatorGenesis::default() + .fee_rate_governor(FeeRateGovernor::new(0, 0)) + .rent(Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }) + .start_with_mint_address(mint_address) + .expect("validator start failed") } - /// 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. pub fn with_custom_fees(mint_address: Pubkey, target_lamports_per_signature: u64) -> Self { - let ledger_path = Self::initialize_ledger( - None, - TestValidatorGenesisConfig { - fee_rate_governor: FeeRateGovernor::new(target_lamports_per_signature, 0), - mint_address, - rent: Rent { - lamports_per_byte_year: 1, - exemption_threshold: 1.0, - ..Rent::default() - }, - }, - ) - .unwrap(); - Self::start(&ledger_path, TestValidatorStartConfig::default()).unwrap() + TestValidatorGenesis::default() + .fee_rate_governor(FeeRateGovernor::new(target_lamports_per_signature, 0)) + .rent(Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }) + .start_with_mint_address(mint_address) + .expect("validator start failed") } - /// 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 - /// 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. - pub fn initialize_ledger( - ledger_path: Option<&Path>, - config: TestValidatorGenesisConfig, + fn initialize_ledger( + mint_address: Pubkey, + config: &TestValidatorGenesis, ) -> Result> { - let TestValidatorGenesisConfig { - fee_rate_governor, - mint_address, - rent, - } = config; - - let validator_identity_keypair = Keypair::new(); + let validator_identity = Keypair::new(); let validator_vote_account = Keypair::new(); let validator_stake_account = Keypair::new(); let validator_identity_lamports = sol_to_lamports(500.); let validator_stake_lamports = sol_to_lamports(1_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( mint_lamports, &mint_address, - &validator_identity_keypair.pubkey(), + &validator_identity.pubkey(), &validator_vote_account.pubkey(), &validator_stake_account.pubkey(), validator_stake_lamports, validator_identity_lamports, - fee_rate_governor, - rent, + config.fee_rate_governor.clone(), + config.rent, 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, Some(ledger_path) => { + if ledger_path.join("validator-keypair.json").exists() { + return Ok(ledger_path.to_path_buf()); + } + let _ = create_new_ledger( ledger_path, &genesis_config, @@ -174,7 +290,7 @@ impl TestValidator { }; write_keypair_file( - &validator_identity_keypair, + &validator_identity, ledger_path.join("validator-keypair.json").to_str().unwrap(), )?; write_keypair_file( @@ -189,11 +305,14 @@ impl TestValidator { } /// Starts a TestValidator at the provided ledger directory - pub fn start( - ledger_path: &Path, - config: TestValidatorStartConfig, + fn start( + mint_address: Pubkey, + config: &TestValidatorGenesis, ) -> Result> { - 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())?; let validator_vote_account = read_keypair_file( ledger_path @@ -202,7 +321,7 @@ impl TestValidator { .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 { node.info.rpc = SocketAddr::new(node.info.gossip.ip(), rpc); node.info.rpc_pubsub = SocketAddr::new(node.info.gossip.ip(), rpc_pubsub); @@ -216,7 +335,7 @@ impl TestValidator { let validator_config = ValidatorConfig { 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, account_paths: vec![ledger_path.join("accounts")], poh_verify: false, // Skip PoH verification of ledger on startup for speed @@ -232,7 +351,7 @@ impl TestValidator { let validator = Some(Validator::new( node, - &Arc::new(validator_identity_keypair), + &Arc::new(validator_identity), &ledger_path, &validator_vote_account.pubkey(), vec![Arc::new(validator_vote_account)], @@ -244,12 +363,12 @@ impl TestValidator { // test validators concurrently... 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 - // consistency - // (fees from genesis are zero until the first block with a transaction in it is completed) + // This is a hack to delay until the fees are non-zero for test consistency + // (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 = - 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 .get_fee_rate_governor() .expect("get_fee_rate_governor") @@ -268,8 +387,8 @@ impl TestValidator { } Ok(TestValidator { - ledger_path: ledger_path.to_path_buf(), - preserve_ledger: false, + ledger_path, + preserve_ledger, rpc_pubsub_url, rpc_url, 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 { &self.tpu } - /// Return the test validator's Gossip address + /// Return the validator's Gossip address pub fn gossip(&self) -> &SocketAddr { &self.gossip } - /// Return the test validator's JSON RPC URL + /// Return the validator's JSON RPC URL pub fn rpc_url(&self) -> String { 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 { 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 { 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 { diff --git a/faucet/src/faucet.rs b/faucet/src/faucet.rs index 6a60024ed0..3f8142a128 100644 --- a/faucet/src/faucet.rs +++ b/faucet/src/faucet.rs @@ -256,14 +256,14 @@ pub fn request_airdrop_transaction( Ok(transaction) } -// For integration tests. Listens on random open port and reports port to Sender. -pub fn run_local_faucet( +pub fn run_local_faucet_with_port( faucet_keypair: Keypair, sender: Sender, per_time_cap: Option, + port: u16, ) { thread::spawn(move || { - let faucet_addr = socketaddr!(0, 0); + let faucet_addr = socketaddr!(0, port); let faucet = Arc::new(Mutex::new(Faucet::new( faucet_keypair, 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, + per_time_cap: Option, +) { + run_local_faucet_with_port(faucet_keypair, sender, per_time_cap, 0) +} + pub fn run_faucet( faucet: Arc>, faucet_addr: SocketAddr, diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index 63a4e9fa77..258a64e80d 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -338,7 +338,17 @@ impl program_stubs::SyscallStubs for SyscallStubs { } } -fn find_file(filename: &str, search_path: &[PathBuf]) -> Option { +pub fn find_file(filename: &str) -> Option { + 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 { let candidate = path.join(&filename); if candidate.exists() { @@ -348,7 +358,7 @@ fn find_file(filename: &str, search_path: &[PathBuf]) -> Option { None } -fn read_file>(path: P) -> Vec { +pub fn read_file>(path: P) -> Vec { let path = path.as_ref(); let mut file = File::open(path) .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err)); @@ -364,7 +374,6 @@ pub struct ProgramTest { builtins: Vec, bpf_compute_max_units: Option, prefer_bpf: bool, - search_path: Vec, } impl Default for ProgramTest { @@ -388,25 +397,13 @@ impl Default for ProgramTest { solana_runtime::system_instruction_processor=trace,\ solana_program_test=info", ); - let mut prefer_bpf = false; - - 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); + let prefer_bpf = std::env::var("BPF_OUT_DIR").is_ok(); Self { accounts: vec![], builtins: vec![], bpf_compute_max_units: None, prefer_bpf, - search_path, } } } @@ -449,7 +446,7 @@ impl ProgramTest { address, Account { 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); })), owner, @@ -495,7 +492,7 @@ impl ProgramTest { process_instruction: Option, ) { 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() { panic!("Unable to add program {} ({})", program_name, program_id); diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 07b3866c15..af5e001398 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -10,6 +10,7 @@ homepage = "https://solana.com/" default-run = "solana-validator" [dependencies] +base64 = "0.12.3" clap = "2.33.1" chrono = { version = "0.4.11", features = ["serde"] } console = "0.11.3" diff --git a/validator/src/bin/solana-test-validator.rs b/validator/src/bin/solana-test-validator.rs index 55d983dd52..a57b07c83f 100644 --- a/validator/src/bin/solana-test-validator.rs +++ b/validator/src/bin/solana-test-validator.rs @@ -5,19 +5,25 @@ use { solana_clap_utils::{input_parsers::pubkey_of, input_validators::is_pubkey}, solana_client::{client_error, rpc_client::RpcClient}, solana_core::rpc::JsonRpcConfig, + solana_faucet::faucet::{run_local_faucet_with_port, FAUCET_PORT}, solana_sdk::{ + account::Account, clock::{Slot, DEFAULT_TICKS_PER_SLOT, MS_PER_TICK}, commitment_config::CommitmentConfig, - fee_calculator::FeeRateGovernor, - rent::Rent, + native_token::sol_to_lamports, + pubkey::Pubkey, rpc_port, - signature::{read_keypair_file, Signer}, + signature::{read_keypair_file, write_keypair_file, Keypair, Signer}, + system_program, }, solana_validator::{start_logger, test_validator::*}, std::{ + fs, + net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, process::exit, - thread::sleep, + sync::mpsc::channel, + thread, time::{Duration, SystemTime, UNIX_EPOCH}, }, }; @@ -47,7 +53,8 @@ fn println_name_value(name: &str, value: &str) { fn main() { 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!()) .arg({ let arg = Arg::with_name("config_file") @@ -68,7 +75,10 @@ fn main() { .value_name("PUBKEY") .validator(is_pubkey) .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::with_name("ledger_path") @@ -86,14 +96,14 @@ fn main() { .long("quiet") .takes_value(false) .conflicts_with("log") - .help("Quiet mode: suppress normal output") + .help("Quiet mode: suppress normal output"), ) .arg( Arg::with_name("log") .long("log") .takes_value(false) .conflicts_with("quiet") - .help("Log mode: stream the validator log") + .help("Log mode: stream the validator log"), ) .arg( Arg::with_name("rpc_port") @@ -104,6 +114,15 @@ fn main() { .validator(solana_validator::port_validator) .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(); let cli_config = if let Some(config_file) = matches.value_of("config_file") { @@ -132,42 +151,56 @@ fn main() { } else { 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 rpc_port = value_t_or_exit!(matches, "rpc_port", u16); - (rpc_port, rpc_port + 1) - }; + let mut programs = vec![]; + if let Some(values) = matches.values_of("bpf_program") { + let values: Vec<&str> = values.collect::>(); + for address_program in values.chunks(2) { + match address_program { + [address, program] => { + let address = address.parse::().unwrap_or_else(|err| { + eprintln!("Error: invalid address {}: {}", address, err); + exit(1); + }); - if !ledger_path.exists() { - let _progress_bar = if output == Output::Dashboard { - println_name_value("Mint address:", &mint_address.to_string()); - let progress_bar = new_spinner_progress_bar(); - progress_bar.set_message("Creating ledger..."); - Some(progress_bar) - } else { - None - }; + let program_path = PathBuf::from(program); + if !program_path.exists() { + eprintln!( + "Error: program file does not exist: {}", + program_path.display() + ); + exit(1); + } - TestValidator::initialize_ledger( - Some(&ledger_path), - TestValidatorGenesisConfig { - mint_address, - fee_rate_governor: FeeRateGovernor::default(), - rent: Rent::default(), - }, - ) - .unwrap_or_else(|err| { - eprintln!( - "Error: failed to initialize ledger at {}: {}", - ledger_path.display(), - err - ); - exit(1); - }); + programs.push(ProgramInfo { + program_id: address, + loader: solana_sdk::bpf_loader::id(), + program_path, + }); + } + _ => unreachable!(), + } + } } let validator_log_symlink = ledger_path.join("validator.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!( "validator-{}.log", SystemTime::now() @@ -176,7 +209,7 @@ fn main() { .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(); Some( @@ -189,11 +222,35 @@ fn main() { } else { None }; - 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 _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("Log:", &format!("{}", validator_log_symlink.display())); let progress_bar = new_spinner_progress_bar(); @@ -203,24 +260,33 @@ fn main() { None }; - TestValidator::start( - &ledger_path, - TestValidatorStartConfig { - preserve_ledger: true, - rpc_config: JsonRpcConfig { - enable_validator_exit: true, - enable_rpc_transaction_history: true, - ..JsonRpcConfig::default() - }, - rpc_ports: Some(rpc_ports), - }, - ) + TestValidatorGenesis::default() + .ledger_path(ledger_path) + .add_account( + faucet_keypair.pubkey(), + Account::new(faucet_lamports, 0, &system_program::id()), + ) + .rpc_config(JsonRpcConfig { + enable_validator_exit: true, + enable_rpc_transaction_history: true, + faucet_addr, + ..JsonRpcConfig::default() + }) + .rpc_port(rpc_port) + .add_programs_with_path(&programs) + .start_with_mint_address(mint_address) } .unwrap_or_else(|err| { eprintln!("Error: failed to start validator: {}", err); 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 { println_name_value("JSON RPC URL:", &test_validator.rpc_url()); println_name_value( @@ -229,9 +295,12 @@ fn main() { ); println_name_value("Gossip Address:", &test_validator.gossip().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 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)> { let max_slot = rpc_client.get_slot_with_commitment(CommitmentConfig::max())?; @@ -253,7 +322,7 @@ fn main() { progress_bar.set_message(&format!("{}", err)); } } - sleep(Duration::from_millis( + thread::sleep(Duration::from_millis( MS_PER_TICK * DEFAULT_TICKS_PER_SLOT / 2, )); }