From b84f084ce781fa430276db412fe03e5fa7bba294 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Sun, 6 Nov 2022 18:56:24 +0900 Subject: [PATCH 1/5] blockchain: Add InactiveTips() to blockindex InactiveTips() returns all the tips of the branches of the blockchain tree that are not in the best chain. This function is useful for supporting the getchaintips rpc call. --- blockchain/blockindex.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/blockchain/blockindex.go b/blockchain/blockindex.go index aa2db6a755..7412aa1129 100644 --- a/blockchain/blockindex.go +++ b/blockchain/blockindex.go @@ -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 { From 46bfb784a6a9256d13cc148fd81f044f2017182d Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Mon, 7 Nov 2022 14:55:37 +0900 Subject: [PATCH 2/5] 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. --- blockchain/chain.go | 113 +++++++++++++++++++++++ blockchain/chain_test.go | 191 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 304 insertions(+) diff --git a/blockchain/chain.go b/blockchain/chain.go index c013ff3b15..77680ba9f1 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 8d6ca89174..1ac08f9a76 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()) + } + } + } + } +} From 892ae679b3950addb96eedca7922a290c32e3878 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Mon, 7 Nov 2022 15:00:05 +0900 Subject: [PATCH 3/5] btcjson, main: Implement the getchaintips call getchaintips call is implemented and the behavior mimics that of Bitcoin Core. Resolves https://github.com/btcsuite/btcd/issues/1912. --- btcjson/chainsvrresults.go | 8 ++++++++ rpcserver.go | 25 ++++++++++++++++++++++++- rpcserverhelp.go | 10 ++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 4feaeda338..fe28e25324 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -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"` diff --git a/rpcserver.go b/rpcserver.go index b917263df5..cd9ac04802 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -146,6 +146,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "getblockhash": handleGetBlockHash, "getblockheader": handleGetBlockHeader, "getblocktemplate": handleGetBlockTemplate, + "getchaintips": handleGetChainTips, "getcfilter": handleGetCFilter, "getcfilterheader": handleGetCFilterHeader, "getconnectioncount": handleGetConnectionCount, @@ -231,7 +232,6 @@ var rpcAskWallet = map[string]struct{}{ // Commands that are currently unimplemented, but should ultimately be. var rpcUnimplemented = map[string]struct{}{ "estimatepriority": {}, - "getchaintips": {}, "getmempoolentry": {}, "getnetworkinfo": {}, "getwork": {}, @@ -266,6 +266,7 @@ var rpcLimited = map[string]struct{}{ "getblockcount": {}, "getblockhash": {}, "getblockheader": {}, + "getchaintips": {}, "getcfilter": {}, "getcfilterheader": {}, "getcurrentnet": {}, @@ -2192,6 +2193,28 @@ func handleGetBlockTemplate(s *rpcServer, cmd interface{}, closeChan <-chan stru } } +// handleGetChainTips implements the getchaintips command. +func handleGetChainTips(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + chainTips := s.cfg.Chain.ChainTips() + + ret := make([]btcjson.GetChainTipsResult, 0, len(chainTips)) + for _, chainTip := range chainTips { + ret = append(ret, struct { + Height int32 "json:\"height\"" + Hash string "json:\"hash\"" + BranchLen int32 "json:\"branchlen\"" + Status string "json:\"status\"" + }{ + Height: chainTip.Height, + Hash: chainTip.BlockHash.String(), + BranchLen: chainTip.BranchLen, + Status: chainTip.Status.String(), + }) + } + + return ret, nil +} + // handleGetCFilter implements the getcfilter command. func handleGetCFilter(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { if s.cfg.CfIndex == nil { diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 16bbb62a2b..262660962b 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -347,6 +347,15 @@ var helpDescsEnUS = map[string]string{ "getblocktemplate--condition2": "mode=proposal, accepted", "getblocktemplate--result1": "An error string which represents why the proposal was rejected or nothing if accepted", + // GetChainTipsResult help. + "getchaintipsresult-chaintips": "The chaintips that this node is aware of", + "getchaintipsresult-height": "The height of the chain tip", + "getchaintipsresult-hash": "The block hash of the chain tip", + "getchaintipsresult-branchlen": "Returns zero for main chain. Otherwise is the length of branch connecting the tip to the main chain", + "getchaintipsresult-status": "Status of the chain. Returns \"active\" for the main chain", + // GetChainTipsCmd help. + "getchaintips--synopsis": "Returns information about all known tips in the block tree, including the main chain as well as orphaned branches.", + // GetCFilterCmd help. "getcfilter--synopsis": "Returns a block's committed filter given its hash.", "getcfilter-filtertype": "The type of filter to return (0=regular)", @@ -728,6 +737,7 @@ var rpcResultTypes = map[string][]interface{}{ "getblockheader": {(*string)(nil), (*btcjson.GetBlockHeaderVerboseResult)(nil)}, "getblocktemplate": {(*btcjson.GetBlockTemplateResult)(nil), (*string)(nil), nil}, "getblockchaininfo": {(*btcjson.GetBlockChainInfoResult)(nil)}, + "getchaintips": {(*[]btcjson.GetChainTipsResult)(nil)}, "getcfilter": {(*string)(nil)}, "getcfilterheader": {(*string)(nil)}, "getconnectioncount": {(*int32)(nil)}, From e2d9e2092b088f96ea40d41173b686c0dc2b77ac Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Fri, 11 Nov 2022 14:37:38 +0900 Subject: [PATCH 4/5] docs: Update json_rpc_api.md for getchaintips rpc --- docs/json_rpc_api.md | 53 +++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/json_rpc_api.md b/docs/json_rpc_api.md index d1b43ce8c4..2c7d455457 100644 --- a/docs/json_rpc_api.md +++ b/docs/json_rpc_api.md @@ -168,26 +168,27 @@ the method name for further details such as parameter and return information. |8|[getblockcount](#getblockcount)|Y|Returns the number of blocks in the longest block chain.| |9|[getblockhash](#getblockhash)|Y|Returns hash of the block in best block chain at the given height.| |10|[getblockheader](#getblockheader)|Y|Returns the block header of the block.| -|11|[getconnectioncount](#getconnectioncount)|N|Returns the number of active connections to other peers.| -|12|[getdifficulty](#getdifficulty)|Y|Returns the proof-of-work difficulty as a multiple of the minimum difficulty.| -|13|[getgenerate](#getgenerate)|N|Return if the server is set to generate coins (mine) or not.| -|14|[gethashespersec](#gethashespersec)|N|Returns a recent hashes per second performance measurement while generating coins (mining).| -|15|[getinfo](#getinfo)|Y|Returns a JSON object containing various state info.| -|16|[getmempoolinfo](#getmempoolinfo)|N|Returns a JSON object containing mempool-related information.| -|17|[getmininginfo](#getmininginfo)|N|Returns a JSON object containing mining-related information.| -|18|[getnettotals](#getnettotals)|Y|Returns a JSON object containing network traffic statistics.| -|19|[getnetworkhashps](#getnetworkhashps)|Y|Returns the estimated network hashes per second for the block heights provided by the parameters.| -|20|[getpeerinfo](#getpeerinfo)|N|Returns information about each connected network peer as an array of json objects.| -|21|[getrawmempool](#getrawmempool)|Y|Returns an array of hashes for all of the transactions currently in the memory pool.| -|22|[getrawtransaction](#getrawtransaction)|Y|Returns information about a transaction given its hash.| -|23|[help](#help)|Y|Returns a list of all commands or help for a specified command.| -|24|[ping](#ping)|N|Queues a ping to be sent to each connected peer.| -|25|[sendrawtransaction](#sendrawtransaction)|Y|Submits the serialized, hex-encoded transaction to the local peer and relays it to the network.
btcd does not yet implement the `allowhighfees` parameter, so it has no effect| -|26|[setgenerate](#setgenerate) |N|Set the server to generate coins (mine) or not.
NOTE: Since btcd does not have the wallet integrated to provide payment addresses, btcd must be configured via the `--miningaddr` option to provide which payment addresses to pay created blocks to for this RPC to function.| -|27|[stop](#stop)|N|Shutdown btcd.| -|28|[submitblock](#submitblock)|Y|Attempts to submit a new serialized, hex-encoded block to the network.| -|29|[validateaddress](#validateaddress)|Y|Verifies the given address is valid. NOTE: Since btcd does not have a wallet integrated, btcd will only return whether the address is valid or not.| -|30|[verifychain](#verifychain)|N|Verifies the block chain database.| +|11|[getchaintips](#getchaintips)|Y|Returns information about all known tips in the block tree, including the main chain as well as orphaned branches.| +|12|[getconnectioncount](#getconnectioncount)|N|Returns the number of active connections to other peers.| +|13|[getdifficulty](#getdifficulty)|Y|Returns the proof-of-work difficulty as a multiple of the minimum difficulty.| +|14|[getgenerate](#getgenerate)|N|Return if the server is set to generate coins (mine) or not.| +|15|[gethashespersec](#gethashespersec)|N|Returns a recent hashes per second performance measurement while generating coins (mining).| +|16|[getinfo](#getinfo)|Y|Returns a JSON object containing various state info.| +|17|[getmempoolinfo](#getmempoolinfo)|N|Returns a JSON object containing mempool-related information.| +|18|[getmininginfo](#getmininginfo)|N|Returns a JSON object containing mining-related information.| +|19|[getnettotals](#getnettotals)|Y|Returns a JSON object containing network traffic statistics.| +|20|[getnetworkhashps](#getnetworkhashps)|Y|Returns the estimated network hashes per second for the block heights provided by the parameters.| +|21|[getpeerinfo](#getpeerinfo)|N|Returns information about each connected network peer as an array of json objects.| +|22|[getrawmempool](#getrawmempool)|Y|Returns an array of hashes for all of the transactions currently in the memory pool.| +|23|[getrawtransaction](#getrawtransaction)|Y|Returns information about a transaction given its hash.| +|24|[help](#help)|Y|Returns a list of all commands or help for a specified command.| +|25|[ping](#ping)|N|Queues a ping to be sent to each connected peer.| +|26|[sendrawtransaction](#sendrawtransaction)|Y|Submits the serialized, hex-encoded transaction to the local peer and relays it to the network.
btcd does not yet implement the `allowhighfees` parameter, so it has no effect| +|27|[setgenerate](#setgenerate) |N|Set the server to generate coins (mine) or not.
NOTE: Since btcd does not have the wallet integrated to provide payment addresses, btcd must be configured via the `--miningaddr` option to provide which payment addresses to pay created blocks to for this RPC to function.| +|28|[stop](#stop)|N|Shutdown btcd.| +|29|[submitblock](#submitblock)|Y|Attempts to submit a new serialized, hex-encoded block to the network.| +|30|[validateaddress](#validateaddress)|Y|Verifies the given address is valid. NOTE: Since btcd does not have a wallet integrated, btcd will only return whether the address is valid or not.| +|31|[verifychain](#verifychain)|N|Verifies the block chain database.| @@ -319,6 +320,18 @@ the method name for further details such as parameter and return information. |Example Return (verbose=true)|`{`
  `"hash": "00000000009e2958c15ff9290d571bf9459e93b19765c6801ddeccadbb160a1e",`
  `"confirmations": 392076,`
  `"height": 100000,`
  `"version": 2,`
  `"merkleroot": "d574f343976d8e70d91cb278d21044dd8a396019e6db70755a0a50e4783dba38",`
  `"time": 1376123972,`
  `"nonce": 1005240617,`
  `"bits": "1c00f127",`
  `"difficulty": 271.75767393,`
  `"previousblockhash": "000000004956cc2edd1a8caa05eacfa3c69f4c490bfc9ace820257834115ab35",`
  `"nextblockhash": "0000000000629d100db387f37d0f37c51118f250fb0946310a8c37316cbc4028"`
`}`| [Return to Overview](#MethodOverview)
+*** +
+ +| | | +|---|---| +|Method|getchaintips| +|Parameters|None| +|Description|Returns information about all known tips in the block tree, including the main chain as well as orphaned branches| +|Returns|`(A json object array)`
`height`: `(numeric)` The height of the chain tip.
`hash`: `(string)` The block hash of the chain tip.
`branchlen`: `(numeric)` Returns zero for main chain. Otherwise is the length of branch connecting the tip to the main chain.
`status`: `(string)` Status of the chain. Returns "active" for the main chain.`| +|Example Return|`["{"height": 1, "hash": "78b945a390c561cf8b9ccf0598be15d7d85c67022bf71083c0b0bd8042fc30d7", "branchlen": 1, "status": "valid-fork"}, {"height": 1, "hash": "584c830a4783c6331e59cb984686cfec14bccc596fe8bbd1660b90cda359b42a", "branchlen": 0, "status": "active"}"]`| +[Return to Overview](#MethodOverview)
+ ***
From fc99e96b595f3798c3fd391d244f14c931c2c26c Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Sat, 12 Nov 2022 20:34:53 +0900 Subject: [PATCH 5/5] rpcclient, integration: Add test for getchaintips call rpcclient now support calling the getchaintips rpc call. getchaintips_test.go adds test for the getchaintips rpc call. The test includes hard-coded blocks which the test will feed the node via rpc and it'll check that the returned chain tips from the getchaintips call are chaintips that we expect to be returned. --- integration/getchaintips_test.go | 350 +++++++++++++++++++++++++++++++ rpcclient/chain.go | 38 ++++ 2 files changed, 388 insertions(+) create mode 100644 integration/getchaintips_test.go diff --git a/integration/getchaintips_test.go b/integration/getchaintips_test.go new file mode 100644 index 0000000000..1570ba740c --- /dev/null +++ b/integration/getchaintips_test.go @@ -0,0 +1,350 @@ +package integration + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/stretchr/testify/require" +) + +func getBlockFromString(t *testing.T, hexStr string) *btcutil.Block { + t.Helper() + + serializedBlock, err := hex.DecodeString(hexStr) + if err != nil { + t.Fatalf("couldn't decode hex string of %s", hexStr) + } + + block, err := btcutil.NewBlockFromBytes(serializedBlock) + if err != nil { + t.Fatalf("couldn't make a new block from bytes. "+ + "Decoded hex string: %s", hexStr) + } + + return block +} + +// compareMultipleChainTips checks that all the expected chain tips are included in got chain tips and +// verifies that the got chain tip matches the expected chain tip. +func compareMultipleChainTips(t *testing.T, gotChainTips, expectedChainTips []*btcjson.GetChainTipsResult) error { + if len(gotChainTips) != len(expectedChainTips) { + return fmt.Errorf("Expected %d chaintips but got %d", len(expectedChainTips), len(gotChainTips)) + } + + gotChainTipsMap := make(map[string]btcjson.GetChainTipsResult) + for _, gotChainTip := range gotChainTips { + gotChainTipsMap[gotChainTip.Hash] = *gotChainTip + } + + for _, expectedChainTip := range expectedChainTips { + gotChainTip, found := gotChainTipsMap[expectedChainTip.Hash] + if !found { + return fmt.Errorf("Couldn't find expected chaintip with hash %s", expectedChainTip.Hash) + } + + require.Equal(t, gotChainTip, *expectedChainTip) + } + + return nil +} + +func TestGetChainTips(t *testing.T) { + // block1Hex is a block that builds on top of the regtest genesis block. + // Has blockhash of "36c056247e8c0589f6307995e4e13acf2b2b79cad9ecd5a4eeab2131ed0ecde5". + block1Hex := "0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf18891" + + "0f71881025ae0d41ce8748b79ac40e5f3197af3bb83a594def7943aff0fce504c638ea6d63f" + + "fff7f2000000000010200000000010100000000000000000000000000000000000000000000" + + "00000000000000000000ffffffff025100ffffffff0200f2052a010000001600149b0f9d020" + + "8b3b425246e16830562a63bf1c701180000000000000000266a24aa21a9ede2f61c3f71d1de" + + "fd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000" + + "000000000000000000000000000000000000000000000000000" + + // block2Hex is a block that builds on top of block1Hex. + // Has blockhash of "664b51334782a4ad16e8471b530dcd0027c75b8c25187b41dfc85ecd353295c6". + block2Hex := "00000020e5cd0eed3121abeea4d5ecd9ca792b2bcf3ae1e4957930f689058c7e2456c0" + + "362a78a11b875d31af2ea493aa5b6b623e0d481f11e69f7147ab974be9da087f3e24696f63f" + + "fff7f2001000000010200000000010100000000000000000000000000000000000000000000" + + "00000000000000000000ffffffff025200ffffffff0200f2052a0100000016001470fea1feb" + + "4969c1f237753ae29c0217c6637835c0000000000000000266a24aa21a9ede2f61c3f71d1de" + + "fd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000" + + "000000000000000000000000000000000000000000000000000" + + // block3Hex is a block that builds on top of block2Hex. + // Has blockhash of "17a5c5cb90ecde5a46dd195d434eea46b653e35e4517070eade429db3ac83944". + block3Hex := "00000020c6953235cd5ec8df417b18258c5bc72700cd0d531b47e816ada4824733514b" + + "66c3ad4d567a36c20df07ea0b7fce1e4b4ee5be3eaf0b946b0ae73f3a74d47f0cf99696f63f" + + "fff7f2000000000010200000000010100000000000000000000000000000000000000000000" + + "00000000000000000000ffffffff025300ffffffff0200f2052a010000001600140e835869b" + + "154f647d11376634b5e8c785e7d21060000000000000000266a24aa21a9ede2f61c3f71d1de" + + "fd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000" + + "000000000000000000000000000000000000000000000000000" + + // block4Hex is a block that builds on top of block3Hex. + // Has blockhash of "7b357f3073c4397d6d069a32a09141c32560f3c62233ca138eb5e03c5991f45c". + block4Hex := "000000204439c83adb29e4ad0e0717455ee353b646ea4e435d19dd465adeec90cbc5a5" + + "17ab639a5dd622e90f5f9feffc1c7c28f47a2caf85c21d7dd52cd223a7164619e37a6a6f63f" + + "fff7f2004000000010200000000010100000000000000000000000000000000000000000000" + + "00000000000000000000ffffffff025400ffffffff0200f2052a01000000160014a157c74b4" + + "42a3e11b45cf5273f8c0c032c5a40ed0000000000000000266a24aa21a9ede2f61c3f71d1de" + + "fd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000" + + "000000000000000000000000000000000000000000000000000" + + // block2aHex is a block that builds on top of block1Hex. + // Has blockhash of "5181a4e34cc23ed95c69749dedf4cc7ebd659243bc1683372f8940c8cd8f9b68". + block2aHex := "00000020e5cd0eed3121abeea4d5ecd9ca792b2bcf3ae1e4957930f689058c7e2456c" + + "036f7d4ebe524260c9b6c2b5e3d105cad0b7ddfaeaa29971363574fc1921a3f2f7ad66b6f63" + + "ffff7f200100000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025200ffffffff0200f2052a0100000016001466fca22d" + + "0e4679d119ea1e127c984746a1f7e66c0000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // block3aHex is a block that builds on top of block2aHex. + // Has blockhash of "0b0216936d1a5c01362256d06a9c9a2b13768fa2f2748549a71008af36dd167f". + block3aHex := "00000020689b8fcdc840892f378316bc439265bd7eccf4ed9d74695cd93ec24ce3a48" + + "15161a430ce5cae955b1254b753bc95854d942947855d3ae59002de9773b7fe65fdf16b6f63" + + "ffff7f200100000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025300ffffffff0200f2052a0100000016001471da0afb" + + "883c228b18af6bd0cabc471aebe8d1750000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // block4aHex is a block that builds on top of block3aHex. + // Has blockhash of "65a00a026eaa83f6e7a7f4a920faa090f3f9d3565a56df2362db2ab2fa14ccec". + block4aHex := "000000207f16dd36af0810a7498574f2a28f76132b9a9c6ad0562236015c1a6d93160" + + "20b951fa5ee5072d88d6aef9601999307dbd8d96dad067b80bfe04afe81c7a8c21beb706f63" + + "ffff7f200000000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025400ffffffff0200f2052a01000000160014fd1f118c" + + "95a712b8adef11c3cc0643bcb6b709f10000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // block5aHex is a block that builds on top of block4aHex. + // Has blockhash of "5c8814bc034a4c37fa5ccdc05e09b45a771bd7505d68092f21869a912737ee10". + block5aHex := "00000020eccc14fab22adb6223df565a56d3f9f390a0fa20a9f4a7e7f683aa6e020aa" + + "0656331bd4fcd3db611de7fbf72ef3dff0b85b244b5a983d5c0270e728214f67f9aaa766f63" + + "ffff7f200600000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025500ffffffff0200f2052a0100000016001438335896" + + "ad1d087e3541436a5b293c0d23ad27e60000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // block4bHex is a block that builds on top of block3aHex. + // Has blockhash of "130458e795cc46f2759195e92737426fb0ada2a07f98434551ffb7500b23c161". + block4bHex := "000000207f16dd36af0810a7498574f2a28f76132b9a9c6ad0562236015c1a6d93160" + + "20b14f9ce93d0144c383fea72f408b06b268a1523a029b825a1edfa15b367f6db2cfd7d6f63" + + "ffff7f200200000001020000000001010000000000000000000000000000000000000000000" + + "000000000000000000000ffffffff025400ffffffff0200f2052a0100000016001405b5ba2d" + + "1e549c4c84a623de3575948d3ef8a27f0000000000000000266a24aa21a9ede2f61c3f71d1d" + + "efd3fa999dfa36953755c690689799962b48bebd836974e8cf9012000000000000000000000" + + "0000000000000000000000000000000000000000000000000000" + + // Set up regtest chain. + r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil, "") + if err != nil { + t.Fatal("TestGetChainTips fail. Unable to create primary harness: ", err) + } + if err := r.SetUp(true, 0); err != nil { + t.Fatalf("TestGetChainTips fail. Unable to setup test chain: %v", err) + } + defer r.TearDown() + + // Immediately call getchaintips after setting up regtest. + gotChainTips, err := r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + // We expect a single genesis block. + expectedChainTips := []*btcjson.GetChainTipsResult{ + { + Height: 0, + Hash: chaincfg.RegressionNetParams.GenesisHash.String(), + BranchLen: 0, + Status: "active", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit 4 blocks. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 + blockStrings := []string{block1Hex, block2Hex, block3Hex, block4Hex} + for _, blockString := range blockStrings { + block := getBlockFromString(t, blockString) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + } + + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, blockStrings[len(blockStrings)-1]).Hash().String(), + BranchLen: 0, + Status: "active", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit 2 blocks that don't build on top of the current active tip. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 (active) + // \ -> 2a -> 3a (valid-fork) + blockStrings = []string{block2aHex, block3aHex} + for _, blockString := range blockStrings { + block := getBlockFromString(t, blockString) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + } + + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, block4Hex).Hash().String(), + BranchLen: 0, + Status: "active", + }, + { + Height: 3, + Hash: getBlockFromString(t, block3aHex).Hash().String(), + BranchLen: 2, + Status: "valid-fork", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit a single block that don't build on top of the current active tip. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 (active) + // \ -> 2a -> 3a -> 4a (valid-fork) + block := getBlockFromString(t, block4aHex) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, block4Hex).Hash().String(), + BranchLen: 0, + Status: "active", + }, + { + Height: 4, + Hash: getBlockFromString(t, block4aHex).Hash().String(), + BranchLen: 3, + Status: "valid-fork", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit a single block that changes the active branch to 5a. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 (valid-fork) + // \ -> 2a -> 3a -> 4a -> 5a (active) + block = getBlockFromString(t, block5aHex) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, block4Hex).Hash().String(), + BranchLen: 3, + Status: "valid-fork", + }, + { + Height: 5, + Hash: getBlockFromString(t, block5aHex).Hash().String(), + BranchLen: 0, + Status: "active", + }, + } + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } + + // Submit a single block that builds on top of 3a. + // + // Our chain view looks like so: + // (genesis block) -> 1 -> 2 -> 3 -> 4 (valid-fork) + // \ -> 2a -> 3a -> 4a -> 5a (active) + // \ -> 4b (valid-fork) + block = getBlockFromString(t, block4bHex) + err = r.Client.SubmitBlock(block, nil) + if err != nil { + t.Fatal(err) + } + gotChainTips, err = r.Client.GetChainTips() + if err != nil { + t.Fatal(err) + } + expectedChainTips = []*btcjson.GetChainTipsResult{ + { + Height: 4, + Hash: getBlockFromString(t, block4Hex).Hash().String(), + BranchLen: 3, + Status: "valid-fork", + }, + { + Height: 5, + Hash: getBlockFromString(t, block5aHex).Hash().String(), + BranchLen: 0, + Status: "active", + }, + { + Height: 4, + Hash: getBlockFromString(t, block4bHex).Hash().String(), + BranchLen: 1, + Status: "valid-fork", + }, + } + + err = compareMultipleChainTips(t, gotChainTips, expectedChainTips) + if err != nil { + t.Fatalf("TestGetChainTips fail. Error: %v", err) + } +} diff --git a/rpcclient/chain.go b/rpcclient/chain.go index a97543fd3c..b21665991f 100644 --- a/rpcclient/chain.go +++ b/rpcclient/chain.go @@ -685,6 +685,44 @@ func (c *Client) GetBlockHeaderVerbose(blockHash *chainhash.Hash) (*btcjson.GetB return c.GetBlockHeaderVerboseAsync(blockHash).Receive() } +// FutureGetChainTipsResult is a future promise to deliver the result of a +// GetChainTips RPC invocation (or an applicable error). +type FutureGetChainTipsResult chan *Response + +// Receive waits for the Response promised by the future and returns the +// data structure of all the chain tips the node is aware of. +func (r FutureGetChainTipsResult) Receive() ([]*btcjson.GetChainTipsResult, error) { + res, err := ReceiveFuture(r) + if err != nil { + return nil, err + } + + // Unmarshal result as a string. + var chainTips []*btcjson.GetChainTipsResult + err = json.Unmarshal(res, &chainTips) + if err != nil { + return nil, err + } + + return chainTips, nil +} + +// GetChainTipsAsync returns an instance of a type that can be used to get the +// result of the RPC at some future time by invoking the Receive function on the +// returned instance. +// +// See GetChainTips for the blocking version and more details. +func (c *Client) GetChainTipsAsync() FutureGetChainTipsResult { + cmd := btcjson.NewGetChainTipsCmd() + return c.SendCmd(cmd) +} + +// GetChainTips returns a slice of data structure with information about all the +// current chain tips that this node is aware of. +func (c *Client) GetChainTips() ([]*btcjson.GetChainTipsResult, error) { + return c.GetChainTipsAsync().Receive() +} + // FutureGetMempoolEntryResult is a future promise to deliver the result of a // GetMempoolEntryAsync RPC invocation (or an applicable error). type FutureGetMempoolEntryResult chan *Response