Merge bitcoin/bitcoin#31384: mining: bugfix: Fix duplicate coinbase tx weight reservation

386eecff5f doc: add release notes (ismaelsadeeq)
3eaa0a3b66 miner: init: add `-blockreservedweight` startup option (ismaelsadeeq)
777434a2cd doc: rpc: improve `getmininginfo` help text (ismaelsadeeq)
c8acd4032d init: fail to start when `-blockmaxweight` exceeds `MAX_BLOCK_WEIGHT` (ismaelsadeeq)
5bb31633cc test: add `-blockmaxweight` startup option functional test (ismaelsadeeq)
2c7d90a6d6 miner: bugfix: fix duplicate weight reservation in block assembler (ismaelsadeeq)

Pull request description:

  * This PR attempts to fix the duplicate coinbase weight reservation issue we currently have.
  * Fixes #21950

  We reserve 4000 weight units for coinbase transaction in `DEFAULT_BLOCK_MAX_WEIGHT`

  7590e93bc7/src/policy/policy.h (L23)

  And also reserve additional `4000` weight units in the default `BlockCreationOptions` struct.

  7590e93bc7/src/node/types.h (L36-L40)

  **Motivation**

  - This issue was first noticed during a review here https://github.com/bitcoin/bitcoin/pull/11100#discussion_r136157411)
  - It was later reported in issue #21950.
  - I also came across the bug while writing a test for building the block template. I could not create a block template above `3,992,000` in the block assembler, and this was not documented anywhere. It took me a while to realize that we were reserving space for the coinbase transaction weight twice.

  ---
  This PR fixes this by consolidating the reservation to be in a single location in the codebase.

  This PR then adds a new startup option `-blockreservedweight` whose default is `8000` that can be used to lower or increase the block reserved weight for block header, txs count, coinbase tx.

ACKs for top commit:
  Sjors:
    ACK 386eecff5f
  fjahr:
    Code review ACK 386eecff5f
  glozow:
    utACK 386eecff5f, nonblocking nits. I do think the release notes should be clarified more
  pinheadmz:
    ACK 386eecff5f

Tree-SHA512: f27efa1da57947b7f4d42b9322b83d13afe73dd749dd9cac49360002824dd41c99a876a610554ac2d67bad7485020b9dcc423a8e6748fc79d6a10de6d4357d4c
This commit is contained in:
glozow
2025-02-10 08:14:50 -05:00
11 changed files with 202 additions and 19 deletions

View File

