Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
argsman.AddArg("-bytespersigopstrict", strprintf("Minimum bytes per sigop in transactions we relay and mine (default: %u)", DEFAULT_BYTES_PER_SIGOP_STRICT), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-datacarrier", strprintf("Relay and mine data carrier transactions (default: %u)", DEFAULT_ACCEPT_DATACARRIER), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-datacarriercost", strprintf("Treat extra data in transactions as at least N vbytes per actual byte (default: %s)", DEFAULT_WEIGHT_PER_DATA_BYTE / 4.0), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-oneofnkeypenalty", strprintf("Reduce modified fees by the minrelaytxfee charge for the witness discount on all but one public key in exact 1-of-N multisig scripts. Also affects legitimate matching wallet spends; wallet fee estimates include a conservative allowance (default: %u)", DEFAULT_ONE_OF_N_KEY_PENALTY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-datacarrierfullcount", strprintf("Apply datacarriersize limit to all known datacarrier methods (default: %u)", DEFAULT_DATACARRIER_FULLCOUNT), ArgsManager::ALLOW_ANY | (DEFAULT_DATACARRIER_FULLCOUNT ? uint32_t{ArgsManager::DEBUG_ONLY} : 0), OptionsCategory::NODE_RELAY);
argsman.AddArg("-datacarriersize",
strprintf("Maximum size of data in data carrier transactions we relay and mine, in bytes (maximum %s, default: %u)",
Expand Down Expand Up @@ -877,6 +878,7 @@ void InitParameterInteraction(ArgsManager& args)
args.SoftSetArg("-rejecttokens", "0");
args.SoftSetArg("-subdustfeepenalty", "0");
args.SoftSetArg("-datacarriercost", "0.25");
args.SoftSetBoolArg("-oneofnkeypenalty", false);
args.SoftSetArg("-datacarrierfullcount", "0");
args.SoftSetArg("-maxtxlegacysigops", strprintf("%s", std::numeric_limits<unsigned int>::max()));
args.SoftSetArg("-maxscriptsize", strprintf("%s", std::numeric_limits<unsigned int>::max()));
Expand Down Expand Up @@ -1222,6 +1224,7 @@ bool AppInitParameterInteraction(const ArgsManager& args)
g_script_size_policy_limit = args.GetIntArg("-maxscriptsize", g_script_size_policy_limit);

nBytesPerSigOp = args.GetIntArg("-bytespersigop", nBytesPerSigOp);
g_one_of_n_key_penalty = args.GetBoolArg("-oneofnkeypenalty", DEFAULT_ONE_OF_N_KEY_PENALTY);
nBytesPerSigOpStrict = args.GetIntArg("-bytespersigopstrict", nBytesPerSigOpStrict);

if (!g_wallet_init_interface.ParameterInteraction()) return false;
Expand Down
32 changes: 32 additions & 0 deletions src/policy/policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <vector>

unsigned int g_script_size_policy_limit{DEFAULT_SCRIPT_SIZE_POLICY_LIMIT};
bool g_one_of_n_key_penalty{DEFAULT_ONE_OF_N_KEY_PENALTY};

CAmount GetDustThreshold(const CTxOut& txout, const CFeeRate& dustRelayFeeIn)
{
Expand Down Expand Up @@ -654,6 +655,37 @@ std::pair<size_t, size_t> DatacarrierBytes(const CTransaction& tx, const CCoinsV
return ret;
}

int64_t GetOneOfNExtraWeight(const CScript& script)
{
// Match whole scripts, not fragments of recovery paths or other conditions.
// These keys may all belong to a wallet; this does not classify them as data.
std::vector<std::vector<unsigned char>> solutions;
if (Solver(script, solutions) == TxoutType::MULTISIG && solutions.front()[0] == 1) {
size_t key_bytes{0}, largest_key{0};
for (size_t i{1}; i + 1 < solutions.size(); ++i) {
key_bytes += solutions[i].size();
largest_key = std::max(largest_key, solutions[i].size());
}
return int64_t(key_bytes - largest_key) * (WITNESS_SCALE_FACTOR - 1);
}
if (const auto multi_a = MatchMultiA(script); multi_a && multi_a->first == 1) {
return int64_t(multi_a->second.size() - 1) * 32 * (WITNESS_SCALE_FACTOR - 1);
}
return 0;
}

int64_t GetOneOfNTxExtraWeight(const CTransaction& tx, const CCoinsViewCache& view)
{
int64_t extra_weight{0};
for (const CTxIn& txin : tx.vin) {
if (txin.scriptWitness.IsNull()) continue;
const auto& prev_script = view.AccessCoin(txin.prevout).out.scriptPubKey;
const auto [script, weight_per_byte] = GetScriptForTransactionInput(prev_script, txin);
if (weight_per_byte == 1) extra_weight += GetOneOfNExtraWeight(script);
}
return extra_weight;
}

int32_t CalculateExtraTxWeight(const CTransaction& tx, const CCoinsViewCache& view, const unsigned int weight_per_data_byte)
{
int64_t mod_weight{0};
Expand Down
8 changes: 8 additions & 0 deletions src/policy/policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ static constexpr unsigned int DEFAULT_BYTES_PER_SIGOP{20};
static constexpr unsigned int DEFAULT_BYTES_PER_SIGOP_STRICT{20};
/** Default for -datacarriercost (multiplied by WITNESS_SCALE_FACTOR) */
static constexpr unsigned int DEFAULT_WEIGHT_PER_DATA_BYTE{4};
/** Default for -oneofnkeypenalty */
static constexpr bool DEFAULT_ONE_OF_N_KEY_PENALTY{true};
/** Default for -rejecttokens */
static constexpr bool DEFAULT_REJECT_TOKENS{true};
/** Default for -subdustfeepenalty */
Expand Down Expand Up @@ -243,6 +245,12 @@ std::pair<CScript, unsigned int> GetScriptForTransactionInput(CScript prevScript

std::pair<size_t, size_t> DatacarrierBytes(const CTransaction& tx, const CCoinsViewCache& view);

/** Policy weight needed to price all but one key in an exact 1-of-N witness script at one vbyte per byte. */
int64_t GetOneOfNExtraWeight(const CScript& script);

/** Additional key weight across witness inputs, used only to calculate the fee penalty. */
int64_t GetOneOfNTxExtraWeight(const CTransaction& tx, const CCoinsViewCache& view);

int32_t CalculateExtraTxWeight(const CTransaction& tx, const CCoinsViewCache& view, const unsigned int weight_per_data_byte);

#endif // BITCOIN_POLICY_POLICY_H
1 change: 1 addition & 0 deletions src/policy/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ extern unsigned int g_script_size_policy_limit;
extern unsigned int nBytesPerSigOp;
extern unsigned int nBytesPerSigOpStrict;
extern unsigned int g_weight_per_data_byte;
extern bool g_one_of_n_key_penalty;

#endif // BITCOIN_POLICY_SETTINGS_H
37 changes: 37 additions & 0 deletions src/test/transaction_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1423,4 +1423,41 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
}
}

BOOST_AUTO_TEST_CASE(oneofn_policy_weight)
{
const auto pubkey = GenerateRandomKey().GetPubKey();
const auto key = ToByteVector(pubkey);
const auto xonly = ToByteVector(XOnlyPubKey{pubkey});
for (const int n : {1, 2, 3, 4, 15, 20}) {
for (const int threshold : {1, n}) {
CScript multi;
multi << threshold;
for (int i{0}; i < n; ++i) multi << key;
multi << n << OP_CHECKMULTISIG;
const int64_t expected = threshold == 1 ? (n - 1) * 33 * 3 : 0;
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(multi), expected);

// Do not infer thresholds inside more complex scripts.
CScript prefixed = CScript{} << OP_0 << OP_CHECKSEQUENCEVERIFY << OP_DROP;
prefixed.insert(prefixed.end(), multi.begin(), multi.end());
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(prefixed), 0);
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(CScript{multi} << OP_NOP), 0);
}
}
for (const int n : {1, 2, 3, 4, 15, 999}) {
for (const int threshold : {1, n}) {
CScript multi_a;
for (int i{0}; i < n; ++i) multi_a << xonly << (i == 0 ? OP_CHECKSIG : OP_CHECKSIGADD);
multi_a << threshold << OP_NUMEQUAL;
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(multi_a), threshold == 1 ? (n - 1) * 32 * 3 : 0);
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(CScript{multi_a} << OP_NOP), 0);
multi_a.pop_back();
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(multi_a), 0);
}
}
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(CScript{}), 0);
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(CScript{} << xonly << OP_CHECKSIG), 0);
BOOST_CHECK_EQUAL(GetOneOfNExtraWeight(CScript{} << OP_PUSHDATA1), 0);
}

