diff --git a/blockchain/chain.go b/blockchain/chain.go index c013ff3b..77680ba9 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -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. diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index 8d6ca891..1ac08f9a 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -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()) + } + } + } + } +}