Skip to content

Commit

Permalink
Merge bitcoin/bitcoin#28121: include verbose "reject-details" field i…
Browse files Browse the repository at this point in the history
…n testmempoolaccept response

b6f0593 doc: add release note about testmempoolaccept debug-message (Matthew Zipkin)
f9cac63 test: cover testmempoolaccept debug-message in RBF test (Matthew Zipkin)
f9650e1 rbf: remove unecessary newline at end of error string (Matthew Zipkin)
221c789 rpc: include verbose reject-details field in testmempoolaccept response (Matthew Zipkin)

Pull request description:

  Adds a new field `reject-details` in `testmempoolaccept` responses to include `m_debug_message` from `ValidationState`. This string is the complete error message thrown by the mempool in response to `sendrawtransaction`.

  The extra verbosity is helpful to consumers of `testmempoolaccept`, which is sort of a debug tool anyway.

  example:
  >
  > {
  >   "txid": "07d7a59a7bdad4c3a5070659ea04147c9b755ad9e173c52b6a38e017abf0f5b8",
  >   "wtxid": "5dc243b1b92ee2f5a43134eb3e23449be03d1abb3d7f3c03c836ed0f13c50185",
  >   "allowed": false,
  >   "reject-reason": "insufficient fee",
  >   "reject-details": "insufficient fee, rejecting replacement 07d7a59a7bdad4c3a5070659ea04147c9b755ad9e173c52b6a38e017abf0f5b8; new feerate 0.00300000 BTC/kvB <= old feerate 0.00300000 BTC/kvB"
  > }

ACKs for top commit:
  rkrux:
    re-ACK b6f0593
  glozow:
    ACK b6f0593

Tree-SHA512: 340b8023d59cefa84598879c4efdb7c399a3f62da126e87c595523f302e53d33098fc69da9c5f8c92b7580dc75466c66cea372051f935b197265648fe15c43a3
  • Loading branch information
glozow committed Jan 3, 2025
2 parents 228aba2 + b6f0593 commit 604bf2e
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 28 deletions.
2 changes: 2 additions & 0 deletions doc/release-notes-28121.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The RPC `testmempoolaccept` response now includes a "reject-details" field in some cases,
similar to the complete error messages returned by `sendrawtransaction` (#28121)
2 changes: 1 addition & 1 deletion src/policy/rbf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ std::optional<std::string> GetEntriesForConflicts(const CTransaction& tx,
// descendants (i.e. if multiple conflicts share a descendant, it will be counted multiple
// times), but we just want to be conservative to avoid doing too much work.
if (nConflictingCount > MAX_REPLACEMENT_CANDIDATES) {
return strprintf("rejecting replacement %s; too many potential replacements (%d > %d)\n",
return strprintf("rejecting replacement %s; too many potential replacements (%d > %d)",
txid.ToString(),
nConflictingCount,
MAX_REPLACEMENT_CANDIDATES);
Expand Down
4 changes: 3 additions & 1 deletion src/rpc/mempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ static RPCHelpMan testmempoolaccept()
{RPCResult{RPCResult::Type::STR_HEX, "", "transaction wtxid in hex"},
}},
}},
{RPCResult::Type::STR, "reject-reason", /*optional=*/true, "Rejection string (only present when 'allowed' is false)"},
{RPCResult::Type::STR, "reject-reason", /*optional=*/true, "Rejection reason (only present when 'allowed' is false)"},
{RPCResult::Type::STR, "reject-details", /*optional=*/true, "Rejection details (only present when 'allowed' is false and rejection details exist)"},
}},
}
},
Expand Down Expand Up @@ -245,6 +246,7 @@ static RPCHelpMan testmempoolaccept()
result_inner.pushKV("reject-reason", "missing-inputs");
} else {
result_inner.pushKV("reject-reason", state.GetRejectReason());
result_inner.pushKV("reject-details", state.ToString());
}
}
rpc_result.push_back(std::move(result_inner));
Expand Down
11 changes: 9 additions & 2 deletions test/functional/feature_cltv.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ def run_test(self):
# create and test one invalid tx per CLTV failure reason (5 in total)
for i in range(5):
spendtx = wallet.create_self_transfer()['tx']
assert_equal(len(spendtx.vin), 1)
coin = spendtx.vin[0]
coin_txid = format(coin.prevout.hash, '064x')
coin_vout = coin.prevout.n
cltv_invalidate(spendtx, i)

