Skip to content
Open
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
run: |
# Run tests on commits after the last merge commit and before the PR head commit
# Use clang++, because it is a bit faster and uses less memory than g++
git rebase --exec "echo Running test-one-commit on \$( git log -1 ) && CC=clang CXX=clang++ cmake -B build -DWERROR=ON -DWITH_ZMQ=ON -DBUILD_GUI=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DWITH_BDB=ON -DWITH_MINIUPNPC=ON -DWITH_USDT=ON -DCMAKE_CXX_FLAGS='-Wno-error=unused-member-function' && cmake --build build -j $(nproc) && ctest --output-on-failure --stop-on-failure --test-dir build -j $(nproc) && ./build/test/functional/test_runner.py -j $(( $(nproc) * 2 )) --combinedlogslen=99999999" ${{ env.TEST_BASE }}
git rebase --exec "echo Running test-one-commit on \$( git log -1 ) && CC=clang CXX=clang++ cmake -B build -DWERROR=ON -DWITH_ZMQ=ON -DBUILD_GUI=ON -DBUILD_BENCH=ON -DBUILD_FUZZ_BINARY=ON -DWITH_BDB=ON -DWARN_INCOMPATIBLE_BDB=OFF -DWITH_MINIUPNPC=ON -DWITH_USDT=ON -DCMAKE_CXX_FLAGS='-Wno-error=unused-member-function' && cmake --build build -j $(nproc) && ctest --output-on-failure --stop-on-failure --test-dir build -j $(nproc) && ./build/test/functional/test_runner.py -j $(( $(nproc) * 2 )) --combinedlogslen=99999999" ${{ env.TEST_BASE }}

macos-native-arm64:
name: ${{ matrix.job-name }}
Expand Down Expand Up @@ -189,7 +189,7 @@ jobs:
job-type: [standard, fuzz]
include:
- job-type: standard
generate-options: '-DBUILD_GUI=OFF -DWITH_BDB=ON -DWITH_MINIUPNPC=ON -DWITH_ZMQ=ON -DBUILD_BENCH=ON -DWERROR=ON'
generate-options: '-DBUILD_GUI=OFF -DWITH_BDB=ON -DWARN_INCOMPATIBLE_BDB=OFF -DWITH_MINIUPNPC=ON -DWITH_ZMQ=ON -DBUILD_BENCH=ON -DWERROR=ON'
job-name: 'Win64 native, VS 2022'
- job-type: fuzz
generate-options: '-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON -DVCPKG_MANIFEST_FEATURES="sqlite" -DBUILD_GUI=OFF -DBUILD_FOR_FUZZING=ON -DWERROR=ON'
Expand Down
4 changes: 4 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,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("-coinblocksvsizediscount", strprintf("Discount vsize for transactions with high coinblocks (default: %u)", DEFAULT_COINBLOCKS_VSIZE_DISCOUNT), 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 (default: %u)",
Expand Down Expand Up @@ -866,6 +867,7 @@ void InitParameterInteraction(ArgsManager& args)
args.SoftSetArg("-permitephemeral", "anchor,send,dust");
args.SoftSetArg("-spkreuse", "allow");
args.SoftSetArg("-blockprioritysize", "0");
args.SoftSetArg("-coinblocksvsizediscount", "0");
args.SoftSetArg("-blockmaxsize", "4000000");
args.SoftSetArg("-blockmaxweight", "4000000");
}
Expand Down Expand Up @@ -1198,6 +1200,8 @@ bool AppInitParameterInteraction(const ArgsManager& args)
g_weight_per_data_byte = ((*parsed * WITNESS_SCALE_FACTOR) + 99) / 100;
}

g_coinblocks_vsize_discount = args.GetBoolArg("-coinblocksvsizediscount", DEFAULT_COINBLOCKS_VSIZE_DISCOUNT);

g_script_size_policy_limit = args.GetIntArg("-maxscriptsize", g_script_size_policy_limit);

nBytesPerSigOp = args.GetIntArg("-bytespersigop", nBytesPerSigOp);
Expand Down
23 changes: 16 additions & 7 deletions src/kernel/mempool_entry.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <util/epochguard.h>
#include <util/overflow.h>

