Merge pull request #1918 from kcalvinalvin/2022-11-06-implement-getchaintips

blockchain, btcjson: Implement getchaintips rpc call
This commit is contained in:
Olaoluwa Osuntokun 2023-11-14 17:16:15 -08:00 committed by GitHub
commit f7e9fba086
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 805 additions and 21 deletions

View file

@ -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 {

View file

@ -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.

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())
}
}
}
}
}

View file

@ -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"`

View file

@ -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.<br /><font color="orange">btcd does not yet implement the `allowhighfees` parameter, so it has no effect</font>|
|26|[setgenerate](#setgenerate) |N|Set the server to generate coins (mine) or not.<br/>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.<br /><font color="orange">btcd does not yet implement the `allowhighfees` parameter, so it has no effect</font>|
|27|[setgenerate](#setgenerate) |N|Set the server to generate coins (mine) or not.<br/>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.|
<a name="MethodDetails" />
@ -319,6 +320,18 @@ the method name for further details such as parameter and return information.
|Example Return (verbose=true)|`{`<br />&nbsp;&nbsp;`"hash": "00000000009e2958c15ff9290d571bf9459e93b19765c6801ddeccadbb160a1e",`<br />&nbsp;&nbsp;`"confirmations": 392076,`<br />&nbsp;&nbsp;`"height": 100000,`<br />&nbsp;&nbsp;`"version": 2,`<br />&nbsp;&nbsp;`"merkleroot": "d574f343976d8e70d91cb278d21044dd8a396019e6db70755a0a50e4783dba38",`<br />&nbsp;&nbsp;`"time": 1376123972,`<br />&nbsp;&nbsp;`"nonce": 1005240617,`<br />&nbsp;&nbsp;`"bits": "1c00f127",`<br />&nbsp;&nbsp;`"difficulty": 271.75767393,`<br />&nbsp;&nbsp;`"previousblockhash": "000000004956cc2edd1a8caa05eacfa3c69f4c490bfc9ace820257834115ab35",`<br />&nbsp;&nbsp;`"nextblockhash": "0000000000629d100db387f37d0f37c51118f250fb0946310a8c37316cbc4028"`<br />`}`|
[Return to Overview](#MethodOverview)<br />
***
<a name="getchaintips"/>
| | |
|---|---|
|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)`<br />`height`: `(numeric)` The height of the chain tip.<br />`hash`: `(string)` The block hash of the chain tip.<br />`branchlen`: `(numeric)` Returns zero for main chain. Otherwise is the length of branch connecting the tip to the main chain.<br />`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)<br />
***
<a name="getconnectioncount"/>

View file

@ -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)
}
}

View file

@ -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

View file

@ -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": {},
@ -2206,6 +2207,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 {

View file

@ -349,6 +349,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)",
@ -730,6 +739,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)},