Skip to content

Conversation

darosior
Copy link
Member

@darosior darosior commented Apr 29, 2025

Opening as draft to facilitate discussions, this has some issues.

This special-case was introduced in #18044 to avoid hindering relay of witness transactions in adversarial situations where most connections on the network were still using txid-based relay. This is not a concern anymore. Since it is expensive to detect, get rid of the special case.

This may however cause issues for orphan resolution in adversarial situations, since fetching the parent is done using its txid. Greg Sanders pointed out to me on IRC this could be an issue for package relay. I'm opening here as draft to facilitate discussion.

@DrahtBot
Copy link
Contributor

DrahtBot commented Apr 29, 2025

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

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32379.

Reviews

See the guideline for information on the review process.
A summary of reviews will appear here.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #32421 (test: refactor: overhaul (w)txid determination for CTransaction objects by theStack)
  • #32317 (kernel: Separate UTXO set access from validation functions by TheCharlatan)

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.

@DrahtBot DrahtBot added the P2P label Apr 29, 2025
Comment on lines -262 to -263
# The parent should be requested since the unstripped wtxid would differ. Delayed because
# it's by txid and this is not a preferred relay peer.
Copy link
Member Author

Choose a reason for hiding this comment

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

It seems this comment was wrong in the first place. The parent is requested by txid, so the unstripped wtxid doesn't matter. The only reason that this was working is because WITNESS_STRIPPED was special cased as a failure and we didn't add the wtxid of the witness-stripped transaction (i.e. its txid) to the reject filters. Not special-casing this issue prevents requesting the parent since now its txid would be in the reject filter.

Copy link
Member

Choose a reason for hiding this comment

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

Yep. Should say 'parent should be requested since witness stripped-transaction rejections shouldn't be cached at all'

@glozow
Copy link
Member

glozow commented Apr 29, 2025

This may however cause issues for orphan resolution in adversarial situations, since fetching the parent is done using its txid. Greg Sanders pointed out to me on IRC this could be an issue for package relay.

Just want to point out this is one of the motivations for (real) package relay, so we can get rid of txid-based fetching. https://github.com/bitcoin/bips/blob/master/bip-0331.mediawiki#handle-orphans-better

Today however, you'd be able to block (opportunistic) "package relay" by sending the parent witness-stripped first.

Comment on lines -262 to -263
# The parent should be requested since the unstripped wtxid would differ. Delayed because
# it's by txid and this is not a preferred relay peer.
Copy link
Member

Choose a reason for hiding this comment

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

Yep. Should say 'parent should be requested since witness stripped-transaction rejections shouldn't be cached at all'

We previously didn't for witness-stripped transactions due to compatibility concerns with
non-wtxid-relay peers.

The issue we were trying to prevent is attacker node A stripping the witness of a transaction
broadcast by honest node B and submitting it stripped to a large part of the network. It would
result in those nodes including the txid of the transaction (== witness-stripped wtxid) to their
recent reject filter, which would significantly hinder the propagation of the honest non-stripped
transaction if most connections on the network are txid based relay.

Support for wtxid based relay was released with 0.21 in January 2021 and is now ubiquitous on the
network. Therefore this concern does not apply anymore and we can get rid of this special case.
This was performed to identify transactions whose witness was stripped and is not necessary anymore.
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.

From the offline conversation, here's a branch that drops the "don't resolve orphans with a parent in RecentRejects" logic, meaning we'll still keep an orphan of a witness stripped-transaction after this PR. This doesn't fully fix the fact that 1p1c is disrupted, because the parent is still not requested (its txid is AlreadyHave). However, keeping the orphan means we still accept the 1p1c if we receive the parent via wtxid.

Also recommend squashing the "Remove now-unused WITNESS_STRIPPED variant of TxValidationResult" commit into "tx download: add wtxid to reject filter even if witness was stripped"

@DrahtBot
Copy link
Contributor

🐙 This pull request conflicts with the target branch and needs rebase.

@darosior
Copy link
Member Author

darosior commented Jun 18, 2025

