diff --git a/blockchain/chain.go b/blockchain/chain.go index d00557e7..3b89446c 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -1322,6 +1322,48 @@ func (b *BlockChain) HeightRange(startHeight, endHeight int32) ([]chainhash.Hash return hashes, nil } +// HeightToHashRange returns a range of block hashes for the given start height +// and end hash, inclusive on both ends. The hashes are for all blocks that are +// ancestors of endHash with height greater than or equal to startHeight. The +// end hash must belong to a block that is known to be valid. +// +// This function is safe for concurrent access. +func (b *BlockChain) HeightToHashRange(startHeight int32, + endHash *chainhash.Hash, maxResults int) ([]chainhash.Hash, error) { + + endNode := b.index.LookupNode(endHash) + if endNode == nil { + return nil, fmt.Errorf("no known block header with hash %v", endHash) + } + if !b.index.NodeStatus(endNode).KnownValid() { + return nil, fmt.Errorf("block %v is not yet validated", endHash) + } + endHeight := endNode.height + + if startHeight < 0 { + return nil, fmt.Errorf("start height (%d) is below 0", startHeight) + } + if startHeight > endHeight { + return nil, fmt.Errorf("start height (%d) is past end height (%d)", + startHeight, endHeight) + } + + resultsLength := int(endHeight - startHeight + 1) + if resultsLength > maxResults { + return nil, fmt.Errorf("number of results (%d) would exceed max (%d)", + resultsLength, maxResults) + } + + // Walk backwards from endHeight to startHeight, collecting block hashes. + node := endNode + hashes := make([]chainhash.Hash, resultsLength) + for i := resultsLength - 1; i >= 0; i-- { + hashes[i] = node.hash + node = node.parent + } + return hashes, nil +} + // locateInventory returns the node of the block after the first known block in // the locator along with the number of subsequent nodes needed to either reach // the provided stop hash or the provided max number of entries. diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index afb07f9a..d8459b68 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -800,3 +800,95 @@ func TestLocateInventory(t *testing.T) { } } } + +// TestHeightToHashRange ensures that fetching a range of block hashes by start +// height and end hash works as expected. +func TestHeightToHashRange(t *testing.T) { + // Construct a synthetic block chain with a block index consisting of + // the following structure. + // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 + // \-> 16a -> 17a -> 18a (unvalidated) + tip := tstTip + chain := newFakeChain(&chaincfg.MainNetParams) + branch0Nodes := chainedNodes(chain.bestChain.Genesis(), 18) + branch1Nodes := chainedNodes(branch0Nodes[14], 3) + for _, node := range branch0Nodes { + chain.index.SetStatusFlags(node, statusValid) + chain.index.AddNode(node) + } + for _, node := range branch1Nodes { + if node.height < 18 { + chain.index.SetStatusFlags(node, statusValid) + } + chain.index.AddNode(node) + } + chain.bestChain.SetTip(tip(branch0Nodes)) + + tests := []struct { + name string + startHeight int32 // locator for requested inventory + endHash chainhash.Hash // stop hash for locator + maxResults int // max to locate, 0 = wire const + hashes []chainhash.Hash // expected located hashes + expectError bool + }{ + { + name: "blocks below tip", + startHeight: 11, + endHash: branch0Nodes[14].hash, + maxResults: 10, + hashes: nodeHashes(branch0Nodes, 10, 11, 12, 13, 14), + }, + { + name: "blocks on main chain", + startHeight: 15, + endHash: branch0Nodes[17].hash, + maxResults: 10, + hashes: nodeHashes(branch0Nodes, 14, 15, 16, 17), + }, + { + name: "blocks on stale chain", + startHeight: 15, + endHash: branch1Nodes[1].hash, + maxResults: 10, + hashes: append(nodeHashes(branch0Nodes, 14), + nodeHashes(branch1Nodes, 0, 1)...), + }, + { + name: "invalid start height", + startHeight: 19, + endHash: branch0Nodes[17].hash, + maxResults: 10, + expectError: true, + }, + { + name: "too many results", + startHeight: 1, + endHash: branch0Nodes[17].hash, + maxResults: 10, + expectError: true, + }, + { + name: "unvalidated block", + startHeight: 15, + endHash: branch1Nodes[2].hash, + maxResults: 10, + expectError: true, + }, + } + for _, test := range tests { + hashes, err := chain.HeightToHashRange(test.startHeight, &test.endHash, + test.maxResults) + if err != nil { + if !test.expectError { + t.Errorf("%s: unexpected error: %v", test.name, err) + } + continue + } + + if !reflect.DeepEqual(hashes, test.hashes) { + t.Errorf("%s: unxpected hashes -- got %v, want %v", + test.name, hashes, test.hashes) + } + } +}