diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -6093,6 +6093,9 @@ bool shouldActivateBestChain = false; + const bool fPreConsensus = + gArgs.GetBoolArg("-avalanchepreconsensus", false); + for (const auto &u : updates) { const avalanche::AnyVoteItem &item = u.getVoteItem(); @@ -6205,6 +6208,56 @@ break; } } + + if (!fPreConsensus) { + continue; + } + + if (auto pitem = std::get_if(&item)) { + const CTransactionRef tx = *pitem; + assert(tx != nullptr); + + const TxId &txid = tx->GetId(); + logVoteUpdate(u, "tx", txid); + + switch (u.getStatus()) { + case avalanche::VoteStatus::Rejected: + break; + case avalanche::VoteStatus::Invalid: { + // Remove from the mempool and the finalized tree, as + // well as all the children txs. + // FIXME Remember the tx has been invalidated so we + // don't poll for it again and again. + LOCK(m_mempool.cs); + auto it = m_mempool.GetIter(txid); + if (it.has_value()) { + m_mempool.removeRecursive( + *tx, MemPoolRemovalReason::AVALANCHE); + } + + break; + } + case avalanche::VoteStatus::Accepted: + break; + case avalanche::VoteStatus::Finalized: { + LOCK(m_mempool.cs); + auto it = m_mempool.GetIter(txid); + if (!it.has_value()) { + LogPrint(BCLog::AVALANCHE, + "Error: finalized tx (%s) is not in the " + "mempool\n", + txid.ToString()); + break; + } + + m_mempool.setAvalancheFinalized(**it); + + break; + } + case avalanche::VoteStatus::Stale: + break; + } + } } if (shouldActivateBestChain) { diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -1420,6 +1420,7 @@ const NodeContext &node = EnsureAnyNodeContext(request.context); ChainstateManager &chainman = EnsureChainman(node); + const CTxMemPool &mempool = EnsureMemPool(node); const TxId txid = TxId(ParseHashV(request.params[0], "txid")); CBlockIndex *pindex = nullptr; @@ -1442,8 +1443,8 @@ BlockHash hash_block; const CTransactionRef tx = GetTransaction( - pindex, node.mempool.get(), txid, - config.GetChainParams().GetConsensus(), hash_block); + pindex, &mempool, txid, config.GetChainParams().GetConsensus(), + hash_block); if (!g_avalanche->isQuorumEstablished()) { throw JSONRPCError(RPC_MISC_ERROR, @@ -1461,8 +1462,8 @@ errmsg = "No such transaction found in the provided block."; } else if (!g_txindex) { errmsg = "No such transaction. Use -txindex or provide a " - "block " - "hash to enable blockchain transaction queries."; + "block hash to enable blockchain transaction " + "queries."; } else if (!f_txindex_ready) { errmsg = "No such transaction. Blockchain transactions are " "still in the process of being indexed."; @@ -1477,10 +1478,18 @@ pindex = chainman.m_blockman.LookupBlockIndex(hash_block); } - // The first 2 checks are redundant as we expect to throw a JSON RPC - // error for these cases, but they are almost free so they are kept - // as a safety net. - return tx != nullptr && !node.mempool->exists(txid) && + if (!tx) { + // Tx not found, we should have raised an error at this stage + return false; + } + + if (mempool.isAvalancheFinalized(txid)) { + // The transaction is finalized + return true; + } + + // Return true if the tx is in a finalized block + return !node.mempool->exists(txid) && chainman.ActiveChainstate().IsBlockAvalancheFinalized( pindex); }, diff --git a/src/txmempool.h b/src/txmempool.h --- a/src/txmempool.h +++ b/src/txmempool.h @@ -12,8 +12,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -199,6 +201,15 @@ } }; +/** + * Radix tree adapter for storing a CTxMemPoolEntry as a tree element. + */ +struct MemPoolEntryRadixTreeAdapter { + Uint256RadixKey getId(const CTxMemPoolEntry &entry) const { + return entry.GetSharedTx()->GetId(); + } +}; + // used by the entry_time index struct CompareTxMemPoolEntryByEntryTime { bool operator()(const CTxMemPoolEntryRef &a, @@ -290,7 +301,9 @@ //! Removed for conflict with in-block transaction CONFLICT, //! Removed for replacement - REPLACED + REPLACED, + //! Removed by avalanche vote + AVALANCHE, }; /** @@ -431,6 +444,8 @@ typedef std::set setEntries; typedef std::set setRevTopoEntries; + RadixTree finalizedTxs; + private: void UpdateParent(txiter entry, txiter parent, bool add) EXCLUSIVE_LOCKS_REQUIRED(cs); @@ -639,6 +654,16 @@ return mapTx.count(txid) != 0; } + bool setAvalancheFinalized(const CTxMemPoolEntryRef &tx) + EXCLUSIVE_LOCKS_REQUIRED(cs) { + return finalizedTxs.insert(tx); + } + + bool isAvalancheFinalized(const TxId &txid) const { + LOCK(cs); + return finalizedTxs.get(txid) != nullptr; + } + CTransactionRef get(const TxId &txid) const; TxMempoolInfo info(const TxId &txid) const; std::vector infoAll() const; diff --git a/src/txmempool.cpp b/src/txmempool.cpp --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -5,6 +5,8 @@ #include +#include +#include #include #include // for GetConsensus. #include @@ -243,7 +245,10 @@ } /* add logging because unchecked */ - RemoveUnbroadcastTx((*it)->GetTx().GetId(), true); + const TxId &txid = (*it)->GetTx().GetId(); + RemoveUnbroadcastTx(txid, true); + + finalizedTxs.remove(txid); totalTxSize -= (*it)->GetTxSize(); m_total_fee -= (*it)->GetFee(); diff --git a/test/functional/abc_p2p_avalanche_transaction_voting.py b/test/functional/abc_p2p_avalanche_transaction_voting.py --- a/test/functional/abc_p2p_avalanche_transaction_voting.py +++ b/test/functional/abc_p2p_avalanche_transaction_voting.py @@ -19,7 +19,7 @@ ) from test_framework.test_framework import BitcoinTestFramework from test_framework.txtools import pad_tx -from test_framework.util import assert_equal, uint256_hex +from test_framework.util import assert_equal, assert_raises_rpc_error, uint256_hex from test_framework.wallet import MiniWallet QUORUM_NODE_COUNT = 16 @@ -170,9 +170,47 @@ tip = node.getbestblockhash() self.wait_until(lambda: has_finalized_block(tip)) + # Now we can focus on transactions + def has_finalized_tx(txid): + can_find_inv_in_poll(quorum, int(txid, 16)) + return node.isfinaltransaction(txid) + + def has_invalidated_tx(txid): + can_find_inv_in_poll( + quorum, int(txid, 16), response=AvalancheTxVoteError.INVALID + ) + return txid not in node.getrawmempool() + txid = wallet.send_self_transfer(from_node=node)["txid"] assert txid in node.getrawmempool() - self.wait_until(lambda: can_find_inv_in_poll(quorum, int(txid, 16))) + assert not node.isfinaltransaction(txid) + self.wait_until(lambda: has_finalized_tx(txid)) + assert txid in node.getrawmempool() + + self.log.info("Check the node drops transactions invalidated by avalanche") + + parent_tx = wallet.send_self_transfer(from_node=node) + parent_txid = parent_tx["txid"] + child_tx = wallet.send_self_transfer( + from_node=node, utxo_to_spend=parent_tx["new_utxo"] + ) + child_txid = child_tx["txid"] + + assert parent_txid in node.getrawmempool() + assert child_txid in node.getrawmempool() + assert not node.isfinaltransaction(parent_txid) + assert not node.isfinaltransaction(child_txid) + + self.wait_until(lambda: has_invalidated_tx(parent_txid)) + + assert parent_txid not in node.getrawmempool() + assert child_txid not in node.getrawmempool() + assert_raises_rpc_error( + -5, "No such transaction", node.isfinaltransaction, parent_txid + ) + assert_raises_rpc_error( + -5, "No such transaction", node.isfinaltransaction, child_txid + ) if __name__ == "__main__":