#include <algorithm>
#include <cassert>
#include <chrono>
#include <functional>
Expand Down Expand Up @@ -96,12 +97,14 @@ class CTxMemPoolEntry
const int64_t nTime; //!< Local time when entering the mempool
const uint64_t entry_sequence; //!< Sequence number used to determine whether this transaction is too recent for relay
const int64_t sigOpCost; //!< Total sigop cost
const int32_t m_extra_weight; //!< Policy-only additional transaction weight beyond nTxWeight
const size_t nModSize; //!< Cached modified size for priority
const int32_t m_base_extra_weight; //!< Policy-only extra weight (e.g. datacarrier), never changes
int32_t m_coinblocks_weight_discount; //!< Coinblocks vsize discount (negative or zero), updated as coin age increases
size_t nModSize; //!< Cached modified size for priority
const double entryPriority; //!< Priority when entering the mempool
const unsigned int entryHeight; //!< Chain height when entering the mempool
double cachedPriority; //!< Last calculated priority
unsigned int cachedHeight; //!< Height at which priority was last calculated
double m_cached_coin_age; //!< Cached sum coin-age of confirmed inputs (satoshi-blocks)
CAmount inChainInputValue; //!< Sum of all txin values that are already in blockchain
const bool spendsCoinbase; //!< keep track of transactions that spend a coinbase
CAmount m_modified_fee; //!< Used for determining the priority of the transaction for mining in a block
Expand Down Expand Up @@ -136,13 +139,17 @@ class CTxMemPoolEntry
nTime{time},
entry_sequence{entry_sequence},
sigOpCost{sigops_cost},
m_extra_weight{extra_weight},
m_base_extra_weight{extra_weight},
m_coinblocks_weight_discount{::g_coinblocks_vsize_discount ?
CalculateCoinblocksWeightDiscount(coin_age_cache.inputs_coin_age, GetTransactionWeight(*tx),
MIN_STANDARD_TX_NONWITNESS_SIZE * WITNESS_SCALE_FACTOR) : 0},
nModSize{CalculateModifiedSize(*tx, GetTxSize())},
entryPriority{ComputePriority2(coin_age_cache.inputs_coin_age, nModSize)},
entryHeight{entry_height},
cachedPriority{entryPriority},
// Since entries arrive *after* the tip's height, their entry priority is for the height+1
cachedHeight{entry_height + 1},
m_cached_coin_age{coin_age_cache.inputs_coin_age},
inChainInputValue{coin_age_cache.in_chain_input_value},
spendsCoinbase{spends_coinbase},
m_modified_fee{nFee},
Expand All @@ -168,7 +175,7 @@ class CTxMemPoolEntry
double GetStartingPriority() const {return entryPriority; }
CoinAgeCache GetInternalCoinAgeCache() const {
return {
.inputs_coin_age = ReversePriority2(cachedPriority, nModSize),
.inputs_coin_age = m_cached_coin_age,
.in_chain_input_value = inChainInputValue,
};
}
Expand All @@ -180,18 +187,20 @@ class CTxMemPoolEntry
/**
* Recalculate the cached priority as of currentHeight and adjust inChainInputValue by
* valueInCurrentBlock which represents input that was just added to or removed from the blockchain.
* Also recalculates the coinblocks weight discount based on updated coin age.
* @return The change in GetTxSize() caused by the discount update (0 if unchanged).
*/
void UpdateCachedPriority(unsigned int currentHeight, CAmount valueInCurrentBlock);
int32_t UpdateCachedPriority(unsigned int currentHeight, CAmount valueInCurrentBlock);
const CAmount& GetFee() const { return nFee; }
int32_t GetTxSize() const
{
return GetVirtualTransactionSize(nTxWeight + m_extra_weight, sigOpCost, ::nBytesPerSigOp);
return std::max<int32_t>(GetVirtualTransactionSize(nTxWeight + m_base_extra_weight + m_coinblocks_weight_discount, sigOpCost, ::nBytesPerSigOp), 1);
}
int32_t GetTxWeight() const { return nTxWeight; }
std::chrono::seconds GetTime() const { return std::chrono::seconds{nTime}; }
unsigned int GetHeight() const { return entryHeight; }
uint64_t GetSequence() const { return entry_sequence; }
int32_t GetExtraWeight() const { return m_extra_weight; }
int32_t GetExtraWeight() const { return m_base_extra_weight + m_coinblocks_weight_discount; }
int64_t GetSigOpCost() const { return sigOpCost; }
CAmount GetModifiedFee() const { return m_modified_fee; }
size_t DynamicMemoryUsage() const { return nUsageSize; }
Expand Down
49 changes: 43 additions & 6 deletions src/policy/coin_age_priority.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

#include <policy/coin_age_priority.h>

#include <algorithm>
#include <cmath>

