blockchain: Add ChainTips() method to BlockChain

ChainTips method allows for callers to get all the chain tips the node
is aware of. This is useful for supporting the getchaintips rpc call.
This commit is contained in:
Calvin Kim 2022-11-07 14:55:37 +09:00
parent b84f084ce7
commit 46bfb784a6
2 changed files with 304 additions and 0 deletions

View file

@ -1256,6 +1256,119 @@ func (b *BlockChain) BestSnapshot() *BestState {
return snapshot
}
// TipStatus is the status of a chain tip.
type TipStatus byte
const (
// StatusUnknown indicates that the tip status isn't any of the defined
// statuses.
StatusUnknown TipStatus = iota
// StatusActive indicates that the tip is considered active and is in
// the best chain.
StatusActive
// StatusInvalid indicates that this tip or any of the ancestors of this
// tip are invalid.
StatusInvalid
// StatusValidFork is given if:
// 1: Not a part of the best chain.
// 2: Is not invalid.
// 3: Has the block data stored to disk.
StatusValidFork
)
// String returns the status flags as string.
func (ts TipStatus) String() string {
switch ts {
case StatusActive:
return "active"
case StatusInvalid:
return "invalid"
case StatusValidFork:
return "valid-fork"
}
return fmt.Sprintf("unknown: %b", ts)
}
// ChainTip represents the last block in a branch of the block tree.
type ChainTip struct {
// Height of the tip.
Height int32
// BlockHash hash of the tip.
BlockHash chainhash.Hash
// BranchLen is length of the fork point of this chain from the main chain.
// Returns 0 if the chain tip is a part of the best chain.
BranchLen int32
// Status is the validity status of the branch this tip is in.
Status TipStatus
}
// ChainTips returns all the chain tips the node itself is aware of. Each tip is
// represented by its height, block hash, branch length, and status.
//
// This function is safe for concurrent access.
func (b *BlockChain) ChainTips() []ChainTip {
b.chainLock.RLock()
defer b.chainLock.RUnlock()
// Grab all the inactive tips.
tips := b.index.InactiveTips(b.bestChain)
// Add the current tip.
tips = append(tips, b.bestChain.Tip())
chainTips := make([]ChainTip, 0, len(tips))
// Go through all the tips and grab the height, hash, branch length, and the block
// status.
for _, tip := range tips {
var status TipStatus
switch {
// The tip is considered active if it's in the best chain.
case b.bestChain.Contains(tip):
status = StatusActive
// This block or any of the ancestors of this block are invalid.
case tip.status.KnownInvalid():
status = StatusInvalid
// If the tip meets the following criteria:
// 1: Not a part of the best chain.
// 2: Is not invalid.
// 3: Has the block data stored to disk.
//
// The tip is considered a valid fork.
//
// We can check if a tip is a valid-fork by checking that
// its data is available. Since the behavior is to give a
// block node the statusDataStored status once it passes
// the proof of work checks and basic chain validity checks.
//
// We can't use the KnownValid status since it's only given
// to blocks that passed the validation AND were a part of
// the bestChain.
case tip.status.HaveData():
status = StatusValidFork
}
chainTip := ChainTip{
Height: tip.height,
BlockHash: tip.hash,
BranchLen: tip.height - b.bestChain.FindFork(tip).height,
Status: status,
}
chainTips = append(chainTips, chainTip)
}
return chainTips
}
// HeaderByHash returns the block header identified by the given hash or an
// error if it doesn't exist. Note that this will return headers from both the
// main and side chains.

View file

