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
2 changes: 1 addition & 1 deletion doc/man/bitcoin-qt.1
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ fee,\-optin)
Behaviour for transactions requesting TRUC limits: "reject" the
transactions entirely, "accept" them just like any other, or
"enforce" to impose their requested restrictions (default:
accept)
enforce)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer not to update manpages or other generated files in PRs

.HP
\fB\-minrelaycoinblocks=\fR<n>
.IP
Expand Down
2 changes: 1 addition & 1 deletion doc/man/bitcoind.1
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ fee,\-optin)
Behaviour for transactions requesting TRUC limits: "reject" the
transactions entirely, "accept" them just like any other, or
"enforce" to impose their requested restrictions (default:
accept)
enforce)
.HP
\fB\-minrelaycoinblocks=\fR<n>
.IP
Expand Down
14 changes: 14 additions & 0 deletions doc/release-notes-439.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Transaction relay and mining policy
----------------------------------

- The default `mempooltruc` setting is now `enforce`, applying the requested
TRUC topology and package-fee rules to version-3 transactions. Explicit
`mempooltruc=accept` and `mempooltruc=reject` settings remain available.
The redundant TRUC override in `corepolicy` has been removed; `corepolicy`
itself and its other overrides are unchanged. The sub-dust fee penalty
remains enabled by default. This change does not alter ephemeral-dust fee
eligibility.

- Transactions with dust outputs are excluded from individual priority-space
mining selection. They remain eligible for fee-based package selection,
preventing priority space from mining an ephemeral-dust parent alone.
2 changes: 1 addition & 1 deletion share/examples/bitcoin.conf
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@
# Behaviour for transactions requesting TRUC limits: "reject" the
# transactions entirely, "accept" them just like any other, or
# "enforce" to impose their requested restrictions (default:
# accept)
# enforce)
#mempooltruc=1

