diff --git a/src/node/miner.cpp b/src/node/miner.cpp index 396b083e62..b9bc58e9f6 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -68,9 +68,12 @@ void RegenerateCommitments(CBlock& block, ChainstateManager& chainman) BlockCreateOptions BlockCreateOptions::Clamped() const { BlockAssembler::Options options = *this; - Assert(options.coinbase_max_additional_size <= MAX_BLOCK_SERIALIZED_SIZE - 1000); - Assert(options.coinbase_max_additional_weight <= DEFAULT_BLOCK_MAX_WEIGHT); - Assert(options.coinbase_output_max_additional_sigops <= MAX_BLOCK_SIGOPS_COST); + constexpr size_t theoretical_min_gentx_sz = 1+4+1+36+1+1+4+1+4; + constexpr size_t theoretical_min_gentx_weight = theoretical_min_gentx_sz * WITNESS_SCALE_FACTOR; + CHECK_NONFATAL(options.coinbase_max_additional_size <= MAX_BLOCK_SERIALIZED_SIZE - 1000); + CHECK_NONFATAL(options.coinbase_max_additional_weight <= DEFAULT_BLOCK_MAX_WEIGHT); + CHECK_NONFATAL(options.coinbase_max_additional_weight >= theoretical_min_gentx_weight); + CHECK_NONFATAL(options.coinbase_output_max_additional_sigops <= MAX_BLOCK_SIGOPS_COST); // Limit size to between coinbase_max_additional_size and MAX_BLOCK_SERIALIZED_SIZE-1K for sanity: options.nBlockMaxSize = std::clamp(options.nBlockMaxSize, options.coinbase_max_additional_size, MAX_BLOCK_SERIALIZED_SIZE - 1000); // Limit weight to between coinbase_max_additional_weight and DEFAULT_BLOCK_MAX_WEIGHT for sanity: diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index f9ad07bb3a..c4a2f046da 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -586,6 +586,9 @@ static RPCHelpMan getblocktemplate() {"mode", RPCArg::Type::STR, /* treat as named arg */ RPCArg::Optional::OMITTED, "This must be set to \"template\", \"proposal\" (see BIP 23), or omitted"}, {"blockmaxsize", RPCArg::Type::NUM, RPCArg::DefaultHint{"set by -blockmaxsize"}, "limit returned block to specified size (disables template cache)"}, {"blockmaxweight", RPCArg::Type::NUM, RPCArg::DefaultHint{"set by -blockmaxweight"}, "limit returned block to specified weight (disables template cache)"}, + {"blockreservedsigops", RPCArg::Type::NUM, RPCArg::Default{node::BlockCreateOptions{}.coinbase_output_max_additional_sigops}, "reserve specified number of sigops in returned block for generation transaction (disables template cache)"}, + {"blockreservedsize", RPCArg::Type::NUM, RPCArg::Default{node::BlockCreateOptions{}.coinbase_max_additional_size}, "reserve specified size in returned block for generation transaction (disables template cache)"}, + {"blockreservedweight", RPCArg::Type::NUM, RPCArg::Default{node::BlockCreateOptions{}.coinbase_max_additional_weight}, "reserve specified weight in returned block for generation transaction (disables template cache)"}, {"capabilities", RPCArg::Type::ARR, /* treat as named arg */ RPCArg::Optional::OMITTED, "A list of strings", { {"str", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "client side supported feature, 'longpoll', 'coinbasevalue', 'proposal', 'skip_validity_test', 'serverlist', 'workid'"}, @@ -740,6 +743,15 @@ static RPCHelpMan getblocktemplate() if (!oparam["blockmaxweight"].isNull()) { options.nBlockMaxWeight = oparam["blockmaxweight"].getInt(); } + if (!oparam["blockreservedsize"].isNull()) { + options.coinbase_max_additional_size = oparam["blockreservedsize"].getInt(); + } + if (!oparam["blockreservedweight"].isNull()) { + options.coinbase_max_additional_weight = oparam["blockreservedweight"].getInt(); + } + if (!oparam["blockreservedsigops"].isNull()) { + options.coinbase_output_max_additional_sigops = oparam["blockreservedsigops"].getInt(); + } if (!oparam["minfeerate"].isNull()) { options.blockMinFeeRate = CFeeRate{AmountFromValue(oparam["minfeerate"]), COIN /* sat/vB */}; } diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 1322ebe47b..4cd7f8efb7 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -33,7 +33,7 @@ from test_framework.util import ( assert_raises_rpc_error, get_fee, ) -from test_framework.wallet import MiniWallet +from test_framework.wallet import MiniWallet, MiniWalletMode DIFFICULTY_ADJUSTMENT_INTERVAL = 144 @@ -44,6 +44,7 @@ ASSUMED_BLOCK_OVERHEAD_WEIGHT = ASSUMED_BLOCK_OVERHEAD_SIZE * WITNESS_SCALE_FACT VERSIONBITS_TOP_BITS = 0x20000000 VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT = 28 DEFAULT_BLOCK_MIN_TX_FEE = 1000 # default `-blockmintxfee` setting [sat/kvB] +MAX_SIGOP_COST = 80000 def assert_template(node, block, expect, rehash=True): @@ -142,13 +143,25 @@ class MiningTest(BitcoinTestFramework): node = self.nodes[0] self.log.info("Preparing mempool") + self.restart_node(0, extra_args=['-limitancestorcount=1000', '-limitancestorsize=7000', '-limitdescendantcount=1000', '-limitdescendantsize=7000']) # Fill the mempool - while node.getmempoolinfo()['bytes'] < 200000: - self.wallet.send_self_transfer_multi( + target_mempool_size = 200000 + last_tx_size = 0 + utxo = self.wallet.get_utxo() # save for small coins + while node.getmempoolinfo()['bytes'] < target_mempool_size - last_tx_size: + tx = self.wallet.send_self_transfer_multi( from_node=self.nodes[0], num_outputs=1000, ) + last_tx_size = len(tx['hex']) / 2 + while node.getmempoolinfo()['bytes'] < 200000: + tx = self.wallet.send_self_transfer_multi( + utxos_to_spend=[utxo], + from_node=node, + num_outputs=1, + ) + utxo = tx['new_utxos'][0] self.log.info("Test blockmaxsize RPC param") req = copy.deepcopy(NORMAL_GBT_REQUEST_PARAMS) @@ -164,6 +177,21 @@ class MiningTest(BitcoinTestFramework): assert blk_size > last_size last_size = blk_size + self.log.info("Test blockreservedsize RPC param") + req = copy.deepcopy(NORMAL_GBT_REQUEST_PARAMS) + req['blockmaxsize'] = 150000 + normal_size = (sum(len(tx['data']) for tx in self.nodes[0].getblocktemplate(req)['transactions']) // 2) + last_size = 0 + for reserved_size in (100000, 10000, 100): + self.log.info(f"-> Test RPC param blockreservedsize={reserved_size}...") + req['blockreservedsize'] = reserved_size + tmpl = self.nodes[0].getblocktemplate(req) + blk_size = (sum(len(tx['data']) for tx in tmpl['transactions']) // 2) + assert blk_size < normal_size if reserved_size > 1000 else blk_size > normal_size + assert blk_size + reserved_size <= req['blockmaxsize'] + assert blk_size > last_size + last_size = blk_size + self.log.info("Test blockmaxweight RPC param") req = copy.deepcopy(NORMAL_GBT_REQUEST_PARAMS) normal_weight = ASSUMED_BLOCK_OVERHEAD_WEIGHT + sum(tx['weight'] for tx in self.nodes[0].getblocktemplate(req)['transactions']) @@ -178,6 +206,38 @@ class MiningTest(BitcoinTestFramework): assert blk_weight > last_weight last_weight = blk_weight + self.log.info("Test blockreservedweight RPC param") + req = copy.deepcopy(NORMAL_GBT_REQUEST_PARAMS) + req['blockmaxweight'] = 600000 + normal_weight = sum(tx['weight'] for tx in self.nodes[0].getblocktemplate(req)['transactions']) + last_weight = 0 + for reserved_weight in (400000, 40000, 400): + self.log.info(f"-> Test RPC param blockreservedweight={reserved_weight}...") + req['blockreservedweight'] = reserved_weight + tmpl = self.nodes[0].getblocktemplate(req) + blk_weight = sum(tx['weight'] for tx in tmpl['transactions']) + assert blk_weight < normal_weight if reserved_weight > 4000 else blk_weight > normal_weight + assert blk_weight + reserved_weight <= req['blockmaxweight'] + assert blk_weight > last_weight + last_weight = blk_weight + + self.log.info("Test blockreservedsigops RPC param") + req = copy.deepcopy(NORMAL_GBT_REQUEST_PARAMS) + normal_sigops = sum(tx['sigops'] for tx in self.nodes[0].getblocktemplate(req)['transactions']) + assert normal_sigops + last_sigops = 0 + baseline_sigops = MAX_SIGOP_COST - normal_sigops + for reserved_sigops in (800, 400, 100): + reserved_sigops += baseline_sigops + self.log.info(f"-> Test RPC param blockreservedsigops={reserved_sigops}...") + req['blockreservedsigops'] = reserved_sigops + tmpl = self.nodes[0].getblocktemplate(req) + blk_sigops = sum(tx['sigops'] for tx in tmpl['transactions']) + assert blk_sigops < normal_sigops if reserved_sigops > 400 else blk_sigops > normal_sigops + assert blk_sigops + reserved_sigops <= MAX_SIGOP_COST + assert blk_sigops > last_sigops + last_sigops = blk_sigops + def test_timewarp(self): self.log.info("Test timewarp attack mitigation (BIP94)") node = self.nodes[0] @@ -229,7 +289,7 @@ class MiningTest(BitcoinTestFramework): def run_test(self): node = self.nodes[0] - self.wallet = MiniWallet(node) + self.wallet = MiniWallet(node, mode=MiniWalletMode.RAW_P2PK) self.mine_chain() def assert_submitblock(block, result_str_1, result_str_2=None):