Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 34 additions & 23 deletions spv/backend.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2018-2025 The Decred developers
// Copyright (c) 2018-2026 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand Down Expand Up @@ -60,6 +60,37 @@ func pickForGetCfilters(lastHeaderHeight int32) func(rp *p2p.RemotePeer) bool {
}
}

// blocksFromPeer requests blocks from rp and verifies the transaction trees in
// the block match what is promised by the merkle commitment in the block
// header. The remote peer is disconnected if it returns a block that fails
// this verification.
func blocksFromPeer(ctx context.Context, rp *p2p.RemotePeer, blockHashes []*chainhash.Hash) ([]*wire.MsgBlock, error) {
blocks, err := rp.Blocks(ctx, blockHashes)
if err != nil {
return nil, err
}
for _, b := range blocks {
if b == nil {
continue
}

// A block hash only commits to the header, so a peer is free to return
// any transactions it likes for a requested hash and still satisfy the
// 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.

if err != nil {
err = validate.DCP0005MerkleRoot(b)
}
if err != nil {
rp.Disconnect(err)
return nil, err
}
}
return blocks, nil
}

// Blocks implements the Blocks method of the wallet.Peer interface.
func (s *Syncer) Blocks(ctx context.Context, blockHashes []*chainhash.Hash) ([]*wire.MsgBlock, error) {
for {
Expand All @@ -70,7 +101,7 @@ func (s *Syncer) Blocks(ctx context.Context, blockHashes []*chainhash.Hash) ([]*
if err != nil {
return nil, err
}
blocks, err := rp.Blocks(ctx, blockHashes)
blocks, err := blocksFromPeer(ctx, rp, blockHashes)
if err != nil {
log.Debugf("Unable to fetch blocks from %v: %v", rp, err)
continue
Expand Down Expand Up @@ -556,32 +587,12 @@ func (s *Syncer) Rescan(ctx context.Context, blockHashes []chainhash.Hash, save
return err
}

blocks, err := rp.Blocks(ctx, fmatches)
blocks, err := blocksFromPeer(ctx, rp, fmatches)
if err != nil {
continue PickPeer
}

for j, b := range blocks {
// Validate fetched blocks before rescanning transactions. PoW
// and PoS difficulties have already been validated since the
// header is saved by the wallet, and modifications to these in
// the downloaded block would result in a different block hash
// and failure to fetch the block.
//
// Block filters were also validated
// against the header (assuming dcp0005
// was activated).
err = validate.MerkleRoots(b)
if err != nil {
err = validate.DCP0005MerkleRoot(b)
}
if err != nil {
err := errors.E(op, err)
rp.Disconnect(err)
rp = nil
continue PickPeer
}

i := fmatchidx[j]
blockMatches[i] = b
}
Expand Down
17 changes: 3 additions & 14 deletions spv/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -1069,11 +1069,12 @@ func (s *Syncer) handleBlockInvs(ctx context.Context, rp *p2p.RemotePeer, hashes
return nil
}

blocks, err := rp.Blocks(ctx, hashes)
blocks, err := blocksFromPeer(ctx, rp, hashes)
if err != nil {
op := errors.Opf(opf, rp)
return errors.E(op, err)
}

headers := make([]*wire.BlockHeader, len(blocks))
bmap := make(map[chainhash.Hash]*wire.MsgBlock)
for i, block := range blocks {
Expand Down Expand Up @@ -1336,24 +1337,12 @@ func (s *Syncer) scanChain(ctx context.Context, rp *p2p.RemotePeer, chain []*wal
wg.Wait()

if len(fmatches) != 0 {
blocks, err := rp.Blocks(ctx, fmatches)
blocks, err := blocksFromPeer(ctx, rp, fmatches)
if err != nil {
return nil, err
}
for j, b := range blocks {
i := fmatchidx[j]

// Perform context-free validation on the block.
// Disconnect peer when invalid.
err := validate.MerkleRoots(b)
if err != nil {
err = validate.DCP0005MerkleRoot(b)
}
if err != nil {
rp.Disconnect(err)
return nil, err
}

fetched[i] = b
}
}
Expand Down
12 changes: 1 addition & 11 deletions wallet/discovery.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2015-2020 The Decred developers
// Copyright (c) 2015-2026 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand All @@ -11,7 +11,6 @@ import (
"sync"

"decred.org/dcrwallet/v5/errors"
"decred.org/dcrwallet/v5/validate"
"decred.org/dcrwallet/v5/wallet/udb"
"decred.org/dcrwallet/v5/wallet/walletdb"
"github.com/decred/dcrd/blockchain/stake/v5"
Expand Down Expand Up @@ -346,15 +345,6 @@ func (a *addrFinder) filter(ctx context.Context, fs []*udb.BlockCFilter, data bl
}
for i, b := range blocks {
g.Go(func() error {
// validate blocks
err := validate.MerkleRoots(b)
if err != nil {
err = validate.DCP0005MerkleRoot(b)
}
if err != nil {
return err
}

c := blockCommitments(b)
a.mu.Lock()
a.commitments[*fetch[i]] = c
Expand Down
Loading