It took me a while to remember the context here, since we had those discussions offline. I'll try to put a short summary here for anybody else who did not participate in those discussions or doesn't remember the details.

The current behaviour in master is the following. Upon Script validation failure of an unconfirmed transaction:

bitcoin/src/validation.cpp

Lines 1238 to 1240 in 5e6dbfd

// Check input scripts and signatures.
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
if (!CheckInputScripts(tx, state, m_view, scriptVerifyFlags, true, false, ws.m_precomputed_txdata, GetValidationCache())) {

If the transaction has no witness attached, Script validation will be performed a second and third time to detect whether it is spending a witness program:

bitcoin/src/validation.cpp

Lines 1245 to 1246 in 5e6dbfd

if (!tx.HasWitness() && CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~(SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_CLEANSTACK), true, false, ws.m_precomputed_txdata, GetValidationCache()) &&
!CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~SCRIPT_VERIFY_CLEANSTACK, true, false, ws.m_precomputed_txdata, GetValidationCache())) {

This 3-step logic was introduced along with Segwit in 8b49040, to avoid adding the txid of transactions with a mutated witness to the reject filter. This was changed in #8525, after which witness transactions which failed validation were never added to the reject filter at all. This is because it would otherwise trivial for an attacker to hinder the propagation of a witness transaction until wtxid-relay (see #8279).

The witness malleation issue was fixed with wtxid-relay in #18044, since at this point inserting the wtxid of a rejected transaction in the reject filter won't prevent the same transaction with a different (valid) witness to be accepted in the future (if it is announced on wtxid relay connection). However, this 3-step script validation logic was still necessary until the network was mostly upgraded to wtxid relay. This is because otherwise one malicious peer could still interfere with the honest transaction relay of all txid-relay peers. The attack is as follows:

  • An honest node has N txid-relay peers and one malicious wtxid-relay peer (the malicious peer can be wtxid or txid it doesn't matter).
  • A valid witness transaction is broadcast. Before any of its honest txid-relay peers sends the valid witness transaction to the node, the malicious peer sends it after stripping its witness.
  • The honest node now has the wtxid of the witness-stripped transaction, i.e. the txid of the transaction, in its reject filter. Any honest txid-relay peer trying to announce the valid witness transaction will hit the reject filter.

This is not a concern anymore because wtxid-relay was released on 2021-01-15 with 0.21.0. Nowadays, virtually all connections on the network are wtxid-relay. Therefore we can get rid of performing 3 times the expensive Script validation on non-witness transactions relayed to us for ~free. This is the argument made by OP.

However, this is not the full story. Since wtxid-relay was introduced a new form of dependency on "txid-based transaction resolution" was introduced with 1p1c package relay (h/t @instagibbs and @glozow). It is still possible for a malicious peer to interfere with parent resolution in this case. That is, if this resolution checks the reject filter, which @glozow shared a branch to stop doing: #32379 (review).

Therefore this change is in the end a tradeoff between getting rid of the 3x script validation and being able to cache rejected parents in 1p1c.

@darosior
Copy link
Member Author

darosior commented Jun 18, 2025

This doesn't fully fix the fact that 1p1c is disrupted, because the parent is still not requested (its txid is AlreadyHave).

Couldn't AlreadyHave() be tweaked to optionally not look up the reject filter similarly to include_reconsiderable:

if (include_reconsiderable && RecentRejectsReconsiderableFilter().contains(hash)) return true;

It seems like a smaller change than not checking the reject filter anymore for parent resolution, so we might as well do both so as to not affect 1p1c behaviour at all?

@glozow
Copy link
Member

glozow commented Jun 18, 2025

Since wtxid-relay was introduced a new form of "txid-based transaction resolution" was introduced with 1p1c package relay

No, txid-based orphan resolution has been around for many years - I think before segwit. Opportunistic 1p1c just submits the parent and child together when the parent arrives.

@darosior
Copy link
Member Author

Yes what i meant is "we now rely on something else for which it's pretty bad if one peer can block us from seeing a transaction". Changed the wording, thanks.

@glozow
Copy link
Member

glozow commented Jun 18, 2025

Couldn't AlreadyHave() be tweaked to optionally not look up the reject filter:

I assume you mean just for the check that filters getdatas. That means we'll then request transactions we've already rejected (we don't know whether this is an orphan resolution or a normal tx). Although maybe that won't lead to too many redownloads in practice, since the filter will still exist at inv receipt. Or maybe we also skip when the getdata is of type txid? Will have a think.

@glozow
Copy link
Member

glozow commented Jun 18, 2025

Thanks for writeup @darosior 👍

This is not a concern anymore because wtxid-relay was released on 2021-01-15 with 0.21.0. Nowadays, virtually all connections on the network are wtxid-relay.

Digging out some things from my (faint) memory, there was also some discussion about whether it's problematic if some service uses txid-based relay and an attacker tries to censor their transactions by sending everybody witness stripped versions ahead of time. I think we concluded that this level of attack (where attacker is the first person to hear about it AND manages to frontrun every single connection made by the victim) is out of scope.

@darosior
Copy link
Member Author

Yes, especially if we consider the costs to mitigating this. We shouldn't impose to all Bitcoin nodes on the network to perform 3 times expensive Script validation just for some company that 1) doesn't want to update their service to wtxid-relay after 5 years* while 2) running the risk of a somewhat sophisticated attack.

* If this is released in 30.0, by the time most nodes on the network upgrade it will have been more than 5 years since wtxid-relay was released.

@glozow
Copy link
Member

glozow commented Jul 10, 2025

Following up, this branch removes the logic to halt orphan-processing when there are rejected parents, and removes rejection cache checking by txid altogether. It also adds a new delay for peers that don't support BIP 339 (distinct from the delay for requests by txid): https://github.com/glozow/bitcoin/tree/2025-07-wtxid-only-rej It should be sufficient to address the 1p1c issue.

There is a theoretical bandwidth increase from redownloading transactions, but my guess is it's minimal. Nonsegwit transactions, peers that don't support BIP 339, and rejections in general should be fairly rare. I'll use this branch to gather some data on how big these numbers actually are and get back to you?

@glozow
Copy link
Member

glozow commented Jul 25, 2025

I ran the above patch for a while and added logging to collect some stats and see how much more redownloading we’d do. TLDR it seems like we can definitely go ahead with it.

Caveats: Traffic has been very low so I set my mempool expiry to 1 hour to try to induce more orphan-fetching. I’m a default policy node so I don’t really expect to be relayed many transactions I’ll reject. There isn’t much opportunity to test reconsiderable rejections right now because of how low transaction traffic is.

My stats in the last 7 days*:

  • 100% of my peers have supported wtxidrelay
  • 2,482,307 of 2,650,818 (93.64%) of transactions I accepted to my mempool had witnesses.
  • 99,900 out of 2,740,857 (3.64%) of the GETDATAs I sent were by txid (we can infer that 100% of these are orphan parent fetching since all my peers were wtxidrelay).
  • Out of the 64,832 orphans I saw, I didn’t see any of their parents in the RecentRejects filter. So I kept all of them (which means these changes didn’t affect the number of orphans I kept).
  • I sent 99,900 requests for 36,319 unique parents. Of the 33,242 transactions I received, 96.20% of them had witnesses and 91.53% were accepted to mempool.

The impact of this patch is that we’ll redownload a transaction that is (1) nonsegwit (2) when we are requesting by txid (in practice, always orphan resolution) and (3) we’ve already rejected it.
I don’t have meaningful stats on rejections, but nonsegwit orphan parents were 0.047% of the transactions I downloaded - a couple dozen per day. The fRejectedParents logic seems to have very little benefit.

@glozow
Copy link
Member

glozow commented Aug 11, 2025

Can this be closed?

@darosior
Copy link
Member Author

I was waiting to see the outcome of #33066 to see what to do of this, but i'm happy to close and reopen if necessary. 🤷‍♂️

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.

3 participants