Compare commits

...

4 commits

Author SHA1 Message Date
Gregory Sanders
b28d6ee6f4
Merge 846a138728 into 66aa6a47bd 2025-01-08 20:43:05 +01:00
glozow
66aa6a47bd
Merge bitcoin/bitcoin#30391: BlockAssembler: return selected packages virtual size and fee
Some checks are pending
CI / test each commit (push) Waiting to run
CI / macOS 14 native, arm64, no depends, sqlite only, gui (push) Waiting to run
CI / macOS 14 native, arm64, fuzz (push) Waiting to run
CI / Win64 native, VS 2022 (push) Waiting to run
CI / Win64 native fuzz, VS 2022 (push) Waiting to run
CI / ASan + LSan + UBSan + integer, no depends, USDT (push) Waiting to run
7c123c08dd  miner: add package feerate vector to CBlockTemplate (ismaelsadeeq)

Pull request description:

  This PR enables `BlockAssembler` to add all selected packages' fee and virtual size to a vector, and then return the vector as a member of `CBlockTemplate` struct.

  This PR is the first step in the https://github.com/bitcoin/bitcoin/issues/30392 project.

  The packages' vsize and fee are used in #30157 to select a percentile fee rate of the top block in the mempool.

ACKs for top commit:
  rkrux:
    tACK 7c123c08dd
  ryanofsky:
    Code review ACK 7c123c08dd. Changes since last review are rebasing due to a test conflict, giving the new field a better name and description, resolving the test conflict, and renaming a lot of test variables. The actual code change is still one-line change.
  glozow:
    reACK 7c123c08dd

Tree-SHA512: 767b0b3d4273cf1589fd2068d729a66c7414c0f9574b15989fbe293f8c85cd6c641dd783cde55bfabab32cd047d7d8a071d6897b06ed4295c0d071e588de0861
2025-01-08 13:01:23 -05:00
ismaelsadeeq
7c123c08dd
miner: add package feerate vector to CBlockTemplate
- The package feerates are ordered by the sequence in which
  packages are selected for inclusion in the block template.

- The commit also tests this new behaviour.

Co-authored-by: willcl-ark <will@256k1.dev>
2025-01-07 15:29:17 -05:00
Greg Sanders
846a138728 func test: Expand tx download preference tests
1. Check that outbound nodes are treated
the same as whitelisted connections for
the purposes of getdata delays

2. Add test case that demonstrates
download retries are preferentially
given to outbound (preferred) connections
even when multiple announcements are
considered ready.
2024-12-09 10:25:03 -05:00
4 changed files with 115 additions and 14 deletions

View file

@ -421,6 +421,7 @@ void BlockAssembler::addPackageTxs(int& nPackagesSelected, int& nDescendantsUpda
}
++nPackagesSelected;
pblocktemplate->m_package_feerates.emplace_back(packageFees, static_cast<int32_t>(packageSize));
// Update transactions that depend on each of these
nDescendantsUpdated += UpdatePackagesForAdded(mempool, ancestors, mapModifiedTx);

View file

@ -10,6 +10,7 @@
#include <policy/policy.h>
#include <primitives/block.h>
#include <txmempool.h>
#include <util/feefrac.h>
#include <memory>
#include <optional>
@ -39,6 +40,9 @@ struct CBlockTemplate
std::vector<CAmount> vTxFees;
std::vector<int64_t> vTxSigOpsCost;
std::vector<unsigned char> vchCoinbaseCommitment;
/* A vector of package fee rates, ordered by the sequence in which
* packages are selected for inclusion in the block template.*/
std::vector<FeeFrac> m_package_feerates;
};
// Container for tracking updates to ancestor feerate as we include (parent)

View file

