More reliable way to detect expired transactions (#10482)

When the root slot is beyond the last valid slot, we can say
with certainty that the blockhash is expired. Unfortunately,
we still can't say the transaction didn't land. It may have
landed a long time ago and the validator has since purged
its transaction status.
This commit is contained in:
Greg Fitzgerald 2020-06-10 17:00:13 -06:00 committed by GitHub
parent 1e3554b33d
commit 9c2c64f8c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 43 additions and 57 deletions

View File

@ -160,7 +160,7 @@ fn distribute_tokens<T: Client>(
let fee_payer_pubkey = args.fee_payer.pubkey(); let fee_payer_pubkey = args.fee_payer.pubkey();
let message = Message::new_with_payer(&instructions, Some(&fee_payer_pubkey)); let message = Message::new_with_payer(&instructions, Some(&fee_payer_pubkey));
match client.send_message(message, &signers) { match client.send_message(message, &signers) {
Ok((transaction, _last_valid_slot)) => { Ok((transaction, last_valid_slot)) => {
db::set_transaction_info( db::set_transaction_info(
db, db,
&allocation.recipient.parse().unwrap(), &allocation.recipient.parse().unwrap(),
@ -168,6 +168,7 @@ fn distribute_tokens<T: Client>(
&transaction, &transaction,
Some(&new_stake_account_address), Some(&new_stake_account_address),
false, false,
last_valid_slot,
)?; )?;
} }
Err(e) => { Err(e) => {
@ -332,20 +333,20 @@ fn update_finalized_transactions<T: Client>(
if info.finalized_date.is_some() { if info.finalized_date.is_some() {
None None
} else { } else {
Some(&info.transaction) Some((&info.transaction, info.last_valid_slot))
} }
}) })
.collect(); .collect();
let unconfirmed_signatures: Vec<_> = unconfirmed_transactions let unconfirmed_signatures: Vec<_> = unconfirmed_transactions
.iter() .iter()
.map(|tx| tx.signatures[0]) .map(|(tx, _slot)| tx.signatures[0])
.filter(|sig| *sig != Signature::default()) // Filter out dry-run signatures .filter(|sig| *sig != Signature::default()) // Filter out dry-run signatures
.collect(); .collect();
let transaction_statuses = client.get_signature_statuses(&unconfirmed_signatures)?; let transaction_statuses = client.get_signature_statuses(&unconfirmed_signatures)?;
let recent_blockhashes = client.get_recent_blockhashes()?; let root_slot = client.get_slot()?;
let mut confirmations = None; let mut confirmations = None;
for (transaction, opt_transaction_status) in unconfirmed_transactions for ((transaction, last_valid_slot), opt_transaction_status) in unconfirmed_transactions
.into_iter() .into_iter()
.zip(transaction_statuses.into_iter()) .zip(transaction_statuses.into_iter())
{ {
@ -353,8 +354,8 @@ fn update_finalized_transactions<T: Client>(
db, db,
&transaction.signatures[0], &transaction.signatures[0],
opt_transaction_status, opt_transaction_status,
&transaction.message.recent_blockhash, last_valid_slot,
&recent_blockhashes, root_slot,
) { ) {
Ok(Some(confs)) => { Ok(Some(confs)) => {
confirmations = Some(cmp::min(confs, confirmations.unwrap_or(usize::MAX))); confirmations = Some(cmp::min(confs, confirmations.unwrap_or(usize::MAX)));

View File

@ -1,7 +1,7 @@
use chrono::prelude::*; use chrono::prelude::*;
use pickledb::{error::Error, PickleDb, PickleDbDumpPolicy}; use pickledb::{error::Error, PickleDb, PickleDbDumpPolicy};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Signature, transaction::Transaction}; use solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature, transaction::Transaction};
use solana_transaction_status::TransactionStatus; use solana_transaction_status::TransactionStatus;
use std::{cmp::Ordering, fs, io, path::Path}; use std::{cmp::Ordering, fs, io, path::Path};
@ -12,6 +12,7 @@ pub struct TransactionInfo {
pub new_stake_account_address: Option<Pubkey>, pub new_stake_account_address: Option<Pubkey>,
pub finalized_date: Option<DateTime<Utc>>, pub finalized_date: Option<DateTime<Utc>>,
pub transaction: Transaction, pub transaction: Transaction,
pub last_valid_slot: Slot,
} }
#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
@ -35,6 +36,7 @@ impl Default for TransactionInfo {
new_stake_account_address: None, new_stake_account_address: None,
finalized_date: None, finalized_date: None,
transaction, transaction,
last_valid_slot: 0,
} }
} }
} }
@ -103,6 +105,7 @@ pub fn set_transaction_info(
transaction: &Transaction, transaction: &Transaction,
new_stake_account_address: Option<&Pubkey>, new_stake_account_address: Option<&Pubkey>,
finalized: bool, finalized: bool,
last_valid_slot: Slot,
) -> Result<(), Error> { ) -> Result<(), Error> {
let finalized_date = if finalized { Some(Utc::now()) } else { None }; let finalized_date = if finalized { Some(Utc::now()) } else { None };
let transaction_info = TransactionInfo { let transaction_info = TransactionInfo {
@ -111,6 +114,7 @@ pub fn set_transaction_info(
new_stake_account_address: new_stake_account_address.cloned(), new_stake_account_address: new_stake_account_address.cloned(),
finalized_date, finalized_date,
transaction: transaction.clone(), transaction: transaction.clone(),
last_valid_slot,
}; };
let signature = transaction.signatures[0]; let signature = transaction.signatures[0];
db.set(&signature.to_string(), &transaction_info)?; db.set(&signature.to_string(), &transaction_info)?;
@ -119,20 +123,22 @@ pub fn set_transaction_info(
// Set the finalized bit in the database if the transaction is rooted. // Set the finalized bit in the database if the transaction is rooted.
// Remove the TransactionInfo from the database if the transaction failed. // Remove the TransactionInfo from the database if the transaction failed.
// Return the number of confirmations on the transaction or None if finalized. // Return the number of confirmations on the transaction or None if either
// finalized or discarded.
pub fn update_finalized_transaction( pub fn update_finalized_transaction(
db: &mut PickleDb, db: &mut PickleDb,
signature: &Signature, signature: &Signature,
opt_transaction_status: Option<TransactionStatus>, opt_transaction_status: Option<TransactionStatus>,
blockhash: &Hash, last_valid_slot: Slot,
recent_blockhashes: &[Hash], root_slot: Slot,
) -> Result<Option<usize>, Error> { ) -> Result<Option<usize>, Error> {
if opt_transaction_status.is_none() { if opt_transaction_status.is_none() {
if !recent_blockhashes.contains(blockhash) { if root_slot > last_valid_slot {
eprintln!( eprintln!(
"Signature not found {} and blockhash not found, likely expired", "Signature not found {} and blockhash expired. Transaction either dropped or the validator purged the transaction status.",
signature signature
); );
// Don't discard the transaction, because we are not certain the // Don't discard the transaction, because we are not certain the
// blockhash is expired. Instead, return None to signal that // blockhash is expired. Instead, return None to signal that
// we don't need to wait for confirmations. // we don't need to wait for confirmations.
@ -161,7 +167,7 @@ pub fn update_finalized_transaction(
return Ok(None); return Ok(None);
} }
// Transaction is rooted. Set finalized in the database. // Transaction is rooted. Set the finalized date in the database.
let mut transaction_info = db.get::<TransactionInfo>(&signature.to_string()).unwrap(); let mut transaction_info = db.get::<TransactionInfo>(&signature.to_string()).unwrap();
transaction_info.finalized_date = Some(Utc::now()); transaction_info.finalized_date = Some(Utc::now());
db.set(&signature.to_string(), &transaction_info)?; db.set(&signature.to_string(), &transaction_info)?;
@ -230,12 +236,10 @@ mod tests {
let mut db = let mut db =
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump); PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
let signature = Signature::default(); let signature = Signature::default();
let blockhash = Hash::default();
let transaction_info = TransactionInfo::default(); let transaction_info = TransactionInfo::default();
db.set(&signature.to_string(), &transaction_info).unwrap(); db.set(&signature.to_string(), &transaction_info).unwrap();
assert!(matches!( assert!(matches!(
update_finalized_transaction(&mut db, &signature, None, &blockhash, &[blockhash]) update_finalized_transaction(&mut db, &signature, None, 0, 0).unwrap(),
.unwrap(),
Some(0) Some(0)
)); ));
@ -247,7 +251,7 @@ mod tests {
// Same as before, but now with an expired blockhash // Same as before, but now with an expired blockhash
assert_eq!( assert_eq!(
update_finalized_transaction(&mut db, &signature, None, &blockhash, &[]).unwrap(), update_finalized_transaction(&mut db, &signature, None, 0, 1).unwrap(),
None None
); );
@ -264,7 +268,6 @@ mod tests {
let mut db = let mut db =
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump); PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
let signature = Signature::default(); let signature = Signature::default();
let blockhash = Hash::default();
let transaction_info = TransactionInfo::default(); let transaction_info = TransactionInfo::default();
db.set(&signature.to_string(), &transaction_info).unwrap(); db.set(&signature.to_string(), &transaction_info).unwrap();
let transaction_status = TransactionStatus { let transaction_status = TransactionStatus {
@ -274,14 +277,8 @@ mod tests {
err: None, err: None,
}; };
assert_eq!( assert_eq!(
update_finalized_transaction( update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0)
&mut db, .unwrap(),
&signature,
Some(transaction_status),
&blockhash,
&[blockhash]
)
.unwrap(),
Some(1) Some(1)
); );
@ -298,7 +295,6 @@ mod tests {
let mut db = let mut db =
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump); PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
let signature = Signature::default(); let signature = Signature::default();
let blockhash = Hash::default();
let transaction_info = TransactionInfo::default(); let transaction_info = TransactionInfo::default();
db.set(&signature.to_string(), &transaction_info).unwrap(); db.set(&signature.to_string(), &transaction_info).unwrap();
let status = Err(TransactionError::AccountNotFound); let status = Err(TransactionError::AccountNotFound);
@ -309,14 +305,8 @@ mod tests {
err: None, err: None,
}; };
assert_eq!( assert_eq!(
update_finalized_transaction( update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0)
&mut db, .unwrap(),
&signature,
Some(transaction_status),
&blockhash,
&[blockhash]
)
.unwrap(),
None None
); );
@ -330,7 +320,6 @@ mod tests {
let mut db = let mut db =
PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump); PickleDb::new_yaml(NamedTempFile::new().unwrap(), PickleDbDumpPolicy::NeverDump);
let signature = Signature::default(); let signature = Signature::default();
let blockhash = Hash::default();
let transaction_info = TransactionInfo::default(); let transaction_info = TransactionInfo::default();
db.set(&signature.to_string(), &transaction_info).unwrap(); db.set(&signature.to_string(), &transaction_info).unwrap();
let transaction_status = TransactionStatus { let transaction_status = TransactionStatus {
@ -340,14 +329,8 @@ mod tests {
err: None, err: None,
}; };
assert_eq!( assert_eq!(
update_finalized_transaction( update_finalized_transaction(&mut db, &signature, Some(transaction_status), 0, 0)
&mut db, .unwrap(),
&signature,
Some(transaction_status),
&blockhash,
&[blockhash]
)
.unwrap(),
None None
); );

View File

@ -12,10 +12,6 @@ use solana_sdk::{
signature::{Signature, Signer}, signature::{Signature, Signer},
signers::Signers, signers::Signers,
system_instruction, system_instruction,
sysvar::{
recent_blockhashes::{self, RecentBlockhashes},
Sysvar,
},
transaction::Transaction, transaction::Transaction,
transport::{Result, TransportError}, transport::{Result, TransportError},
}; };
@ -29,6 +25,7 @@ pub trait Client {
) -> Result<Vec<Option<TransactionStatus>>>; ) -> Result<Vec<Option<TransactionStatus>>>;
fn get_balance1(&self, pubkey: &Pubkey) -> Result<u64>; fn get_balance1(&self, pubkey: &Pubkey) -> Result<u64>;
fn get_fees1(&self) -> Result<(Hash, FeeCalculator, Slot)>; fn get_fees1(&self) -> Result<(Hash, FeeCalculator, Slot)>;
fn get_slot1(&self) -> Result<Slot>;
fn get_account1(&self, pubkey: &Pubkey) -> Result<Option<Account>>; fn get_account1(&self, pubkey: &Pubkey) -> Result<Option<Account>>;
} }
@ -64,6 +61,11 @@ impl Client for RpcClient {
Ok(result.value) Ok(result.value)
} }
fn get_slot1(&self) -> Result<Slot> {
self.get_slot()
.map_err(|e| TransportError::Custom(e.to_string()))
}
fn get_account1(&self, pubkey: &Pubkey) -> Result<Option<Account>> { fn get_account1(&self, pubkey: &Pubkey) -> Result<Option<Account>> {
self.get_account(pubkey) self.get_account(pubkey)
.map(Some) .map(Some)
@ -103,6 +105,10 @@ impl Client for BankClient {
self.get_recent_blockhash_with_commitment(CommitmentConfig::default()) self.get_recent_blockhash_with_commitment(CommitmentConfig::default())
} }
fn get_slot1(&self) -> Result<Slot> {
self.get_slot()
}
fn get_account1(&self, pubkey: &Pubkey) -> Result<Option<Account>> { fn get_account1(&self, pubkey: &Pubkey) -> Result<Option<Account>> {
self.get_account(pubkey) self.get_account(pubkey)
} }
@ -170,6 +176,10 @@ impl<C: Client> ThinClient<C> {
self.client.get_fees1() self.client.get_fees1()
} }
pub fn get_slot(&self) -> Result<Slot> {
self.client.get_slot1()
}
pub fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> { pub fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
self.client.get_balance1(pubkey) self.client.get_balance1(pubkey)
} }
@ -177,12 +187,4 @@ impl<C: Client> ThinClient<C> {
pub fn get_account(&self, pubkey: &Pubkey) -> Result<Option<Account>> { pub fn get_account(&self, pubkey: &Pubkey) -> Result<Option<Account>> {
self.client.get_account1(pubkey) self.client.get_account1(pubkey)
} }
pub fn get_recent_blockhashes(&self) -> Result<Vec<Hash>> {
let opt_blockhashes_account = self.get_account(&recent_blockhashes::id())?;
let blockhashes_account = opt_blockhashes_account.unwrap();
let recent_blockhashes = RecentBlockhashes::from_account(&blockhashes_account).unwrap();
let hashes = recent_blockhashes.iter().map(|x| x.blockhash).collect();
Ok(hashes)
}
} }