expected_cltv_reject_reason = [
Expand All @@ -159,12 +163,15 @@ def run_test(self):
][i]
# First we show that this tx is valid except for CLTV by getting it
# rejected from the mempool for exactly that reason.
spendtx_txid = spendtx.hash
spendtx_wtxid = spendtx.getwtxid()
assert_equal(
[{
'txid': spendtx.hash,
'wtxid': spendtx.getwtxid(),
'txid': spendtx_txid,
'wtxid': spendtx_wtxid,
'allowed': False,
'reject-reason': expected_cltv_reject_reason,
'reject-details': expected_cltv_reject_reason + f", input 0 of {spendtx_txid} (wtxid {spendtx_wtxid}), spending {coin_txid}:{coin_vout}"
}],
self.nodes[0].testmempoolaccept(rawtxs=[spendtx.serialize().hex()], maxfeerate=0),
)
Expand Down
11 changes: 8 additions & 3 deletions test/functional/feature_dersig.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,23 @@ def run_test(self):
self.log.info("Test that transactions with non-DER signatures cannot appear in a block")
block.nVersion = 4

spendtx = self.create_tx(self.coinbase_txids[1])
coin_txid = self.coinbase_txids[1]
spendtx = self.create_tx(coin_txid)
unDERify(spendtx)
spendtx.rehash()

# First we show that this tx is valid except for DERSIG by getting it
# rejected from the mempool for exactly that reason.
spendtx_txid = spendtx.hash
spendtx_wtxid = spendtx.getwtxid()
assert_equal(
[{
'txid': spendtx.hash,
'wtxid': spendtx.getwtxid(),
'txid': spendtx_txid,
'wtxid': spendtx_wtxid,
'allowed': False,
'reject-reason': 'mandatory-script-verify-flag-failed (Non-canonical DER signature)',
'reject-details': 'mandatory-script-verify-flag-failed (Non-canonical DER signature), ' +
f"input 0 of {spendtx_txid} (wtxid {spendtx_wtxid}), spending {coin_txid}:0"
}],
self.nodes[0].testmempoolaccept(rawtxs=[spendtx.serialize().hex()], maxfeerate=0),
)
Expand Down
60 changes: 48 additions & 12 deletions test/functional/feature_rbf.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,22 @@ def test_simple_doublespend(self):
"""Simple doublespend"""
# we use MiniWallet to create a transaction template with inputs correctly set,
# and modify the output (amount, scriptPubKey) according to our needs
tx = self.wallet.create_self_transfer()["tx"]
tx = self.wallet.create_self_transfer(fee_rate=Decimal("0.003"))["tx"]
tx1a_txid = self.nodes[0].sendrawtransaction(tx.serialize().hex())

# Should fail because we haven't changed the fee
tx.vout[0].scriptPubKey[-1] ^= 1
tx.rehash()
tx_hex = tx.serialize().hex()

# This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx.serialize().hex(), 0)
reject_reason = "insufficient fee"
reject_details = f"{reject_reason}, rejecting replacement {tx.hash}; new feerate 0.00300000 BTC/kvB <= old feerate 0.00300000 BTC/kvB"
res = self.nodes[0].testmempoolaccept(rawtxs=[tx_hex])[0]
assert_equal(res["reject-reason"], reject_reason)
assert_equal(res["reject-details"], reject_details)
assert_raises_rpc_error(-26, f"{reject_details}", self.nodes[0].sendrawtransaction, tx_hex, 0)


# Extra 0.1 BTC fee
tx.vout[0].nValue -= int(0.1 * COIN)
Expand Down Expand Up @@ -154,7 +162,14 @@ def test_doublespend_chain(self):
dbl_tx_hex = dbl_tx.serialize().hex()

# This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0)
reject_reason = "insufficient fee"
reject_details = f"{reject_reason}, rejecting replacement {dbl_tx.hash}, less fees than conflicting txs; 3.00 < 4.00"
res = self.nodes[0].testmempoolaccept(rawtxs=[dbl_tx_hex])[0]
assert_equal(res["reject-reason"], reject_reason)
assert_equal(res["reject-details"], reject_details)
assert_raises_rpc_error(-26, f"{reject_details}", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0)



# Accepted with sufficient fee
dbl_tx.vout[0].nValue = int(0.1 * COIN)
Expand Down Expand Up @@ -273,22 +288,30 @@ def test_spends_of_conflicting_outputs(self):
utxo1 = self.make_utxo(self.nodes[0], int(1.2 * COIN))
utxo2 = self.make_utxo(self.nodes[0], 3 * COIN)

