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 b8e05bd4b5..c15ae9ad0e 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, @@ -1578,7 +1579,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 000c8174bf..6987d69f4c 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -37,6 +37,7 @@ use solana_transaction_status::{ }; use solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY; use std::{ + cmp::min, net::SocketAddr, sync::RwLock, thread::sleep, @@ -1359,29 +1360,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 ac05513001..45b31f5d04 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 0cd278f806..e07f449464 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 37f874790f..3b23d34af4 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; @@ -257,6 +258,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 a3affe6f8b..5285423dbe 100644 --- a/tokens/src/commands.rs +++ b/tokens/src/commands.rs @@ -1038,6 +1038,7 @@ mod tests { use solana_core::test_validator::TestValidator; use solana_sdk::signature::{read_keypair_file, write_keypair_file, Signer}; use solana_stake_program::stake_instruction::StakeInstruction; + use solana_transaction_status::TransactionConfirmationStatus; #[test] fn test_process_token_allocations() { @@ -2105,6 +2106,7 @@ mod tests { confirmations: Some(15), status: Ok(()), err: None, + confirmation_status: Some(TransactionConfirmationStatus::Finalized), })], &mut confirmations, ) @@ -2124,6 +2126,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 3a0a47b0cf..ef15a2af99 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -20,7 +20,7 @@ use solana_account_decoder::parse_token::UiTokenAmount; 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}, @@ -260,6 +260,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 { @@ -267,12 +275,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, + } } } @@ -545,9 +569,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 { @@ -555,9 +583,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())); } }