Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

include verbose "debug-message" field in testmempoolaccept response #28121

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

pinheadmz
Copy link
Member

@pinheadmz pinheadmz commented Jul 21, 2023

Adds a new field debug-message in testmempoolaccept responses to include m_debug_message from ValidationState. This string is part of 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",
  "debug-message": "rejecting replacement 07d7a59a7bdad4c3a5070659ea04147c9b755ad9e173c52b6a38e017abf0f5b8; new feerate 0.00300000 BTC/kvB <= old feerate 0.00300000 BTC/kvB"
}

@DrahtBot
Copy link
Contributor

DrahtBot commented Jul 21, 2023

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK glozow, instagibbs, kristapsk, jonatack

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #28676 ([WIP] Cluster mempool implementation by sdaftuar)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@luke-jr
Copy link
Member

luke-jr commented Jul 25, 2023

Why not pass the details in m_debug_message and add that as a new field?

@pinheadmz
Copy link
Member Author

Why not pass the details in m_debug_message and add that as a new field?

My thinking was to match exactly the single combined string returned by sendrawtransaction. But if adding the new field gets your ACK @luke-jr I'll make the change ;-)

Copy link
Member

@glozow glozow left a comment

Choose a reason for hiding this comment

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

Concept ACK to providing the debug string, I'm sure it'd be helpful for users. Would be nice to do this for submitpackage as well.

I'm not sure if we try to keep strings for reject-reason stable at all. If we do, I'd be in favor of adding a new field error_msg with the full string instead of a separate field with just the debug message.

@pinheadmz
Copy link
Member Author

I'm not sure if we try to keep strings for reject-reason stable at all.

You mean like how block reject reasons are defined in BIP22?

Copy link
Contributor

@jonatack jonatack left a comment

Choose a reason for hiding this comment

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

lgtm ACK bcdc364 modulo the question of a new field vs the appending to the reject_reason output.

assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx.serialize().hex(), 0)
res = self.nodes[0].testmempoolaccept(rawtxs=[tx.serialize().hex()])[0]
assert not res['allowed']
assert debug_message in res['reject-reason']
Copy link
Contributor

Choose a reason for hiding this comment

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

In commit bafd175, can make a helper for this.

diff

--- a/test/functional/feature_rbf.py
+++ b/test/functional/feature_rbf.py
@@ -107,6 +107,11 @@ class ReplaceByFeeTest(BitcoinTestFramework):
 
         return self.wallet.get_utxo(txid=tx["txid"], vout=tx["sent_vout"])
 
+    def assert_rejected_by_testmempoolaccept(self, tx_hex, msg):
+        result = self.nodes[0].testmempoolaccept(rawtxs=[tx_hex])[0]
+        assert not result["allowed"]
+        assert msg in result["reject-reason"]
+
     def test_simple_doublespend(self):
         """Simple doublespend"""
         # we use MiniWallet to create a transaction template with inputs correctly set,
@@ -121,9 +126,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception due to insufficient fee
         debug_message = f"insufficient fee, rejecting replacement {tx.hash}; new feerate"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx.serialize().hex(), 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[tx.serialize().hex()])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(tx.serialize().hex(), debug_message)
 
         # Extra 0.1 BTC fee
         tx.vout[0].nValue -= int(0.1 * COIN)
@@ -168,9 +171,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception due to insufficient fee
         debug_message = f"insufficient fee, rejecting replacement {dbl_tx['txid']}; less fees than conflicting txs"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, dbl_tx["hex"], 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[dbl_tx["hex"]])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(dbl_tx["hex"], debug_message)
 
         # Accepted with sufficient fee
         dbl_tx["tx"].vout[0].nValue = int(0.1 * COIN)
