diff --git a/book/src/jsonrpc-api.md b/book/src/jsonrpc-api.md index 9510b3571..582b92820 100644 --- a/book/src/jsonrpc-api.md +++ b/book/src/jsonrpc-api.md @@ -26,6 +26,7 @@ Methods * [getBalance](#getbalance) * [getRecentBlockhash](#getrecentblockhash) * [getSignatureStatus](#getsignaturestatus) +* [getNumBlocksSinceSignatureConfirmation](#getnumblockssincesignatureconfirmation) * [getTransactionCount](#gettransactioncount) * [requestAirdrop](#requestairdrop) * [sendTransaction](#sendtransaction) @@ -185,6 +186,27 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, " ``` --- + +### getNumBlocksSinceSignatureConfirmation +Returns the current number of blocks since signature has been confirmed. + +##### Parameters: +* `string` - Signature of Transaction to confirm, as base-58 encoded string + +##### Results: +* `integer` - count, as unsigned 64-bit integer + +##### Example: +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getNumBlocksSinceSignatureConfirmation", "params":["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW"]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":8,"id":1} +``` + +--- + ### getTransactionCount Returns the current Transaction count from the ledger diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 673216cd1..84f1b0f57 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -438,6 +438,78 @@ impl RpcClient { }; } } + + /// Poll the server to confirm a transaction. + pub fn poll_for_signature_confirmation( + &self, + signature: &Signature, + min_confirmed_blocks: usize, + ) -> io::Result<()> { + let mut now = Instant::now(); + let mut confirmed_blocks = 0; + loop { + let response = self.get_num_blocks_since_signature_confirmation(signature); + match response { + Ok(count) => { + if confirmed_blocks != count { + info!( + "signature {} confirmed {} out of {}", + signature, count, min_confirmed_blocks + ); + now = Instant::now(); + confirmed_blocks = count; + } + if count >= min_confirmed_blocks { + break; + } + } + Err(err) => { + debug!("check_confirmations request failed: {:?}", err); + } + }; + if now.elapsed().as_secs() > 15 { + // TODO: Return a better error. + return Err(io::Error::new(io::ErrorKind::Other, "signature not found")); + } + sleep(Duration::from_millis(250)); + } + Ok(()) + } + + pub fn get_num_blocks_since_signature_confirmation( + &self, + sig: &Signature, + ) -> io::Result { + let params = json!([format!("{}", sig)]); + let response = self + .client + .send( + &RpcRequest::GetNumBlocksSinceSignatureConfirmation, + Some(params.clone()), + 1, + ) + .map_err(|error| { + debug!( + "Response get_num_blocks_since_signature_confirmation: {}", + error + ); + io::Error::new( + io::ErrorKind::Other, + "GetNumBlocksSinceSignatureConfirmation request failure", + ) + })?; + serde_json::from_value(response).map_err(|error| { + debug!( + "ParseError: get_num_blocks_since_signature_confirmation: {}", + error + ); + io::Error::new( + io::ErrorKind::Other, + "GetNumBlocksSinceSignatureConfirmation parse failure", + ) + }) + } + pub fn fullnode_exit(&self) -> io::Result { let response = self .client diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index b1c24e900..02442b2be 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -18,6 +18,7 @@ pub enum RpcRequest { GetStorageEntryHeight, GetStoragePubkeysForEntryHeight, FullnodeExit, + GetNumBlocksSinceSignatureConfirmation, } impl RpcRequest { @@ -39,6 +40,9 @@ impl RpcRequest { RpcRequest::GetStorageEntryHeight => "getStorageEntryHeight", RpcRequest::GetStoragePubkeysForEntryHeight => "getStoragePubkeysForEntryHeight", RpcRequest::FullnodeExit => "fullnodeExit", + RpcRequest::GetNumBlocksSinceSignatureConfirmation => { + "getNumBlocksSinceSignatureConfirmation" + } }; let mut request = json!({ "jsonrpc": jsonrpc, diff --git a/client/src/thin_client.rs b/client/src/thin_client.rs index a80d8908c..48d7189cc 100644 --- a/client/src/thin_client.rs +++ b/client/src/thin_client.rs @@ -73,6 +73,36 @@ impl ThinClient { Ok(transaction.signatures[0]) } + /// Retry a sending a signed Transaction to the server for processing. + pub fn retry_transfer_until_confirmed( + &self, + keypair: &Keypair, + transaction: &mut Transaction, + tries: usize, + min_confirmed_blocks: usize, + ) -> io::Result { + for x in 0..tries { + transaction.sign(&[keypair], self.get_recent_blockhash()?); + let mut buf = vec![0; transaction.serialized_size().unwrap() as usize]; + let mut wr = std::io::Cursor::new(&mut buf[..]); + serialize_into(&mut wr, &transaction) + .expect("serialize Transaction in pub fn transfer_signed"); + self.transactions_socket + .send_to(&buf[..], &self.transactions_addr)?; + if self + .poll_for_signature_confirmation(&transaction.signatures[0], min_confirmed_blocks) + .is_ok() + { + return Ok(transaction.signatures[0]); + } + info!("{} tries failed transfer to {}", x, self.transactions_addr); + } + Err(io::Error::new( + io::ErrorKind::Other, + "retry_transfer failed", + )) + } + /// Retry a sending a signed Transaction to the server for processing. pub fn retry_transfer( &self, @@ -140,7 +170,18 @@ impl ThinClient { pub fn poll_for_signature(&self, signature: &Signature) -> io::Result<()> { self.rpc_client.poll_for_signature(signature) } + /// Poll the server until the signature has been confirmed by at least `min_confirmed_blocks` + pub fn poll_for_signature_confirmation( + &self, + signature: &Signature, + min_confirmed_blocks: usize, + ) -> io::Result<()> { + self.rpc_client + .poll_for_signature_confirmation(signature, min_confirmed_blocks) + } + /// Check a signature in the bank. This method blocks + /// until the server sends a response. pub fn check_signature(&self, signature: &Signature) -> bool { self.rpc_client.check_signature(signature) } @@ -148,6 +189,13 @@ impl ThinClient { pub fn fullnode_exit(&self) -> io::Result { self.rpc_client.fullnode_exit() } + pub fn get_num_blocks_since_signature_confirmation( + &mut self, + sig: &Signature, + ) -> io::Result { + self.rpc_client + .get_num_blocks_since_signature_confirmation(sig) + } } pub fn create_client((rpc, tpu): (SocketAddr, SocketAddr), range: (u16, u16)) -> ThinClient { diff --git a/core/src/cluster_tests.rs b/core/src/cluster_tests.rs index 8b362a174..e9153c675 100644 --- a/core/src/cluster_tests.rs +++ b/core/src/cluster_tests.rs @@ -7,11 +7,14 @@ use crate::cluster_info::FULLNODE_PORT_RANGE; use crate::contact_info::ContactInfo; use crate::entry::{Entry, EntrySlice}; use crate::gossip_service::discover; +use crate::locktower::VOTE_THRESHOLD_DEPTH; use solana_client::thin_client::create_client; use solana_sdk::hash::Hash; use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; use solana_sdk::system_transaction::SystemTransaction; -use solana_sdk::timing::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND}; +use solana_sdk::timing::{ + DEFAULT_TICKS_PER_SLOT, NUM_CONSECUTIVE_LEADER_SLOTS, NUM_TICKS_PER_SECOND, +}; use std::io; use std::thread::sleep; use std::time::Duration; @@ -40,12 +43,13 @@ pub fn spend_and_verify_all_nodes( client.get_recent_blockhash().unwrap(), 0, ); + let confs = VOTE_THRESHOLD_DEPTH + 1; let sig = client - .retry_transfer(&funding_keypair, &mut transaction, 5) + .retry_transfer_until_confirmed(&funding_keypair, &mut transaction, 5, confs) .unwrap(); for validator in &cluster_nodes { let client = create_client(validator.client_facing_addr(), FULLNODE_PORT_RANGE); - client.poll_for_signature(&sig).unwrap(); + client.poll_for_signature_confirmation(&sig, confs).unwrap(); } } } @@ -127,14 +131,18 @@ pub fn kill_entry_and_spend_and_verify_rest( let cluster_nodes = discover(&entry_point_info.gossip, nodes).unwrap(); assert!(cluster_nodes.len() >= nodes); let client = create_client(entry_point_info.client_facing_addr(), FULLNODE_PORT_RANGE); - info!("sleeping for an epoch"); - sleep(Duration::from_millis(SLOT_MILLIS * DEFAULT_SLOTS_PER_EPOCH)); - info!("done sleeping for an epoch"); + info!("sleeping for 2 leader fortnights"); + sleep(Duration::from_millis( + SLOT_MILLIS * NUM_CONSECUTIVE_LEADER_SLOTS * 2, + )); + info!("done sleeping for 2 fortnights"); info!("killing entry point"); assert!(client.fullnode_exit().unwrap()); - info!("sleeping for a slot"); - sleep(Duration::from_millis(SLOT_MILLIS)); - info!("done sleeping for a slot"); + info!("sleeping for 2 leader fortnights"); + sleep(Duration::from_millis( + SLOT_MILLIS * NUM_CONSECUTIVE_LEADER_SLOTS, + )); + info!("done sleeping for 2 fortnights"); for ingress_node in &cluster_nodes { if ingress_node.id == entry_point_info.id { continue; @@ -163,8 +171,15 @@ pub fn kill_entry_and_spend_and_verify_rest( 0, ); + let confs = VOTE_THRESHOLD_DEPTH + 1; let sig = { - match client.retry_transfer(&funding_keypair, &mut transaction, 5) { + let sig = client.retry_transfer_until_confirmed( + &funding_keypair, + &mut transaction, + 5, + confs, + ); + match sig { Err(e) => { result = Err(e); continue; @@ -174,7 +189,7 @@ pub fn kill_entry_and_spend_and_verify_rest( } }; - match poll_all_nodes_for_signature(&entry_point_info, &cluster_nodes, &sig) { + match poll_all_nodes_for_signature(&entry_point_info, &cluster_nodes, &sig, confs) { Err(e) => { result = Err(e); } @@ -190,13 +205,14 @@ fn poll_all_nodes_for_signature( entry_point_info: &ContactInfo, cluster_nodes: &[ContactInfo], sig: &Signature, + confs: usize, ) -> io::Result<()> { for validator in cluster_nodes { if validator.id == entry_point_info.id { continue; } let client = create_client(validator.client_facing_addr(), FULLNODE_PORT_RANGE); - client.poll_for_signature(&sig)?; + client.poll_for_signature_confirmation(&sig, confs)?; } Ok(()) diff --git a/core/src/leader_schedule_utils.rs b/core/src/leader_schedule_utils.rs index 4fac35d60..925a94e59 100644 --- a/core/src/leader_schedule_utils.rs +++ b/core/src/leader_schedule_utils.rs @@ -44,10 +44,9 @@ pub fn slot_leader_at(slot: u64, bank: &Bank) -> Option { } /// Return the next slot after the given current_slot that the given node will be leader -pub fn next_leader_slot(pubkey: &Pubkey, current_slot: u64, bank: &Bank) -> Option { - let (epoch, slot_index) = bank.get_epoch_and_slot_index(current_slot + 1); - - if let Some(leader_schedule) = leader_schedule(epoch, bank) { +pub fn next_leader_slot(pubkey: &Pubkey, mut current_slot: u64, bank: &Bank) -> Option { + let (mut epoch, mut start_index) = bank.get_epoch_and_slot_index(current_slot + 1); + while let Some(leader_schedule) = leader_schedule(epoch, bank) { // clippy thinks I should do this: // for (i, ) in leader_schedule // .iter() @@ -57,11 +56,15 @@ pub fn next_leader_slot(pubkey: &Pubkey, current_slot: u64, bank: &Bank) -> Opti // // but leader_schedule doesn't implement Iter... #[allow(clippy::needless_range_loop)] - for i in slot_index..bank.get_slots_in_epoch(epoch) { + for i in start_index..bank.get_slots_in_epoch(epoch) { + current_slot += 1; if *pubkey == leader_schedule[i] { - return Some(current_slot + 1 + (i - slot_index) as u64); + return Some(current_slot); } } + + epoch += 1; + start_index = 0; } None } @@ -80,8 +83,10 @@ pub fn tick_height_to_slot(ticks_per_slot: u64, tick_height: u64) -> u64 { mod tests { use super::*; use crate::staking_utils; + use crate::voting_keypair::tests::new_vote_account_with_delegate; use solana_sdk::genesis_block::{GenesisBlock, BOOTSTRAP_LEADER_LAMPORTS}; use solana_sdk::signature::{Keypair, KeypairUtil}; + use std::sync::Arc; #[test] fn test_next_leader_slot() { @@ -117,6 +122,58 @@ mod tests { ); } + #[test] + fn test_next_leader_slot_next_epoch() { + let pubkey = Keypair::new().pubkey(); + let (mut genesis_block, mint_keypair) = GenesisBlock::new_with_leader( + 2 * BOOTSTRAP_LEADER_LAMPORTS, + &pubkey, + BOOTSTRAP_LEADER_LAMPORTS, + ); + genesis_block.epoch_warmup = false; + + let bank = Bank::new(&genesis_block); + let delegate_id = Keypair::new().pubkey(); + + // Create new vote account + let new_voting_keypair = Keypair::new(); + new_vote_account_with_delegate( + &mint_keypair, + &new_voting_keypair, + &delegate_id, + &bank, + BOOTSTRAP_LEADER_LAMPORTS, + ); + + // Have to wait until the epoch at after the epoch stakes generated at genesis + // for the new votes to take effect. + let mut target_slot = 1; + let epoch = bank.get_stakers_epoch(0); + while bank.get_stakers_epoch(target_slot) == epoch { + target_slot += 1; + } + + let bank = Bank::new_from_parent(&Arc::new(bank), &Pubkey::default(), target_slot); + let mut expected_slot = 0; + let epoch = bank.get_stakers_epoch(target_slot); + for i in 0..epoch { + expected_slot += bank.get_slots_in_epoch(i); + } + + let schedule = leader_schedule(epoch, &bank).unwrap(); + let mut index = 0; + while schedule[index] != delegate_id { + index += 1 + } + + expected_slot += index; + + assert_eq!( + next_leader_slot(&delegate_id, 0, &bank), + Some(expected_slot) + ); + } + #[test] fn test_leader_schedule_via_bank() { let pubkey = Keypair::new().pubkey(); diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 933fc3fde..8e4620d6d 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -162,7 +162,7 @@ impl ReplayStage { .filter(|(b, stake_lockouts)| { let vote_threshold = locktower.check_vote_stake_threshold(b.slot(), &stake_lockouts); - trace!("bank vote_threshold: {} {}", b.slot(), vote_threshold); + debug!("bank vote_threshold: {} {}", b.slot(), vote_threshold); vote_threshold }) .map(|(b, stake_lockouts)| { diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 446f31fef..0ed595567 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -80,7 +80,20 @@ impl JsonRpcRequestProcessor { } pub fn get_signature_status(&self, signature: Signature) -> Option> { - self.bank().get_signature_status(&signature) + self.get_signature_confirmation_status(signature) + .map(|x| x.1) + } + + pub fn get_signature_confirmations(&self, signature: Signature) -> Option { + self.get_signature_confirmation_status(signature) + .map(|x| x.0) + } + + pub fn get_signature_confirmation_status( + &self, + signature: Signature, + ) -> Option<(usize, bank::Result<()>)> { + self.bank().get_signature_confirmation_status(&signature) } fn get_transaction_count(&self) -> Result { @@ -202,6 +215,20 @@ pub trait RpcSol { #[rpc(meta, name = "fullnodeExit")] fn fullnode_exit(&self, _: Self::Metadata) -> Result; + + #[rpc(meta, name = "getNumBlocksSinceSignatureConfirmation")] + fn get_num_blocks_since_signature_confirmation( + &self, + _: Self::Metadata, + _: String, + ) -> Result; + + #[rpc(meta, name = "getSignatureConfirmation")] + fn get_signature_confirmation( + &self, + _: Self::Metadata, + _: String, + ) -> Result<(usize, RpcSignatureStatus)>; } pub struct RpcSolImpl; @@ -239,19 +266,33 @@ impl RpcSol for RpcSolImpl { } fn get_signature_status(&self, meta: Self::Metadata, id: String) -> Result { - info!("get_signature_status rpc request received: {:?}", id); + self.get_signature_confirmation(meta, id).map(|x| x.1) + } + + fn get_num_blocks_since_signature_confirmation( + &self, + meta: Self::Metadata, + id: String, + ) -> Result { + self.get_signature_confirmation(meta, id).map(|x| x.0) + } + + fn get_signature_confirmation( + &self, + meta: Self::Metadata, + id: String, + ) -> Result<(usize, RpcSignatureStatus)> { + info!("get_signature_confirmation rpc request received: {:?}", id); let signature = verify_signature(&id)?; let res = meta .request_processor .read() .unwrap() - .get_signature_status(signature); + .get_signature_confirmation_status(signature); let status = { - if res.is_none() { - RpcSignatureStatus::SignatureNotFound - } else { - match res.unwrap() { + if let Some((count, res)) = res { + let res = match res { Ok(_) => RpcSignatureStatus::Confirmed, Err(TransactionError::AccountInUse) => RpcSignatureStatus::AccountInUse, Err(TransactionError::AccountLoadedTwice) => { @@ -264,10 +305,16 @@ impl RpcSol for RpcSolImpl { trace!("mapping {:?} to GenericFailure", err); RpcSignatureStatus::GenericFailure } - } + }; + (count, res) + } else { + (0, RpcSignatureStatus::SignatureNotFound) } }; - info!("get_signature_status rpc request status: {:?}", status); + info!( + "get_signature_confirmation rpc request status: {:?}", + status + ); Ok(status) } diff --git a/core/tests/local_cluster.rs b/core/tests/local_cluster.rs index 9917b7c01..44a345cdc 100644 --- a/core/tests/local_cluster.rs +++ b/core/tests/local_cluster.rs @@ -21,7 +21,6 @@ fn test_spend_and_verify_all_nodes_1() { } #[test] -#[ignore] //TODO: confirmations are not useful: #3346 fn test_spend_and_verify_all_nodes_2() { solana_logger::setup(); let num_nodes = 2; @@ -34,7 +33,6 @@ fn test_spend_and_verify_all_nodes_2() { } #[test] -#[ignore] //TODO: confirmations are not useful: #3346 fn test_spend_and_verify_all_nodes_3() { solana_logger::setup(); let num_nodes = 3; @@ -65,34 +63,20 @@ fn test_fullnode_exit_2() { cluster_tests::fullnode_exit(&local.entry_point_info, num_nodes); } +// Cluster needs a supermajority to remain, so the minimum size for this test is 4 #[test] -#[ignore] -fn test_leader_failure_2() { - let num_nodes = 2; +fn test_leader_failure_4() { + solana_logger::setup(); + let num_nodes = 4; let mut fullnode_config = FullnodeConfig::default(); fullnode_config.rpc_config.enable_fullnode_exit = true; - let local = LocalCluster::new_with_config(&[100; 2], 10_000, &fullnode_config); + let local = LocalCluster::new_with_config(&[100; 4], 10_000, &fullnode_config); cluster_tests::kill_entry_and_spend_and_verify_rest( &local.entry_point_info, &local.funding_keypair, num_nodes, ); } - -#[test] -#[ignore] -fn test_leader_failure_3() { - let num_nodes = 3; - let mut fullnode_config = FullnodeConfig::default(); - fullnode_config.rpc_config.enable_fullnode_exit = true; - let local = LocalCluster::new_with_config(&[100; 3], 10_000, &fullnode_config); - cluster_tests::kill_entry_and_spend_and_verify_rest( - &local.entry_point_info, - &local.funding_keypair, - num_nodes, - ); -} - #[test] fn test_two_unbalanced_stakes() { let mut fullnode_config = FullnodeConfig::default(); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index a6bda8e60..9d38b757f 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -790,13 +790,21 @@ impl Bank { self.accounts.transaction_count(self.accounts_id) } - pub fn get_signature_status(&self, signature: &Signature) -> Option> { + pub fn get_signature_confirmation_status( + &self, + signature: &Signature, + ) -> Option<(usize, Result<()>)> { let parents = self.parents(); let mut caches = vec![self.status_cache.read().unwrap()]; caches.extend(parents.iter().map(|b| b.status_cache.read().unwrap())); StatusCache::get_signature_status_all(&caches, signature) } + pub fn get_signature_status(&self, signature: &Signature) -> Option> { + self.get_signature_confirmation_status(signature) + .map(|v| v.1) + } + pub fn has_signature(&self, signature: &Signature) -> bool { let parents = self.parents(); let mut caches = vec![self.status_cache.read().unwrap()]; diff --git a/runtime/src/status_cache.rs b/runtime/src/status_cache.rs index c96fa0310..45752f6b6 100644 --- a/runtime/src/status_cache.rs +++ b/runtime/src/status_cache.rs @@ -192,13 +192,13 @@ impl StatusCache { pub fn get_signature_status_all( checkpoints: &[U], signature: &Signature, - ) -> Option> + ) -> Option<(usize, Result<(), T>)> where U: Deref, { - for c in checkpoints { + for (i, c) in checkpoints.iter().enumerate() { if let Some(status) = c.get_signature_status(signature) { - return Some(status); + return Some((i, status)); } } None @@ -257,7 +257,7 @@ mod tests { let checkpoints = [&second, &first]; assert_eq!( BankStatusCache::get_signature_status_all(&checkpoints, &sig), - Some(Ok(())), + Some((1, Ok(()))), ); assert!(StatusCache::has_signature_all(&checkpoints, &sig)); }