Skip to content

Commit

Permalink
Fail transaction logs when input txos are consumed (#1028)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nick-mobilecoin authored Dec 3, 2024
1 parent cb2f46e commit 316d50d
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 39 deletions.
87 changes: 52 additions & 35 deletions full-service/src/db/transaction_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -407,9 +426,7 @@ impl TransactionLogModel for TransactionLog {
}

fn get(id: &TransactionId, conn: Conn) -> Result<TransactionLog, WalletDbError> {
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::<TransactionLog>(conn)
{
Expand All @@ -423,8 +440,6 @@ impl TransactionLogModel for TransactionLog {
}

fn get_associated_txos(&self, conn: Conn) -> Result<AssociatedTxos, WalletDbError> {
use crate::db::schema::{transaction_input_txos, transaction_output_txos, txos};

let inputs: Vec<Txo> = txos::table
.inner_join(transaction_input_txos::table)
.filter(transaction_input_txos::transaction_log_id.eq(&self.id))
Expand Down Expand Up @@ -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)?;
Expand All @@ -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)?;
Expand All @@ -488,8 +499,6 @@ impl TransactionLogModel for TransactionLog {
tombstone_block_index: Option<i64>,
conn: Conn,
) -> Result<(), WalletDbError> {
use crate::db::schema::transaction_logs;

diesel::update(self)
.set((
transaction_logs::tx.eq(tx),
Expand All @@ -507,8 +516,6 @@ impl TransactionLogModel for TransactionLog {
max_block_index: Option<u64>,
conn: Conn,
) -> Result<Vec<(TransactionLog, AssociatedTxos, ValueMap)>, WalletDbError> {
use crate::db::schema::transaction_logs;

let mut query = transaction_logs::table.into_boxed();

if let Some(account_id) = account_id {
Expand Down Expand Up @@ -550,7 +557,6 @@ impl TransactionLogModel for TransactionLog {
account_id: &AccountID,
conn: Conn,
) -> Result<TransactionLog, WalletDbError> {
use crate::db::schema::{transaction_input_txos, transaction_logs};
// Verify that the account exists.
Account::get(account_id, conn)?;

Expand Down Expand Up @@ -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<TransactionInputTxo> = transaction_input_txos::table
.inner_join(transaction_logs::table)
.filter(transaction_logs::account_id.eq(account_id_hex))
Expand Down Expand Up @@ -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<String> = transaction_logs::table
Expand All @@ -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<String> = 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))
Expand Down Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions full-service/src/service/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 316d50d

Please sign in to comment.