Skip to content

Conversation

@dergoegge
Copy link
Member

This PR proposes to check for mutated blocks early as a defense-in-depth mitigation against attacks leveraging mutated blocks.

We introduce IsBlockMutated which catches all known forms of block malleation and use it to do an early mutation check whenever we receive a block message.

We have observed attacks that abused mutated blocks in the past, which could have been prevented by simply not processing mutated blocks (e.g. #27608 for which a regression test is included in this PR).

@DrahtBot
Copy link
Contributor

DrahtBot commented Feb 8, 2024

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
ACK maflcko, fjahr, sr-gi, achow101
Concept ACK TheCharlatan, epiccurious, instagibbs, glozow, naumenkogs

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

Conflicts

No conflicts as of last run.

@DrahtBot
Copy link
Contributor

DrahtBot commented Feb 8, 2024

🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.

Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.

Leave a comment here, if you need help tracking down a confusing failure.

Debug: https://github.com/bitcoin/bitcoin/runs/21374346056

@DrahtBot DrahtBot removed the CI failed label Feb 8, 2024
@epiccurious
Copy link
Contributor

How do you define a mutated block? What are the known forms of mutated blocks?

@dergoegge
Copy link
Member Author

How do you define a mutated block? What are the known forms of mutated blocks?

Looking at IsBlockMutated in this PR should provide the answers for these questions, but to recap:

@sedited
Copy link
Contributor

sedited commented Feb 9, 2024

Concept ACK

@epiccurious
Copy link
Contributor

Concept ACK.

@instagibbs
Copy link
Member

concept ACK

might be good to recap why it was only added in BLOCK processing but not other ProcessBlocks: In every other case we already don't punish the sender of compact blocks for failing these higher level checks, while full blocks allow for punishment.

@dergoegge
Copy link
Member Author

might be good to recap why it was only added in BLOCK processing but not other ProcessBlocks: In every other case we already don't punish the sender of compact blocks for failing these higher level checks, while full blocks allow for punishment.

We call ProcessBlock when we receive a block message (handled in this PR) or as the result of a compact block reconstruction. Compact blocks are relayed before full validation occurs, therefore we don't punish peers for sending us invalid blocks through compact block relay. Block mutation might also occur randomly during compact bock relay due to short-id collisions, which is another reason not to punish.

Copy link
Member

@sr-gi sr-gi 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

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, I agree with the approach of catching and dropping these earlier rather than later

@dergoegge
Copy link
Member Author

I would like to see this in v27, can we add it to the milestone?

@maflcko maflcko added this to the 27.0 milestone Feb 20, 2024
Copy link
Contributor

@fjahr fjahr 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

"CBlock::GetHash() is a foot-gun without a prior mutation check", I hadn't felt this strongly about but I get it. I think it makes sense to rename it. I have drafted it here: fjahr@8e11e9c If it sounds valuable I will open a PR.

@naumenkogs
Copy link
Contributor

Concept ACK. Looking forward to addressing already pending comments, then i review.

Copy link
Member

Choose a reason for hiding this comment

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

have you thought about having this function return an optional error string so unit tests can check expected failure reason?

Copy link
Member

@maflcko maflcko Feb 23, 2024

Choose a reason for hiding this comment

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

(reply to #29412 (comment))

note: unit tests may use the ASSERT_DEBUG_LOG, as an alternative.

Copy link
Member Author

Choose a reason for hiding this comment

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

have you thought about having this function return an optional error string so unit tests can check expected failure reason?

I'm considering returning the mutation type but I will not be asserting logs...

Copy link
Member

Choose a reason for hiding this comment

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

in e2d1eb2e2001c31b43154df47ab5be215a26774f
does this need a with p2p_lock?

Copy link
Member Author

Choose a reason for hiding this comment

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

Does it? None of the other wait_for_* helpers require it.

@0xB10C
Copy link
Contributor

0xB10C commented Feb 23, 2024

I'm running this PR on my mainnet monitoring infrastructure as I was looking at the currently broadcast mutated blocks (bad-witness-nonce-size) in detail anyway. Can report back in a few days (currently, I receive only 1 or 2 per week).

@0xB10C
Copy link
Contributor

0xB10C commented Feb 23, 2024

Received two bad-witness-nonce-size blocks from a mining pools custom client shortly after my last comment. In both cases I my node had reconstructed a cmpctblock two seconds before the mutated block arrived. In this case, the block is "mutated" because the coinbase witness is empty. That's probably a bug on the mining pool side. I have the IPs of the pool clients as noban-peers, so I didn't actually see them getting disconnected.

2024-02-23T10:33:55Z [msghand] [blockencodings.cpp:217] [FillBlock] [cmpctblock] Successfully reconstructed block 000000000000000000017f8957106aae020ce65f69f3ee189559d97fc0f974bb with 1 txn prefilled, 2936 txn from mempool (incl at least 5 from extra pool) and 45 txn requested
2024-02-23T10:33:55Z [msghand] [validationinterface.cpp:273] [NewPoWValidBlock] [validation] NewPoWValidBlock: block hash=000000000000000000017f8957106aae020ce65f69f3ee189559d97fc0f974bb
2024-02-23T10:33:55Z [msghand] [validationinterface.cpp:267] [BlockChecked] [validation] BlockChecked: block hash=000000000000000000017f8957106aae020ce65f69f3ee189559d97fc0f974bb state=Valid
2024-02-23T10:33:55Z [msghand] [validationinterface.cpp:243] [MempoolTransactionsRemovedForBlock] [validation] Enqueuing MempoolTransactionsRemovedForBlock: block height=831673 txs removed=2931
2024-02-23T10:33:55Z [msghand] [validation.cpp:2738] [UpdateTipLog] UpdateTip: new best=000000000000000000017f8957106aae020ce65f69f3ee189559d97fc0f974bb height=831673 version=0x2bc70000 log2_work=94.750438 tx=968653267 date='2024-02-23T10:33:34Z' progress=1.000000 cache=87.0MiB(713889txo)

...

2024-02-23T10:33:57Z [msghand] [net_processing.cpp:3396] [ProcessMessage] [net] received: block (1519889 bytes) peer=153
2024-02-23T10:33:57Z [msghand] [net_processing.cpp:4728] [ProcessMessage] [net] received block 000000000000000000017f8957106aae020ce65f69f3ee189559d97fc0f974bb peer=153
2024-02-23T10:33:57Z [msghand] [validation.cpp:3862] [IsBlockMutated] [validation] IsBlockMutated: bad-witness-nonce-size, CheckWitnessCommitment : invalid witness reserved value size
2024-02-23T10:33:57Z [msghand] [net_processing.cpp:4734] [ProcessMessage] [net] Received mutated block from peer=153
2024-02-23T10:33:57Z [msghand] [net_processing.cpp:1791] [Misbehaving] [net] Misbehaving: peer=153 (0 -> 100) DISCOURAGE THRESHOLD EXCEEDED: mutated block
2024-02-23T10:33:57Z [msghand] [net_processing.cpp:5037] [MaybeDiscourageAndDisconnect] Warning: not punishing noban peer 153!
2024-02-23T11:17:32Z [msghand] [blockencodings.cpp:217] [FillBlock] [cmpctblock] Successfully reconstructed block 0000000000000000000345e15958b691f5079b84a6d5a3f959a8d84ee7989659 with 1 txn prefilled, 1024 txn from mempool (incl at least 0 from extra pool) and 5 txn requested
2024-02-23T11:17:32Z [msghand] [validationinterface.cpp:273] [NewPoWValidBlock] [validation] NewPoWValidBlock: block hash=0000000000000000000345e15958b691f5079b84a6d5a3f959a8d84ee7989659
2024-02-23T11:17:32Z [msghand] [validationinterface.cpp:267] [BlockChecked] [validation] BlockChecked: block hash=0000000000000000000345e15958b691f5079b84a6d5a3f959a8d84ee7989659 state=Valid
2024-02-23T11:17:32Z [msghand] [net.cpp:3784] [PushMessage] [net] sending sendcmpct (9 bytes) peer=117
2024-02-23T11:17:32Z [msghand] [validationinterface.cpp:243] [MempoolTransactionsRemovedForBlock] [validation] Enqueuing MempoolTransactionsRemovedForBlock: block height=831677 txs removed=1024
2024-02-23T11:17:32Z [msghand] [validation.cpp:2738] [UpdateTipLog] UpdateTip: new best=0000000000000000000345e15958b691f5079b84a6d5a3f959a8d84ee7989659 height=831677 version=0x2fffe000 log2_work=94.750499 tx=968663963 date='2024-02-23T11:17:27Z' progress=1.000000 cache=78.7MiB(689008txo)

...

2024-02-23T11:17:34Z [msghand] [net_processing.cpp:3396] [ProcessMessage] [net] received: block (1509042 bytes) peer=20
2024-02-23T11:17:34Z [msghand] [net_processing.cpp:4728] [ProcessMessage] [net] received block 0000000000000000000345e15958b691f5079b84a6d5a3f959a8d84ee7989659 peer=20
2024-02-23T11:17:34Z [msghand] [validation.cpp:3862] [IsBlockMutated] [validation] IsBlockMutated: bad-witness-nonce-size, CheckWitnessCommitment : invalid witness reserved value size
2024-02-23T11:17:34Z [msghand] [net_processing.cpp:4734] [ProcessMessage] [net] Received mutated block from peer=20
2024-02-23T11:17:34Z [msghand] [net_processing.cpp:1791] [Misbehaving] [net] Misbehaving: peer=20 (0 -> 100) DISCOURAGE THRESHOLD EXCEEDED: mutated block
2024-02-23T11:17:34Z [msghand] [net_processing.cpp:5037] [MaybeDiscourageAndDisconnect] Warning: not punishing noban peer 20!
2024-02-23T11:17:34Z [msghand] [net_processing.cpp:3396] [ProcessMessage] [net] received: getdata (37 bytes) peer=20
2024-02-23T11:17:34Z [msghand] [net_processing.cpp:3998] [ProcessMessage] [net] received getdata (1 invsz) peer=20
2024-02-23T11:17:34Z [msghand] [net_processing.cpp:4001] [ProcessMessage] [net] received getdata for: witness-block 0000000000000000000345e15958b691f5079b84a6d5a3f959a8d84ee7989659 peer=20
2024-02-23T11:17:34Z [msghand] [net.cpp:3784] [PushMessage] [net] sending block (1509078 bytes) peer=20

Copy link
Member

@maflcko maflcko left a comment

Choose a reason for hiding this comment

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

left some initial nits/questions

0f356b0b2fb23aef96ed7396890aa36410aa1d59 💬

Show signature

Signature:

untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
trusted comment: 0f356b0b2fb23aef96ed7396890aa36410aa1d59 💬
kBfK9UKECARIswfAVfqnH6nAuO9mA7Tcad0J1J6aCTeTkHT4VK7z3GpY/0Wz6H0SeVaxH2Thgu3Qvz87+QF6CA==

Copy link
Member

@maflcko maflcko Feb 23, 2024

Choose a reason for hiding this comment

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

(reply to #29412 (comment))

note: unit tests may use the ASSERT_DEBUG_LOG, as an alternative.

Copy link
Member

Choose a reason for hiding this comment

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

future work nit: the mutated arg is never non-nullptr and has no test coverage it seems.

Copy link
Member

Choose a reason for hiding this comment

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

future work nit: the mutated arg is never non-nullptr and has no test coverage it seems.

I presume the reason is that it can't be mutated and all callers are expected to pass nullptr? Seems fine to remove the arg, but should be fine either way.

This was referenced Aug 4, 2025
DashCoreAutoGuix pushed a commit to DashCoreAutoGuix/dash that referenced this pull request Aug 9, 2025
d8087ad [test] IsBlockMutated unit tests (dergoegge)
1ed2c98 Add transaction_identifier::size to allow Span conversion (dergoegge)
1ec6bbe [validation] Cache merkle root and witness commitment checks (dergoegge)
5bf4f5b [test] Add regression test for bitcoin#27608 (dergoegge)
49257c0 [net processing] Don't process mutated blocks (dergoegge)
2d8495e [validation] Merkle root malleation should be caught by IsBlockMutated (dergoegge)
66abce1 [validation] Introduce IsBlockMutated (dergoegge)
e7669e1 [refactor] Cleanup merkle root checks (dergoegge)
95bddb9 [validation] Isolate merkle root checks (dergoegge)

Pull request description:

  This PR proposes to check for mutated blocks early as a defense-in-depth mitigation against attacks leveraging mutated blocks.

  We introduce `IsBlockMutated` which catches all known forms of block malleation and use it to do an early mutation check whenever we receive a `block` message.

  We have observed attacks that abused mutated blocks in the past, which could have been prevented by simply not processing mutated blocks (e.g. bitcoin#27608 for which a regression test is included in this PR).

ACKs for top commit:
  achow101:
    ACK d8087ad
  maflcko:
    ACK d8087ad 🏄
  fjahr:
    Code review ACK d8087ad
  sr-gi:
    Code review ACK bitcoin@d8087ad

Tree-SHA512: 618ff4ea7f168e10f07504d3651290efbb1bb2ab3b838ffff3527c028caf6c52dedad18d04d3dbc627977479710930e200f2dfae18a08f627efe7e64a57e535f
fanquake pushed a commit to fanquake/bitcoin that referenced this pull request Sep 12, 2025
Since bitcoin#29412, we have not allowed mutated blocks to continue
being processed immediately the block is received, but this
is only done for the legacy BLOCK message.

Extend these checks as belt-and-suspenders to not allow
similar mutation strategies to affect relay by honest peers
by applying the check inside
PartiallyDownloadedBlock::FillBlock, immediately before
returning READ_STATUS_OK.

This also removes the extraneous CheckBlock call.

Github-Pull: bitcoin#32646
Rebased-From: bac9ee4
roqqit pushed a commit to doged-io/doged that referenced this pull request Sep 23, 2025
Summary:
Add a test to show that an attacker cannot clear the download state for other peers by send an invalid mutated block.

This is a partial backport of [[bitcoin/bitcoin#29412 | core#29412]]
bitcoin/bitcoin@5bf4f5b

Test Plan:
Run this test before and after D16879, to show that the test failed before and works after this diff

```
git checkout 2701a514b014122ad024b6d2cf9e0098cd7adc5b
ninja && test/functional/test_runner.py p2p_mutated_blocks

git checkout 5a3dd8183dfc2459aa976deccff2b4f03a737180
ninja && test/functional/test_runner.py p2p_mutated_blocks

```
A minor patch is required in blocktools.py to make this test work on those old commits: add the `txlist` argument to `create_coinbase` (see D17691)

Reviewers: #bitcoin_abc, roqqit

Reviewed By: #bitcoin_abc, roqqit

Subscribers: Fabien

Differential Revision: https://reviews.bitcoinabc.org/D18005
roqqit pushed a commit to doged-io/doged that referenced this pull request Sep 23, 2025
Summary:
> We preemptively perform a block mutation check before further processing
> a block message (similar to early sanity checks on other messsage
> types). The main reasons for this change are as follows:
>
> - `CBlock::GetHash()` is a foot-gun without a prior mutation check, as
>   the hash returned only commits to the header but not to the actual
>   transactions (`CBlock::vtx`) contained in the block.
> - We have observed attacks that abused mutated blocks in the past, which
>   could have been prevented by simply not processing mutated blocks
>   (e.g. bitcoin/bitcoin#27608).

PR description
> This PR proposes to check for mutated blocks early as a defense-in-depth mitigation against attacks leveraging mutated blocks.
>
> We introduce IsBlockMutated which catches all known forms of block malleation and use it to do an early mutation check whenever we receive a block message.
>
> We have observed attacks that abused mutated blocks in the past, which could have been prevented by simply not processing mutated blocks (e.g. #27608 for which a regression test is included in this PR).

This concludes backport of [[bitcoin/bitcoin#29412 | core#29412]]
bitcoin/bitcoin@95bddb9
bitcoin/bitcoin@e7669e1
bitcoin/bitcoin@66abce1
bitcoin/bitcoin@2d8495e
bitcoin/bitcoin@49257c0
bitcoin/bitcoin@1ec6bbe
bitcoin/bitcoin@d8087ad

Depends on D18005

Test Plan: `ninja all check-all`

Reviewers: #bitcoin_abc, Fabien

Reviewed By: #bitcoin_abc, Fabien

Subscribers: Fabien

Differential Revision: https://reviews.bitcoinabc.org/D18006
@Sjors
Copy link
Member

Sjors commented Nov 12, 2025

Past reviewers may be interested in the cleanup followup #33805.

delta1 added a commit to delta1/elements that referenced this pull request Dec 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.