From 7cf90f2c6fa1efa5ea3896489c4032923d33b22f Mon Sep 17 00:00:00 2001 From: Nick Santana Date: Tue, 3 Dec 2024 13:48:35 -0800 Subject: [PATCH] Fail transaction logs when input txos are consumed Previously a transaction log would need to wait for the tombstone block to occur. Now when an input txo is spent any transaction logs that haven't been finalized and use that input txo will be marked failed. --- full-service/src/db/transaction_log.rs | 87 +++++++++++-------- .../build_submit/build_then_submit.rs | 7 +- full-service/src/service/sync.rs | 4 + 3 files changed, 59 insertions(+), 39 deletions(-) diff --git a/full-service/src/db/transaction_log.rs b/full-service/src/db/transaction_log.rs index 3b1490d75..3c706939b 100644 --- a/full-service/src/db/transaction_log.rs +++ b/full-service/src/db/transaction_log.rs @@ -15,6 +15,11 @@ use crate::{ Account, NewTransactionInputTxo, NewTransactionLog, TransactionInputTxo, TransactionLog, TransactionOutputTxo, Txo, }, + schema::{ + transaction_input_txos, transaction_logs, + transaction_logs::dsl::{id as dsl_id, transaction_logs as dsl_transaction_logs}, + transaction_output_txos, txos, + }, txo::{TxoID, TxoModel}, Conn, WalletDbError, }, @@ -322,24 +327,38 @@ pub trait TransactionLogModel { conn: Conn ) -> Result<(), WalletDbError>; - /// Update the finalized block index to all pending transaction logs that associate with a given transaction output (txo). + /// Update the finalized block index to all pending transaction logs that have an output + /// transaction corresponding to `transaction_output_txo_id_hex`. /// /// # Arguments - /// - ///| Name | Purpose | Notes | - ///|-------------------------|------------------------------------------------------------------------------|-------| - ///| `txo_id_hex` | The txo ID for which to get all transaction logs associated with this txo ID.| | - ///| `finalized_block_index` | The block index at which the transaction will be completed and finalized. | | - ///| `conn` | An reference to the pool connection of wallet database | | - /// - /// # Returns - /// * unit + /// * `transaction_output_txo_id_hex` - The txo ID for which to get all transaction logs + /// associated with this txo ID. + /// * `finalized_block_index` - The block index at which the transaction will be completed and + /// finalized. + /// * `conn` - A reference to the pool connection of wallet database fn update_pending_associated_with_txo_to_succeeded( - txo_id_hex: &str, + transaction_output_txo_id_hex: &str, finalized_block_index: u64, conn: Conn, ) -> Result<(), WalletDbError>; + + /// Update all transaction logs that have an input transaction corresponding to + /// `transaction_input_txo_id_hex` to failed. + /// + /// Note: When processing inputs and outputs from the same block, be sure to mark the + /// appropriate transaction logs as succeeded prior to calling this method. See + /// `update_pending_associated_with_txo_to_succeeded()`. + /// + /// # Arguments + /// * `transaction_input_txo_id_hex` - The txo ID for which to get all transaction logs + /// associated with this txo ID. + /// * `conn` - A reference to the pool connection of wallet database + fn update_consumed_txo_to_failed( + transaction_input_txo_id_hex: &str, + conn: Conn, + ) -> Result<(), WalletDbError>; + /// Set the status of a transaction log to failed if its tombstone_block_index is less than the given block index. /// /// # Arguments @@ -407,9 +426,7 @@ impl TransactionLogModel for TransactionLog { } fn get(id: &TransactionId, conn: Conn) -> Result { - use crate::db::schema::transaction_logs::dsl::{id as dsl_id, transaction_logs}; - - match transaction_logs + match dsl_transaction_logs .filter(dsl_id.eq(id.to_string())) .get_result::(conn) { @@ -423,8 +440,6 @@ impl TransactionLogModel for TransactionLog { } fn get_associated_txos(&self, conn: Conn) -> Result { - use crate::db::schema::{transaction_input_txos, transaction_output_txos, txos}; - let inputs: Vec = txos::table .inner_join(transaction_input_txos::table) .filter(transaction_input_txos::transaction_log_id.eq(&self.id)) @@ -463,8 +478,6 @@ impl TransactionLogModel for TransactionLog { submitted_block_index: u64, conn: Conn, ) -> Result<(), WalletDbError> { - use crate::db::schema::transaction_logs; - diesel::update(self) .set(transaction_logs::submitted_block_index.eq(Some(submitted_block_index as i64))) .execute(conn)?; @@ -473,8 +486,6 @@ impl TransactionLogModel for TransactionLog { } fn update_comment(&self, comment: String, conn: Conn) -> Result<(), WalletDbError> { - use crate::db::schema::transaction_logs; - diesel::update(self) .set(transaction_logs::comment.eq(comment)) .execute(conn)?; @@ -488,8 +499,6 @@ impl TransactionLogModel for TransactionLog { tombstone_block_index: Option, conn: Conn, ) -> Result<(), WalletDbError> { - use crate::db::schema::transaction_logs; - diesel::update(self) .set(( transaction_logs::tx.eq(tx), @@ -507,8 +516,6 @@ impl TransactionLogModel for TransactionLog { max_block_index: Option, conn: Conn, ) -> Result, WalletDbError> { - use crate::db::schema::transaction_logs; - let mut query = transaction_logs::table.into_boxed(); if let Some(account_id) = account_id { @@ -550,7 +557,6 @@ impl TransactionLogModel for TransactionLog { account_id: &AccountID, conn: Conn, ) -> Result { - use crate::db::schema::{transaction_input_txos, transaction_logs}; // Verify that the account exists. Account::get(account_id, conn)?; @@ -744,10 +750,6 @@ impl TransactionLogModel for TransactionLog { } fn delete_all_for_account(account_id_hex: &str, conn: Conn) -> Result<(), WalletDbError> { - use crate::db::schema::{ - transaction_input_txos, transaction_logs, transaction_output_txos, - }; - let transaction_input_txos: Vec = transaction_input_txos::table .inner_join(transaction_logs::table) .filter(transaction_logs::account_id.eq(account_id_hex)) @@ -781,7 +783,6 @@ impl TransactionLogModel for TransactionLog { finalized_block_index: u64, conn: Conn, ) -> Result<(), WalletDbError> { - use crate::db::schema::{transaction_logs, transaction_output_txos}; // Find all submitted transaction logs associated with this txo that have not // yet been finalized (there should only ever be one). let transaction_log_ids: Vec = transaction_logs::table @@ -802,13 +803,32 @@ impl TransactionLogModel for TransactionLog { Ok(()) } + fn update_consumed_txo_to_failed( + transaction_input_txo_id_hex: &str, + conn: Conn, + ) -> Result<(), WalletDbError> { + let transaction_log_ids: Vec = transaction_logs::table + .inner_join(transaction_input_txos::table) + .filter(transaction_input_txos::txo_id.eq(transaction_input_txo_id_hex)) + .filter(transaction_logs::failed.eq(false)) + .filter(transaction_logs::finalized_block_index.is_null()) + .select(transaction_logs::id) + .load(conn)?; + + diesel::update( + transaction_logs::table.filter(transaction_logs::id.eq_any(transaction_log_ids)), + ) + .set((transaction_logs::failed.eq(true),)) + .execute(conn)?; + + Ok(()) + } + fn update_pending_exceeding_tombstone_block_index_to_failed( account_id: &AccountID, block_index: u64, conn: Conn, ) -> Result<(), WalletDbError> { - use crate::db::schema::transaction_logs; - diesel::update( transaction_logs::table .filter(transaction_logs::account_id.eq(&account_id.0)) @@ -1169,9 +1189,6 @@ mod tests { #[async_test_with_logger] async fn test_delete_transaction_logs_for_account(logger: Logger) { - use crate::db::schema::{ - transaction_input_txos, transaction_logs, transaction_output_txos, - }; use diesel::dsl::count_star; let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_then_submit.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_then_submit.rs index d758dcb81..33b1769c3 100644 --- a/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_then_submit.rs +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_then_submit.rs @@ -827,10 +827,9 @@ mod e2e_transaction { // The first transaction log will have succeeded, because it's outputs // show up on the ledger. - // The second transaction log will still be pending, because it's outputs - // don't exist in the ledger. This second transaction log will - // eventually fail once the tombstone block is reached. - let expected_statuses = [TxStatus::Succeeded, TxStatus::Pending]; + // The second transaction log will still be failed, because it's inputs + // were consumed but its outputs don't exist + let expected_statuses = [TxStatus::Succeeded, TxStatus::Failed]; tx_log_id_and_proposals .iter() diff --git a/full-service/src/service/sync.rs b/full-service/src/service/sync.rs index d3b6d251e..77550c8e4 100644 --- a/full-service/src/service/sync.rs +++ b/full-service/src/service/sync.rs @@ -299,6 +299,10 @@ pub fn sync_account_next_chunk( for (block_index, txo_id_hex) in &spent_txos { Txo::update_spent_block_index(txo_id_hex, *block_index, conn)?; + // NB: This needs to be done after calling + // `TransactionLog::update_pending_associated_with_txo_to_succeeded()` so we + // don't fail a transaction log that is finalized for this block. + TransactionLog::update_consumed_txo_to_failed(txo_id_hex, conn)?; } TransactionLog::update_pending_exceeding_tombstone_block_index_to_failed(