BOOST_AUTO_TEST_SUITE_END()
13 changes: 13 additions & 0 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,19 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
}
}

// Apply after the ephemeral zero-fee check: this automatic penalty must not
// turn a zero-fee parent into a nonzero-fee transaction for that check.
// Keep size accounting unchanged, in particular TRUC and ancestor limits.
if (g_one_of_n_key_penalty) {
const auto penalty = m_pool.m_opts.min_relay_feerate.GetFee(GetVirtualTransactionSize(GetOneOfNTxExtraWeight(tx, m_view), 0, 0));
if (penalty > 0) {
m_subpackage.m_changeset->m_to_add.modify(ws.m_tx_handle, [&](CTxMemPoolEntry& e) {
e.UpdateModifiedFee(-penalty);
});
ws.m_modified_fees = SaturatingAdd(ws.m_modified_fees, -penalty);
}
}

// To avoid rejecting low-sigop bare-multisig transactions, the sigops
// are counted a second time more accurately.
if ((nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST) || (nBytesPerSigOpStrict && GetAccurateTransactionSigOpCost(tx, m_view, STANDARD_SCRIPT_VERIFY_FLAGS) > ws.m_vsize * WITNESS_SCALE_FACTOR / nBytesPerSigOpStrict)) {
Expand Down
62 changes: 53 additions & 9 deletions src/wallet/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
#include <common/system.h>
#include <consensus/amount.h>
#include <consensus/validation.h>
#include <hash.h>
#include <interfaces/chain.h>
#include <node/types.h>
#include <numeric>
#include <policy/policy.h>
#include <policy/settings.h>
#include <primitives/transaction.h>
#include <script/script.h>
#include <script/signingprovider.h>
Expand Down Expand Up @@ -87,13 +89,44 @@ static std::optional<int64_t> MaxInputWeight(const Descriptor& desc, const std::
return {};
}

// Reserve the undiscounted key weight at the wallet's chosen feerate. This
// covers the penalty at minrelaytxfee and is conservative at higher feerates.
// Keep it separate from consensus weight and transaction weight limits.
static int64_t MaximumOneOfNExtraWeight(const CScript& script_pubkey, const SigningProvider& provider)
{
if (!g_one_of_n_key_penalty) return 0;
CScript script{script_pubkey};
std::vector<std::vector<unsigned char>> solutions;
auto type = Solver(script, solutions);
if (type == TxoutType::SCRIPTHASH) {
if (!provider.GetCScript(CScriptID{uint160{solutions[0]}}, script)) return 0;
type = Solver(script, solutions);
}
if (type == TxoutType::WITNESS_V0_SCRIPTHASH) {
if (!provider.GetCScript(CScriptID{RIPEMD160(solutions[0])}, script)) return 0;
return GetVirtualTransactionSize(GetOneOfNExtraWeight(script), 0, 0) * WITNESS_SCALE_FACTOR;
}
if (type == TxoutType::WITNESS_V1_TAPROOT) {
TaprootSpendData spenddata;
if (!provider.GetTaprootSpendData(XOnlyPubKey{solutions[0]}, spenddata)) return 0;
int64_t extra_weight{0};
for (const auto& [leaf, controls] : spenddata.scripts) {
if (leaf.second == TAPROOT_LEAF_TAPSCRIPT) {
extra_weight = std::max(extra_weight, GetOneOfNExtraWeight(CScript(leaf.first.begin(), leaf.first.end())));
}
}
return extra_weight;
}
return 0;
}

int CalculateMaximumSignedInputSize(const CTxOut& txout, const COutPoint outpoint, const SigningProvider* provider, bool can_grind_r, const CCoinControl* coin_control)
{
if (!provider) return -1;

if (const auto desc = InferDescriptor(txout.scriptPubKey, *provider)) {
if (const auto weight = MaxInputWeight(*desc, CTxIn{outpoint}, coin_control, true, can_grind_r)) {
return static_cast<int>(GetVirtualTransactionSize(*weight, 0, 0));
return static_cast<int>(GetVirtualTransactionSize(*weight + MaximumOneOfNExtraWeight(txout.scriptPubKey, *provider), 0, 0));
}
}

Expand All @@ -106,9 +139,7 @@ int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet,
return CalculateMaximumSignedInputSize(txout, COutPoint(), provider.get(), wallet->CanGrindR(), coin_control);
}

/** Infer a descriptor for the given output script. */
static std::unique_ptr<Descriptor> GetDescriptor(const CWallet* wallet, const CCoinControl* coin_control,
const CScript script_pubkey)
static MultiSigningProvider GetSigningProvider(const CWallet* wallet, const CCoinControl* coin_control, const CScript& script_pubkey)
{
MultiSigningProvider providers;
for (const auto spkman: wallet->GetScriptPubKeyMans(script_pubkey)) {
Expand All @@ -117,7 +148,14 @@ static std::unique_ptr<Descriptor> GetDescriptor(const CWallet* wallet, const CC
if (coin_control) {
providers.AddProvider(std::make_unique<FlatSigningProvider>(coin_control->m_external_provider));
}
return InferDescriptor(script_pubkey, providers);
return providers;
}

/** Infer a descriptor for the given output script. */
static std::unique_ptr<Descriptor> GetDescriptor(const CWallet* wallet, const CCoinControl* coin_control,
const CScript script_pubkey)
{
return InferDescriptor(script_pubkey, GetSigningProvider(wallet, coin_control, script_pubkey));
}

/** Infer the maximum size of this input after it will be signed. */
Expand Down Expand Up @@ -157,15 +195,19 @@ TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *walle
for (const auto& txo : tx.vout) weight += GetSerializeSize(txo) * WITNESS_SCALE_FACTOR;

// Add the size of the transaction inputs as if they were signed.
int64_t extra_weight{0};
for (uint32_t i = 0; i < txouts.size(); i++) {
const auto txin_weight = GetSignedTxinWeight(wallet, coin_control, tx.vin[i], txouts[i], is_segwit, wallet->CanGrindR());
if (!txin_weight) return TxSize{-1, -1};
assert(*txin_weight > -1);
weight += *txin_weight;
if (g_one_of_n_key_penalty) {
extra_weight += MaximumOneOfNExtraWeight(txouts[i].scriptPubKey, GetSigningProvider(wallet, coin_control, txouts[i].scriptPubKey));
}
}

// It's ok to use 0 as the number of sigops since we never create any pathological transaction.
return TxSize{GetVirtualTransactionSize(weight, 0, 0), weight};
return TxSize{GetVirtualTransactionSize(weight + extra_weight, 0, 0), weight};
}

TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const CCoinControl* coin_control)
Expand Down Expand Up @@ -272,9 +314,6 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
std::map<COutPoint, CAmount> map_of_bump_fees = wallet.chain().calculateIndividualBumpFees(coin_control.ListSelected(), coin_selection_params.m_effective_feerate);
for (const COutPoint& outpoint : coin_control.ListSelected()) {
int64_t input_bytes = coin_control.GetInputWeight(outpoint).value_or(-1);
if (input_bytes != -1) {
input_bytes = GetVirtualTransactionSize(input_bytes, 0, 0);
}
CTxOut txout;
if (auto ptr_wtx = wallet.GetWalletTx(outpoint.hash)) {
// Clearly invalid input, fail
Expand All @@ -295,6 +334,11 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
txout = *out;
}

if (const auto input_weight = coin_control.GetInputWeight(outpoint)) {
const auto extra_weight = g_one_of_n_key_penalty ? MaximumOneOfNExtraWeight(txout.scriptPubKey, GetSigningProvider(&wallet, &coin_control, txout.scriptPubKey)) : 0;
input_bytes = GetVirtualTransactionSize(*input_weight + extra_weight, 0, 0);
}

if (input_bytes == -1) {
input_bytes = CalculateMaximumSignedInputSize(txout, outpoint, &coin_control.m_external_provider, can_grind_r, &coin_control);
}
Expand Down
Loading