tx1a_utxo = self.wallet.send_self_transfer(
tx1a = self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=utxo1,
sequence=0,
fee=Decimal("0.1"),
)["new_utxo"]
)
tx1a_utxo = tx1a["new_utxo"]

# Direct spend an output of the transaction we're replacing.
tx2_hex = self.wallet.create_self_transfer_multi(
tx2 = self.wallet.create_self_transfer_multi(
utxos_to_spend=[utxo1, utxo2, tx1a_utxo],
sequence=0,
amount_per_output=int(COIN * tx1a_utxo["value"]),
)["hex"]
)["tx"]
tx2_hex = tx2.serialize().hex()

# This will raise an exception
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0)
reject_reason = "bad-txns-spends-conflicting-tx"
reject_details = f"{reject_reason}, {tx2.hash} spends conflicting transaction {tx1a['tx'].hash}"
res = self.nodes[0].testmempoolaccept(rawtxs=[tx2_hex])[0]
assert_equal(res["reject-reason"], reject_reason)
assert_equal(res["reject-details"], reject_details)
assert_raises_rpc_error(-26, f"{reject_details}", self.nodes[0].sendrawtransaction, tx2_hex, 0)


# Spend tx1a's output to test the indirect case.
tx1b_utxo = self.wallet.send_self_transfer(
Expand Down Expand Up @@ -319,14 +342,21 @@ def test_new_unconfirmed_inputs(self):
fee=Decimal("0.1"),
)

tx2_hex = self.wallet.create_self_transfer_multi(
tx2 = self.wallet.create_self_transfer_multi(
utxos_to_spend=[confirmed_utxo, unconfirmed_utxo],
sequence=0,
amount_per_output=1 * COIN,
)["hex"]
)["tx"]
tx2_hex = tx2.serialize().hex()

# This will raise an exception
assert_raises_rpc_error(-26, "replacement-adds-unconfirmed", self.nodes[0].sendrawtransaction, tx2_hex, 0)
reject_reason = "replacement-adds-unconfirmed"
reject_details = f"{reject_reason}, replacement {tx2.hash} adds unconfirmed input, idx 1"
res = self.nodes[0].testmempoolaccept(rawtxs=[tx2_hex])[0]
assert_equal(res["reject-reason"], reject_reason)
assert_equal(res["reject-details"], reject_details)
assert_raises_rpc_error(-26, f"{reject_details}", self.nodes[0].sendrawtransaction, tx2_hex, 0)


def test_too_many_replacements(self):
"""Replacements that evict too many transactions are rejected"""
Expand Down Expand Up @@ -368,7 +398,13 @@ def test_too_many_replacements(self):
double_tx_hex = double_tx.serialize().hex()

# This will raise an exception
assert_raises_rpc_error(-26, "too many potential replacements", self.nodes[0].sendrawtransaction, double_tx_hex, 0)
reject_reason = "too many potential replacements"
reject_details = f"{reject_reason}, rejecting replacement {double_tx.hash}; too many potential replacements ({MAX_REPLACEMENT_LIMIT + 1} > {MAX_REPLACEMENT_LIMIT})"
res = self.nodes[0].testmempoolaccept(rawtxs=[double_tx_hex])[0]
assert_equal(res["reject-reason"], reject_reason)
assert_equal(res["reject-details"], reject_details)
assert_raises_rpc_error(-26, f"{reject_details}", self.nodes[0].sendrawtransaction, double_tx_hex, 0)


# If we remove an input, it should pass
double_tx.vin.pop()
Expand Down
2 changes: 2 additions & 0 deletions test/functional/mempool_accept.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def check_mempool_result(self, result_expected, *args, **kwargs):
if "fees" in r:
r["fees"].pop("effective-feerate")
r["fees"].pop("effective-includes")
if "reject-details" in r:
r.pop("reject-details")
assert_equal(result_expected, result_test)
assert_equal(self.nodes[0].getmempoolinfo()['size'], self.mempool_size) # Must not change mempool state

