Compare commits

...

5 commits

Author SHA1 Message Date
Matias Furszyfer
7bd95a0775
Merge e1d6179b61 into 66aa6a47bd 2025-01-08 20:43:51 +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
furszy
e1d6179b61
descriptors: inference process, do not return unparsable descriptors
The internal script-to-descriptor inference procedure shouldn't return invalid
descriptors or ones that fail the string-to-descriptor parsing process. These
two procedures should always support seamless roundtrips.

This inlines `InferScript`/`InferDescriptor` to the actual descriptors
`ParseScript` logic for the multisig path, ensuring only valid descriptors are
produced.

Examples where this presents an issue:
1) Because the legacy wallet allowed the import of invalid scripts, the process
   could end up inferring and storing an unparsable descriptor, which crashes
   the software during any subsequent wallet load

2) The `decodescript()` RPC command currently return unusable multisig descriptors.
   For example, providing a P2SH multisig redeem script with more than 520 bytes
   results in a descriptor when it shouldn't due to its large size.
2024-12-28 09:42:56 -05:00
furszy
088bbb81e3
refactor: replace hardcoded number, introduce 'MAX_BARE_MULTISIG_PUBKEYS_NUM' 2024-12-02 11:13:16 -05:00
12 changed files with 79 additions and 18 deletions

View file

