From acf64f84766835ac47745c4ef9a0506409336e2d Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 6 Apr 2020 04:04:54 -0600 Subject: [PATCH] Update getSignatureStatuses to return historical statuses (#9314) automerge --- client/src/mock_rpc_client_request.rs | 2 +- client/src/rpc_client.rs | 20 ++-- core/src/rpc.rs | 55 +++++++-- docs/src/apps/jsonrpc-api.md | 14 ++- ledger/src/blockstore.rs | 156 ++++++++++++++++++++++++++ transaction-status/src/lib.rs | 36 ++++++ 6 files changed, 257 insertions(+), 26 deletions(-) diff --git a/client/src/mock_rpc_client_request.rs b/client/src/mock_rpc_client_request.rs index f8a0e28ca..69866fbe0 100644 --- a/client/src/mock_rpc_client_request.rs +++ b/client/src/mock_rpc_client_request.rs @@ -94,7 +94,7 @@ impl GenericRpcClientRequest for MockRpcClientRequest { Some(TransactionStatus { status, slot: 1, - confirmations: Some(0), + confirmations: None, err, }) }; diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index c083dd9a8..a1080d166 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -77,13 +77,13 @@ impl RpcClient { signature: &Signature, commitment_config: CommitmentConfig, ) -> RpcResult { - let Response { context, value } = - self.get_signature_statuses_with_commitment(&[*signature], commitment_config)?; + let Response { context, value } = self.get_signature_statuses(&[*signature])?; Ok(Response { context, value: value[0] .as_ref() + .filter(|result| result.satisfies_commitment(commitment_config)) .map(|result| result.status.is_ok()) .unwrap_or_default(), }) @@ -112,17 +112,14 @@ impl RpcClient { self.get_signature_status_with_commitment(signature, CommitmentConfig::default()) } - pub fn get_signature_statuses_with_commitment( + pub fn get_signature_statuses( &self, signatures: &[Signature], - commitment_config: CommitmentConfig, ) -> RpcResult>> { let signatures: Vec<_> = signatures.iter().map(|s| s.to_string()).collect(); - let signature_status = self.client.send( - &RpcRequest::GetSignatureStatuses, - json!([&signatures, commitment_config]), - 5, - )?; + let signature_status = + self.client + .send(&RpcRequest::GetSignatureStatuses, json!([signatures]), 5)?; Ok(serde_json::from_value(signature_status) .map_err(|err| ClientError::new_with_command(err.into(), "GetSignatureStatuses"))?) } @@ -134,7 +131,7 @@ impl RpcClient { ) -> ClientResult>> { let signature_status = self.client.send( &RpcRequest::GetSignatureStatuses, - json!([[signature.to_string()], commitment_config]), + json!([[signature.to_string()]]), 5, )?; let result: Response>> = @@ -142,6 +139,7 @@ impl RpcClient { .map_err(|err| ClientError::new_with_command(err.into(), "GetSignatureStatuses"))?; Ok(result.value[0] .clone() + .filter(|result| result.satisfies_commitment(commitment_config)) .map(|status_meta| status_meta.status)) } @@ -957,7 +955,7 @@ impl RpcClient { .client .send( &RpcRequest::GetSignatureStatuses, - json!([[signature.to_string()], CommitmentConfig::recent().ok()]), + json!([[signature.to_string()]]), 1, ) .map_err(|err| err.into_with_command("GetSignatureStatuses"))?; diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 806fd5d18..7dc8888e2 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -36,6 +36,8 @@ use std::{ time::{Duration, Instant}, }; +const MAX_QUERY_ITEMS: usize = 256; + type RpcResponse = Result>; fn new_response(bank: &Bank, value: T) -> RpcResponse { @@ -52,6 +54,12 @@ pub struct JsonRpcConfig { pub faucet_addr: Option, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcSignatureStatusConfig { + pub search_transaction_history: bool, +} + #[derive(Clone)] pub struct JsonRpcRequestProcessor { bank_forks: Arc>, @@ -425,17 +433,37 @@ impl JsonRpcRequestProcessor { .map(|(_, status)| status) } - pub fn get_signature_statuses_with_commitment( + pub fn get_signature_statuses( &self, signatures: Vec, - commitment: Option, + config: Option, ) -> RpcResponse>> { let mut statuses: Vec> = vec![]; - let bank = self.bank(commitment); + let search_transaction_history = config + .map(|x| x.search_transaction_history) + .unwrap_or(false); + let bank = self.bank(Some(CommitmentConfig::recent())); for signature in signatures { - let status = self.get_transaction_status(signature, &bank); + let status = if let Some(status) = self.get_transaction_status(signature, &bank) { + Some(status) + } else if self.config.enable_rpc_transaction_history && search_transaction_history { + self.blockstore + .get_transaction_status(signature) + .map_err(|_| Error::internal_error())? + .map(|(slot, status_meta)| { + let err = status_meta.status.clone().err(); + TransactionStatus { + slot, + status: status_meta.status, + confirmations: None, + err, + } + }) + } else { + None + }; statuses.push(status); } Ok(Response { @@ -611,11 +639,11 @@ pub trait RpcSol { fn get_fee_rate_governor(&self, meta: Self::Metadata) -> RpcResponse; #[rpc(meta, name = "getSignatureStatuses")] - fn get_signature_statuses_with_commitment( + fn get_signature_statuses( &self, meta: Self::Metadata, signature_strs: Vec, - commitment: Option, + config: Option, ) -> RpcResponse>>; #[rpc(meta, name = "getSlot")] @@ -973,12 +1001,18 @@ impl RpcSol for RpcSolImpl { .get_signature_status(signature, commitment)) } - fn get_signature_statuses_with_commitment( + fn get_signature_statuses( &self, meta: Self::Metadata, signature_strs: Vec, - commitment: Option, + config: Option, ) -> RpcResponse>> { + if signature_strs.len() > MAX_QUERY_ITEMS { + return Err(Error::invalid_params(format!( + "Too many inputs provided; max {}", + MAX_QUERY_ITEMS + ))); + } let mut signatures: Vec = vec![]; for signature_str in signature_strs { signatures.push(verify_signature(&signature_str)?); @@ -986,7 +1020,7 @@ impl RpcSol for RpcSolImpl { meta.request_processor .read() .unwrap() - .get_signature_statuses_with_commitment(signatures, commitment) + .get_signature_statuses(signatures, config) } fn get_slot(&self, meta: Self::Metadata, commitment: Option) -> Result { @@ -1079,9 +1113,10 @@ impl RpcSol for RpcSolImpl { .request_processor .read() .unwrap() - .get_signature_statuses_with_commitment(vec![signature], commitment.clone())? + .get_signature_statuses(vec![signature], None)? .value[0] .clone() + .filter(|result| result.satisfies_commitment(commitment.unwrap_or_default())) .map(|x| x.status); if signature_status == Some(Ok(())) { diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 2b5e7b322..90e5291c6 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -633,13 +633,16 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m ### getSignatureStatuses -Returns the statuses of a list of signatures. This method only searches the recent status cache of signatures, which retains statuses for all active slots plus `MAX_RECENT_BLOCKHASHES` rooted slots. +Returns the statuses of a list of signatures. Unless the +`searchTransactionHistory` configuration parameter is included, this method only +searches the recent status cache of signatures, which retains statuses for all +active slots plus `MAX_RECENT_BLOCKHASHES` rooted slots. #### Parameters: * `` - An array of transaction signatures to confirm, as base-58 encoded strings -* `` - (optional) Extended Rpc configuration, containing the following optional fields: - * `commitment: ` - [Commitment](jsonrpc-api.md#configuring-state-commitment) +* `` - (optional) Configuration object containing the following field: + * `searchTransactionHistory: ` - if true, a Solana node will search its ledger cache for any signatures not found in the recent status cache #### Results: @@ -662,7 +665,10 @@ An array of: ```bash // Request -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getSignatureStatuses", "params":[["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW", "5j7s6NiJS3JAkvgkoc18WVAsiSaci2pxB2A6ueCJP4tprA2TFg9wSyTLeYouxPBJEMzJinENTkpA52YStRW5Dia7"]]]}' http://localhost:8899 +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getSignatureStatuses", "params":[["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW", "5j7s6NiJS3JAkvgkoc18WVAsiSaci2pxB2A6ueCJP4tprA2TFg9wSyTLeYouxPBJEMzJinENTkpA52YStRW5Dia7"]]}' http://localhost:8899 + +// Request with configuration +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getSignatureStatuses", "params":[["5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW"], {"searchTransactionHistory": true}]}' http://localhost:8899 // Result {"jsonrpc":"2.0","result":{"context":{"slot":82},"value":[{"slot": 72, "confirmations": 10, "err": null, "status": {"Ok": null}}, null]},"id":1} diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index a0d33f65a..9f05432e9 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -1644,6 +1644,41 @@ impl Blockstore { self.transaction_status_cf.put(index, status) } + // Returns a transaction status if it was processed in a root, as well as a loop counter for + // unit testing + fn get_transaction_status_with_counter( + &self, + signature: Signature, + ) -> Result<(Option<(Slot, TransactionStatusMeta)>, u64)> { + let mut counter = 0; + for transaction_status_cf_primary_index in 0..=1 { + let index_iterator = self.transaction_status_cf.iter(IteratorMode::From( + (transaction_status_cf_primary_index, signature, 0), + IteratorDirection::Forward, + ))?; + for ((_, sig, slot), data) in index_iterator { + counter += 1; + if sig != signature { + break; + } + if self.is_root(slot) { + let status: TransactionStatusMeta = deserialize(&data)?; + return Ok((Some((slot, status)), counter)); + } + } + } + Ok((None, counter)) + } + + /// Returns a transaction status if it was processed in a root + pub fn get_transaction_status( + &self, + signature: Signature, + ) -> Result> { + self.get_transaction_status_with_counter(signature) + .map(|(status, _)| status) + } + pub fn read_rewards(&self, index: Slot) -> Result> { self.rewards_cf.get(index) } @@ -5596,6 +5631,127 @@ pub mod tests { Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); } + #[test] + fn test_get_transaction_status() { + let blockstore_path = get_tmp_ledger_path!(); + { + let blockstore = Blockstore::open(&blockstore_path).unwrap(); + let transaction_status_cf = blockstore.db.column::(); + + let pre_balances_vec = vec![1, 2, 3]; + let post_balances_vec = vec![3, 2, 1]; + let status = TransactionStatusMeta { + status: solana_sdk::transaction::Result::<()>::Ok(()), + fee: 42u64, + pre_balances: pre_balances_vec.clone(), + post_balances: post_balances_vec.clone(), + }; + + let signature1 = Signature::new(&[1u8; 64]); + let signature2 = Signature::new(&[2u8; 64]); + let signature3 = Signature::new(&[3u8; 64]); + let signature4 = Signature::new(&[4u8; 64]); + let signature5 = Signature::new(&[5u8; 64]); + let signature6 = Signature::new(&[6u8; 64]); + + // Initialize index 0, including: + // signature2 in non-root and root, + // signature4 in 2 non-roots, + // extra entries + transaction_status_cf + .put((0, signature2.clone(), 1), &status) + .unwrap(); + + transaction_status_cf + .put((0, signature2.clone(), 2), &status) + .unwrap(); + + transaction_status_cf + .put((0, signature4.clone(), 0), &status) + .unwrap(); + + transaction_status_cf + .put((0, signature4.clone(), 1), &status) + .unwrap(); + + transaction_status_cf + .put((0, signature5.clone(), 0), &status) + .unwrap(); + + transaction_status_cf + .put((0, signature5.clone(), 1), &status) + .unwrap(); + + // Initialize index 1, including: + // signature4 in non-root and root, + // extra entries + transaction_status_cf + .put((1, signature4.clone(), 1), &status) + .unwrap(); + + transaction_status_cf + .put((1, signature4.clone(), 2), &status) + .unwrap(); + + transaction_status_cf + .put((1, signature5.clone(), 0), &status) + .unwrap(); + + transaction_status_cf + .put((1, signature5.clone(), 1), &status) + .unwrap(); + + blockstore.set_roots(&[2]).unwrap(); + + // Signature exists, root found in index 0 + if let (Some((slot, _status)), counter) = blockstore + .get_transaction_status_with_counter(signature2) + .unwrap() + { + assert_eq!(slot, 2); + assert_eq!(counter, 2); + } + + // Signature exists, root found in index 1 + if let (Some((slot, _status)), counter) = blockstore + .get_transaction_status_with_counter(signature4) + .unwrap() + { + assert_eq!(slot, 2); + assert_eq!(counter, 5); + } + + // Signature exists, no root found + let (status, counter) = blockstore + .get_transaction_status_with_counter(signature5) + .unwrap(); + assert_eq!(status, None); + assert_eq!(counter, 5); + + // Signature does not exist, smaller than existing entries + let (status, counter) = blockstore + .get_transaction_status_with_counter(signature1) + .unwrap(); + assert_eq!(status, None); + assert_eq!(counter, 2); + + // Signature does not exist, between existing entries + let (status, counter) = blockstore + .get_transaction_status_with_counter(signature3) + .unwrap(); + assert_eq!(status, None); + assert_eq!(counter, 2); + + // Signature does not exist, larger than existing entries + let (status, counter) = blockstore + .get_transaction_status_with_counter(signature6) + .unwrap(); + assert_eq!(status, None); + assert_eq!(counter, 1); + } + Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); + } + #[test] fn test_get_last_hash() { let mut entries: Vec = vec![]; diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index dc19bf2e0..ce5c1ea32 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -4,6 +4,7 @@ extern crate serde_derive; use bincode; use solana_sdk::{ clock::Slot, + commitment_config::CommitmentConfig, message::MessageHeader, transaction::{Result, Transaction, TransactionError}, }; @@ -68,6 +69,13 @@ pub struct TransactionStatus { pub err: Option, } +impl TransactionStatus { + pub fn satisfies_commitment(&self, commitment_config: CommitmentConfig) -> bool { + (commitment_config == CommitmentConfig::default() && self.confirmations.is_none()) + || commitment_config == CommitmentConfig::recent() + } +} + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Reward { pub pubkey: String, @@ -171,3 +179,31 @@ impl EncodedTransaction { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_satisfies_commitment() { + let status = TransactionStatus { + slot: 0, + confirmations: None, + status: Ok(()), + err: None, + }; + + assert!(status.satisfies_commitment(CommitmentConfig::default())); + assert!(status.satisfies_commitment(CommitmentConfig::recent())); + + let status = TransactionStatus { + slot: 0, + confirmations: Some(10), + status: Ok(()), + err: None, + }; + + assert!(!status.satisfies_commitment(CommitmentConfig::default())); + assert!(status.satisfies_commitment(CommitmentConfig::recent())); + } +}