Skip to content

Commit 745996b

Browse files
jholdstockjrick
authored andcommitted
[release-v2.1] spv: Validate merkle root for all fetched blocks
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). Backport of 2c37f24.
1 parent a46848a commit 745996b

2 files changed

Lines changed: 37 additions & 37 deletions

File tree

spv/backend.go

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2018-2025 The Decred developers
1+
// Copyright (c) 2018-2026 The Decred developers
22
// Use of this source code is governed by an ISC
33
// license that can be found in the LICENSE file.
44

@@ -60,6 +60,37 @@ func pickForGetCfilters(lastHeaderHeight int32) func(rp *p2p.RemotePeer) bool {
6060
}
6161
}
6262

63+
// blocksFromPeer requests blocks from rp and verifies the transaction trees in
64+
// the block match what is promised by the merkle commitment in the block
65+
// header. The remote peer is disconnected if it returns a block that fails
66+
// this verification.
67+
func blocksFromPeer(ctx context.Context, rp *p2p.RemotePeer, blockHashes []*chainhash.Hash) ([]*wire.MsgBlock, error) {
68+
blocks, err := rp.Blocks(ctx, blockHashes)
69+
if err != nil {
70+
return nil, err
71+
}
72+
for _, b := range blocks {
73+
if b == nil {
74+
continue
75+
}
76+
77+
// A block hash only commits to the header, so a peer is free to return
78+
// any transactions it likes for a requested hash and still satisfy the
79+
// request. Every block obtained from a remote peer must therefore have
80+
// its transaction trees checked against the merkle root commitments of
81+
// the header before the transactions are used for anything.
82+
err := validate.MerkleRoots(b)
83+
if err != nil {
84+
err = validate.DCP0005MerkleRoot(b)
85+
}
86+
if err != nil {
87+
rp.Disconnect(err)
88+
return nil, err
89+
}
90+
}
91+
return blocks, nil
92+
}
93+
6394
// Blocks implements the Blocks method of the wallet.Peer interface.
6495
func (s *Syncer) Blocks(ctx context.Context, blockHashes []*chainhash.Hash) ([]*wire.MsgBlock, error) {
6596
for {
@@ -70,7 +101,7 @@ func (s *Syncer) Blocks(ctx context.Context, blockHashes []*chainhash.Hash) ([]*
70101
if err != nil {
71102
return nil, err
72103
}
73-
blocks, err := rp.Blocks(ctx, blockHashes)
104+
blocks, err := blocksFromPeer(ctx, rp, blockHashes)
74105
if err != nil {
75106
log.Debugf("Unable to fetch blocks from %v: %v", rp, err)
76107
continue
@@ -556,32 +587,12 @@ func (s *Syncer) Rescan(ctx context.Context, blockHashes []chainhash.Hash, save
556587
return err
557588
}
558589

559-
blocks, err := rp.Blocks(ctx, fmatches)
590+
blocks, err := blocksFromPeer(ctx, rp, fmatches)
560591
if err != nil {
561592
continue PickPeer
562593
}
563594

564595
for j, b := range blocks {
565-
// Validate fetched blocks before rescanning transactions. PoW
566-
// and PoS difficulties have already been validated since the
567-
// header is saved by the wallet, and modifications to these in
568-
// the downloaded block would result in a different block hash
569-
// and failure to fetch the block.
570-
//
571-
// Block filters were also validated
572-
// against the header (assuming dcp0005
573-
// was activated).
574-
err = validate.MerkleRoots(b)
575-
if err != nil {
576-
err = validate.DCP0005MerkleRoot(b)
577-
}
578-
if err != nil {
579-
err := errors.E(op, err)
580-
rp.Disconnect(err)
581-
rp = nil
582-
continue PickPeer
583-
}
584-
585596
i := fmatchidx[j]
586597
blockMatches[i] = b
587598
}

spv/sync.go

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,11 +1069,12 @@ func (s *Syncer) handleBlockInvs(ctx context.Context, rp *p2p.RemotePeer, hashes
10691069
return nil
10701070
}
10711071

1072-
blocks, err := rp.Blocks(ctx, hashes)
1072+
blocks, err := blocksFromPeer(ctx, rp, hashes)
10731073
if err != nil {
10741074
op := errors.Opf(opf, rp)
10751075
return errors.E(op, err)
10761076
}
1077+
10771078
headers := make([]*wire.BlockHeader, len(blocks))
10781079
bmap := make(map[chainhash.Hash]*wire.MsgBlock)
10791080
for i, block := range blocks {
@@ -1348,24 +1349,12 @@ func (s *Syncer) scanChain(ctx context.Context, rp *p2p.RemotePeer, chain []*wal
13481349
wg.Wait()
13491350

13501351
if len(fmatches) != 0 {
1351-
blocks, err := rp.Blocks(ctx, fmatches)
1352+
blocks, err := blocksFromPeer(ctx, rp, fmatches)
13521353
if err != nil {
13531354
return nil, err
13541355
}
13551356
for j, b := range blocks {
13561357
i := fmatchidx[j]
1357-
1358-
// Perform context-free validation on the block.
1359-
// Disconnect peer when invalid.
1360-
err := validate.MerkleRoots(b)
1361-
if err != nil {
1362-
err = validate.DCP0005MerkleRoot(b)
1363-
}
1364-
if err != nil {
1365-
rp.Disconnect(err)
1366-
return nil, err
1367-
}
1368-
13691358
fetched[i] = b
13701359
}
13711360
}

0 commit comments

Comments
 (0)