@ -16,6 +16,7 @@
#include <txmempool.h>
#include <uint256.h>
#include <util/check.h>
#include <util/feefrac.h>
#include <util/strencodings.h>
#include <util/time.h>
#include <util/translation.h>
@ -25,6 +26,7 @@
#include <test/util/setup_common.h>
#include <memory>
#include <vector>
#include <boost/test/unit_test.hpp>
@ -123,19 +125,22 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
tx.vout[0].nValue = 5000000000LL - 1000;
// This tx has a low fee: 1000 satoshis
Txid hashParentTx = tx.GetHash(); // save this txid for later use
AddToMempool(tx_mempool, entry.Fee(1000).Time(Now<NodeSeconds>()).SpendsCoinbase(true).FromTx(tx));
const auto parent_tx{entry.Fee(1000).Time(Now<NodeSeconds>()).SpendsCoinbase(true).FromTx(tx)};
AddToMempool(tx_mempool, parent_tx);
// This tx has a medium fee: 10000 satoshis
tx.vin[0].prevout.hash = txFirst[1]->GetHash();
tx.vout[0].nValue = 5000000000LL - 10000;
Txid hashMediumFeeTx = tx.GetHash();
AddToMempool(tx_mempool, entry.Fee(10000).Time(Now<NodeSeconds>()).SpendsCoinbase(true).FromTx(tx));
const auto medium_fee_tx{entry.Fee(10000).Time(Now<NodeSeconds>()).SpendsCoinbase(true).FromTx(tx)};
AddToMempool(tx_mempool, medium_fee_tx);
// This tx has a high fee, but depends on the first transaction
tx.vin[0].prevout.hash = hashParentTx;
tx.vout[0].nValue = 5000000000LL - 1000 - 50000; // 50k satoshi fee
Txid hashHighFeeTx = tx.GetHash();
AddToMempool(tx_mempool, entry.Fee(50000).Time(Now<NodeSeconds>()).SpendsCoinbase(false).FromTx(tx));
const auto high_fee_tx{entry.Fee(50000).Time(Now<NodeSeconds>()).SpendsCoinbase(false).FromTx(tx)};
AddToMempool(tx_mempool, high_fee_tx);
std::unique_ptr<BlockTemplate> block_template = mining->createNewBlock(options);
BOOST_REQUIRE(block_template);
@ -145,6 +150,21 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
BOOST_CHECK(block.vtx[2]->GetHash() == hashHighFeeTx);
BOOST_CHECK(block.vtx[3]->GetHash() == hashMediumFeeTx);
// Test the inclusion of package feerates in the block template and ensure they are sequential.
const auto block_package_feerates = BlockAssembler{m_node.chainman->ActiveChainstate(), &tx_mempool, options}.CreateNewBlock()->m_package_feerates;
BOOST_CHECK(block_package_feerates.size() == 2);
// parent_tx and high_fee_tx are added to the block as a package.
const auto combined_txs_fee = parent_tx.GetFee() + high_fee_tx.GetFee();
const auto combined_txs_size = parent_tx.GetTxSize() + high_fee_tx.GetTxSize();
FeeFrac package_feefrac{combined_txs_fee, combined_txs_size};
// The package should be added first.
BOOST_CHECK(block_package_feerates[0] == package_feefrac);
// The medium_fee_tx should be added next.
FeeFrac medium_tx_feefrac{medium_fee_tx.GetFee(), medium_fee_tx.GetTxSize()};
BOOST_CHECK(block_package_feerates[1] == medium_tx_feefrac);
// Test that a package below the block min tx fee doesn't get included
tx.vin[0].prevout.hash = hashHighFeeTx;
tx.vout[0].nValue = 5000000000LL - 1000 - 50000; // 0 fee

View file