#include <coins.h>
#include <common/args.h>
#include <consensus/validation.h>
#include <node/miner.h>
#include <policy/policy.h>
#include <policy/settings.h>
#include <primitives/transaction.h>
#include <txmempool.h>
#include <util/check.h>
Expand All @@ -23,7 +27,7 @@ unsigned int CalculateModifiedSize(const CTransaction& tx, unsigned int nTxSize)
// is enough to cover a compressed pubkey p2sh redemption) for priority.
// Providing any more cleanup incentive than making additional inputs free would
// risk encouraging people to create junk outputs to redeem later.
Assert(nTxSize > 0);
if (nTxSize == 0) return 0;
for (std::vector<CTxIn>::const_iterator it(tx.vin.begin()); it != tx.vin.end(); ++it)
{
unsigned int offset = 41U + std::min(110U, (unsigned int)it->scriptSig.size());
Expand Down Expand Up @@ -65,14 +69,29 @@ CoinAgeCache GetCoinAge(const CTransaction &tx, const CCoinsViewCache& view, int
return r;
}

void CTxMemPoolEntry::UpdateCachedPriority(unsigned int currentHeight, CAmount valueInCurrentBlock)
int32_t CTxMemPoolEntry::UpdateCachedPriority(unsigned int currentHeight, CAmount valueInCurrentBlock)
{
int heightDiff = int(currentHeight) - int(cachedHeight);
double deltaPriority = ((double)heightDiff*inChainInputValue)/nModSize;
cachedPriority += deltaPriority;
cachedHeight = currentHeight;

m_cached_coin_age += (double)heightDiff * inChainInputValue;
inChainInputValue += valueInCurrentBlock;
assert(MoneyRange(inChainInputValue));

if (!::g_coinblocks_vsize_discount) return 0;

const int32_t old_size = GetTxSize();
static constexpr int32_t min_weight = MIN_STANDARD_TX_NONWITNESS_SIZE * WITNESS_SCALE_FACTOR;
m_coinblocks_weight_discount = CalculateCoinblocksWeightDiscount(m_cached_coin_age, nTxWeight, min_weight);
const int32_t new_size = GetTxSize();

if (new_size != old_size) {
nModSize = CalculateModifiedSize(GetTx(), new_size);
}

return new_size - old_size;
}

struct update_priority
Expand All @@ -82,11 +101,11 @@ struct update_priority
{}

void operator() (CTxMemPoolEntry &e)
{ e.UpdateCachedPriority(height, value); }
{ size_delta = e.UpdateCachedPriority(height, value); }

private:
unsigned int height;
CAmount value;
unsigned int height;
CAmount value;
int32_t size_delta{0};
};

void CTxMemPool::UpdateDependentPriorities(const CTransaction &tx, unsigned int nBlockHeight, bool addToChain)
Expand All @@ -98,7 +117,9 @@ void CTxMemPool::UpdateDependentPriorities(const CTransaction &tx, unsigned int
continue;
uint256 hash = it->second->GetHash();
txiter iter = mapTx.find(hash);
const int32_t old_size = iter->GetTxSize();
mapTx.modify(iter, update_priority(nBlockHeight, addToChain ? tx.vout[i].nValue : -tx.vout[i].nValue));
totalTxSize += iter->GetTxSize() - old_size;
}
}

Expand All @@ -116,6 +137,22 @@ CTxMemPoolEntry::GetPriority(unsigned int currentHeight) const
return dResult;
}

int32_t CalculateCoinblocksWeightDiscount(double inputs_coin_age, int32_t tx_weight, int32_t min_weight)
{
if (tx_weight <= min_weight || inputs_coin_age <= 0.0) return 0;

const double vsize = static_cast<double>(tx_weight) / WITNESS_SCALE_FACTOR;
const double coinblocks = inputs_coin_age / vsize;
static constexpr double COINBLOCKS_DISCOUNT_LOG_DIVISOR{8.0};
const double discount_ratio = std::min(0.5, std::log2(1.0 + coinblocks / MINIMUM_TX_PRIORITY) / COINBLOCKS_DISCOUNT_LOG_DIVISOR);

int32_t weight_discount = static_cast<int32_t>(-discount_ratio * tx_weight);
weight_discount = std::max(weight_discount, min_weight - tx_weight);

Assume(weight_discount <= 0);
return weight_discount;
}

#ifndef BUILDING_FOR_LIBBITCOINKERNEL
// We want to sort transactions by coin age priority
typedef std::pair<double, CTxMemPool::txiter> TxCoinAgePriority;
Expand Down
28 changes: 28 additions & 0 deletions src/policy/coin_age_priority.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,32 @@ double ReversePriority2(double coin_age_priority, unsigned int mod_vsize);
*/
CoinAgeCache GetCoinAge(const CTransaction &tx, const CCoinsViewCache& view, int nHeight);

