mining: Prevent unnecessary reorg with equal votes.

This corrects the logic related to preventing unnecessary reorgs in the
function that sorts parents based the number of votes available for them
and the current tip block and adds tests to ensure proper functionality.
This commit is contained in:
Dave Collins 2021-12-06 18:37:32 -06:00
parent 1c40d5abe8
commit 44d5aea13e
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
2 changed files with 157 additions and 1 deletions

View File

@ -367,7 +367,10 @@ func SortParentsByVotes(txSource TxSource, currentTopBlock chainhash.Hash, block
// point, all blocks listed in sortedUsefulBlocks definitely also have the
// minimum number of votes required.
curVoteMetadata := txSource.VotesForBlocks([]chainhash.Hash{currentTopBlock})
numTopBlockVotes := uint16(len(curVoteMetadata))
var numTopBlockVotes uint16
if len(curVoteMetadata) > 0 {
numTopBlockVotes = uint16(len(curVoteMetadata[0]))
}
if filtered[0].NumVotes == numTopBlockVotes && filtered[0].Hash !=
currentTopBlock {

View File

@ -5,7 +5,9 @@
package mining
import (
"encoding/binary"
"errors"
"reflect"
"testing"
"github.com/decred/dcrd/blockchain/v4"
@ -518,3 +520,154 @@ func TestNewBlockTemplateAutoRevocationsVotesOnly(t *testing.T) {
t.Fatalf("unexpected error when checking block sanity: %v", err)
}
}
// TestSortParentsByVotes ensures the function that sorts parent blocks based on
// the number of votes available for them and the current tip block works as
// intended, including reorg prevention for an equal number of votes.
func TestSortParentsByVotes(t *testing.T) {
params := chaincfg.MainNetParams()
tests := []struct {
name string // test description
numBlocks uint8 // number of sibling blocks to generate
votes []uint8 // mock votes to generate per block
curTip uint8 // index of the current tip block
want []uint8 // indexes of expected sorted blocks
}{{
name: "0 blocks, 0 votes (sanity check for impossible condition)",
numBlocks: 0,
votes: nil,
curTip: 0,
want: nil,
}, {
name: "1 block, 5 votes",
numBlocks: 1,
votes: []uint8{5},
curTip: 0,
want: []uint8{0},
}, {
name: "1 block, 4 votes",
numBlocks: 1,
votes: []uint8{4},
curTip: 0,
want: []uint8{0},
}, {
name: "1 block, 3 votes",
numBlocks: 1,
votes: []uint8{3},
curTip: 0,
want: []uint8{0},
}, {
name: "1 block, 2 votes (no useful blocks)",
numBlocks: 1,
votes: []uint8{2},
curTip: 0,
want: nil,
}, {
name: "2 blocks, votes{0:5, 1:5}, tip block 0 (no reorg)",
numBlocks: 2,
votes: []uint8{5, 5},
curTip: 0,
want: []uint8{0, 1},
}, {
name: "2 blocks, votes{0:5, 1:5}, tip block 1 (no reorg)",
numBlocks: 2,
votes: []uint8{5, 5},
curTip: 1,
want: []uint8{1, 0},
}, {
name: "2 blocks, votes{0:4, 1:5}, tip block 0",
numBlocks: 2,
votes: []uint8{4, 5},
curTip: 0,
want: []uint8{1, 0},
}, {
name: "2 blocks, votes{0:4, 1:5}, tip block 1",
numBlocks: 2,
votes: []uint8{4, 5},
curTip: 1,
want: []uint8{1, 0},
}, {
name: "3 blocks, votes{0:4, 1:5, 2:2}, tip block 0 (2 not useful)",
numBlocks: 3,
votes: []uint8{4, 5, 2},
curTip: 0,
want: []uint8{1, 0},
}, {
name: "3 blocks, votes{0:3, 1:5, 2:4}, tip block 0",
numBlocks: 3,
votes: []uint8{3, 5, 4},
curTip: 0,
want: []uint8{1, 2, 0},
}, {
name: "3 blocks, votes{0:3, 1:5, 2:4}, tip block 2",
numBlocks: 3,
votes: []uint8{3, 5, 4},
curTip: 2,
want: []uint8{1, 2, 0},
}, {
name: "3 blocks, votes{0:3, 1:3, 2:4}, tip block 1",
numBlocks: 3,
votes: []uint8{3, 3, 4},
curTip: 1,
want: []uint8{2, 0, 1},
}}
// nextHash is a convenience function that generates a unique hash each time
// it is called.
var genHashIndex uint32
nextHash := func() chainhash.Hash {
var buf [4]byte
binary.LittleEndian.PutUint32(buf[:], genHashIndex)
genHashIndex++
return chainhash.HashH(buf[:])
}
for _, test := range tests {
// Generate mock blocks and votes based on test data.
blockHashes := make([]chainhash.Hash, test.numBlocks)
for i := uint8(0); i < test.numBlocks; i++ {
blockHashes[i] = nextHash()
}
votes := make(map[chainhash.Hash][]VoteDesc, len(test.votes))
for i, numVotes := range test.votes {
blockVotedOn := blockHashes[i]
voteDescs := make([]VoteDesc, 0, numVotes)
for i := uint8(0); i < numVotes; i++ {
voteDescs = append(voteDescs, VoteDesc{
VoteHash: nextHash(),
TicketHash: nextHash(),
ApprovesParent: true,
})
}
votes[blockVotedOn] = voteDescs
}
var curTip chainhash.Hash
if len(blockHashes) == 0 {
curTip = nextHash()
} else {
curTip = blockHashes[test.curTip]
}
var want []chainhash.Hash
if len(test.want) > 0 {
want = make([]chainhash.Hash, 0, len(test.want))
for _, blockIndex := range test.want {
want = append(want, blockHashes[blockIndex])
}
}
// Create a fake transaction source instance populated with the votes.
txSource := &fakeTxSource{
votes: votes,
}
// Ensure the result matches the expected values.
got := SortParentsByVotes(txSource, curTip, blockHashes, params)
if !reflect.DeepEqual(got, want) {
t.Errorf("%q: unexpected result -- got %v, want %v", test.name, got,
want)
continue
}
}
}