Skip to content

spv: Validate merkle root for all fetched blocks - #2644

Merged
jrick merged 2 commits into
decred:masterfrom
jholdstock:always-validate-merkle
Aug 18, 2026
Merged

jrick merged 2 commits into
decred:masterfrom
jholdstock:always-validate-merkle

Conversation

@jholdstock

@jholdstock jholdstock commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Blocks are now always fetched from SPV peers through a single blocksFromPeer helper, which checks the transaction trees against the merkle root commitments in the header and disconnects the peer when they do not match.

This not only removes duplication but also adds the merkle root check to two call sites which were previously missing it (cacheMissingCommitments in discovery.go and handleBlockInvs in sync.go).

Bug Bounty Note

This fixes a reported vulnerability where the missing merkle root check in cacheMissingCommitments could allow a malicious SPV peer to announce a genuine block by inv and return forged transaction trees under the real header, causing the wallet to permanently record a confirmed transaction that does not exist on chain.

In practice this vulnerability is not practical to exploit because:

  • SPV wallets do not accept inbound connections, which means it is not practical for an attacker to establish a connection to a specific wallet.
  • The attacker would need knowledge of the wallet they are connected to - e.g addresses or UTXOs belonging to the wallet.
  • The attacker must win the announcement race versus all other peers the victim is connected to.

Blocks are now always fetched from SPV peers through a single
blocksFromPeer helper, which checks the transaction trees against the
merkle root commitments in the header and disconnects the peer when they
do not match.

This not only removes duplication but also adds the merkle root check to
two call sites which were previously missing it (cacheMissingCommitments
in discovery.go and handleBlockInvs in sync.go).
Since the merkle root of blocks fetched from SPV peers are now validated
immediately, and blocks from a local node are already considered to be
trusted, there is no need for the merkle root to be checked again.
Comment thread spv/backend.go
// request. Every block obtained from a remote peer must therefore have
// its transaction trees checked against the merkle root commitments of
// the header before the transactions are used for anything.
err := validate.MerkleRoots(b)

@davecgh davecgh Aug 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

While it is not something specifically for this PR since it's effectively just refactoring code that already exists, this probably should be made more intelligent at some point.

It isn't horribly expensive since the standalone funcs its defers to are overall quite efficient, but the specific blocks heights/hashes that DCP0005 activated at are now well known (and even documented in the DCP).

I would expect that it should be faster to determine if the block under test is a descendant of the activation block for the current network and call the right one accordingly.

@jrick
jrick merged commit f617518 into decred:master Aug 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants