When a mempool transaction is invalidated and removed from node_transaction because of a conflicting transaction, any descendant transactions that depend on it are left orphaned in the table.
Current behavior
Given a chain of unconfirmed transactions A → B → C in node_transaction:
- A block is accepted containing transaction A' which conflicts with A
trigger_node_block_insert correctly deletes A and archives it to node_transaction_history
- B and C remain in
node_transaction despite being invalid (their parent outputs no longer exist in any valid context).
Impact
node_transaction accumulates invalid rows over time, growing unboundedly
- queries for balances or UTXOs return invalid unconfirmed results
- affects chained unconfirmed transactions to arbitrary depth
For some example orphaned transactions, run the following GraphQL query on gql.chaingraph.pat.mn:
query MempoolOrphans {
node_transaction(
where: {
transaction: {
inputs: {
outpoint: {
transaction: {
node_validation_timeline: { replaced_at: { _is_null: false } }
}
}
}
}
}
) {
transaction {
hash
}
validated_at
}
}
As of 2026-04-02 this returns 991 orphaned transactions.
Proposed solution
Add an AFTER INSERT trigger on node_transaction_history that cascades invalidation to descendants:
- Check if
NEW.replaced_at IS NOT NULL (if null, this is a block confirmation and descendants remain valid, so do nothing)
- Look up the outputs created by the invalidated transaction
- Find
node_transaction rows (for the same node) whose transaction spends any of those outputs (via input.outpoint_transaction_hash / outpoint_index)
- Delete those rows from
node_transaction and insert them into node_transaction_history
- The trigger fires recursively down the chain
When a mempool transaction is invalidated and removed from
node_transactionbecause of a conflicting transaction, any descendant transactions that depend on it are left orphaned in the table.Current behavior
Given a chain of unconfirmed transactions A → B → C in
node_transaction:trigger_node_block_insertcorrectly deletes A and archives it tonode_transaction_historynode_transactiondespite being invalid (their parent outputs no longer exist in any valid context).Impact
node_transactionaccumulates invalid rows over time, growing unboundedlyFor some example orphaned transactions, run the following GraphQL query on gql.chaingraph.pat.mn:
As of 2026-04-02 this returns 991 orphaned transactions.
Proposed solution
Add an AFTER INSERT trigger on
node_transaction_historythat cascades invalidation to descendants:NEW.replaced_at IS NOT NULL(if null, this is a block confirmation and descendants remain valid, so do nothing)node_transactionrows (for the same node) whose transaction spends any of those outputs (viainput.outpoint_transaction_hash/outpoint_index)node_transactionand insert them intonode_transaction_history