@ -153,7 +153,9 @@ void ScriptToUniv(const CScript& script, UniValue& out, bool include_hex, bool i
out.pushKV("asm", ScriptToAsmStr(script));
if (include_address) {
out.pushKV("desc", InferDescriptor(script, provider ? *provider : DUMMY_SIGNING_PROVIDER)->ToString());
auto desc = InferDescriptor(script, provider ? *provider : DUMMY_SIGNING_PROVIDER);
// future: make 'InferDescriptor' return the error cause
if (desc) out.pushKV("desc", desc->ToString());
}
if (include_hex) {
out.pushKV("hex", HexStr(script));

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

@ -87,7 +87,7 @@ bool IsStandard(const CScript& scriptPubKey, const std::optional<unsigned>& max_
unsigned char m = vSolutions.front()[0];
unsigned char n = vSolutions.back()[0];
// Support up to x-of-3 multisig txns as standard
if (n < 1 || n > 3)
if (n < 1 || n > MAX_BARE_MULTISIG_PUBKEYS_NUM)
return false;
if (m < 1 || m > n)
return false;

View file

@ -37,6 +37,8 @@ static constexpr unsigned int DEFAULT_INCREMENTAL_RELAY_FEE{1000};
static constexpr unsigned int DEFAULT_BYTES_PER_SIGOP{20};
/** Default for -permitbaremultisig */
static constexpr bool DEFAULT_PERMIT_BAREMULTISIG{true};
/** The maximum number of pubkeys in a bare multisig output script */
static constexpr unsigned int MAX_BARE_MULTISIG_PUBKEYS_NUM{3};
/** The maximum number of witness stack items in a standard P2WSH script */
static constexpr unsigned int MAX_STANDARD_P2WSH_STACK_ITEMS{100};
/** The maximum size in bytes of each witness stack item in a standard P2WSH script */

View file

@ -1105,7 +1105,7 @@ static RPCHelpMan gettxout()
{RPCResult::Type::STR_AMOUNT, "value", "The transaction value in " + CURRENCY_UNIT},
{RPCResult::Type::OBJ, "scriptPubKey", "", {
{RPCResult::Type::STR, "asm", "Disassembly of the output script"},
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
{RPCResult::Type::STR, "desc", /*optional=*/true, "Inferred top level descriptor for the script (if any)"},
{RPCResult::Type::STR_HEX, "hex", "The raw output script bytes, hex-encoded"},
{RPCResult::Type::STR, "type", "The type, eg pubkeyhash"},
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
@ -2293,9 +2293,10 @@ static RPCHelpMan scantxoutset()
FlatSigningProvider provider;
auto scripts = EvalDescriptorStringOrObject(scanobject, provider);
for (CScript& script : scripts) {
std::string inferred = InferDescriptor(script, provider)->ToString();
auto desc = InferDescriptor(script, provider);
if (!desc) throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid descriptor found: '%s'", scanobject.get_str()));
needles.emplace(script);
descriptors.emplace(std::move(script), std::move(inferred));
descriptors.emplace(std::move(script), desc->ToString());
}
}

View file

@ -146,6 +146,7 @@ static RPCHelpMan createmultisig()
// Make the descriptor
std::unique_ptr<Descriptor> descriptor = InferDescriptor(GetScriptForDestination(dest), keystore);
if (!descriptor) throw JSONRPCError(RPC_INTERNAL_ERROR, "Invalid descriptor generated");
UniValue result(UniValue::VOBJ);
result.pushKV("address", EncodeDestination(dest));

View file

@ -493,7 +493,7 @@ static RPCHelpMan decodescript()
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "asm", "Disassembly of the script"},
{RPCResult::Type::STR, "desc", "Inferred descriptor for the script"},
{RPCResult::Type::STR, "desc", /*optional=*/true, "Inferred top level descriptor for the script (if any)"},
{RPCResult::Type::STR, "type", "The output type (e.g. " + GetAllOutputTypes() + ")"},
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
{RPCResult::Type::STR, "p2sh", /*optional=*/true,
@ -505,7 +505,7 @@ static RPCHelpMan decodescript()
{RPCResult::Type::STR_HEX, "hex", "The raw output script bytes, hex-encoded"},
{RPCResult::Type::STR, "type", "The type of the output script (e.g. witness_v0_keyhash or witness_v0_scripthash)"},
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
{RPCResult::Type::STR, "desc", "Inferred descriptor for the script"},
{RPCResult::Type::STR, "desc", /*optional=*/true, "Inferred descriptor for the script (if any)"},
{RPCResult::Type::STR, "p2sh-segwit", "address of the P2SH script wrapping this witness redeem script"},
}},
},
@ -529,9 +529,14 @@ static RPCHelpMan decodescript()
std::vector<std::vector<unsigned char>> solutions_data;
const TxoutType which_type{Solver(script, solutions_data)};
bool can_wrap_p2sh = true;
const bool can_wrap{[&] {
switch (which_type) {
case TxoutType::MULTISIG:
case TxoutType::MULTISIG: {
// Verify it doesn't exceed p2sh consensus limits
can_wrap_p2sh = script.size() <= MAX_SCRIPT_ELEMENT_SIZE;
break;
}
case TxoutType::NONSTANDARD:
case TxoutType::PUBKEY:
case TxoutType::PUBKEYHASH:
@ -561,7 +566,7 @@ static RPCHelpMan decodescript()
}()};
if (can_wrap) {
r.pushKV("p2sh", EncodeDestination(ScriptHash(script)));
if (can_wrap_p2sh) r.pushKV("p2sh", EncodeDestination(ScriptHash(script)));
// P2SH and witness programs cannot be wrapped in P2WSH, if this script
// is a witness program, don't return addresses for a segwit programs.
const bool can_wrap_P2WSH{[&] {
@ -822,7 +827,7 @@ const RPCResult decodepsbt_inputs{
{RPCResult::Type::OBJ, "scriptPubKey", "",
{
{RPCResult::Type::STR, "asm", "Disassembly of the output script"},
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
{RPCResult::Type::STR, "desc", /*optional=*/true, "Inferred top level descriptor for the script (if any)"},
{RPCResult::Type::STR_HEX, "hex", "The raw output script bytes, hex-encoded"},
{RPCResult::Type::STR, "type", "The type, eg 'pubkeyhash'"},
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},

View file

@ -1848,8 +1848,8 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
return {};
}
if (ctx == ParseScriptContext::TOP) {
if (providers.size() > 3) {
error = strprintf("Cannot have %u pubkeys in bare multisig; only at most 3 pubkeys", providers.size());
if (providers.size() > MAX_BARE_MULTISIG_PUBKEYS_NUM) {
error = strprintf("Cannot have %u pubkeys in bare multisig; only at most %u pubkeys", providers.size(), MAX_BARE_MULTISIG_PUBKEYS_NUM);
return {};
}
}
@ -2235,6 +2235,17 @@ std::unique_ptr<DescriptorImpl> InferScript(const CScript& script, ParseScriptCo
break;
}
}
// Check bare multisig pubkeys number limit
if (ctx == ParseScriptContext::TOP && providers.size() > MAX_BARE_MULTISIG_PUBKEYS_NUM) {
return nullptr;
}
// Verify we don't exceed consensus limits
if (ctx == ParseScriptContext::P2SH && script.size() > MAX_SCRIPT_ELEMENT_SIZE) {
return nullptr;
}
if (ok) return std::make_unique<MultisigDescriptor>((int)data[0][0], std::move(providers));
}
if (txntype == TxoutType::SCRIPTHASH && ctx == ParseScriptContext::TOP) {

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

@ -292,6 +292,13 @@ RPCHelpMan addmultisigaddress()
CScript inner;
CTxDestination dest = AddAndGetMultisigDestination(required, pubkeys, output_type, provider, inner);
// Before importing the scripts into the wallet, check we can make a descriptor out of it.
std::unique_ptr<Descriptor> descriptor = InferDescriptor(GetScriptForDestination(dest), spk_man);
if (!descriptor) {
// Shouldn't happen; we crafted the destination internally.
throw JSONRPCError(RPC_INTERNAL_ERROR, "Invalid descriptor generated");
}
// Import scripts into the wallet
for (const auto& [id, script] : provider.scripts) {
// Due to a bug in the legacy wallet, the p2sh maximum script size limit is also imposed on 'p2sh-segwit' and 'bech32' redeem scripts.
@ -313,9 +320,6 @@ RPCHelpMan addmultisigaddress()
// Store destination in the addressbook
pwallet->SetAddressBook(dest, label, AddressPurpose::SEND);
// Make the descriptor
std::unique_ptr<Descriptor> descriptor = InferDescriptor(GetScriptForDestination(dest), spk_man);
UniValue result(UniValue::VOBJ);
result.pushKV("address", EncodeDestination(dest));
result.pushKV("redeemScript", HexStr(inner));

View file

@ -96,6 +96,16 @@ class DecodeScriptTest(BitcoinTestFramework):
assert_equal('witness_v0_scripthash', rpc_result['segwit']['type'])
assert_equal('0 ' + multisig_script_hash, rpc_result['segwit']['asm'])
# Check decoding a multisig redeem script doesn't return an invalid descriptor
self.log.info("- multisig invalid")
# Decode a +520 bytes consensus-invalid p2sh multisig - provided script translates to multi(20, keys)
large_multisig_script = '0114' + push_public_key * 20 + '0114' + 'ae'
rpc_result = self.nodes[0].decodescript(large_multisig_script)
# Verify it is valid only under segwit rules, not for bare multisig nor p2sh.
assert 'desc' not in rpc_result
assert 'p2sh' not in rpc_result
assert 'segwit' in rpc_result
self.log.info ("- P2SH")
# OP_HASH160 <Hash160(redeemScript)> OP_EQUAL.
# push_public_key_hash here should actually be the hash of a redeem script.