@ -6,6 +6,7 @@
Test transaction download behavior
"""
from decimal import Decimal
from enum import Enum
import time
from test_framework.mempool_util import (
@ -44,17 +45,26 @@ class TestP2PConn(P2PInterface):
# Constants from net_processing
GETDATA_TX_INTERVAL = 60 # seconds
INBOUND_PEER_TX_DELAY = 2 # seconds
NONPREF_PEER_TX_DELAY = 2 # seconds
INBOUND_PEER_TX_DELAY = NONPREF_PEER_TX_DELAY # inbound is non-preferred
TXID_RELAY_DELAY = 2 # seconds
OVERLOADED_PEER_DELAY = 2 # seconds
MAX_GETDATA_IN_FLIGHT = 100
MAX_PEER_TX_ANNOUNCEMENTS = 5000
NONPREF_PEER_TX_DELAY = 2
# Python test constants
NUM_INBOUND = 10
MAX_GETDATA_INBOUND_WAIT = GETDATA_TX_INTERVAL + INBOUND_PEER_TX_DELAY + TXID_RELAY_DELAY
class ConnectionType(Enum):
""" Different connection types
1. INBOUND: Incoming connection, not whitelisted
2. OUTBOUND: Outgoing connection
3. WHITELIST: Incoming connection, but whitelisted
"""
INBOUND = 0
OUTBOUND = 1
WHITELIST = 2
class TxDownloadTest(BitcoinTestFramework):
def set_test_params(self):
@ -193,18 +203,31 @@ class TxDownloadTest(BitcoinTestFramework):
peer_notfound.send_and_ping(msg_notfound(vec=[CInv(MSG_WTX, WTXID)])) # Send notfound, so that fallback peer is selected
peer_fallback.wait_until(lambda: peer_fallback.tx_getdata_count >= 1, timeout=1)
def test_preferred_inv(self, preferred=False):
if preferred:
self.log.info('Check invs from preferred peers are downloaded immediately')
def test_preferred_inv(self, connection_type: ConnectionType):
if connection_type == ConnectionType.WHITELIST:
self.log.info('Check invs from preferred (whitelisted) peers are downloaded immediately')
self.restart_node(0, extra_args=['-whitelist=noban@127.0.0.1'])
else:
elif connection_type == ConnectionType.OUTBOUND:
self.log.info('Check invs from preferred (outbound) peers are downloaded immediately')
self.restart_node(0)
elif connection_type == ConnectionType.INBOUND:
self.log.info('Check invs from non-preferred peers are downloaded after {} s'.format(NONPREF_PEER_TX_DELAY))
self.restart_node(0)
else:
raise Exception("invalid connection_type")
mock_time = int(time.time() + 1)
self.nodes[0].setmocktime(mock_time)
peer = self.nodes[0].add_p2p_connection(TestP2PConn())
if connection_type == ConnectionType.OUTBOUND:
peer = self.nodes[0].add_outbound_p2p_connection(
TestP2PConn(), wait_for_verack=True, p2p_idx=1, connection_type="outbound-full-relay")
else:
peer = self.nodes[0].add_p2p_connection(TestP2PConn())
peer.send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)]))
peer.sync_with_ping()
if preferred:
if connection_type != ConnectionType.INBOUND:
peer.wait_until(lambda: peer.tx_getdata_count >= 1, timeout=1)
else:
with p2p_lock:
@ -212,6 +235,58 @@ class TxDownloadTest(BitcoinTestFramework):
self.nodes[0].setmocktime(mock_time + NONPREF_PEER_TX_DELAY)
peer.wait_until(lambda: peer.tx_getdata_count >= 1, timeout=1)
def test_preferred_tiebreaker_inv(self):
self.log.info("Test that preferred peers are always selected over non-preferred when ready")
self.restart_node(0)
self.nodes[0].setmocktime(int(time.time()))
# Peer that is immediately asked, but never responds.
# This will set us up to have two ready requests, one
# of which is preferred and one which is not
unresponsive_peer = self.nodes[0].add_outbound_p2p_connection(
TestP2PConn(), wait_for_verack=True, p2p_idx=0, connection_type="outbound-full-relay")
unresponsive_peer.send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)]))
unresponsive_peer.sync_with_ping()
unresponsive_peer.wait_until(lambda: unresponsive_peer.tx_getdata_count >= 1, timeout=1)
# A bunch of incoming (non-preferred) connections that advertise the same tx
non_pref_peers = []
NUM_INBOUND = 10
for _ in range(NUM_INBOUND):
non_pref_peers.append(self.nodes[0].add_p2p_connection(TestP2PConn()))
non_pref_peers[-1].send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)]))
non_pref_peers[-1].sync_with_ping()
# Check that no request made due to in-flight
self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY)
with p2p_lock:
for peer in non_pref_peers:
assert_equal(peer.tx_getdata_count, 0)
# Now add another outbound (preferred) which is immediately ready for consideration
# upon advertisement
pref_peer = self.nodes[0].add_outbound_p2p_connection(
TestP2PConn(), wait_for_verack=True, p2p_idx=1, connection_type="outbound-full-relay")
pref_peer.send_message(msg_inv([CInv(t=MSG_WTX, h=0xff00ff00)]))
assert_equal(len(self.nodes[0].getpeerinfo()), NUM_INBOUND + 2)
# Still have to wait for in-flight to timeout
with p2p_lock:
assert_equal(pref_peer.tx_getdata_count, 0)
# Timeout in-flight
self.nodes[0].bumpmocktime(GETDATA_TX_INTERVAL - NONPREF_PEER_TX_DELAY)
# Preferred peers are *always* selected next if ready
pref_peer.wait_until(lambda: pref_peer.tx_getdata_count >= 1, timeout=10)
# And none for non-preferred
for non_pref_peer in non_pref_peers:
with p2p_lock:
assert_equal(non_pref_peer.tx_getdata_count, 0)
def test_txid_inv_delay(self, glob_wtxid=False):
self.log.info('Check that inv from a txid-relay peers are delayed by {} s, with a wtxid peer {}'.format(TXID_RELAY_DELAY, glob_wtxid))
self.restart_node(0, extra_args=['-whitelist=noban@127.0.0.1'])
@ -277,8 +352,10 @@ class TxDownloadTest(BitcoinTestFramework):
self.test_expiry_fallback()
self.test_disconnect_fallback()
self.test_notfound_fallback()
self.test_preferred_inv()
self.test_preferred_inv(True)
self.test_preferred_tiebreaker_inv()
self.test_preferred_inv(ConnectionType.INBOUND)
self.test_preferred_inv(ConnectionType.OUTBOUND)
self.test_preferred_inv(ConnectionType.WHITELIST)
self.test_txid_inv_delay()
self.test_txid_inv_delay(True)
self.test_large_inv_batch()
@ -304,6 +381,5 @@ class TxDownloadTest(BitcoinTestFramework):
self.log.info("Nodes are setup with {} incoming connections each".format(NUM_INBOUND))
test()
if __name__ == '__main__':
TxDownloadTest(__file__).main()