# Minimum "coin blocks" (measured in sat per block) that a transaction
Expand Down
3 changes: 1 addition & 2 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-mempoolfullrbf", strprintf("Accept transaction replace-by-fee without requiring replaceability signaling (default: %u)", (DEFAULT_MEMPOOL_RBF_POLICY == RBFPolicy::Always)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-mempoolreplacement", strprintf("Set to 0 to disable RBF entirely, \"fee,optin\" to honour RBF opt-out signal, or \"fee,-optin\" to always RBF aka full RBF (default: %s)", "fee,-optin"), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-mempooltruc", strprintf("Behaviour for transactions requesting TRUC limits: \"reject\" the transactions entirely, \"accept\" them just like any other, or \"enforce\" to impose their requested restrictions (default: %s)", "accept"), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-mempooltruc", strprintf("Behaviour for transactions requesting TRUC limits: \"reject\" the transactions entirely, \"accept\" them just like any other, or \"enforce\" to impose their requested restrictions (default: %s)", "enforce"), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-permitbareanchor",
strprintf("Relay transactions that only have ephemeral anchor outputs (default: %u)", DEFAULT_PERMITBAREANCHOR),
ArgsManager::ALLOW_ANY,
Expand Down Expand Up @@ -880,7 +880,6 @@ void InitParameterInteraction(ArgsManager& args)
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()));
args.SoftSetArg("-mempooltruc", "enforce");
args.SoftSetArg("-permitephemeral", "anchor,send,dust");
args.SoftSetArg("-spkreuse", "allow");
args.SoftSetArg("-blockprioritysize", "0");
Expand Down
2 changes: 1 addition & 1 deletion src/kernel/mempool_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ static constexpr int DEFAULT_MINRELAYMATURITY{0};
/** Default for -mempoolreplacement; must update docs in init.cpp manually */
static constexpr RBFPolicy DEFAULT_MEMPOOL_RBF_POLICY{RBFPolicy::Always};
/** Default for -mempooltruc; must update docs in init.cpp manually */
static constexpr TRUCPolicy DEFAULT_MEMPOOL_TRUC_POLICY{TRUCPolicy::Accept};
static constexpr TRUCPolicy DEFAULT_MEMPOOL_TRUC_POLICY{TRUCPolicy::Enforce};
/** Whether to fall back to legacy V1 serialization when writing mempool.dat */
static constexpr bool DEFAULT_PERSIST_V1_DAT{false};
/** Default for -acceptnonstddatacarrier */
Expand Down
6 changes: 6 additions & 0 deletions src/policy/coin_age_priority.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ void BlockAssembler::addPriorityTxs(const CTxMemPool& mempool, int &nPackagesSel

vecPriority.reserve(mempool.mapTx.size());
for (auto mi = mempool.mapTx.begin(); mi != mempool.mapTx.end(); ++mi) {
// Priority selection considers transactions individually, so it cannot
// guarantee that ephemeral dust is spent in the same block. Leave
// these transactions to fee-based package selection instead.
if (!GetDust(mi->GetTx(), mempool.m_opts.dust_relay_feerate).empty()) {
continue;
}
double dPriority = mi->GetPriority(nHeight);
CAmount dummy;
mempool.ApplyDeltas(mi->GetTx().GetHash(), dPriority, dummy);
Expand Down
26 changes: 26 additions & 0 deletions test/functional/mempool_truc.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,31 @@ def test_minrelay_in_package_combos(self):
self.check_mempool([tx_v2_0fee_parent["txid"], tx_v2_child["txid"], tx_v3_0fee_parent["txid"], tx_v3_child["txid"]])


def test_default_and_overrides(self):
self.log.info("Test native TRUC default and explicit overrides with both corepolicy settings")
node = self.nodes[0]
for corepolicy in (0, 1):
for mode in (None, "accept", "reject", "enforce"):
args = [f"-corepolicy={corepolicy}", "-acceptnonstdtxn=0", "-persistmempool=0"]
if mode is not None:
args.append(f"-mempooltruc={mode}")
self.restart_node(0, extra_args=args)
assert_equal(node.getmempoolinfo()["truc_policy"], mode or "enforce")
self.wallet.rescan_utxos()
parent = self.wallet.create_self_transfer(version=3, confirmed_only=True)
child = self.wallet.create_self_transfer(utxo_to_spend=parent["new_utxo"], version=2)
if mode == "reject":
result = node.testmempoolaccept([parent["hex"]])[0]
assert_equal(result["allowed"], False)
assert_equal(result["reject-reason"], "version")
continue
node.sendrawtransaction(parent["hex"])
result = node.testmempoolaccept([child["hex"]])[0]
assert_equal(result["allowed"], mode == "accept")
if mode != "accept":
assert_equal(result["reject-reason"], "truc-spent-by-nontruc")
self.restart_node(0)

def run_test(self):
self.log.info("Generate blocks to create UTXOs")
node = self.nodes[0]
Expand All @@ -702,6 +727,7 @@ def run_test(self):
self.test_truc_sibling_eviction()
self.test_reorg_sibling_eviction_1p2c()
self.test_minrelay_in_package_combos()
self.test_default_and_overrides()


if __name__ == "__main__":
Expand Down
82 changes: 82 additions & 0 deletions test/functional/mining_ephemeral_priority.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# Copyright (c) 2026 The Bitcoin Knots developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php.
"""Priority space must not select an ephemeral-dust parent independently."""

from decimal import Decimal
import time

from test_framework.messages import COutPoint, CTxIn, CTxInWitness, CTxOut
from test_framework.script_util import PAY_TO_ANCHOR
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
from test_framework.wallet import MiniWallet


class EphemeralPriorityTest(BitcoinTestFramework):
def add_options(self, parser):
parser.add_argument("--test-subdust-penalty", action="store_true",
help="Also test native dust penalties (requires the ephemeral fee precheck fix)")

def set_test_params(self):
self.num_nodes = 1

def template_txids(self):
# Invalidate the five-second mempool-change template cache.
self.mock_time += 10
self.nodes[0].setmocktime(self.mock_time)
return {tx["txid"] for tx in self.nodes[0].getblocktemplate({"rules": ["segwit"]})["transactions"]}

def run_test(self):
self.mock_time = int(time.time())
for penalty in ([0, 1] if self.options.test_subdust_penalty else [0]):
self.test_priority(penalty)

def test_priority(self, penalty):
self.log.info(f"Check priority selection with subdustfeepenalty={penalty}")
args = ["-corepolicy=0", "-acceptnonstdtxn=0", "-persistmempool=0",
"-blockprioritysize=1000000", "-blockmaxsize=1000000",
"-blockmintxfee=0.01000000", f"-subdustfeepenalty={penalty}"]
self.restart_node(0, extra_args=args)
node = self.nodes[0]
wallet = MiniWallet(node)
parent = wallet.create_self_transfer(version=3, fee_rate=0, confirmed_only=True)["tx"]
parent.vout.append(CTxOut(0, PAY_TO_ANCHOR))
parent.rehash()
sponsor = wallet.get_utxo(confirmed_only=True)
child = wallet.create_self_transfer(utxo_to_spend=sponsor, version=3, fee=Decimal("0.00001000"))["tx"]
child.vin.append(CTxIn(COutPoint(parent.sha256, 1)))
child.wit.vtxinwit.append(CTxInWitness())
child.rehash()
assert_equal(node.submitpackage([parent.serialize().hex(), child.serialize().hex()])["package_msg"], "success")

# Even with a child, priority selection must not bypass package fees.
assert parent.hash not in self.template_txids()
assert child.hash not in self.template_txids()

# Replace only the child's independent sponsor spend, stranding the parent.
conflict = wallet.create_self_transfer(utxo_to_spend=sponsor, version=3, fee=Decimal("0.00005000"))["tx"]
node.sendrawtransaction(conflict.serialize().hex())
assert_equal(set(node.getrawmempool()), {parent.hash, conflict.hash})
template = self.template_txids()
assert parent.hash not in template
# The high mining fee floor excludes this ordinary transaction from
# fee selection; its inclusion proves priority space is still active.
assert conflict.hash in template

# Restore the child with a fee meeting the mining package threshold.
restored = wallet.create_self_transfer(version=3, fee=Decimal("0.01000000"), confirmed_only=True)["tx"]
restored.vin.append(CTxIn(COutPoint(parent.sha256, 1)))
restored.wit.vtxinwit.append(CTxInWitness())
restored.rehash()
node.sendrawtransaction(restored.serialize().hex(), 0)
assert_equal(set(node.getrawmempool()), {parent.hash, conflict.hash, restored.hash})
assert {parent.hash, restored.hash}.issubset(self.template_txids())
block = node.getblock(self.generate(node, 1)[0])
assert {parent.hash, restored.hash}.issubset(set(block["tx"]))
assert_equal(node.gettxout(parent.hash, 1), None)


if __name__ == "__main__":
EphemeralPriorityTest(__file__).main()
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
'wallet_txn_clone.py',
'wallet_txn_clone.py --segwit',
'mining_coin_age_priority.py',
'mining_ephemeral_priority.py',
'rpc_getchaintips.py',
'rpc_misc.py',
'p2p_1p1c_network.py',
Expand Down
Loading