@@ -307,9 +308,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception
         debug_message = f"bad-txns-spends-conflicting-tx, {tx2['txid']} spends conflicting transaction {tx1a['txid']}"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx2["hex"], 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[tx2["hex"]])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(tx2["hex"], debug_message)
 
         # Spend tx1a's output to test the indirect case.
         tx1b_utxo = self.wallet.send_self_transfer(
@@ -349,9 +348,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception
         debug_message = f"replacement-adds-unconfirmed, replacement {tx2['txid']} adds unconfirmed input, idx"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx2["hex"], 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[tx2["hex"]])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(tx2["hex"], debug_message)
 
     def test_too_many_replacements(self):
         """Replacements that evict too many transactions are rejected"""
@@ -394,9 +391,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         # This will raise an exception
         debug_message = f"too many potential replacements, rejecting replacement {double_tx['txid']}; too many potential replacements"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, double_tx["hex"], 0)
-        res = self.nodes[0].testmempoolaccept(rawtxs=[double_tx["hex"]])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(double_tx["hex"], debug_message)
 
         # If we remove an input, it should pass
         double_tx["tx"].vin.pop()
@@ -720,9 +715,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
         tx.rehash()
         debug_message = f"insufficient fee, rejecting replacement {tx.hash}; not enough additional fees to relay"
         assert_raises_rpc_error(-26, debug_message, self.nodes[0].sendrawtransaction, tx.serialize().hex())
-        res = self.nodes[0].testmempoolaccept(rawtxs=[tx.serialize().hex()])[0]
-        assert not res['allowed']
-        assert debug_message in res['reject-reason']
+        self.assert_rejected_by_testmempoolaccept(tx.serialize().hex(), debug_message)
 
     def test_fullrbf(self):

@glozow
Copy link
Member

glozow commented Sep 26, 2023

I'm not sure if we try to keep strings for reject-reason stable at all.

You mean like how block reject reasons are defined in BIP22?

Yeah exactly. Like if somebody has a fee bumper that's triggered by reject-reason == "mempool min fee not met" or something.

@pinheadmz
Copy link
Member Author

Yeah exactly. Like if somebody has a fee bumper that's triggered by reject-reason == "mempool min fee not met" or something.

Yeah ok so, the detailed message is appended to the original so "somebody" would just need to change their == to a "starts with" or "includes" or regex checker. In my opinion, this API-breaking change is why we have major version numbers and release notes. So I'm not sure how we decide whats precious or not. Either way is fine with me.

@DrahtBot
Copy link
Contributor

Needs rebase if still relevant

@pinheadmz
Copy link
Member Author

Needs rebase if still relevant

Done 🙏

@instagibbs
Copy link
Member

concept ACK

unfortunately I know people do use reject-reason strings for various informational purposes. Would be a good project to gather those uses and maybe think of better error codes to capture these?

@pinheadmz
Copy link
Member Author

@instagibbs thanks for taking a look! Not sure what you mean about gathering uses though. Are you agreeing with @luke-jr that the debug messages should have their own field in TMA response object and leave reject-reason alone?

@instagibbs
Copy link
Member

instagibbs commented Oct 19, 2023

Not sure what you mean about gathering uses though.

Looking at why error codes aren't enough for production uses

@pinheadmz
Copy link
Member Author

Looking at why error codes aren't enough for production uses

I'm working on an RBF-batcher with CardCoins and we were getting insufficient fee errors in test from testmempoolaccept(), which could be reported from two different places. Inspiration for this PR came from that ambiguity.

return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "insufficient fee", *err_string);

return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "insufficient fee", *err_string);

@kristapsk
Copy link
Contributor

Concept ACK

@0xB10C
Copy link
Contributor

0xB10C commented Apr 9, 2024

I have a slight preference to keep the reject-reason string as is, but like the idea of having more information on rejections. In https://github.com/0xB10C/find-non-standard-tx and for https://bitcoin-data.github.io/non-standard-transactions/ I use the short reject-reason. Maybe add a new field for details?

@pinheadmz pinheadmz changed the title include verbose debug messages in testmempoolaccept reject-reason include verbose "debug-message" field in testmempoolaccept response Apr 16, 2024
@pinheadmz
Copy link
Member Author

