Page MenuHomePhabricator

D17717.diff
No OneTemporary

D17717.diff

diff --git a/src/txmempool.h b/src/txmempool.h
--- a/src/txmempool.h
+++ b/src/txmempool.h
@@ -521,15 +521,7 @@
}
bool setAvalancheFinalized(const CTxMemPoolEntryRef &tx)
- EXCLUSIVE_LOCKS_REQUIRED(cs) {
- const bool ret = finalizedTxs.insert(tx);
- if (ret) {
- totalFinalizedTxSize += tx->GetTxSize();
- totalFinalizedTxSigchecks += tx->GetSigChecks();
- }
-
- return ret;
- }
+ EXCLUSIVE_LOCKS_REQUIRED(cs);
bool isAvalancheFinalized(const TxId &txid) const {
LOCK(cs);
diff --git a/src/txmempool.cpp b/src/txmempool.cpp
--- a/src/txmempool.cpp
+++ b/src/txmempool.cpp
@@ -511,6 +511,36 @@
return ret;
}
+bool CTxMemPool::setAvalancheFinalized(const CTxMemPoolEntryRef &tx) {
+ AssertLockHeld(cs);
+
+ auto it = mapTx.find(tx->GetTx().GetId());
+ if (it == mapTx.end()) {
+ // Trying to finalize a tx that is not in the mempool !
+ return false;
+ }
+
+ setEntries setAncestors;
+ setAncestors.insert(it);
+ if (!CalculateMemPoolAncestors(tx, setAncestors,
+ /*fSearchForParents=*/false)) {
+ // Failed to get a list of parents for this tx. If we finalize it we
+ // might be missing a parent and generate an invalid block.
+ return false;
+ }
+
+ for (txiter ancestor_it : setAncestors) {
+ // It is possible (and normal) that an ancestor is already finalized.
+ // Beware to not account for it in this case.
+ if (finalizedTxs.insert(*ancestor_it)) {
+ totalFinalizedTxSize += (*ancestor_it)->GetTxSize();
+ totalFinalizedTxSigchecks += (*ancestor_it)->GetSigChecks();
+ }
+ }
+
+ return true;
+}
+
CTransactionRef CTxMemPool::get(const TxId &txid) const {
LOCK(cs);
indexed_transaction_set::const_iterator i = mapTx.find(txid);
diff --git a/test/functional/abc_p2p_avalanche_transaction_finalization.py b/test/functional/abc_p2p_avalanche_transaction_finalization.py
new file mode 100644
--- /dev/null
+++ b/test/functional/abc_p2p_avalanche_transaction_finalization.py
@@ -0,0 +1,175 @@
+# Copyright (c) 2025 The Bitcoin developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test avalanche transaction finalization."""
+
+from test_framework.avatools import can_find_inv_in_poll, get_ava_p2p_interface
+from test_framework.blocktools import COINBASE_MATURITY
+from test_framework.messages import AvalancheTxVoteError
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal, uint256_hex
+from test_framework.wallet import MiniWallet
+
+QUORUM_NODE_COUNT = 16
+
+
+class AvalancheTransactionFinalizationTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.noban_tx_relay = True
+ self.extra_args = [
+ [
+ "-avalanchepreconsensus=1",
+ "-avacooldown=0",
+ "-avaproofstakeutxoconfirmations=1",
+ # Low enough for coinbase transactions to be staked in valid proofs
+ "-avaproofstakeutxodustthreshold=1000000",
+ "-avaminquorumstake=0",
+ "-avaminavaproofsnodecount=0",
+ ]
+ ]
+
+ def finalize_tip(self):
+ tip = self.nodes[0].getbestblockhash()
+
+ def vote_until_final():
+ can_find_inv_in_poll(
+ self.quorum,
+ int(tip, 16),
+ response=AvalancheTxVoteError.ACCEPTED,
+ other_response=AvalancheTxVoteError.UNKNOWN,
+ )
+ return self.nodes[0].isfinalblock(tip)
+
+ self.wait_until(vote_until_final)
+
+ def finalize_tx(self, txid, other_response=AvalancheTxVoteError.ACCEPTED):
+ def vote_until_final():
+ can_find_inv_in_poll(
+ self.quorum,
+ txid,
+ response=AvalancheTxVoteError.ACCEPTED,
+ other_response=other_response,
+ )
+ return self.nodes[0].isfinaltransaction(uint256_hex(txid))
+
+ self.wait_until(vote_until_final, timeout=10)
+
+ def test_simple_txs(self):
+ self.log.info("Check the finalization of simple non-chained txs")
+
+ node = self.nodes[0]
+
+ # Make some valid txs
+ num_txs = 5
+ self.generate(self.wallet, num_txs)
+ self.finalize_tip()
+
+ assert_equal(node.getmempoolinfo()["size"], 0)
+ txids = [
+ int(self.wallet.send_self_transfer(from_node=node)["txid"], 16)
+ for _ in range(num_txs)
+ ]
+ assert_equal(node.getmempoolinfo()["size"], num_txs)
+
+ for txid in txids:
+ self.finalize_tx(txid)
+
+ def test_chained_txs(self):
+ self.log.info("Check the finalization of chained txs")
+
+ node = self.nodes[0]
+
+ # Make some valid chained txs
+ num_txs = 5
+ self.generate(self.wallet, num_txs)
+ self.finalize_tip()
+
+ assert_equal(node.getmempoolinfo()["size"], 0)
+ txs = self.wallet.send_self_transfer_chain(from_node=node, chain_length=num_txs)
+ assert_equal(node.getmempoolinfo()["size"], num_txs)
+
+ # Keep the chain ordering. We only vote for the penultimate child tx
+ txids = [tx["txid"] for tx in txs]
+ self.finalize_tx(
+ int(txids[-2], 16), other_response=AvalancheTxVoteError.UNKNOWN
+ )
+
+ # The ancestors should all be final as well
+ assert all(node.isfinaltransaction(txid) for txid in txids[:-1])
+ # But not the descendant
+ assert not node.isfinaltransaction(txids[-1])
+
+ def test_diamond_txs(self):
+ self.log.info("Check the finalization of diamond shaped txs")
+
+ node = self.nodes[0]
+
+ # tx2
+ # / \
+ # tx1 tx4
+ # \ /
+ # tx3
+ # \
+ # tx5
+ num_txs = 5
+ self.generate(self.wallet, num_txs)
+ self.finalize_tip()
+
+ assert_equal(node.getmempoolinfo()["size"], 0)
+ tx1 = self.wallet.create_self_transfer_multi(num_outputs=2)
+ tx2 = self.wallet.create_self_transfer(utxo_to_spend=tx1["new_utxos"][0])
+ tx3 = self.wallet.create_self_transfer_multi(
+ utxos_to_spend=[tx1["new_utxos"][1]], num_outputs=2
+ )
+ tx4 = self.wallet.create_self_transfer_multi(
+ utxos_to_spend=[tx2["new_utxo"], tx3["new_utxos"][0]]
+ )
+ tx5 = self.wallet.create_self_transfer(utxo_to_spend=tx3["new_utxos"][1])
+
+ txids = [
+ self.wallet.sendrawtransaction(from_node=node, tx_hex=tx["hex"])
+ for tx in (tx1, tx2, tx3, tx4, tx5)
+ ]
+ assert_equal(node.getmempoolinfo()["size"], num_txs)
+
+ # Finalizing tx4 should finalize all the ancestors as well
+ self.finalize_tx(
+ int(txids[-2], 16), other_response=AvalancheTxVoteError.UNKNOWN
+ )
+ assert all(node.isfinaltransaction(txid) for txid in txids[:-1])
+
+ # But not tx5
+ assert not node.isfinaltransaction(txids[-1])
+
+ def run_test(self):
+ def get_quorum():
+ return [
+ get_ava_p2p_interface(self, self.nodes[0])
+ for _ in range(0, QUORUM_NODE_COUNT)
+ ]
+
+ self.quorum = get_quorum()
+ assert self.nodes[0].getavalancheinfo()["ready_to_poll"]
+
+ # Let's clean up the non transaction inventories from our avalanche polls
+ def has_finalized_proof(proofid):
+ can_find_inv_in_poll(self.quorum, proofid)
+ return self.nodes[0].getrawavalancheproof(uint256_hex(proofid))["finalized"]
+
+ for q in self.quorum:
+ self.wait_until(lambda: has_finalized_proof(q.proof.proofid))
+
+ self.finalize_tip()
+
+ self.wallet = MiniWallet(self.nodes[0])
+ # Mature the coinbases
+ self.generate(self.wallet, COINBASE_MATURITY)
+
+ self.test_simple_txs()
+ self.test_chained_txs()
+ self.test_diamond_txs()
+
+
+if __name__ == "__main__":
+ AvalancheTransactionFinalizationTest().main()

File Metadata

Mime Type
text/plain
Expires
Sat, Mar 1, 12:03 (3 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5187759
Default Alt Text
D17717.diff (8 KB)

Event Timeline