Skip to content

Commit

Permalink
Merge pull request #1918 from kcalvinalvin/2022-11-06-implement-getch…
Browse files Browse the repository at this point in the history
…aintips

blockchain, btcjson: Implement getchaintips rpc call
  • Loading branch information
Roasbeef authored Nov 15, 2023
2 parents d15dd71 + fc99e96 commit f7e9fba
Show file tree
Hide file tree
Showing 9 changed files with 805 additions and 21 deletions.
38 changes: 38 additions & 0 deletions blockchain/blockindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,44 @@ func (bi *blockIndex) UnsetStatusFlags(node *blockNode, flags blockStatus) {
bi.Unlock()
}

// InactiveTips returns all the block nodes that aren't in the best chain.
//
// This function is safe for concurrent access.
func (bi *blockIndex) InactiveTips(bestChain *chainView) []*blockNode {
bi.RLock()
defer bi.RUnlock()

// Look through the entire blockindex and look for nodes that aren't in
// the best chain. We're gonna keep track of all the orphans and the parents
// of the orphans.
orphans := make(map[chainhash.Hash]*blockNode)
orphanParent := make(map[chainhash.Hash]*blockNode)
for hash, node := range bi.index {
found := bestChain.Contains(node)
if !found {
orphans[hash] = node
orphanParent[node.parent.hash] = node.parent
}
}

// If an orphan isn't pointed to by another orphan, it is a chain tip.
//
// We can check this by looking for the orphan in the orphan parent map.
// If the orphan exists in the orphan parent map, it means that another
// orphan is pointing to it.
tips := make([]*blockNode, 0, len(orphans))
for hash, orphan := range orphans {
_, found := orphanParent[hash]
if !found {
tips = append(tips, orphan)
}

delete(orphanParent, hash)
}

return tips
}

// flushToDB writes all dirty block nodes to the database. If all writes
// succeed, this clears the dirty set.
func (bi *blockIndex) flushToDB() error {
Expand Down
113 changes: 113 additions & 0 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,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.
Expand Down
191 changes: 191 additions & 0 deletions blockchain/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package blockchain

import (
"fmt"
"reflect"
"testing"
"time"
Expand Down Expand Up @@ -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())
}
}
}
}
}
8 changes: 8 additions & 0 deletions btcjson/chainsvrresults.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ type GetBlockVerboseTxResult struct {
NextHash string `json:"nextblockhash,omitempty"`
}

// GetChainTipsResult models the data from the getchaintips command.
type GetChainTipsResult struct {
Height int32 `json:"height"`
Hash string `json:"hash"`
BranchLen int32 `json:"branchlen"`
Status string `json:"status"`
}

// GetChainTxStatsResult models the data from the getchaintxstats command.
type GetChainTxStatsResult struct {
Time int64 `json:"time"`
Expand Down
Loading

0 comments on commit f7e9fba

Please sign in to comment.