/**
* Calculate a weight discount for transactions spending older coins.
*
* The discount reduces a transaction's effective vsize based on the coin age
* density (coinblocks per vbyte) of its inputs. The formula uses a logarithmic
* curve that rises quickly for young coins and flattens as they age:
*
* discount_ratio = min(0.5, log2(1 + coinblocks_per_vbyte / MINIMUM_TX_PRIORITY) / 8)
*
* where coinblocks_per_vbyte = inputs_coin_age / vsize, and inputs_coin_age is
* the sum of (value_sat * confirmation_depth) across all confirmed inputs.
*
* The divisor of 8 controls saturation speed: the 50% cap is reached when
* coinblocks_per_vbyte >= 255 * MINIMUM_TX_PRIORITY (i.e. log2(256) / 8 = 1.0,
* clamped to 0.5). In practice this means a typical 1-input, 1 BTC transaction
* reaches the maximum discount after roughly 36 days of confirmations.
*
* The returned value is always <= 0 (a negative weight offset). The total
* effective weight (tx_weight + discount) is clamped so it never drops below
* min_weight, preventing transactions from having an unreasonably small vsize.
*
* @param[in] inputs_coin_age Sum of (value_sat * height_depth) for confirmed inputs.
* @param[in] tx_weight Consensus transaction weight.
* @param[in] min_weight Floor for effective weight after discount.
* @return Negative weight discount, or 0 if no discount applies.
*/
int32_t CalculateCoinblocksWeightDiscount(double inputs_coin_age, int32_t tx_weight, int32_t min_weight);

#endif // BITCOIN_POLICY_COIN_AGE_PRIORITY_H
2 changes: 2 additions & 0 deletions src/policy/policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ static constexpr unsigned int DEFAULT_BYTES_PER_SIGOP_STRICT{20};
static constexpr unsigned int DEFAULT_WEIGHT_PER_DATA_BYTE{4};
/** Default for -rejecttokens */
static constexpr bool DEFAULT_REJECT_TOKENS{false};
/** Default for -coinblocksvsizediscount */
static constexpr bool DEFAULT_COINBLOCKS_VSIZE_DISCOUNT{true};

// NOTE: Changes to these three require manually adjusting doc in init.cpp
/** Default for -permitephemeral=send */
Expand Down
1 change: 1 addition & 0 deletions src/policy/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
unsigned int nBytesPerSigOp = DEFAULT_BYTES_PER_SIGOP;
unsigned int nBytesPerSigOpStrict = DEFAULT_BYTES_PER_SIGOP_STRICT;
unsigned int g_weight_per_data_byte = DEFAULT_WEIGHT_PER_DATA_BYTE;
bool g_coinblocks_vsize_discount = DEFAULT_COINBLOCKS_VSIZE_DISCOUNT;
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_coinblocks_vsize_discount;

#endif // BITCOIN_POLICY_SETTINGS_H
2 changes: 1 addition & 1 deletion src/test/fuzz/util/mempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ CTxMemPoolEntry ConsumeTxMemPoolEntry(FuzzedDataProvider& fuzzed_data_provider,
const double coin_age = fuzzed_data_provider.ConsumeFloatingPoint<double>();
const unsigned int entry_height = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(0, std::numeric_limits<unsigned int>::max() - 1);
const bool spends_coinbase = fuzzed_data_provider.ConsumeBool();
const int32_t extra_weight = fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(0, GetTransactionWeight(tx) * 3);
const int32_t extra_weight = fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(-(GetTransactionWeight(tx) / 2), GetTransactionWeight(tx) * 3);
const unsigned int sig_op_cost = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(0, MAX_BLOCK_SIGOPS_COST);
return CTxMemPoolEntry{MakeTransactionRef(tx), fee, time, entry_height, entry_sequence, {
.inputs_coin_age = coin_age,
Expand Down
7 changes: 7 additions & 0 deletions src/test/txpackage_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <policy/packages.h>
#include <policy/policy.h>
#include <policy/rbf.h>
#include <policy/settings.h>
#include <primitives/transaction.h>
#include <script/script.h>
#include <serialize.h>
Expand All @@ -27,6 +28,12 @@ using namespace util::hex_literals;
static const CAmount low_fee_amt{200};

struct TxPackageTest : TestChain100Setup {
TxPackageTest() {
g_coinblocks_vsize_discount = false;
}
~TxPackageTest() {
g_coinblocks_vsize_discount = DEFAULT_COINBLOCKS_VSIZE_DISCOUNT;
}
// Create placeholder transactions that have no meaning.
inline CTransactionRef create_placeholder_tx(size_t num_inputs, size_t num_outputs)
{
Expand Down
Loading
Loading