diff --git a/blockchain/chainquery.go b/blockchain/chainquery.go new file mode 100644 index 00000000..162a1d47 --- /dev/null +++ b/blockchain/chainquery.go @@ -0,0 +1,123 @@ +package blockchain + +import ( + "sort" + "strings" + + btcutil "github.com/lbryio/lbcutil" +) + +type ChainTip struct { // duplicate of btcjson.GetChainTipsResult to avoid circular reference + Height int64 + Hash string + BranchLen int64 + Status string +} + +// nodeHeightSorter implements sort.Interface to allow a slice of nodes to +// be sorted by height in ascending order. +type nodeHeightSorter []ChainTip + +// Len returns the number of nodes in the slice. It is part of the +// sort.Interface implementation. +func (s nodeHeightSorter) Len() int { + return len(s) +} + +// Swap swaps the nodes at the passed indices. It is part of the +// sort.Interface implementation. +func (s nodeHeightSorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less returns whether the node with index i should sort before the node with +// index j. It is part of the sort.Interface implementation. +func (s nodeHeightSorter) Less(i, j int) bool { + // To ensure stable order when the heights are the same, fall back to + // sorting based on hash. + if s[i].Height == s[j].Height { + return strings.Compare(s[i].Hash, s[j].Hash) < 0 + } + return s[i].Height < s[j].Height +} + +// ChainTips returns information, in JSON-RPC format, about all the currently +// known chain tips in the block index. +func (b *BlockChain) ChainTips() []ChainTip { + // we need our current tip + // we also need all of our orphans that aren't in the prevOrphans + var results []ChainTip + + tip := b.bestChain.Tip() + results = append(results, ChainTip{ + Height: int64(tip.height), + Hash: tip.hash.String(), + BranchLen: 0, + Status: "active", + }) + + b.orphanLock.RLock() + defer b.orphanLock.RUnlock() + + notInBestChain := func(block *btcutil.Block) bool { + node := b.bestChain.NodeByHeight(block.Height()) + if node == nil { + return false + } + return node.hash.IsEqual(block.Hash()) + } + + for hash, orphan := range b.orphans { + if len(b.prevOrphans[hash]) > 0 { + continue + } + fork := orphan.block + for fork != nil && notInBestChain(fork) { + fork = b.orphans[*fork.Hash()].block + } + + result := ChainTip{ + Height: int64(orphan.block.Height()), + Hash: hash.String(), + BranchLen: int64(orphan.block.Height() - fork.Height()), + } + + // Determine the status of the chain tip. + // + // active: + // The current best chain tip. + // + // invalid: + // The block or one of its ancestors is invalid. + // + // headers-only: + // The block or one of its ancestors does not have the full block data + // available which also means the block can't be validated or + // connected. + // + // valid-fork: + // The block is fully validated which implies it was probably part of + // main chain at one point and was reorganized. + // + // valid-headers: + // The full block data is available and the header is valid, but the + // block was never validated which implies it was probably never part + // of the main chain. + tipStatus := b.index.LookupNode(&hash).status + if tipStatus.KnownInvalid() { + result.Status = "invalid" + } else if !tipStatus.HaveData() { + result.Status = "headers-only" + } else if tipStatus.KnownValid() { + result.Status = "valid-fork" + } else { + result.Status = "valid-headers" + } + + results = append(results, result) + } + + // Generate the results sorted by descending height. + sort.Sort(sort.Reverse(nodeHeightSorter(results))) + return results +} diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 46d454c5..811883c7 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -325,6 +325,14 @@ type GetMempoolEntryResult struct { Depends []string `json:"depends"` } +// GetChainTipsResult models the data returns from the getchaintips command. +type GetChainTipsResult struct { + Height int64 `json:"height"` + Hash string `json:"hash"` + BranchLen int64 `json:"branchlen"` + Status string `json:"status"` +} + // GetMempoolInfoResult models the data returned from the getmempoolinfo // command. type GetMempoolInfoResult struct { diff --git a/rpcserver.go b/rpcserver.go index 4efcdb21..9848ac6d 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -147,6 +147,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "getblockcount": handleGetBlockCount, "getblockhash": handleGetBlockHash, "getblockheader": handleGetBlockHeader, + "getchaintips": handleGetChainTips, "getblocktemplate": handleGetBlockTemplate, "getcfilter": handleGetCFilter, "getcfilterheader": handleGetCFilterHeader, @@ -1471,6 +1472,16 @@ func handleGetBlockHeader(s *rpcServer, cmd interface{}, closeChan <-chan struct return blockHeaderReply, nil } +// handleGetChainTips implements the getchaintips command. +func handleGetChainTips(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + tips := s.cfg.Chain.ChainTips() + results := make([]btcjson.GetChainTipsResult, 0, len(tips)) + for _, tip := range tips { + results = append(results, btcjson.GetChainTipsResult(tip)) + } + return results, nil +} + // encodeTemplateID encodes the passed details into an ID that can be used to // uniquely identify a block template. func encodeTemplateID(prevHash *chainhash.Hash, lastGenerated time.Time) string { diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 271a7184..449cb712 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -353,6 +353,22 @@ 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", + // GetChainTips help. + "getchaintips--synopsis": "Returns information about all known chain tips the in the block tree.\n\n" + + "The statuses in the result have the following meanings:\n" + + "active: The current best chain tip.\n" + + "invalid: The block or one of its ancestors is invalid.\n" + + "headers-only: The block or one of its ancestors does not have the full block data available which also means the block can't be validated or connected.\n" + + "valid-fork: The block is fully validated which implies it was probably part of the main chain at one point and was reorganized.\n" + + "valid-headers: The full block data is available and the header is valid, but the block was never validated which implies it was probably never part of the main chain.", + + // GetChainTipsResult help. + "getchaintipsresult-height": "The height of the chain tip", + "getchaintipsresult-hash": "The block hash of the chain tip", + "getchaintipsresult-branchlen": "The length of the branch that connects the tip to the main chain (0 for the main chain tip)", + "getchaintipsresult-status": "The status of the chain (active, invalid, headers-only, valid-fork, valid-headers)", + "getchaintipsresults--result0": "test", + // GetCFilterCmd help. "getcfilter--synopsis": "Returns a block's committed filter given its hash.", "getcfilter-filtertype": "The type of filter to return (0=regular)", @@ -837,6 +853,7 @@ var rpcResultTypes = map[string][]interface{}{ "getblockchaininfo": {(*btcjson.GetBlockChainInfoResult)(nil)}, "getcfilter": {(*string)(nil)}, "getcfilterheader": {(*string)(nil)}, + "getchaintips": {(*[]btcjson.GetChainTipsResult)(nil)}, "getconnectioncount": {(*int32)(nil)}, "getcurrentnet": {(*uint32)(nil)}, "getdifficulty": {(*float64)(nil)},