@@ -26,7 +26,11 @@ from test_framework.messages import (
CBlock,
CBlockHeader,
COIN,
DEFAULT_BLOCK_RESERVED_WEIGHT,
MAX_BLOCK_WEIGHT,
MINIMUM_BLOCK_RESERVED_WEIGHT,
ser_uint256,
WITNESS_SCALE_FACTOR
)
from test_framework.p2p import P2PDataStore
from test_framework.test_framework import BitcoinTestFramework
@@ -77,7 +81,7 @@ class MiningTest(BitcoinTestFramework):
mining_info = self.nodes[0].getmininginfo()
assert_equal(mining_info['blocks'], 200)
assert_equal(mining_info['currentblocktx'], 0)
assert_equal(mining_info['currentblockweight'], 4000)
assert_equal(mining_info['currentblockweight'], DEFAULT_BLOCK_RESERVED_WEIGHT)
self.log.info('test blockversion')
self.restart_node(0, extra_args=[f'-mocktime={t}', '-blockversion=1337'])
@@ -195,6 +199,116 @@ class MiningTest(BitcoinTestFramework):
assert_equal(result, "inconclusive")
assert_equal(prune_node.getblock(pruned_blockhash, verbosity=0), pruned_block)
def send_transactions(self, utxos, fee_rate, target_vsize):
"""
Helper to create and send transactions with the specified target virtual size and fee rate.
"""
for utxo in utxos:
self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=utxo,
target_vsize=target_vsize,
fee_rate=fee_rate,
)
def verify_block_template(self, expected_tx_count, expected_weight):
"""
Create a block template and check that it satisfies the expected transaction count and total weight.
"""
response = self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)
self.log.info(f"Testing block template: contains {expected_tx_count} transactions, and total weight <= {expected_weight}")
assert_equal(len(response["transactions"]), expected_tx_count)
total_weight = sum(transaction["weight"] for transaction in response["transactions"])
assert_greater_than_or_equal(expected_weight, total_weight)
def test_block_max_weight(self):
self.log.info("Testing default and custom -blockmaxweight startup options.")
# Restart the node to allow large transactions
LARGE_TXS_COUNT = 10
LARGE_VSIZE = int(((MAX_BLOCK_WEIGHT - DEFAULT_BLOCK_RESERVED_WEIGHT) / WITNESS_SCALE_FACTOR) / LARGE_TXS_COUNT)
HIGH_FEERATE = Decimal("0.0003")
self.restart_node(0, extra_args=[f"-datacarriersize={LARGE_VSIZE}"])
# Ensure the mempool is empty
assert_equal(len(self.nodes[0].getrawmempool()), 0)
# Generate UTXOs and send 10 large transactions with a high fee rate
utxos = [self.wallet.get_utxo(confirmed_only=True) for _ in range(LARGE_TXS_COUNT + 4)] # Add 4 more utxos that will be used in the test later
self.send_transactions(utxos[:LARGE_TXS_COUNT], HIGH_FEERATE, LARGE_VSIZE)
# Send 2 normal transactions with a lower fee rate
NORMAL_VSIZE = int(2000 / WITNESS_SCALE_FACTOR)
NORMAL_FEERATE = Decimal("0.0001")
self.send_transactions(utxos[LARGE_TXS_COUNT:LARGE_TXS_COUNT + 2], NORMAL_FEERATE, NORMAL_VSIZE)
# Check that the mempool contains all transactions
self.log.info(f"Testing that the mempool contains {LARGE_TXS_COUNT + 2} transactions.")
assert_equal(len(self.nodes[0].getrawmempool()), LARGE_TXS_COUNT + 2)
# Verify the block template includes only the 10 high-fee transactions
self.log.info("Testing that the block template includes only the 10 large transactions.")
self.verify_block_template(
expected_tx_count=LARGE_TXS_COUNT,
expected_weight=MAX_BLOCK_WEIGHT - DEFAULT_BLOCK_RESERVED_WEIGHT,
)
# Test block template creation with custom -blockmaxweight
custom_block_weight = MAX_BLOCK_WEIGHT - 2000
# Reducing the weight by 2000 units will prevent 1 large transaction from fitting into the block.
self.restart_node(0, extra_args=[f"-datacarriersize={LARGE_VSIZE}", f"-blockmaxweight={custom_block_weight}"])
self.log.info("Testing the block template with custom -blockmaxweight to include 9 large and 2 normal transactions.")
self.verify_block_template(
expected_tx_count=11,
expected_weight=MAX_BLOCK_WEIGHT - DEFAULT_BLOCK_RESERVED_WEIGHT - 2000,
)
# Ensure the block weight does not exceed the maximum
self.log.info(f"Testing that the block weight will never exceed {MAX_BLOCK_WEIGHT - DEFAULT_BLOCK_RESERVED_WEIGHT}.")
self.restart_node(0, extra_args=[f"-datacarriersize={LARGE_VSIZE}", f"-blockmaxweight={MAX_BLOCK_WEIGHT}"])
self.log.info("Sending 2 additional normal transactions to fill the mempool to the maximum block weight.")
self.send_transactions(utxos[LARGE_TXS_COUNT + 2:], NORMAL_FEERATE, NORMAL_VSIZE)
self.log.info(f"Testing that the mempool's weight matches the maximum block weight: {MAX_BLOCK_WEIGHT}.")
assert_equal(self.nodes[0].getmempoolinfo()['bytes'] * WITNESS_SCALE_FACTOR, MAX_BLOCK_WEIGHT)
self.log.info("Testing that the block template includes only 10 transactions and cannot reach full block weight.")
self.verify_block_template(
expected_tx_count=LARGE_TXS_COUNT,
expected_weight=MAX_BLOCK_WEIGHT - DEFAULT_BLOCK_RESERVED_WEIGHT,
)
self.log.info("Test -blockreservedweight startup option.")
# Lowering the -blockreservedweight by 4000 will allow for two more transactions.
self.restart_node(0, extra_args=[f"-datacarriersize={LARGE_VSIZE}", "-blockreservedweight=4000"])
self.verify_block_template(
expected_tx_count=12,
expected_weight=MAX_BLOCK_WEIGHT - 4000,
)
self.log.info("Test that node will fail to start when user provide invalid -blockreservedweight")
self.stop_node(0)
self.nodes[0].assert_start_raises_init_error(
extra_args=[f"-blockreservedweight={MAX_BLOCK_WEIGHT + 1}"],
expected_msg=f"Error: Specified -blockreservedweight ({MAX_BLOCK_WEIGHT + 1}) exceeds consensus maximum block weight ({MAX_BLOCK_WEIGHT})",
)
self.log.info(f"Test that node will fail to start when user provide -blockreservedweight below {MINIMUM_BLOCK_RESERVED_WEIGHT}")
self.stop_node(0)
self.nodes[0].assert_start_raises_init_error(
extra_args=[f"-blockreservedweight={MINIMUM_BLOCK_RESERVED_WEIGHT - 1}"],
expected_msg=f"Error: Specified -blockreservedweight ({MINIMUM_BLOCK_RESERVED_WEIGHT - 1}) is lower than minimum safety value of ({MINIMUM_BLOCK_RESERVED_WEIGHT})",
)
self.log.info("Test that node will fail to start when user provide invalid -blockmaxweight")
self.stop_node(0)
self.nodes[0].assert_start_raises_init_error(
extra_args=[f"-blockmaxweight={MAX_BLOCK_WEIGHT + 1}"],
expected_msg=f"Error: Specified -blockmaxweight ({MAX_BLOCK_WEIGHT + 1}) exceeds consensus maximum block weight ({MAX_BLOCK_WEIGHT})",
)
def run_test(self):
node = self.nodes[0]
self.wallet = MiniWallet(node)
@@ -420,6 +534,7 @@ class MiningTest(BitcoinTestFramework):
assert_equal(node.submitblock(hexdata=block.serialize().hex()), 'duplicate') # valid
self.test_blockmintxfee_parameter()
self.test_block_max_weight()
self.test_timewarp()
self.test_pruning()

View File

@@ -33,6 +33,8 @@ from test_framework.util import assert_equal
MAX_LOCATOR_SZ = 101
MAX_BLOCK_WEIGHT = 4000000
DEFAULT_BLOCK_RESERVED_WEIGHT = 8000
MINIMUM_BLOCK_RESERVED_WEIGHT = 2000
MAX_BLOOM_FILTER_SIZE = 36000
MAX_BLOOM_HASH_FUNCS = 50