Skip to content

Commit

Permalink
blockchain: Faster chain view block locator.
Browse files Browse the repository at this point in the history
This exposes the ability to more efficiently create a block locator from
a chain view for a given block node by using their ability to do O(1)
lookups.

It also adds tests to ensure the behavior is correct.
  • Loading branch information
davecgh committed Jul 20, 2018
1 parent 60d7fe0 commit 6254fea
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 0 deletions.
82 changes: 82 additions & 0 deletions blockchain/chainview.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,85 @@ func (c *chainView) FindFork(node *blockNode) *blockNode {
c.mtx.Unlock()
return fork
}

// blockLocator returns a block locator for the passed block node. The passed
// node can be nil in which case the block locator for the current tip
// associated with the view will be returned. This only differs from the
// exported version in that it is up to the caller to ensure the lock is held.
//
// See the exported BlockLocator function comments for more details.
//
// This function MUST be called with the view mutex locked (for reads).
func (c *chainView) blockLocator(node *blockNode) BlockLocator {
// Use the current tip if requested.
if node == nil {
node = c.tip()
if node == nil {
return nil
}
}

// Calculate the max number of entries that will ultimately be in the
// block locator. See the description of the algorithm for how these
// numbers are derived.
var maxEntries uint8
if node.height <= 12 {
maxEntries = uint8(node.height) + 1
} else {
// Requested hash itself + previous 10 entries + genesis block.
// Then floor(log2(height-10)) entries for the skip portion.
adjustedHeight := uint32(node.height) - 10
maxEntries = 12 + fastLog2Floor(adjustedHeight)
}
locator := make(BlockLocator, 0, maxEntries)

step := int64(1)
for node != nil {
locator = append(locator, &node.hash)

// Nothing more to add once the genesis block has been added.
if node.height == 0 {
break
}

// Calculate height of previous node to include ensuring the
// final node is the genesis block.
height := node.height - step
if height < 0 {
height = 0
}

// When the node is in the current chain view, all of its
// ancestors must be too, so use a much faster O(1) lookup in
// that case. Otherwise, fall back to walking backwards through
// the nodes of the other chain to the correct ancestor.
if c.contains(node) {
node = c.nodes[height]
} else {
node = node.Ancestor(height)
}

// Once 11 entries have been included, start doubling the
// distance between included hashes.
if len(locator) > 10 {
step *= 2
}
}

return locator
}

// BlockLocator returns a block locator for the passed block node. The passed
// node can be nil in which case the block locator for the current tip
// associated with the view will be returned.
//
// See the BlockLocator type for details on the algorithm used to create a block
// locator.
//
// This function is safe for concurrent access.
func (c *chainView) BlockLocator(node *blockNode) BlockLocator {
c.mtx.Lock()
locator := c.blockLocator(node)
c.mtx.Unlock()
return locator
}
48 changes: 48 additions & 0 deletions blockchain/chainview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package blockchain

import (
"fmt"
"reflect"
"testing"
)

Expand All @@ -15,6 +16,16 @@ func (node blockNode) String() string {
return fmt.Sprintf("%s(%d)", node.hash, node.height)
}

// zipLocators is a convenience function that returns a single block locator
// given a variable number of them and is used in the tests.
func zipLocators(locators ...BlockLocator) BlockLocator {
var hashes BlockLocator
for _, locator := range locators {
hashes = append(hashes, locator...)
}
return hashes
}

// TestChainView ensures all of the exported functionality of chain views works
// as intended with the expection of some special cases which are handled in
// other tests.
Expand All @@ -40,6 +51,7 @@ func TestChainView(t *testing.T) {
noContains []*blockNode // expected nodes NOT in active view
equal *chainView // view expected equal to active view
unequal *chainView // view expected NOT equal to active
locator BlockLocator // expected locator for active view tip
}{
{
// Create a view for branch 0 as the active chain and
Expand All @@ -55,6 +67,7 @@ func TestChainView(t *testing.T) {
noContains: branch1Nodes,
equal: newChainView(branchTip(branch0Nodes)),
unequal: newChainView(branchTip(branch1Nodes)),
locator: locatorHashes(branch0Nodes, 4, 3, 2, 1, 0),
},
{
// Create a view for branch 1 as the active chain and
Expand All @@ -70,6 +83,10 @@ func TestChainView(t *testing.T) {
noContains: branch2Nodes,
equal: newChainView(branchTip(branch1Nodes)),
unequal: newChainView(branchTip(branch2Nodes)),
locator: zipLocators(
locatorHashes(branch1Nodes, 24, 23, 22, 21, 20,
19, 18, 17, 16, 15, 14, 13, 11, 7),
locatorHashes(branch0Nodes, 1, 0)),
},
{
// Create a view for branch 2 as the active chain and
Expand All @@ -85,6 +102,10 @@ func TestChainView(t *testing.T) {
noContains: branch0Nodes[2:],
equal: newChainView(branchTip(branch2Nodes)),
unequal: newChainView(branchTip(branch0Nodes)),
locator: zipLocators(
locatorHashes(branch2Nodes, 2, 1, 0),
locatorHashes(branch1Nodes, 0),
locatorHashes(branch0Nodes, 1, 0)),
},
}
testLoop:
Expand Down Expand Up @@ -225,6 +246,15 @@ testLoop:
continue testLoop
}
}

// Ensure the block locator for the tip of the active view
// consists of the expected hashes.
locator := test.view.BlockLocator(test.view.tip())
if !reflect.DeepEqual(locator, test.locator) {
t.Errorf("%s: unexpected locator -- got %v, want %v",
test.name, locator, test.locator)
continue
}
}
}

Expand Down Expand Up @@ -395,4 +425,22 @@ func TestChainViewNil(t *testing.T) {
if fork := view.FindFork(nil); fork != nil {
t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork)
}

// Ensure attempting to get a block locator for the tip doesn't produce
// one since the tip is nil.
if locator := view.BlockLocator(nil); locator != nil {
t.Fatalf("BlockLocator: unexpected locator -- got %v, want nil",
locator)
}

// Ensure attempting to get a block locator for a node that exists still
// works as intended.
branchNodes := chainedFakeNodes(nil, 50)
wantLocator := locatorHashes(branchNodes, 49, 48, 47, 46, 45, 44, 43,
42, 41, 40, 39, 38, 36, 32, 24, 8, 0)
locator := view.BlockLocator(branchTip(branchNodes))
if !reflect.DeepEqual(locator, wantLocator) {
t.Fatalf("BlockLocator: unexpected locator -- got %v, want %v",
locator, wantLocator)
}
}

0 comments on commit 6254fea

Please sign in to comment.