@ -5,6 +5,7 @@
package blockchain
import (
"fmt"
"reflect"
"testing"
"time"
@ -964,3 +965,193 @@ func TestIntervalBlockHashes(t *testing.T) {
}
}
}
func TestChainTips(t *testing.T) {
tests := []struct {
name string
chainTipGen func() (*BlockChain, map[chainhash.Hash]ChainTip)
}{
{
name: "one active chain tip",
chainTipGen: func() (*BlockChain, map[chainhash.Hash]ChainTip) {
// Construct a synthetic block chain with a block index consisting of
// the following structure.
// genesis -> 1 -> 2 -> 3
tip := tstTip
chain := newFakeChain(&chaincfg.MainNetParams)
branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 3)
for _, node := range branch0Nodes {
chain.index.SetStatusFlags(node, statusDataStored)
chain.index.SetStatusFlags(node, statusValid)
chain.index.AddNode(node)
}
chain.bestChain.SetTip(tip(branch0Nodes))
activeTip := ChainTip{
Height: 3,
BlockHash: (tip(branch0Nodes)).hash,
BranchLen: 0,
Status: StatusActive,
}
chainTips := make(map[chainhash.Hash]ChainTip)
chainTips[activeTip.BlockHash] = activeTip
return chain, chainTips
},
},
{
name: "one active chain tip, one unknown chain tip",
chainTipGen: func() (*BlockChain, map[chainhash.Hash]ChainTip) {
// Construct a synthetic block chain with a block index consisting of
// the following structure.
// genesis -> 1 -> 2 -> 3 ... -> 10 -> 11 -> 12 -> 13 (active)
// \-> 11a -> 12a (unknown)
tip := tstTip
chain := newFakeChain(&chaincfg.MainNetParams)
branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 13)
for _, node := range branch0Nodes {
chain.index.SetStatusFlags(node, statusDataStored)
chain.index.SetStatusFlags(node, statusValid)
chain.index.AddNode(node)
}
chain.bestChain.SetTip(tip(branch0Nodes))
branch1Nodes := chainedNodes(branch0Nodes[9], 2)
for _, node := range branch1Nodes {
chain.index.AddNode(node)
}
activeTip := ChainTip{
Height: 13,
BlockHash: (tip(branch0Nodes)).hash,
BranchLen: 0,
Status: StatusActive,
}
unknownTip := ChainTip{
Height: 12,
BlockHash: (tip(branch1Nodes)).hash,
BranchLen: 2,
Status: StatusUnknown,
}
chainTips := make(map[chainhash.Hash]ChainTip)
chainTips[activeTip.BlockHash] = activeTip
chainTips[unknownTip.BlockHash] = unknownTip
return chain, chainTips
},
},
{
name: "1 inactive tip, 1 invalid tip, 1 active tip",
chainTipGen: func() (*BlockChain, map[chainhash.Hash]ChainTip) {
// Construct a synthetic block chain with a block index consisting of
// the following structure.
// genesis -> 1 -> 2 -> 3 (active)
// \ -> 1a (valid-fork)
// \ -> 1b (invalid)
tip := tstTip
chain := newFakeChain(&chaincfg.MainNetParams)
branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 3)
for _, node := range branch0Nodes {
chain.index.SetStatusFlags(node, statusDataStored)
chain.index.SetStatusFlags(node, statusValid)
chain.index.AddNode(node)
}
chain.bestChain.SetTip(tip(branch0Nodes))
branch1Nodes := chainedNodes(chain.bestChain.Genesis(), 1)
for _, node := range branch1Nodes {
chain.index.SetStatusFlags(node, statusDataStored)
chain.index.SetStatusFlags(node, statusValid)
chain.index.AddNode(node)
}
branch2Nodes := chainedNodes(chain.bestChain.Genesis(), 1)
for _, node := range branch2Nodes {
chain.index.SetStatusFlags(node, statusDataStored)
chain.index.SetStatusFlags(node, statusValidateFailed)
chain.index.AddNode(node)
}
activeTip := ChainTip{
Height: tip(branch0Nodes).height,
BlockHash: (tip(branch0Nodes)).hash,
BranchLen: 0,
Status: StatusActive,
}
inactiveTip := ChainTip{
Height: tip(branch1Nodes).height,
BlockHash: (tip(branch1Nodes)).hash,
BranchLen: 1,
Status: StatusValidFork,
}
invalidTip := ChainTip{
Height: tip(branch2Nodes).height,
BlockHash: (tip(branch2Nodes)).hash,
BranchLen: 1,
Status: StatusInvalid,
}
chainTips := make(map[chainhash.Hash]ChainTip)
chainTips[activeTip.BlockHash] = activeTip
chainTips[inactiveTip.BlockHash] = inactiveTip
chainTips[invalidTip.BlockHash] = invalidTip
return chain, chainTips
},
},
}
for _, test := range tests {
chain, expectedChainTips := test.chainTipGen()
gotChainTips := chain.ChainTips()
if len(gotChainTips) != len(expectedChainTips) {
t.Errorf("TestChainTips Failed test %s. Expected %d "+
"chain tips, got %d", test.name, len(expectedChainTips), len(gotChainTips))
}
for _, gotChainTip := range gotChainTips {
testChainTip, found := expectedChainTips[gotChainTip.BlockHash]
if !found {
t.Errorf("TestChainTips Failed test %s. Couldn't find an expected "+
"chain tip with height %d, hash %s, branchlen %d, status \"%s\"",
test.name, testChainTip.Height, testChainTip.BlockHash.String(),
testChainTip.BranchLen, testChainTip.Status.String())
}
if !reflect.DeepEqual(testChainTip, gotChainTip) {
t.Errorf("TestChainTips Failed test %s. Expected chain tip with "+
"height %d, hash %s, branchlen %d, status \"%s\" but got "+
"height %d, hash %s, branchlen %d, status \"%s\"", test.name,
testChainTip.Height, testChainTip.BlockHash.String(),
testChainTip.BranchLen, testChainTip.Status.String(),
gotChainTip.Height, gotChainTip.BlockHash.String(),
gotChainTip.BranchLen, gotChainTip.Status.String())
}
switch testChainTip.Status {
case StatusActive:
if testChainTip.Status.String() != "active" {
t.Errorf("TestChainTips Fail: Expected string of \"active\", got \"%s\"",
testChainTip.Status.String())
}
case StatusInvalid:
if testChainTip.Status.String() != "invalid" {
t.Errorf("TestChainTips Fail: Expected string of \"invalid\", got \"%s\"",
testChainTip.Status.String())
}
case StatusValidFork:
if testChainTip.Status.String() != "valid-fork" {
t.Errorf("TestChainTips Fail: Expected string of \"valid-fork\", got \"%s\"",
testChainTip.Status.String())
}
case StatusUnknown:
if testChainTip.Status.String() != fmt.Sprintf("unknown: %b", testChainTip.Status) {
t.Errorf("TestChainTips Fail: Expected string of \"unknown\", got \"%s\"",
testChainTip.Status.String())
}
}
}
}
}