Expand Down
6 changes: 4 additions & 2 deletions test/functional/mempool_accept_wtxid.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,15 @@ def run_test(self):
"txid": child_one_txid,
"wtxid": child_one_wtxid,
"allowed": False,
"reject-reason": "txn-already-in-mempool"
"reject-reason": "txn-already-in-mempool",
"reject-details": "txn-already-in-mempool"
}])
assert_equal(node.testmempoolaccept([child_two.serialize().hex()])[0], {
"txid": child_two_txid,
"wtxid": child_two_wtxid,
"allowed": False,
"reject-reason": "txn-same-nonwitness-data-in-mempool"
"reject-reason": "txn-same-nonwitness-data-in-mempool",
"reject-details": "txn-same-nonwitness-data-in-mempool"
})

# sendrawtransaction will not throw but quits early when the exact same transaction is already in mempool
Expand Down
4 changes: 2 additions & 2 deletions test/functional/mempool_package_rbf.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def test_package_rbf_max_conflicts(self):
package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])

pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)
assert_equal(f"package RBF failed: too many potential replacements, rejecting replacement {package_child['tx'].rehash()}; too many potential replacements (102 > 100)\n", pkg_results["package_msg"])
assert_equal(f"package RBF failed: too many potential replacements, rejecting replacement {package_child['tx'].rehash()}; too many potential replacements (102 > 100)", pkg_results["package_msg"])
self.assert_mempool_contents(expected=expected_txns)

# Make singleton tx to conflict with in next batch
Expand All @@ -234,7 +234,7 @@ def test_package_rbf_max_conflicts(self):
package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=double_spending_coins, fee_per_output=parent_fee_per_conflict)
package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])
pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)
assert_equal(f"package RBF failed: too many potential replacements, rejecting replacement {package_child['tx'].rehash()}; too many potential replacements (101 > 100)\n", pkg_results["package_msg"])
assert_equal(f"package RBF failed: too many potential replacements, rejecting replacement {package_child['tx'].rehash()}; too many potential replacements (101 > 100)", pkg_results["package_msg"])
self.assert_mempool_contents(expected=expected_txns)

# Finally, evict MAX_REPLACEMENT_CANDIDATES
Expand Down
15 changes: 10 additions & 5 deletions test/functional/rpc_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,21 @@ def test_independent(self, coin):
self.assert_testres_equal(package_bad, testres_bad)

self.log.info("Check testmempoolaccept tells us when some transactions completed validation successfully")
tx_bad_sig_hex = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}],
tx_bad_sig_hex = node.createrawtransaction([{"txid": coin["txid"], "vout": coin["vout"]}],
{address : coin["amount"] - Decimal("0.0001")})
tx_bad_sig = tx_from_hex(tx_bad_sig_hex)
testres_bad_sig = node.testmempoolaccept(self.independent_txns_hex + [tx_bad_sig_hex])
# By the time the signature for the last transaction is checked, all the other transactions
# have been fully validated, which is why the node returns full validation results for all
# transactions here but empty results in other cases.
tx_bad_sig_txid = tx_bad_sig.rehash()
tx_bad_sig_wtxid = tx_bad_sig.getwtxid()
assert_equal(testres_bad_sig, self.independent_txns_testres + [{
"txid": tx_bad_sig.rehash(),
"wtxid": tx_bad_sig.getwtxid(), "allowed": False,
"reject-reason": "mandatory-script-verify-flag-failed (Operation not valid with the current stack size)"
"txid": tx_bad_sig_txid,
"wtxid": tx_bad_sig_wtxid, "allowed": False,
"reject-reason": "mandatory-script-verify-flag-failed (Operation not valid with the current stack size)",
"reject-details": "mandatory-script-verify-flag-failed (Operation not valid with the current stack size), " +
f"input 0 of {tx_bad_sig_txid} (wtxid {tx_bad_sig_wtxid}), spending {coin['txid']}:{coin['vout']}"
}])

self.log.info("Check testmempoolaccept reports txns in packages that exceed max feerate")
Expand Down Expand Up @@ -304,7 +308,8 @@ def test_rbf(self):
assert testres_rbf_single[0]["allowed"]
testres_rbf_package = self.independent_txns_testres_blank + [{
"txid": replacement_tx["txid"], "wtxid": replacement_tx["wtxid"], "allowed": False,
"reject-reason": "bip125-replacement-disallowed"
"reject-reason": "bip125-replacement-disallowed",
"reject-details": "bip125-replacement-disallowed"
}]
self.assert_testres_equal(self.independent_txns_hex + [replacement_tx["hex"]], testres_rbf_package)

Expand Down

0 comments on commit 604bf2e

Please sign in to comment.