diff --git a/banks-interface/src/lib.rs b/banks-interface/src/lib.rs index 46640e91e5..5c03e97382 100644 --- a/banks-interface/src/lib.rs +++ b/banks-interface/src/lib.rs @@ -10,11 +10,19 @@ use solana_sdk::{ transaction::{self, Transaction, TransactionError}, }; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum TransactionConfirmationStatus { + Processed, + Confirmed, + Finalized, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct TransactionStatus { pub slot: Slot, pub confirmations: Option, // None = rooted pub err: Option, + pub confirmation_status: Option, } #[tarpc::service] diff --git a/banks-server/src/banks_server.rs b/banks-server/src/banks_server.rs index 65d0f085d9..21cbb22015 100644 --- a/banks-server/src/banks_server.rs +++ b/banks-server/src/banks_server.rs @@ -4,7 +4,9 @@ use futures::{ future, prelude::stream::{self, StreamExt}, }; -use solana_banks_interface::{Banks, BanksRequest, BanksResponse, TransactionStatus}; +use solana_banks_interface::{ + Banks, BanksRequest, BanksResponse, TransactionConfirmationStatus, TransactionStatus, +}; use solana_runtime::{bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache}; use solana_sdk::{ account::Account, @@ -169,6 +171,10 @@ impl Banks for BanksServer { let (slot, status) = bank.get_signature_status_slot(&signature)?; let r_block_commitment_cache = self.block_commitment_cache.read().unwrap(); + let optimistically_confirmed_bank = self.bank(CommitmentLevel::SingleGossip); + let optimistically_confirmed = + optimistically_confirmed_bank.get_signature_status_slot(&signature); + let confirmations = if r_block_commitment_cache.root() >= slot && r_block_commitment_cache.highest_confirmed_root() >= slot { @@ -182,6 +188,13 @@ impl Banks for BanksServer { slot, confirmations, err: status.err(), + confirmation_status: if confirmations.is_none() { + Some(TransactionConfirmationStatus::Finalized) + } else if optimistically_confirmed.is_some() { + Some(TransactionConfirmationStatus::Confirmed) + } else { + Some(TransactionConfirmationStatus::Processed) + }, }) } diff --git a/cli/src/program.rs b/cli/src/program.rs index fd304acd55..10ea6c13d8 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -41,6 +41,7 @@ use solana_sdk::{ system_program, transaction::Transaction, }; +use solana_transaction_status::TransactionConfirmationStatus; use std::{ cmp::min, collections::HashMap, error, fs::File, io::Read, net::UdpSocket, path::PathBuf, sync::Arc, thread::sleep, time::Duration, @@ -1493,7 +1494,11 @@ fn send_and_confirm_transactions_with_spinner( for (signature, status) in pending_signatures.into_iter().zip(statuses.into_iter()) { if let Some(status) = status { - if status.confirmations.is_none() || status.confirmations.unwrap() > 1 { + if let Some(confirmation_status) = &status.confirmation_status { + if *confirmation_status != TransactionConfirmationStatus::Processed { + let _ = pending_transactions.remove(&signature); + } + } else if status.confirmations.is_none() || status.confirmations.unwrap() > 1 { let _ = pending_transactions.remove(&signature); } } diff --git a/client/src/mock_sender.rs b/client/src/mock_sender.rs index 73cd654f65..da84ec88c3 100644 --- a/client/src/mock_sender.rs +++ b/client/src/mock_sender.rs @@ -12,7 +12,7 @@ use solana_sdk::{ signature::Signature, transaction::{self, Transaction, TransactionError}, }; -use solana_transaction_status::TransactionStatus; +use solana_transaction_status::{TransactionConfirmationStatus, TransactionStatus}; use solana_version::Version; use std::{collections::HashMap, sync::RwLock}; @@ -106,6 +106,7 @@ impl RpcSender for MockSender { slot: 1, confirmations: None, err, + confirmation_status: Some(TransactionConfirmationStatus::Finalized), }) }; let statuses: Vec> = params.as_array().unwrap()[0] diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index dd7d0e9304..4795d12d21 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -40,6 +40,7 @@ use solana_transaction_status::{ }; use solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY; use std::{ + cmp::min, net::SocketAddr, sync::RwLock, thread::sleep, @@ -1364,29 +1365,20 @@ impl RpcClient { } let now = Instant::now(); loop { - match commitment.commitment { - CommitmentLevel::Max | CommitmentLevel::Root => - // Return when default (max) commitment is reached - // Failed transactions have already been eliminated, `is_some` check is sufficient - { - if self.get_signature_status(&signature)?.is_some() { - progress_bar.set_message("Transaction confirmed"); - progress_bar.finish_and_clear(); - return Ok(signature); - } - } - _ => { - // Return when one confirmation has been reached - if confirmations >= desired_confirmations { - progress_bar.set_message("Transaction reached commitment"); - progress_bar.finish_and_clear(); - return Ok(signature); - } - } + // Return when specified commitment is reached + // Failed transactions have already been eliminated, `is_some` check is sufficient + if self + .get_signature_status_with_commitment(&signature, commitment)? + .is_some() + { + progress_bar.set_message("Transaction confirmed"); + progress_bar.finish_and_clear(); + return Ok(signature); } + progress_bar.set_message(&format!( "[{}/{}] Finalizing transaction {}", - confirmations + 1, + min(confirmations + 1, desired_confirmations), desired_confirmations, signature, )); diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 264ee169ba..89e0d1cb49 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -62,7 +62,8 @@ use solana_sdk::{ }; use solana_stake_program::stake_state::StakeState; use solana_transaction_status::{ - EncodedConfirmedBlock, EncodedConfirmedTransaction, TransactionStatus, UiTransactionEncoding, + EncodedConfirmedBlock, EncodedConfirmedTransaction, TransactionConfirmationStatus, + TransactionStatus, UiTransactionEncoding, }; use solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}; use spl_token_v2_0::{ @@ -858,6 +859,7 @@ impl JsonRpcRequestProcessor { status: status_meta.status, confirmations: None, err, + confirmation_status: Some(TransactionConfirmationStatus::Finalized), } }) .or_else(|| { @@ -886,6 +888,10 @@ impl JsonRpcRequestProcessor { let (slot, status) = bank.get_signature_status_slot(&signature)?; let r_block_commitment_cache = self.block_commitment_cache.read().unwrap(); + let optimistically_confirmed_bank = self.bank(Some(CommitmentConfig::single_gossip())); + let optimistically_confirmed = + optimistically_confirmed_bank.get_signature_status_slot(&signature); + let confirmations = if r_block_commitment_cache.root() >= slot && is_confirmed_rooted(&r_block_commitment_cache, bank, &self.blockstore, slot) { @@ -901,6 +907,13 @@ impl JsonRpcRequestProcessor { status, confirmations, err, + confirmation_status: if confirmations.is_none() { + Some(TransactionConfirmationStatus::Finalized) + } else if optimistically_confirmed.is_some() { + Some(TransactionConfirmationStatus::Confirmed) + } else { + Some(TransactionConfirmationStatus::Processed) + }, }) } diff --git a/docs/src/developing/clients/jsonrpc-api.md b/docs/src/developing/clients/jsonrpc-api.md index 82dad92738..c7ff115e44 100644 --- a/docs/src/developing/clients/jsonrpc-api.md +++ b/docs/src/developing/clients/jsonrpc-api.md @@ -1945,6 +1945,7 @@ An array of: - `slot: ` - The slot the transaction was processed - `confirmations: ` - Number of blocks since signature confirmation, null if rooted, as well as finalized by a supermajority of the cluster - `err: ` - Error if transaction failed, null if transaction succeeded. [TransactionError definitions](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction.rs#L24) + - `confirmationStatus: ` - The transaction's cluster confirmation status; either `processed`, `confirmed`, or `finalized`. See [Commitment](jsonrpc-api.md#configuring-state-commitment) for more on optimistic confirmation. - DEPRECATED: `status: ` - Transaction status - `"Ok": ` - Transaction was successful - `"Err": ` - Transaction failed with TransactionError @@ -1983,7 +1984,8 @@ Result: "err": null, "status": { "Ok": null - } + }, + "confirmationStatus": "confirmed", }, null ] @@ -2027,7 +2029,8 @@ Result: "err": null, "status": { "Ok": null - } + }, + "confirmationStatus": "finalized", }, null ] diff --git a/storage-bigtable/src/lib.rs b/storage-bigtable/src/lib.rs index 2ca00e8a86..339624a7ef 100644 --- a/storage-bigtable/src/lib.rs +++ b/storage-bigtable/src/lib.rs @@ -10,7 +10,8 @@ use solana_sdk::{ use solana_storage_proto::convert::generated; use solana_transaction_status::{ ConfirmedBlock, ConfirmedTransaction, ConfirmedTransactionStatusWithSignature, Reward, - TransactionStatus, TransactionStatusMeta, TransactionWithStatusMeta, + TransactionConfirmationStatus, TransactionStatus, TransactionStatusMeta, + TransactionWithStatusMeta, }; use std::{collections::HashMap, convert::TryInto}; use thiserror::Error; @@ -255,6 +256,7 @@ impl From for TransactionStatus { confirmations: None, status, err, + confirmation_status: Some(TransactionConfirmationStatus::Finalized), } } } diff --git a/tokens/src/commands.rs b/tokens/src/commands.rs index 8a9b65145c..1b5301ebe5 100644 --- a/tokens/src/commands.rs +++ b/tokens/src/commands.rs @@ -1042,6 +1042,7 @@ mod tests { signature::{read_keypair_file, write_keypair_file}, }; use solana_stake_program::stake_instruction::StakeInstruction; + use solana_transaction_status::TransactionConfirmationStatus; use std::fs::remove_dir_all; // This is a quick hack until TestValidator can be initialized with fees from block 0 @@ -2191,6 +2192,7 @@ mod tests { confirmations: Some(15), status: Ok(()), err: None, + confirmation_status: Some(TransactionConfirmationStatus::Finalized), })], &mut confirmations, ) @@ -2210,6 +2212,7 @@ mod tests { confirmations: None, status: Ok(()), err: None, + confirmation_status: Some(TransactionConfirmationStatus::Finalized), })], &mut confirmations, ) diff --git a/tokens/src/db.rs b/tokens/src/db.rs index c15aa55d45..1aaa92f06d 100644 --- a/tokens/src/db.rs +++ b/tokens/src/db.rs @@ -211,6 +211,7 @@ mod tests { use super::*; use csv::{ReaderBuilder, Trim}; use solana_sdk::transaction::TransactionError; + use solana_transaction_status::TransactionConfirmationStatus; use tempfile::NamedTempFile; #[test] @@ -307,6 +308,7 @@ mod tests { confirmations: Some(1), err: None, status: Ok(()), + confirmation_status: Some(TransactionConfirmationStatus::Confirmed), }; assert_eq!( update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0) @@ -334,6 +336,7 @@ mod tests { confirmations: None, err: Some(TransactionError::AccountNotFound), status: Ok(()), + confirmation_status: Some(TransactionConfirmationStatus::Finalized), }; assert_eq!( update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0) @@ -358,6 +361,7 @@ mod tests { confirmations: None, err: None, status: Ok(()), + confirmation_status: Some(TransactionConfirmationStatus::Finalized), }; assert_eq!( update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0) diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index d2697d6617..042656fde3 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -18,7 +18,7 @@ use crate::{ pub use solana_runtime::bank::RewardType; use solana_sdk::{ clock::{Slot, UnixTimestamp}, - commitment_config::CommitmentConfig, + commitment_config::{CommitmentConfig, CommitmentLevel}, deserialize_utils::default_on_eof, instruction::CompiledInstruction, message::{Message, MessageHeader}, @@ -214,6 +214,14 @@ impl From for UiTransactionStatusMeta { } } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum TransactionConfirmationStatus { + Processed, + Confirmed, + Finalized, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionStatus { @@ -221,12 +229,28 @@ pub struct TransactionStatus { pub confirmations: Option, // None = rooted pub status: Result<()>, // legacy field pub err: Option, + pub confirmation_status: Option, } impl TransactionStatus { pub fn satisfies_commitment(&self, commitment_config: CommitmentConfig) -> bool { - (commitment_config == CommitmentConfig::default() && self.confirmations.is_none()) - || commitment_config == CommitmentConfig::recent() + match commitment_config.commitment { + CommitmentLevel::Max | CommitmentLevel::Root => self.confirmations.is_none(), + CommitmentLevel::SingleGossip => { + if let Some(status) = &self.confirmation_status { + *status != TransactionConfirmationStatus::Processed + } else { + // These fallback cases handle TransactionStatus RPC responses from older software + self.confirmations.is_some() && self.confirmations.unwrap() > 1 + || self.confirmations.is_none() + } + } + CommitmentLevel::Single => match self.confirmations { + Some(confirmations) => confirmations >= 1, + None => true, + }, + CommitmentLevel::Recent => true, + } } } @@ -499,9 +523,13 @@ mod test { confirmations: None, status: Ok(()), err: None, + confirmation_status: Some(TransactionConfirmationStatus::Finalized), }; assert!(status.satisfies_commitment(CommitmentConfig::default())); + assert!(status.satisfies_commitment(CommitmentConfig::root())); + assert!(status.satisfies_commitment(CommitmentConfig::single())); + assert!(status.satisfies_commitment(CommitmentConfig::single_gossip())); assert!(status.satisfies_commitment(CommitmentConfig::recent())); let status = TransactionStatus { @@ -509,9 +537,69 @@ mod test { confirmations: Some(10), status: Ok(()), err: None, + confirmation_status: Some(TransactionConfirmationStatus::Confirmed), }; assert!(!status.satisfies_commitment(CommitmentConfig::default())); + assert!(!status.satisfies_commitment(CommitmentConfig::root())); + assert!(status.satisfies_commitment(CommitmentConfig::single())); + assert!(status.satisfies_commitment(CommitmentConfig::single_gossip())); assert!(status.satisfies_commitment(CommitmentConfig::recent())); + + let status = TransactionStatus { + slot: 0, + confirmations: Some(1), + status: Ok(()), + err: None, + confirmation_status: Some(TransactionConfirmationStatus::Processed), + }; + + assert!(!status.satisfies_commitment(CommitmentConfig::default())); + assert!(!status.satisfies_commitment(CommitmentConfig::root())); + assert!(status.satisfies_commitment(CommitmentConfig::single())); + assert!(!status.satisfies_commitment(CommitmentConfig::single_gossip())); + assert!(status.satisfies_commitment(CommitmentConfig::recent())); + + let status = TransactionStatus { + slot: 0, + confirmations: Some(0), + status: Ok(()), + err: None, + confirmation_status: None, + }; + + assert!(!status.satisfies_commitment(CommitmentConfig::default())); + assert!(!status.satisfies_commitment(CommitmentConfig::root())); + assert!(!status.satisfies_commitment(CommitmentConfig::single())); + assert!(!status.satisfies_commitment(CommitmentConfig::single_gossip())); + assert!(status.satisfies_commitment(CommitmentConfig::recent())); + + // Test single_gossip fallback cases + let status = TransactionStatus { + slot: 0, + confirmations: Some(1), + status: Ok(()), + err: None, + confirmation_status: None, + }; + assert!(!status.satisfies_commitment(CommitmentConfig::single_gossip())); + + let status = TransactionStatus { + slot: 0, + confirmations: Some(2), + status: Ok(()), + err: None, + confirmation_status: None, + }; + assert!(status.satisfies_commitment(CommitmentConfig::single_gossip())); + + let status = TransactionStatus { + slot: 0, + confirmations: None, + status: Ok(()), + err: None, + confirmation_status: None, + }; + assert!(status.satisfies_commitment(CommitmentConfig::single_gossip())); } }