diff --git a/blockchain/chain.go b/blockchain/chain.go index 3b89446c..cd233835 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -1364,6 +1364,45 @@ func (b *BlockChain) HeightToHashRange(startHeight int32, return hashes, nil } +// IntervalBlockHashes returns hashes for all blocks that are ancestors of +// endHash where the block height is a positive multiple of interval. +// +// This function is safe for concurrent access. +func (b *BlockChain) IntervalBlockHashes(endHash *chainhash.Hash, interval 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 + + resultsLength := int(endHeight) / interval + hashes := make([]chainhash.Hash, resultsLength) + + b.bestChain.mtx.Lock() + defer b.bestChain.mtx.Unlock() + + blockNode := endNode + for index := int(endHeight) / interval; index > 0; index-- { + // Use the bestChain chainView for faster lookups once lookup intersects + // the best chain. + blockHeight := int32(index * interval) + if b.bestChain.contains(blockNode) { + blockNode = b.bestChain.nodeByHeight(blockHeight) + } else { + blockNode = blockNode.Ancestor(blockHeight) + } + + hashes[index-1] = blockNode.hash + } + + 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 d8459b68..7de323bc 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -892,3 +892,75 @@ func TestHeightToHashRange(t *testing.T) { } } } + +// TestIntervalBlockHashes ensures that fetching block hashes at specified +// intervals by end hash works as expected. +func TestIntervalBlockHashes(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 + endHash chainhash.Hash + interval int + hashes []chainhash.Hash + expectError bool + }{ + { + name: "blocks on main chain", + endHash: branch0Nodes[17].hash, + interval: 8, + hashes: nodeHashes(branch0Nodes, 7, 15), + }, + { + name: "blocks on stale chain", + endHash: branch1Nodes[1].hash, + interval: 8, + hashes: append(nodeHashes(branch0Nodes, 7), + nodeHashes(branch1Nodes, 0)...), + }, + { + name: "no results", + endHash: branch0Nodes[17].hash, + interval: 20, + hashes: []chainhash.Hash{}, + }, + { + name: "unvalidated block", + endHash: branch1Nodes[2].hash, + interval: 8, + expectError: true, + }, + } + for _, test := range tests { + hashes, err := chain.IntervalBlockHashes(&test.endHash, test.interval) + 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) + } + } +}