-
Notifications
You must be signed in to change notification settings - Fork 37.7k
validation: distinguish between same tx and same-nonwitness-data tx in mempool #22253
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK 012341f
A few style comments inline. Feel free to ignore.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I'm Concept ACK on returning a different testmempoolaccept
error message when we hit a same-txid-different-wtxid mempool candidate but I don't think this is necessary for current package mempool accept project ? Enabling witness replacement is a project which is worthy on its own imho, as Taproot make it more probable to have concurrent broadcasts with "feerate-divergent" witnesses.
Note, to allow same-txid-different-wtxid we still need to rework RBF rules, as the rule 5 enforces a replacement penalty which imply an absolute fee increases IIRC. However, an absolute fee is implicitly committed as part of non-witness data (sub outpoints's amount/outputs's value).
} else if (m_pool.exists(GenTxid(false, tx.GetHash()))) { | ||
// Transaction with the same nonwitness data but different witnes (same txid, different | ||
// wtxid) already exists in the mempool. TODO: allow replacements | ||
return state.Invalid(TxValidationResult::TX_CONFLICT, "txn-same-nonwitness-data-in-mempool"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about "mutant-replacement-disallowed" as a rejection message ? Where a mutant is defined as a transaction sharing an equivalent txid of an already in-mempool transaction but offering differing witnesses.
It's quite kind of a glossary point but i don't think we have yet an unified term across the codebase to designate same-txid-diff-wtxid ? Maybe someone has a better naming proposal, but it sounds less verbose rather than duplicating "same nonwitness data but different witness (same txid, different wtxid)" everywhere in the code paths where this subtlety is present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think a user would find "mutant-replacement-disallowed" more helpful than "txn-same-nonwitness-data-in-mempool." It would require them knowing what mutant replacement is (which doesn't exist yet), which requires knowing what a mutant is (which is not widely defined). It's confusing because it can imply that mutant replacement is allowed somehwere else...
If/when the replacement policy is implemented, that would be an excellent time to give it a name. But at this point, what's relevant to users is "a different transaction with the same nonwitness data is already in the mempool."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point, it's quite a chicken-and-egg issue, either you pollute the codebases with multiple times verbose variable names like "same-txid-different-wtxid", which are less unambiguous or you find one name, defined somehwhere in the codebase, and hope to reviewers to refer it during discussion.
Yes it can be added latter for sure, once we add mutant replacement.
src/validation.h
Outdated
@@ -181,12 +181,15 @@ struct MempoolAcceptResult { | |||
const std::optional<std::list<CTransactionRef>> m_replaced_transactions; | |||
/** Raw base fees in satoshis. */ | |||
const std::optional<CAmount> m_base_fees; | |||
/** Size in virtual bytes. */ | |||
const std::optional<int64_t> m_vsize; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you plan to use vbytes as an identifier to dissociate same-txid-different-wtxid transaction in AcceptSingleTransaction
consumers ?
I'm not sure it's absent of collisions, two mutants can have the same vbytes but yet different witnesses ? E.g, a P2WSH 1-of-2 spent by either Alice signature or Bob signature
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. See 188cab0 - we need the vsize in the result because otherwise the RPC code doesn't know it without looking into the mempool (which would require holding the mempool lock).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the commit you're pointing to me is a unittest, exercising this logic with a collision on vbytes ? Do you think we can't have vbytes collision among "same-txid-different-wtxids" candidates ?
we need the vsize in the result because otherwise the RPC code doesn't know it without looking into the mempool (which would require holding the mempool lock).
Sorry, I don't understand, what the RPC code is searching to know, the vbyte's size or an ambiguous identifer between "same-txid-different-wtxids" candidates ?
012341f
to
81d92fa
Compare
81d92fa
to
e6ba22e
Compare
ACK e6ba22e |
You know i'm quite bothered by you reaching offline, asking me to re-use my work with #19645 and the well-foundness of #22252, of which I said it wasn't really required for a v0.1 package-relay and that we should be careful before considering this change. I still appreciate the energy and dedication you're pouring in this serie of work. That said next time if you have questions on any work related to better support for L2s, ping me on #bitcoin-core-dev. Anyone will be able to learn from the discussion, and it would avoid me to repeat twice my arguments :/ |
@ariard We discussed this 8 months ago on github, when you posted that you would open the PR to make this change. I felt that there was a bit of urgency which is why I wanted to PR it, and messaged you privately to make sure we wouldn't be doing redundant work. That's my view, and I can also see that having multiple private/public communication channels can cause wires to cross and require repetitive messages. My apologies. |
e6ba22e
to
ea67802
Compare
Code review ACK ea67802 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utACK ea67802
The last commit has Antoine emails twice:
Not that I care much, just pointing out it might be something odd. At a high level, I'm not getting lost with the justification of this in terms of 19645. If the plan is to have replacement of different-witness in the future, then why adding this error in the first place here? Do we need this in the meanwhile, until different-witness-replacement is merged? |
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're going to rebase, a few v smol suggestions for the test: Diff--- a/test/functional/mempool_accept_wtxid.py
+++ b/test/functional/mempool_accept_wtxid.py
@@ -7,46 +7,42 @@ Test mempool acceptance in case of an already known transaction
with identical non-witness data different witness.
"""
-from test_framework.script import (
- CScript,
- OP_0,
- OP_TRUE,
- OP_IF,
- OP_HASH160,
- OP_EQUAL,
- OP_ELSE,
- OP_ENDIF,
- hash160,
-)
from test_framework.messages import (
+ COIN,
+ COutPoint,
CTransaction,
CTxIn,
CTxInWitness,
CTxOut,
- COutPoint,
sha256,
- COIN,
- ToHex,
)
-from test_framework.test_framework import BitcoinTestFramework
-from test_framework.util import (
- assert_equal,
+from test_framework.p2p import P2PTxInvStore
+from test_framework.script import (
+ CScript,
+ OP_0,
+ OP_ELSE,
+ OP_ENDIF,
+ OP_EQUAL,
+ OP_HASH160,
+ OP_IF,
+ OP_TRUE,
+ hash160,
)
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal
class MempoolWtxidTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
- self.extra_args = [["-incrementalrelayfee=0"]]
def run_test(self):
node = self.nodes[0]
- self.log.info('Start with empty mempool, and 200 blocks')
- privkeys = [node.get_deterministic_priv_key().key]
+ self.log.info("Start with empty mempool")
# The last 100 coinbase transactions are premature
- earliest_blockhash = node.generate(101)[0]
- txid = node.getblock(blockhash=earliest_blockhash, verbosity=2)["tx"][0]["txid"]
+ blockhash = node.generate(101)[0]
+ txid = node.getblock(blockhash=blockhash, verbosity=2)["tx"][0]["txid"]
assert_equal(node.getmempoolinfo()['size'], 0)
self.log.info("Submit parent with multiple script branches to mempool")
@@ -60,11 +56,12 @@ class MempoolWtxidTest(BitcoinTestFramework):
parent.vout.append(CTxOut(int(9.99998 * COIN), script_pubkey))
parent.rehash()
+ privkeys = [node.get_deterministic_priv_key().key]
raw_parent = node.signrawtransactionwithkey(hexstring=parent.serialize().hex(), privkeys=privkeys)['hex']
parent_txid = node.sendrawtransaction(hexstring=raw_parent, maxfeerate=0)
node.generate(1)
- # Create a new segwit transaction with witness solving first branch
+ # Create a new transaction with witness solving first branch
child_witness_script = CScript([OP_TRUE])
child_witness_program = sha256(child_witness_script)
child_script_pubkey = CScript([OP_0, child_witness_program])
@@ -77,7 +74,7 @@ class MempoolWtxidTest(BitcoinTestFramework):
child_one_wtxid = child_one.getwtxid()
child_one_txid = child_one.rehash()
- # Create another identical segwit transaction with witness solving second branch
+ # Create another identical transaction with witness solving second branch
child_two = CTransaction()
child_two.vin.append(CTxIn(COutPoint(int(parent_txid, 16), 0), b""))
child_two.vout.append(CTxOut(int(9.99996 * COIN), child_script_pubkey)) |
Changes behavior.
Co-authored-by: Antoine Riard <ariard@student.42.fr> Co-authored-by: Antoine Riard <antoine.riard@gmail.com>
ea67802
to
b7a8cd9
Compare
Rebased and applied suggestions from #22253 (comment) |
ACK b7a8cd9 |
Code review ACK b7a8cd9 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code-review ACK b7a8cd9
I think the simplest parent witness script to create same-nonwitness-data txs would be OP_DROP OP_TRUE
, with a different arbitrary witness stack element in each child transaction. OTOH multiple script branches are far more realistic, I guess that's the primary scenario :)
Left some non-blocking nits below only regarding the test, feel free to ignore.
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
""" | ||
Test mempool acceptance in case of an already known transaction | ||
with identical non-witness data different witness. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
with identical non-witness data different witness. | |
with identical non-witness data but different witness. |
testres_child_two = node.testmempoolaccept([child_two.serialize().hex()])[0] | ||
assert_equal(testres_child_two, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, could be simplified (like for child_one
above):
testres_child_two = node.testmempoolaccept([child_two.serialize().hex()])[0] | |
assert_equal(testres_child_two, { | |
assert_equal(node.testmempoolaccept([child_two.serialize().hex()]), [{ |
child_two = CTransaction() | ||
child_two.vin.append(CTxIn(COutPoint(int(parent_txid, 16), 0), b"")) | ||
child_two.vout.append(CTxOut(int(9.99996 * COIN), child_script_pubkey)) | ||
child_two.wit.vtxinwit.append(CTxInWitness()) | ||
child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', witness_script] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
possible dedup alternative, since the txs only differ in one part:
child_two = CTransaction() | |
child_two.vin.append(CTxIn(COutPoint(int(parent_txid, 16), 0), b"")) | |
child_two.vout.append(CTxOut(int(9.99996 * COIN), child_script_pubkey)) | |
child_two.wit.vtxinwit.append(CTxInWitness()) | |
child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', witness_script] | |
child_two = copy.deepcopy(child_one) | |
child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', witness_script] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re-utACK b7a8cd9
return state.Invalid(TxValidationResult::TX_CONFLICT, "txn-already-in-mempool"); | ||
} else if (m_pool.exists(GenTxid(false, tx.GetHash()))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(One nicety if you pass this way again.)
- if (m_pool.exists(GenTxid(true, tx.GetWitnessHash()))) {
+ if (m_pool.exists(GenTxid(/* is_wtxid */ true, tx.GetWitnessHash()))) {
// Exact transaction already exists in the mempool.
return state.Invalid(TxValidationResult::TX_CONFLICT, "txn-already-in-mempool");
- } else if (m_pool.exists(GenTxid(false, tx.GetHash()))) {
+ } else if (m_pool.exists(GenTxid(/* is_wtxid */ false, tx.GetHash()))) {
91b0597 Improve mempool_accept_wtxid.py (naiza) Pull request description: Follow-up to #22253 adding changes suggested in [#22253 (review)](#22253 (comment)) ACKs for top commit: glozow: utACK 91b0597 Tree-SHA512: 383064138a5b2160d769c9df370470fd585c91682083013a6fa15e14448a4b481bc09b3a0ed6e75554db2c378df6b2263c65f209f973c9e9d577e15814a4be1d
91b0597 Improve mempool_accept_wtxid.py (naiza) Pull request description: Follow-up to bitcoin#22253 adding changes suggested in [bitcoin#22253 (review)](bitcoin#22253 (comment)) ACKs for top commit: glozow: utACK 91b0597 Tree-SHA512: 383064138a5b2160d769c9df370470fd585c91682083013a6fa15e14448a4b481bc09b3a0ed6e75554db2c378df6b2263c65f209f973c9e9d577e15814a4be1d
On master, if you submit a transaction with the same txid but different witness to the mempool, it thinks the transactions are the same. Users submitting through
BroadcastTransaction()
(i.e.sendrawtransaction
or the wallet) don't get notified that there's a different transaction in the mempool, although it doesn't crash. Users submitting throughtestmempoolaccept()
will get a "txn-already-in-mempool" error.This PR simply distinguishes between
txn-already-in-mempool
andtxn-same-nonwitness-data-in-mempool
, without handling them differently:sendrawtransaction
still will not throw, buttestmempoolaccept
will give you a different error.I believe the intention of #19645 is to allow full swaps of transactions that have different witnesses but identical nonwitness data. Returning a different error message + adding a test was suggested: #19645 (comment) so this is that PR.