Thanks for taking a look, @0xB10C. I've refactored the PR to add a new field instead of changing anything already in place. Edited PR title and description. I made the tests super clear that the complete string is returned by submitrawtransaction whereas its split into two fields in testmempoolaccept

@0xB10C
Copy link
Contributor

0xB10C commented May 5, 2024

I've played around a bit with this in 0xB10C/find-non-standard-tx#2.

I've seen output for the following reasons:

  • too-long-mempool-chain: e.g.
    • exceeds descendant size limit for tx 9f95e53da39ece93ceba3cc78a443a1aef0fc656bf921bbd2aee7f6533f0604f [limit: 101000]
    • exceeds ancestor size limit [limit: 101000]
    • 2256302dedb181f83bc51248215e51f680f77d07e11ad7e9e34c2e483ad10e7b [limit: 26]
  • min relay fee not met: e.g. 0 < 200

Other reasons like e.g. scriptpubkey, tx-size, dust, missing-inputs, multi-op-return have an empty debug-message.

I haven't yet taken a closer look at the code.

@pinheadmz
Copy link
Member Author

Other reasons like e.g. scriptpubkey, tx-size, dust, missing-inputs, multi-op-return have an empty debug-message.

debug-message was empty like ""? I'd expect the key to not be present at all:

if (!state.GetDebugMessage().empty()) result_inner.pushKV("debug-message", state.GetDebugMessage());

@0xB10C
Copy link
Contributor

0xB10C commented May 6, 2024

debug-message was empty like ""? I'd expect the key to not be present at all:

You're right. I should have been clearer in my comment: the field wasn't present. My tooling handles the missing field by setting the value to "".

@pinheadmz
Copy link
Member Author

force-push to fix merge conflict which also looked like a git mistake on my part in the first place 😬

Copy link
Contributor

@jonatack jonatack left a comment

Choose a reason for hiding this comment

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

Concept ACK d2917e7

@@ -141,6 +141,7 @@ static RPCHelpMan testmempoolaccept()
}},
}},
{RPCResult::Type::STR, "reject-reason", /*optional=*/true, "Rejection string (only present when 'allowed' is false)"},
{RPCResult::Type::STR, "debug-message", /*optional=*/true, "Rejection details (only present when 'allowed' is false and a message is provided)"},
Copy link
Contributor

@jonatack jonatack May 16, 2024

Choose a reason for hiding this comment

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

c1be9d4

Suggested change
{RPCResult::Type::STR, "debug-message", /*optional=*/true, "Rejection details (only present when 'allowed' is false and a message is provided)"},
{RPCResult::Type::STR, "reject-details", /*optional=*/true, "Rejection details (only present when 'allowed' is false and rejection details exist)"},

@@ -141,6 +141,7 @@ static RPCHelpMan testmempoolaccept()
}},
}},
{RPCResult::Type::STR, "reject-reason", /*optional=*/true, "Rejection string (only present when 'allowed' is false)"},
Copy link
Contributor

Choose a reason for hiding this comment

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

c1be9d4 while touching the next line, string here might be a relic from when the field type wasn't auto-generated in the help, ISTM reason would be clearer

Suggested change
{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)"},

@@ -0,0 +1,5 @@
RPC
Copy link
Contributor

@jonatack jonatack May 16, 2024

Choose a reason for hiding this comment

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

d2917e7 unsure if Updated RPCs would be better here, feel free to ignore

RPC
---

`testmempoolaccept` response will now include verbose field "debug-message"
Copy link
Contributor

@jonatack jonatack May 16, 2024

Choose a reason for hiding this comment

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

d2917e7 the new field won't always be included (and why "verbose"?)

Suggested change
`testmempoolaccept` response will now include verbose field "debug-message"
The RPC `testmempoolaccept` response now includes a "reject-details" field in some cases

@pinheadmz
Copy link
Member Author

Thanks for reviewing @jonatack I like the idea of "reject-details" and addressed your other comments as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants