RPC/Mining: getblocktemplate: Support overriding blockreserved{sigops,size,weight} per request

This commit is contained in:
Luke Dashjr 2025-01-30 23:47:45 +00:00
parent 9d3fa237bb
commit 2fe5723811
3 changed files with 82 additions and 7 deletions

View File

@ -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<size_t>(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:

View File

@ -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<size_t>();
}
if (!oparam["blockreservedsize"].isNull()) {
options.coinbase_max_additional_size = oparam["blockreservedsize"].getInt<size_t>();
}
if (!oparam["blockreservedweight"].isNull()) {
options.coinbase_max_additional_weight = oparam["blockreservedweight"].getInt<size_t>();
}
if (!oparam["blockreservedsigops"].isNull()) {
options.coinbase_output_max_additional_sigops = oparam["blockreservedsigops"].getInt<size_t>();
}
if (!oparam["minfeerate"].isNull()) {
options.blockMinFeeRate = CFeeRate{AmountFromValue(oparam["minfeerate"]), COIN /* sat/vB */};
}

View File

@ -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):