From c5b7aa911cab7699549a25b970b3312a062a491e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 20 Feb 2024 18:00:57 -0500 Subject: [PATCH 01/68] Implement interval tree for syncing --- .../snowman/bootstrap/interval/interval.go | 27 +++ .../snowman/bootstrap/interval/state.go | 74 ++++++ .../engine/snowman/bootstrap/interval/tree.go | 77 +++++++ .../snowman/bootstrap/interval/tree_test.go | 218 ++++++++++++++++++ 4 files changed, 396 insertions(+) create mode 100644 snow/engine/snowman/bootstrap/interval/interval.go create mode 100644 snow/engine/snowman/bootstrap/interval/state.go create mode 100644 snow/engine/snowman/bootstrap/interval/tree.go create mode 100644 snow/engine/snowman/bootstrap/interval/tree_test.go diff --git a/snow/engine/snowman/bootstrap/interval/interval.go b/snow/engine/snowman/bootstrap/interval/interval.go new file mode 100644 index 000000000000..d83d0c6143d2 --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/interval.go @@ -0,0 +1,27 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +type interval struct { + lowerBound uint64 + upperBound uint64 +} + +func (i *interval) Less(other *interval) bool { + return i.upperBound < other.upperBound +} + +func (i *interval) Contains(height uint64) bool { + return i != nil && i.lowerBound <= height && height <= i.upperBound +} + +// AdjacentToUpperBound returns true if height is 1 greater than upperBound. +func (i *interval) AdjacentToUpperBound(height uint64) bool { + return i != nil && i.upperBound+1 == height +} + +// AdjacentToLowerBound returns true if height is 1 less than lowerBound. +func (i *interval) AdjacentToLowerBound(height uint64) bool { + return i != nil && height+1 == i.lowerBound +} diff --git a/snow/engine/snowman/bootstrap/interval/state.go b/snow/engine/snowman/bootstrap/interval/state.go new file mode 100644 index 000000000000..0d5974a04e3e --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/state.go @@ -0,0 +1,74 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import "github.com/ava-labs/avalanchego/database" + +const ( + rangePrefixByte byte = iota + blockPrefixByte +) + +var ( + rangePrefix = []byte{rangePrefixByte} + blockPrefix = []byte{blockPrefixByte} +) + +func GetIntervals(db database.Iteratee) ([]*interval, error) { + it := db.NewIteratorWithPrefix(rangePrefix) + defer it.Release() + + var intervals []*interval + for it.Next() { + dbKey := it.Key() + rangeKey := dbKey[len(rangePrefix):] + upperBound, err := database.ParseUInt64(rangeKey) + if err != nil { + return nil, err + } + + value := it.Value() + lowerBound, err := database.ParseUInt64(value) + if err != nil { + return nil, err + } + + intervals = append(intervals, &interval{ + lowerBound: lowerBound, + upperBound: upperBound, + }) + } + return intervals, it.Error() +} + +func PutInterval(db database.KeyValueWriter, upperBound uint64, lowerBound uint64) error { + rangeKey := database.PackUInt64(upperBound) + return database.PutUInt64( + db, + append(rangePrefix, rangeKey...), + lowerBound, + ) +} + +func DeleteInterval(db database.KeyValueDeleter, upperBound uint64) error { + rangeKey := database.PackUInt64(upperBound) + return db.Delete( + append(rangePrefix, rangeKey...), + ) +} + +func PutBlock(db database.KeyValueWriter, height uint64, bytes []byte) error { + blockKey := database.PackUInt64(height) + return db.Put( + append(blockPrefix, blockKey...), + bytes, + ) +} + +func DeleteBlock(db database.KeyValueDeleter, height uint64) error { + blockKey := database.PackUInt64(height) + return db.Delete( + append(blockPrefix, blockKey...), + ) +} diff --git a/snow/engine/snowman/bootstrap/interval/tree.go b/snow/engine/snowman/bootstrap/interval/tree.go new file mode 100644 index 000000000000..680c3f3dcabe --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/tree.go @@ -0,0 +1,77 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import ( + "github.com/google/btree" +) + +type Tree struct { + knownBlocks *btree.BTreeG[*interval] +} + +func NewTree() *Tree { + knownBlocks := btree.NewG(2, (*interval).Less) + return &Tree{ + knownBlocks: knownBlocks, + } +} + +func (t *Tree) Add(height uint64) { + var ( + newInterval = &interval{ + lowerBound: height, + upperBound: height, + } + upper *interval + lower *interval + ) + t.knownBlocks.AscendGreaterOrEqual(newInterval, func(item *interval) bool { + upper = item + return false + }) + if upper.Contains(height) { + // height is already in the tree + return + } + + t.knownBlocks.DescendLessOrEqual(newInterval, func(item *interval) bool { + lower = item + return false + }) + + var ( + adjacentToLowerBound = upper.AdjacentToLowerBound(height) + adjacentToUpperBound = lower.AdjacentToUpperBound(height) + ) + switch { + case adjacentToLowerBound && adjacentToUpperBound: + // the upper and lower ranges should be merged + upper.lowerBound = lower.lowerBound + t.knownBlocks.Delete(lower) + case adjacentToLowerBound: + // the upper range should be extended by one on the lower side + upper.lowerBound = height + case adjacentToUpperBound: + // the lower range should be extended by one on the upper side + lower.upperBound = height + default: + t.knownBlocks.ReplaceOrInsert(newInterval) + } +} + +func (t *Tree) Contains(height uint64) bool { + var ( + i = &interval{ + lowerBound: height, + upperBound: height, + } + higher *interval + ) + t.knownBlocks.AscendGreaterOrEqual(i, func(item *interval) bool { + higher = item + return false + }) + return higher.Contains(height) +} diff --git a/snow/engine/snowman/bootstrap/interval/tree_test.go b/snow/engine/snowman/bootstrap/interval/tree_test.go new file mode 100644 index 000000000000..fa9a68a911a3 --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/tree_test.go @@ -0,0 +1,218 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import ( + "testing" + + "github.com/google/btree" + "github.com/stretchr/testify/require" +) + +func flatten(tree *btree.BTreeG[*interval]) []*interval { + intervals := make([]*interval, 0, tree.Len()) + tree.Ascend(func(item *interval) bool { + intervals = append(intervals, item) + return true + }) + return intervals +} + +func newTree(intervals []*interval) *Tree { + tree := NewTree() + for _, toAdd := range intervals { + for i := toAdd.lowerBound; i <= toAdd.upperBound; i++ { + tree.Add(i) + } + } + return tree +} + +func TestTreeAdd(t *testing.T) { + tests := []struct { + name string + toAdd []*interval + expected []*interval + }{ + { + name: "single addition", + toAdd: []*interval{ + { + lowerBound: 10, + upperBound: 10, + }, + }, + expected: []*interval{ + { + lowerBound: 10, + upperBound: 10, + }, + }, + }, + { + name: "extend above", + toAdd: []*interval{ + { + lowerBound: 10, + upperBound: 11, + }, + }, + expected: []*interval{ + { + lowerBound: 10, + upperBound: 11, + }, + }, + }, + { + name: "extend below", + toAdd: []*interval{ + { + lowerBound: 11, + upperBound: 11, + }, + { + lowerBound: 10, + upperBound: 10, + }, + }, + expected: []*interval{ + { + lowerBound: 10, + upperBound: 11, + }, + }, + }, + { + name: "merge", + toAdd: []*interval{ + { + lowerBound: 10, + upperBound: 10, + }, + { + lowerBound: 12, + upperBound: 12, + }, + { + lowerBound: 11, + upperBound: 11, + }, + }, + expected: []*interval{ + { + lowerBound: 10, + upperBound: 12, + }, + }, + }, + { + name: "ignore duplicate", + toAdd: []*interval{ + { + lowerBound: 10, + upperBound: 11, + }, + { + lowerBound: 11, + upperBound: 11, + }, + }, + expected: []*interval{ + { + lowerBound: 10, + upperBound: 11, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tree := newTree(test.toAdd) + require.Equal(t, test.expected, flatten(tree.knownBlocks)) + }) + } +} + +func TestTreeContains(t *testing.T) { + tests := []struct { + name string + tree []*interval + height uint64 + expected bool + }{ + { + name: "below", + tree: []*interval{ + { + lowerBound: 10, + upperBound: 10, + }, + }, + height: 9, + expected: false, + }, + { + name: "above", + tree: []*interval{ + { + lowerBound: 10, + upperBound: 10, + }, + }, + height: 11, + expected: false, + }, + { + name: "equal both", + tree: []*interval{ + { + lowerBound: 10, + upperBound: 10, + }, + }, + height: 10, + expected: true, + }, + { + name: "equal lower", + tree: []*interval{ + { + lowerBound: 10, + upperBound: 11, + }, + }, + height: 10, + expected: true, + }, + { + name: "equal upper", + tree: []*interval{ + { + lowerBound: 9, + upperBound: 10, + }, + }, + height: 10, + expected: true, + }, + { + name: "inside", + tree: []*interval{ + { + lowerBound: 9, + upperBound: 11, + }, + }, + height: 10, + expected: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tree := newTree(test.tree) + require.Equal(t, test.expected, tree.Contains(test.height)) + }) + } +} From 63efd3bdd3d25eb4a6f7f7db35e29d97413f9443 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 20 Feb 2024 18:13:02 -0500 Subject: [PATCH 02/68] Persist intervals --- .../engine/snowman/bootstrap/interval/tree.go | 34 ++++++++++++++++--- .../snowman/bootstrap/interval/tree_test.go | 27 +++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/tree.go b/snow/engine/snowman/bootstrap/interval/tree.go index 680c3f3dcabe..49a1329863d1 100644 --- a/snow/engine/snowman/bootstrap/interval/tree.go +++ b/snow/engine/snowman/bootstrap/interval/tree.go @@ -5,20 +5,34 @@ package interval import ( "github.com/google/btree" + + "github.com/ava-labs/avalanchego/database" ) +const treeDegree = 2 + type Tree struct { + db database.Database knownBlocks *btree.BTreeG[*interval] } -func NewTree() *Tree { - knownBlocks := btree.NewG(2, (*interval).Less) +func NewTree(db database.Database) (*Tree, error) { + intervals, err := GetIntervals(db) + if err != nil { + return nil, err + } + + knownBlocks := btree.NewG(treeDegree, (*interval).Less) + for _, i := range intervals { + knownBlocks.ReplaceOrInsert(i) + } return &Tree{ + db: db, knownBlocks: knownBlocks, - } + }, nil } -func (t *Tree) Add(height uint64) { +func (t *Tree) Add(height uint64) error { var ( newInterval = &interval{ lowerBound: height, @@ -33,7 +47,7 @@ func (t *Tree) Add(height uint64) { }) if upper.Contains(height) { // height is already in the tree - return + return nil } t.knownBlocks.DescendLessOrEqual(newInterval, func(item *interval) bool { @@ -48,16 +62,26 @@ func (t *Tree) Add(height uint64) { switch { case adjacentToLowerBound && adjacentToUpperBound: // the upper and lower ranges should be merged + if err := DeleteInterval(t.db, lower.upperBound); err != nil { + return err + } upper.lowerBound = lower.lowerBound t.knownBlocks.Delete(lower) + return PutInterval(t.db, upper.upperBound, lower.lowerBound) case adjacentToLowerBound: // the upper range should be extended by one on the lower side upper.lowerBound = height + return PutInterval(t.db, upper.upperBound, height) case adjacentToUpperBound: // the lower range should be extended by one on the upper side + if err := DeleteInterval(t.db, lower.upperBound); err != nil { + return err + } lower.upperBound = height + return PutInterval(t.db, height, lower.lowerBound) default: t.knownBlocks.ReplaceOrInsert(newInterval) + return PutInterval(t.db, height, height) } } diff --git a/snow/engine/snowman/bootstrap/interval/tree_test.go b/snow/engine/snowman/bootstrap/interval/tree_test.go index fa9a68a911a3..d0f94aab56cf 100644 --- a/snow/engine/snowman/bootstrap/interval/tree_test.go +++ b/snow/engine/snowman/bootstrap/interval/tree_test.go @@ -8,6 +8,9 @@ import ( "github.com/google/btree" "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" ) func flatten(tree *btree.BTreeG[*interval]) []*interval { @@ -19,11 +22,13 @@ func flatten(tree *btree.BTreeG[*interval]) []*interval { return intervals } -func newTree(intervals []*interval) *Tree { - tree := NewTree() +func newTree(require *require.Assertions, db database.Database, intervals []*interval) *Tree { + tree, err := NewTree(db) + require.NoError(err) + for _, toAdd := range intervals { for i := toAdd.lowerBound; i <= toAdd.upperBound; i++ { - tree.Add(i) + require.NoError(tree.Add(i)) } } return tree @@ -129,8 +134,14 @@ func TestTreeAdd(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - tree := newTree(test.toAdd) - require.Equal(t, test.expected, flatten(tree.knownBlocks)) + require := require.New(t) + + db := memdb.New() + treeFromAdditions := newTree(require, db, test.toAdd) + require.Equal(test.expected, flatten(treeFromAdditions.knownBlocks)) + + treeFromDB := newTree(require, db, nil) + require.Equal(test.expected, flatten(treeFromDB.knownBlocks)) }) } } @@ -211,8 +222,10 @@ func TestTreeContains(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - tree := newTree(test.tree) - require.Equal(t, test.expected, tree.Contains(test.height)) + require := require.New(t) + + tree := newTree(require, memdb.New(), test.tree) + require.Equal(test.expected, tree.Contains(test.height)) }) } } From 2454c460c0cd8cc1ad5448964e3ca258598834cc Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 20 Feb 2024 18:30:33 -0500 Subject: [PATCH 03/68] rename --- .../engine/snowman/bootstrap/interval/tree.go | 22 +++++++++---------- .../snowman/bootstrap/interval/tree_test.go | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/tree.go b/snow/engine/snowman/bootstrap/interval/tree.go index 49a1329863d1..945f84ceaafe 100644 --- a/snow/engine/snowman/bootstrap/interval/tree.go +++ b/snow/engine/snowman/bootstrap/interval/tree.go @@ -12,8 +12,8 @@ import ( const treeDegree = 2 type Tree struct { - db database.Database - knownBlocks *btree.BTreeG[*interval] + db database.Database + knownHeights *btree.BTreeG[*interval] } func NewTree(db database.Database) (*Tree, error) { @@ -22,13 +22,13 @@ func NewTree(db database.Database) (*Tree, error) { return nil, err } - knownBlocks := btree.NewG(treeDegree, (*interval).Less) + knownHeights := btree.NewG(treeDegree, (*interval).Less) for _, i := range intervals { - knownBlocks.ReplaceOrInsert(i) + knownHeights.ReplaceOrInsert(i) } return &Tree{ - db: db, - knownBlocks: knownBlocks, + db: db, + knownHeights: knownHeights, }, nil } @@ -41,7 +41,7 @@ func (t *Tree) Add(height uint64) error { upper *interval lower *interval ) - t.knownBlocks.AscendGreaterOrEqual(newInterval, func(item *interval) bool { + t.knownHeights.AscendGreaterOrEqual(newInterval, func(item *interval) bool { upper = item return false }) @@ -50,7 +50,7 @@ func (t *Tree) Add(height uint64) error { return nil } - t.knownBlocks.DescendLessOrEqual(newInterval, func(item *interval) bool { + t.knownHeights.DescendLessOrEqual(newInterval, func(item *interval) bool { lower = item return false }) @@ -66,7 +66,7 @@ func (t *Tree) Add(height uint64) error { return err } upper.lowerBound = lower.lowerBound - t.knownBlocks.Delete(lower) + t.knownHeights.Delete(lower) return PutInterval(t.db, upper.upperBound, lower.lowerBound) case adjacentToLowerBound: // the upper range should be extended by one on the lower side @@ -80,7 +80,7 @@ func (t *Tree) Add(height uint64) error { lower.upperBound = height return PutInterval(t.db, height, lower.lowerBound) default: - t.knownBlocks.ReplaceOrInsert(newInterval) + t.knownHeights.ReplaceOrInsert(newInterval) return PutInterval(t.db, height, height) } } @@ -93,7 +93,7 @@ func (t *Tree) Contains(height uint64) bool { } higher *interval ) - t.knownBlocks.AscendGreaterOrEqual(i, func(item *interval) bool { + t.knownHeights.AscendGreaterOrEqual(i, func(item *interval) bool { higher = item return false }) diff --git a/snow/engine/snowman/bootstrap/interval/tree_test.go b/snow/engine/snowman/bootstrap/interval/tree_test.go index d0f94aab56cf..5d8bad92a7cf 100644 --- a/snow/engine/snowman/bootstrap/interval/tree_test.go +++ b/snow/engine/snowman/bootstrap/interval/tree_test.go @@ -138,10 +138,10 @@ func TestTreeAdd(t *testing.T) { db := memdb.New() treeFromAdditions := newTree(require, db, test.toAdd) - require.Equal(test.expected, flatten(treeFromAdditions.knownBlocks)) + require.Equal(test.expected, flatten(treeFromAdditions.knownHeights)) treeFromDB := newTree(require, db, nil) - require.Equal(test.expected, flatten(treeFromDB.knownBlocks)) + require.Equal(test.expected, flatten(treeFromDB.knownHeights)) }) } } From 0efa5eba8f87c9f45f7b4874e96bab55e898b9c6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 20 Feb 2024 19:44:13 -0500 Subject: [PATCH 04/68] Add block execution --- .../snowman/bootstrap/interval/blocks.go | 119 ++++++++ .../snowman/bootstrap/interval/interval.go | 22 +- .../snowman/bootstrap/interval/state.go | 17 +- .../engine/snowman/bootstrap/interval/tree.go | 96 +++++-- .../snowman/bootstrap/interval/tree_test.go | 267 +++++++++++++----- 5 files changed, 410 insertions(+), 111 deletions(-) create mode 100644 snow/engine/snowman/bootstrap/interval/blocks.go diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go new file mode 100644 index 000000000000..309864396c6b --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -0,0 +1,119 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import ( + "context" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/utils/set" +) + +type Parser interface { + ParseBlock(context.Context, []byte) (snowman.Block, error) +} + +func GetMissingBlockIDs( + ctx context.Context, + parser Parser, + tree *Tree, + lastAcceptedHeight uint64, +) (set.Set[ids.ID], error) { + var ( + missingBlocks set.Set[ids.ID] + intervals = tree.Flatten() + lastHeightToFetch = lastAcceptedHeight + 1 + ) + for _, i := range intervals { + if i.LowerBound <= lastHeightToFetch { + continue + } + + blkBytes, err := GetBlock(tree.db, i.LowerBound) + if err != nil { + return nil, err + } + + blk, err := parser.ParseBlock(ctx, blkBytes) + if err != nil { + return nil, err + } + + parentID := blk.Parent() + missingBlocks.Add(parentID) + } + return missingBlocks, nil +} + +// Add the block to the tree and return if the parent block should be fetched. +func Add(tree *Tree, lastAcceptedHeight uint64, blk snowman.Block) (bool, error) { + var ( + height = blk.Height() + lastHeightToFetch = lastAcceptedHeight + 1 + ) + if height < lastHeightToFetch { + return false, nil + } + if tree.Contains(height) { + return false, nil + } + + blkBytes := blk.Bytes() + if err := PutBlock(tree.db, height, blkBytes); err != nil { + return false, err + } + + if err := tree.Add(height); err != nil { + return false, err + } + + if height == lastHeightToFetch { + return false, nil + } + + parentHeight := height - 1 + return !tree.Contains(parentHeight), nil +} + +func Execute( + ctx context.Context, + parser Parser, + tree *Tree, + lastAcceptedHeight uint64, +) error { + it := tree.db.NewIteratorWithPrefix(blockPrefix) + defer func() { + it.Release() + }() + + for it.Next() { + blkBytes := it.Value() + blk, err := parser.ParseBlock(ctx, blkBytes) + if err != nil { + return err + } + + height := blk.Height() + if err := DeleteBlock(tree.db, height); err != nil { + return err + } + + if err := tree.Remove(height); err != nil { + return err + } + + if height <= lastAcceptedHeight { + continue + } + + if err := blk.Verify(ctx); err != nil { + return err + } + if err := blk.Accept(ctx); err != nil { + return err + } + } + return it.Error() +} diff --git a/snow/engine/snowman/bootstrap/interval/interval.go b/snow/engine/snowman/bootstrap/interval/interval.go index d83d0c6143d2..a79e3ce12369 100644 --- a/snow/engine/snowman/bootstrap/interval/interval.go +++ b/snow/engine/snowman/bootstrap/interval/interval.go @@ -3,25 +3,25 @@ package interval -type interval struct { - lowerBound uint64 - upperBound uint64 +type Interval struct { + LowerBound uint64 + UpperBound uint64 } -func (i *interval) Less(other *interval) bool { - return i.upperBound < other.upperBound +func (i *Interval) Less(other *Interval) bool { + return i.UpperBound < other.UpperBound } -func (i *interval) Contains(height uint64) bool { - return i != nil && i.lowerBound <= height && height <= i.upperBound +func (i *Interval) Contains(height uint64) bool { + return i != nil && i.LowerBound <= height && height <= i.UpperBound } // AdjacentToUpperBound returns true if height is 1 greater than upperBound. -func (i *interval) AdjacentToUpperBound(height uint64) bool { - return i != nil && i.upperBound+1 == height +func (i *Interval) AdjacentToUpperBound(height uint64) bool { + return i != nil && i.UpperBound+1 == height } // AdjacentToLowerBound returns true if height is 1 less than lowerBound. -func (i *interval) AdjacentToLowerBound(height uint64) bool { - return i != nil && height+1 == i.lowerBound +func (i *Interval) AdjacentToLowerBound(height uint64) bool { + return i != nil && height+1 == i.LowerBound } diff --git a/snow/engine/snowman/bootstrap/interval/state.go b/snow/engine/snowman/bootstrap/interval/state.go index 0d5974a04e3e..f0f47e28b6d8 100644 --- a/snow/engine/snowman/bootstrap/interval/state.go +++ b/snow/engine/snowman/bootstrap/interval/state.go @@ -15,11 +15,11 @@ var ( blockPrefix = []byte{blockPrefixByte} ) -func GetIntervals(db database.Iteratee) ([]*interval, error) { +func GetIntervals(db database.Iteratee) ([]*Interval, error) { it := db.NewIteratorWithPrefix(rangePrefix) defer it.Release() - var intervals []*interval + var intervals []*Interval for it.Next() { dbKey := it.Key() rangeKey := dbKey[len(rangePrefix):] @@ -34,9 +34,9 @@ func GetIntervals(db database.Iteratee) ([]*interval, error) { return nil, err } - intervals = append(intervals, &interval{ - lowerBound: lowerBound, - upperBound: upperBound, + intervals = append(intervals, &Interval{ + LowerBound: lowerBound, + UpperBound: upperBound, }) } return intervals, it.Error() @@ -58,6 +58,13 @@ func DeleteInterval(db database.KeyValueDeleter, upperBound uint64) error { ) } +func GetBlock(db database.KeyValueReader, height uint64) ([]byte, error) { + blockKey := database.PackUInt64(height) + return db.Get( + append(blockPrefix, blockKey...), + ) +} + func PutBlock(db database.KeyValueWriter, height uint64, bytes []byte) error { blockKey := database.PackUInt64(height) return db.Put( diff --git a/snow/engine/snowman/bootstrap/interval/tree.go b/snow/engine/snowman/bootstrap/interval/tree.go index 945f84ceaafe..42057aad7f6b 100644 --- a/snow/engine/snowman/bootstrap/interval/tree.go +++ b/snow/engine/snowman/bootstrap/interval/tree.go @@ -13,7 +13,7 @@ const treeDegree = 2 type Tree struct { db database.Database - knownHeights *btree.BTreeG[*interval] + knownHeights *btree.BTreeG[*Interval] } func NewTree(db database.Database) (*Tree, error) { @@ -22,7 +22,7 @@ func NewTree(db database.Database) (*Tree, error) { return nil, err } - knownHeights := btree.NewG(treeDegree, (*interval).Less) + knownHeights := btree.NewG(treeDegree, (*Interval).Less) for _, i := range intervals { knownHeights.ReplaceOrInsert(i) } @@ -34,14 +34,14 @@ func NewTree(db database.Database) (*Tree, error) { func (t *Tree) Add(height uint64) error { var ( - newInterval = &interval{ - lowerBound: height, - upperBound: height, + newInterval = &Interval{ + LowerBound: height, + UpperBound: height, } - upper *interval - lower *interval + upper *Interval + lower *Interval ) - t.knownHeights.AscendGreaterOrEqual(newInterval, func(item *interval) bool { + t.knownHeights.AscendGreaterOrEqual(newInterval, func(item *Interval) bool { upper = item return false }) @@ -50,7 +50,7 @@ func (t *Tree) Add(height uint64) error { return nil } - t.knownHeights.DescendLessOrEqual(newInterval, func(item *interval) bool { + t.knownHeights.DescendLessOrEqual(newInterval, func(item *Interval) bool { lower = item return false }) @@ -62,40 +62,92 @@ func (t *Tree) Add(height uint64) error { switch { case adjacentToLowerBound && adjacentToUpperBound: // the upper and lower ranges should be merged - if err := DeleteInterval(t.db, lower.upperBound); err != nil { + if err := DeleteInterval(t.db, lower.UpperBound); err != nil { return err } - upper.lowerBound = lower.lowerBound + upper.LowerBound = lower.LowerBound t.knownHeights.Delete(lower) - return PutInterval(t.db, upper.upperBound, lower.lowerBound) + return PutInterval(t.db, upper.UpperBound, lower.LowerBound) case adjacentToLowerBound: // the upper range should be extended by one on the lower side - upper.lowerBound = height - return PutInterval(t.db, upper.upperBound, height) + upper.LowerBound = height + return PutInterval(t.db, upper.UpperBound, height) case adjacentToUpperBound: // the lower range should be extended by one on the upper side - if err := DeleteInterval(t.db, lower.upperBound); err != nil { + if err := DeleteInterval(t.db, lower.UpperBound); err != nil { return err } - lower.upperBound = height - return PutInterval(t.db, height, lower.lowerBound) + lower.UpperBound = height + return PutInterval(t.db, height, lower.LowerBound) default: t.knownHeights.ReplaceOrInsert(newInterval) return PutInterval(t.db, height, height) } } +func (t *Tree) Remove(height uint64) error { + var ( + newInterval = &Interval{ + LowerBound: height, + UpperBound: height, + } + higher *Interval + ) + t.knownHeights.AscendGreaterOrEqual(newInterval, func(item *Interval) bool { + higher = item + return false + }) + if !higher.Contains(height) { + // height isn't in the tree + return nil + } + + switch { + case higher.LowerBound == higher.UpperBound: + t.knownHeights.Delete(higher) + return DeleteInterval(t.db, higher.UpperBound) + case higher.LowerBound == height: + higher.LowerBound++ + return PutInterval(t.db, higher.UpperBound, higher.LowerBound) + case higher.UpperBound == height: + if err := DeleteInterval(t.db, higher.UpperBound); err != nil { + return err + } + higher.UpperBound-- + return PutInterval(t.db, higher.UpperBound, higher.LowerBound) + default: + newInterval.LowerBound = higher.LowerBound + newInterval.UpperBound = height - 1 + t.knownHeights.ReplaceOrInsert(newInterval) + if err := PutInterval(t.db, newInterval.UpperBound, newInterval.LowerBound); err != nil { + return err + } + + higher.LowerBound = height + 1 + return PutInterval(t.db, higher.UpperBound, higher.LowerBound) + } +} + func (t *Tree) Contains(height uint64) bool { var ( - i = &interval{ - lowerBound: height, - upperBound: height, + i = &Interval{ + LowerBound: height, + UpperBound: height, } - higher *interval + higher *Interval ) - t.knownHeights.AscendGreaterOrEqual(i, func(item *interval) bool { + t.knownHeights.AscendGreaterOrEqual(i, func(item *Interval) bool { higher = item return false }) return higher.Contains(height) } + +func (t *Tree) Flatten() []*Interval { + intervals := make([]*Interval, 0, t.knownHeights.Len()) + t.knownHeights.Ascend(func(item *Interval) bool { + intervals = append(intervals, item) + return true + }) + return intervals +} diff --git a/snow/engine/snowman/bootstrap/interval/tree_test.go b/snow/engine/snowman/bootstrap/interval/tree_test.go index 5d8bad92a7cf..6822e9c62780 100644 --- a/snow/engine/snowman/bootstrap/interval/tree_test.go +++ b/snow/engine/snowman/bootstrap/interval/tree_test.go @@ -6,28 +6,18 @@ package interval import ( "testing" - "github.com/google/btree" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" ) -func flatten(tree *btree.BTreeG[*interval]) []*interval { - intervals := make([]*interval, 0, tree.Len()) - tree.Ascend(func(item *interval) bool { - intervals = append(intervals, item) - return true - }) - return intervals -} - -func newTree(require *require.Assertions, db database.Database, intervals []*interval) *Tree { +func newTree(require *require.Assertions, db database.Database, intervals []*Interval) *Tree { tree, err := NewTree(db) require.NoError(err) for _, toAdd := range intervals { - for i := toAdd.lowerBound; i <= toAdd.upperBound; i++ { + for i := toAdd.LowerBound; i <= toAdd.UpperBound; i++ { require.NoError(tree.Add(i)) } } @@ -37,97 +27,97 @@ func newTree(require *require.Assertions, db database.Database, intervals []*int func TestTreeAdd(t *testing.T) { tests := []struct { name string - toAdd []*interval - expected []*interval + toAdd []*Interval + expected []*Interval }{ { name: "single addition", - toAdd: []*interval{ + toAdd: []*Interval{ { - lowerBound: 10, - upperBound: 10, + LowerBound: 10, + UpperBound: 10, }, }, - expected: []*interval{ + expected: []*Interval{ { - lowerBound: 10, - upperBound: 10, + LowerBound: 10, + UpperBound: 10, }, }, }, { name: "extend above", - toAdd: []*interval{ + toAdd: []*Interval{ { - lowerBound: 10, - upperBound: 11, + LowerBound: 10, + UpperBound: 11, }, }, - expected: []*interval{ + expected: []*Interval{ { - lowerBound: 10, - upperBound: 11, + LowerBound: 10, + UpperBound: 11, }, }, }, { name: "extend below", - toAdd: []*interval{ + toAdd: []*Interval{ { - lowerBound: 11, - upperBound: 11, + LowerBound: 11, + UpperBound: 11, }, { - lowerBound: 10, - upperBound: 10, + LowerBound: 10, + UpperBound: 10, }, }, - expected: []*interval{ + expected: []*Interval{ { - lowerBound: 10, - upperBound: 11, + LowerBound: 10, + UpperBound: 11, }, }, }, { name: "merge", - toAdd: []*interval{ + toAdd: []*Interval{ { - lowerBound: 10, - upperBound: 10, + LowerBound: 10, + UpperBound: 10, }, { - lowerBound: 12, - upperBound: 12, + LowerBound: 12, + UpperBound: 12, }, { - lowerBound: 11, - upperBound: 11, + LowerBound: 11, + UpperBound: 11, }, }, - expected: []*interval{ + expected: []*Interval{ { - lowerBound: 10, - upperBound: 12, + LowerBound: 10, + UpperBound: 12, }, }, }, { name: "ignore duplicate", - toAdd: []*interval{ + toAdd: []*Interval{ { - lowerBound: 10, - upperBound: 11, + LowerBound: 10, + UpperBound: 11, }, { - lowerBound: 11, - upperBound: 11, + LowerBound: 11, + UpperBound: 11, }, }, - expected: []*interval{ + expected: []*Interval{ { - lowerBound: 10, - upperBound: 11, + LowerBound: 10, + UpperBound: 11, }, }, }, @@ -138,10 +128,141 @@ func TestTreeAdd(t *testing.T) { db := memdb.New() treeFromAdditions := newTree(require, db, test.toAdd) - require.Equal(test.expected, flatten(treeFromAdditions.knownHeights)) + require.Equal(test.expected, treeFromAdditions.Flatten()) + + treeFromDB := newTree(require, db, nil) + require.Equal(test.expected, treeFromDB.Flatten()) + }) + } +} + +func TestTreeRemove(t *testing.T) { + tests := []struct { + name string + toAdd []*Interval + toRemove []*Interval + expected []*Interval + }{ + { + name: "single removal", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expected: []*Interval{}, + }, + { + name: "reduce above", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + }, + { + name: "reduce below", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expected: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + }, + }, + { + name: "split", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 12, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + { + LowerBound: 12, + UpperBound: 12, + }, + }, + }, + { + name: "ignore missing", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + db := memdb.New() + treeFromModifications := newTree(require, db, test.toAdd) + for _, toRemove := range test.toRemove { + for i := toRemove.LowerBound; i <= toRemove.UpperBound; i++ { + require.NoError(treeFromModifications.Remove(i)) + } + } + require.Equal(test.expected, treeFromModifications.Flatten()) treeFromDB := newTree(require, db, nil) - require.Equal(test.expected, flatten(treeFromDB.knownHeights)) + require.Equal(test.expected, treeFromDB.Flatten()) }) } } @@ -149,16 +270,16 @@ func TestTreeAdd(t *testing.T) { func TestTreeContains(t *testing.T) { tests := []struct { name string - tree []*interval + tree []*Interval height uint64 expected bool }{ { name: "below", - tree: []*interval{ + tree: []*Interval{ { - lowerBound: 10, - upperBound: 10, + LowerBound: 10, + UpperBound: 10, }, }, height: 9, @@ -166,10 +287,10 @@ func TestTreeContains(t *testing.T) { }, { name: "above", - tree: []*interval{ + tree: []*Interval{ { - lowerBound: 10, - upperBound: 10, + LowerBound: 10, + UpperBound: 10, }, }, height: 11, @@ -177,10 +298,10 @@ func TestTreeContains(t *testing.T) { }, { name: "equal both", - tree: []*interval{ + tree: []*Interval{ { - lowerBound: 10, - upperBound: 10, + LowerBound: 10, + UpperBound: 10, }, }, height: 10, @@ -188,10 +309,10 @@ func TestTreeContains(t *testing.T) { }, { name: "equal lower", - tree: []*interval{ + tree: []*Interval{ { - lowerBound: 10, - upperBound: 11, + LowerBound: 10, + UpperBound: 11, }, }, height: 10, @@ -199,10 +320,10 @@ func TestTreeContains(t *testing.T) { }, { name: "equal upper", - tree: []*interval{ + tree: []*Interval{ { - lowerBound: 9, - upperBound: 10, + LowerBound: 9, + UpperBound: 10, }, }, height: 10, @@ -210,10 +331,10 @@ func TestTreeContains(t *testing.T) { }, { name: "inside", - tree: []*interval{ + tree: []*Interval{ { - lowerBound: 9, - upperBound: 11, + LowerBound: 9, + UpperBound: 11, }, }, height: 10, From 6a65a2a33500e67565663cd0c3519f521d44e907 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 20 Feb 2024 21:32:18 -0500 Subject: [PATCH 05/68] Add length + TODOs --- .../snowman/bootstrap/interval/blocks.go | 3 ++ .../engine/snowman/bootstrap/interval/tree.go | 25 ++++++++++++--- .../snowman/bootstrap/interval/tree_test.go | 32 ++++++++++++++----- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 309864396c6b..ad4ce06d9e0c 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -88,6 +88,9 @@ func Execute( it.Release() }() + // TODO: Periodically release the iterator here + // TODO: Periodically log progress + // TODO: Add metrics for it.Next() { blkBytes := it.Value() blk, err := parser.ParseBlock(ctx, blkBytes) diff --git a/snow/engine/snowman/bootstrap/interval/tree.go b/snow/engine/snowman/bootstrap/interval/tree.go index 42057aad7f6b..38dd52f54718 100644 --- a/snow/engine/snowman/bootstrap/interval/tree.go +++ b/snow/engine/snowman/bootstrap/interval/tree.go @@ -12,8 +12,9 @@ import ( const treeDegree = 2 type Tree struct { - db database.Database - knownHeights *btree.BTreeG[*Interval] + db database.Database + knownHeights *btree.BTreeG[*Interval] + numKnownHeights uint64 } func NewTree(db database.Database) (*Tree, error) { @@ -22,13 +23,18 @@ func NewTree(db database.Database) (*Tree, error) { return nil, err } - knownHeights := btree.NewG(treeDegree, (*Interval).Less) + var ( + knownHeights = btree.NewG(treeDegree, (*Interval).Less) + numKnownHeights uint64 + ) for _, i := range intervals { knownHeights.ReplaceOrInsert(i) + numKnownHeights += i.UpperBound - i.LowerBound + 1 } return &Tree{ - db: db, - knownHeights: knownHeights, + db: db, + knownHeights: knownHeights, + numKnownHeights: numKnownHeights, }, nil } @@ -55,6 +61,8 @@ func (t *Tree) Add(height uint64) error { return false }) + t.numKnownHeights++ + var ( adjacentToLowerBound = upper.AdjacentToLowerBound(height) adjacentToUpperBound = lower.AdjacentToUpperBound(height) @@ -102,6 +110,8 @@ func (t *Tree) Remove(height uint64) error { return nil } + t.numKnownHeights-- + switch { case higher.LowerBound == higher.UpperBound: t.knownHeights.Delete(higher) @@ -151,3 +161,8 @@ func (t *Tree) Flatten() []*Interval { }) return intervals } + +// Len returns the number of heights in the tree; not the number of intervals. +func (t *Tree) Len() uint64 { + return t.numKnownHeights +} diff --git a/snow/engine/snowman/bootstrap/interval/tree_test.go b/snow/engine/snowman/bootstrap/interval/tree_test.go index 6822e9c62780..250943434843 100644 --- a/snow/engine/snowman/bootstrap/interval/tree_test.go +++ b/snow/engine/snowman/bootstrap/interval/tree_test.go @@ -26,9 +26,10 @@ func newTree(require *require.Assertions, db database.Database, intervals []*Int func TestTreeAdd(t *testing.T) { tests := []struct { - name string - toAdd []*Interval - expected []*Interval + name string + toAdd []*Interval + expected []*Interval + expectedLen uint64 }{ { name: "single addition", @@ -44,6 +45,7 @@ func TestTreeAdd(t *testing.T) { UpperBound: 10, }, }, + expectedLen: 1, }, { name: "extend above", @@ -59,6 +61,7 @@ func TestTreeAdd(t *testing.T) { UpperBound: 11, }, }, + expectedLen: 2, }, { name: "extend below", @@ -78,6 +81,7 @@ func TestTreeAdd(t *testing.T) { UpperBound: 11, }, }, + expectedLen: 2, }, { name: "merge", @@ -101,6 +105,7 @@ func TestTreeAdd(t *testing.T) { UpperBound: 12, }, }, + expectedLen: 3, }, { name: "ignore duplicate", @@ -120,6 +125,7 @@ func TestTreeAdd(t *testing.T) { UpperBound: 11, }, }, + expectedLen: 2, }, } for _, test := range tests { @@ -129,19 +135,22 @@ func TestTreeAdd(t *testing.T) { db := memdb.New() treeFromAdditions := newTree(require, db, test.toAdd) require.Equal(test.expected, treeFromAdditions.Flatten()) + require.Equal(test.expectedLen, treeFromAdditions.Len()) treeFromDB := newTree(require, db, nil) require.Equal(test.expected, treeFromDB.Flatten()) + require.Equal(test.expectedLen, treeFromDB.Len()) }) } } func TestTreeRemove(t *testing.T) { tests := []struct { - name string - toAdd []*Interval - toRemove []*Interval - expected []*Interval + name string + toAdd []*Interval + toRemove []*Interval + expected []*Interval + expectedLen uint64 }{ { name: "single removal", @@ -157,7 +166,8 @@ func TestTreeRemove(t *testing.T) { UpperBound: 10, }, }, - expected: []*Interval{}, + expected: []*Interval{}, + expectedLen: 0, }, { name: "reduce above", @@ -179,6 +189,7 @@ func TestTreeRemove(t *testing.T) { UpperBound: 10, }, }, + expectedLen: 1, }, { name: "reduce below", @@ -200,6 +211,7 @@ func TestTreeRemove(t *testing.T) { UpperBound: 11, }, }, + expectedLen: 1, }, { name: "split", @@ -225,6 +237,7 @@ func TestTreeRemove(t *testing.T) { UpperBound: 12, }, }, + expectedLen: 2, }, { name: "ignore missing", @@ -246,6 +259,7 @@ func TestTreeRemove(t *testing.T) { UpperBound: 10, }, }, + expectedLen: 1, }, } for _, test := range tests { @@ -260,9 +274,11 @@ func TestTreeRemove(t *testing.T) { } } require.Equal(test.expected, treeFromModifications.Flatten()) + require.Equal(test.expectedLen, treeFromModifications.Len()) treeFromDB := newTree(require, db, nil) require.Equal(test.expected, treeFromDB.Flatten()) + require.Equal(test.expectedLen, treeFromDB.Len()) }) } } From f2234bd5b41b6520206f7e612c19d729259be600 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 20 Feb 2024 21:44:05 -0500 Subject: [PATCH 06/68] nit --- snow/engine/snowman/bootstrap/interval/blocks.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index ad4ce06d9e0c..7b08bcf58079 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -53,10 +53,7 @@ func Add(tree *Tree, lastAcceptedHeight uint64, blk snowman.Block) (bool, error) height = blk.Height() lastHeightToFetch = lastAcceptedHeight + 1 ) - if height < lastHeightToFetch { - return false, nil - } - if tree.Contains(height) { + if height < lastHeightToFetch || tree.Contains(height) { return false, nil } @@ -69,12 +66,7 @@ func Add(tree *Tree, lastAcceptedHeight uint64, blk snowman.Block) (bool, error) return false, err } - if height == lastHeightToFetch { - return false, nil - } - - parentHeight := height - 1 - return !tree.Contains(parentHeight), nil + return height != lastHeightToFetch && !tree.Contains(height-1), nil } func Execute( From 1f6323869ddd11f2a598b4b9531395e265c63e76 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 20 Feb 2024 21:47:50 -0500 Subject: [PATCH 07/68] error rather than panic --- snow/engine/snowman/bootstrap/interval/state.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/state.go b/snow/engine/snowman/bootstrap/interval/state.go index f0f47e28b6d8..320d5c97105f 100644 --- a/snow/engine/snowman/bootstrap/interval/state.go +++ b/snow/engine/snowman/bootstrap/interval/state.go @@ -3,16 +3,24 @@ package interval -import "github.com/ava-labs/avalanchego/database" +import ( + "errors" + + "github.com/ava-labs/avalanchego/database" +) const ( rangePrefixByte byte = iota blockPrefixByte + + prefixLen = 1 ) var ( rangePrefix = []byte{rangePrefixByte} blockPrefix = []byte{blockPrefixByte} + + errInvalidKeyLength = errors.New("invalid key length") ) func GetIntervals(db database.Iteratee) ([]*Interval, error) { @@ -22,7 +30,11 @@ func GetIntervals(db database.Iteratee) ([]*Interval, error) { var intervals []*Interval for it.Next() { dbKey := it.Key() - rangeKey := dbKey[len(rangePrefix):] + if len(dbKey) < prefixLen { + return nil, errInvalidKeyLength + } + + rangeKey := dbKey[prefixLen:] upperBound, err := database.ParseUInt64(rangeKey) if err != nil { return nil, err From 6947876c026fa34e449eea4c19c5a9253de6cf9f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 21 Feb 2024 10:52:58 -0500 Subject: [PATCH 08/68] Refactor database passing --- .../snowman/bootstrap/interval/blocks.go | 120 ++++++++++++++++-- .../engine/snowman/bootstrap/interval/tree.go | 32 +++-- .../snowman/bootstrap/interval/tree_test.go | 4 +- 3 files changed, 123 insertions(+), 33 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 7b08bcf58079..d6ab83382649 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -5,10 +5,22 @@ package interval import ( "context" + "time" + "go.uber.org/zap" + + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer" +) + +const ( + batchWritePeriod = 1 + iteratorReleasePeriod = 1024 + logPeriod = 5 * time.Second ) type Parser interface { @@ -17,6 +29,7 @@ type Parser interface { func GetMissingBlockIDs( ctx context.Context, + db database.KeyValueReader, parser Parser, tree *Tree, lastAcceptedHeight uint64, @@ -31,7 +44,7 @@ func GetMissingBlockIDs( continue } - blkBytes, err := GetBlock(tree.db, i.LowerBound) + blkBytes, err := GetBlock(db, i.LowerBound) if err != nil { return nil, err } @@ -48,7 +61,12 @@ func GetMissingBlockIDs( } // Add the block to the tree and return if the parent block should be fetched. -func Add(tree *Tree, lastAcceptedHeight uint64, blk snowman.Block) (bool, error) { +func Add( + db database.KeyValueWriterDeleter, + tree *Tree, + lastAcceptedHeight uint64, + blk snowman.Block, +) (bool, error) { var ( height = blk.Height() lastHeightToFetch = lastAcceptedHeight + 1 @@ -58,11 +76,11 @@ func Add(tree *Tree, lastAcceptedHeight uint64, blk snowman.Block) (bool, error) } blkBytes := blk.Bytes() - if err := PutBlock(tree.db, height, blkBytes); err != nil { + if err := PutBlock(db, height, blkBytes); err != nil { return false, err } - if err := tree.Add(height); err != nil { + if err := tree.Add(db, height); err != nil { return false, err } @@ -71,34 +89,97 @@ func Add(tree *Tree, lastAcceptedHeight uint64, blk snowman.Block) (bool, error) func Execute( ctx context.Context, + log logging.Logger, + db database.Database, parser Parser, tree *Tree, lastAcceptedHeight uint64, ) error { - it := tree.db.NewIteratorWithPrefix(blockPrefix) + var ( + batch = db.NewBatch() + processedSinceBatchWrite uint + writeBatch = func() error { + if processedSinceBatchWrite == 0 { + return nil + } + processedSinceBatchWrite %= batchWritePeriod + + if err := batch.Write(); err != nil { + return err + } + batch.Reset() + return nil + } + + iterator = db.NewIteratorWithPrefix(blockPrefix) + processedSinceIteratorRelease uint + + startTime = time.Now() + timeOfNextLog = startTime.Add(logPeriod) + totalNumberToProcess = tree.Len() + ) defer func() { - it.Release() + iterator.Release() }() - // TODO: Periodically release the iterator here - // TODO: Periodically log progress - // TODO: Add metrics - for it.Next() { - blkBytes := it.Value() + log.Info("executing blocks", + zap.Uint64("numToExecute", totalNumberToProcess), + ) + + for iterator.Next() { + blkBytes := iterator.Value() blk, err := parser.ParseBlock(ctx, blkBytes) if err != nil { return err } height := blk.Height() - if err := DeleteBlock(tree.db, height); err != nil { + if err := DeleteBlock(batch, height); err != nil { return err } - if err := tree.Remove(height); err != nil { + if err := tree.Remove(batch, height); err != nil { return err } + // Periodically write the batch to disk to avoid memory pressure. + processedSinceBatchWrite++ + if processedSinceBatchWrite >= batchWritePeriod { + if err := writeBatch(); err != nil { + return err + } + } + + // Periodically release and re-grab the database iterator to avoid + // keeping a reference to an old database revision. + processedSinceIteratorRelease++ + if processedSinceIteratorRelease >= iteratorReleasePeriod { + if err := iterator.Error(); err != nil { + return err + } + + // The batch must be written here to avoid re-processing a block. + if err := writeBatch(); err != nil { + return err + } + + iterator.Release() + iterator = db.NewIteratorWithPrefix(blockPrefix) + } + + now := time.Now() + if now.After(timeOfNextLog) { + numProcessed := totalNumberToProcess - tree.Len() + eta := timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) + + log.Info("executing blocks", + zap.Duration("eta", eta), + zap.Uint64("numExecuted", numProcessed), + zap.Uint64("numToExecute", totalNumberToProcess), + ) + timeOfNextLog = now.Add(logPeriod) + } + if height <= lastAcceptedHeight { continue } @@ -110,5 +191,16 @@ func Execute( return err } } - return it.Error() + if err := writeBatch(); err != nil { + return err + } + if err := iterator.Error(); err != nil { + return err + } + + log.Info("executed blocks", + zap.Uint64("numExecuted", totalNumberToProcess), + zap.Duration("duration", time.Since(startTime)), + ) + return nil } diff --git a/snow/engine/snowman/bootstrap/interval/tree.go b/snow/engine/snowman/bootstrap/interval/tree.go index 38dd52f54718..a4f07102f04a 100644 --- a/snow/engine/snowman/bootstrap/interval/tree.go +++ b/snow/engine/snowman/bootstrap/interval/tree.go @@ -12,12 +12,11 @@ import ( const treeDegree = 2 type Tree struct { - db database.Database knownHeights *btree.BTreeG[*Interval] numKnownHeights uint64 } -func NewTree(db database.Database) (*Tree, error) { +func NewTree(db database.Iteratee) (*Tree, error) { intervals, err := GetIntervals(db) if err != nil { return nil, err @@ -32,13 +31,12 @@ func NewTree(db database.Database) (*Tree, error) { numKnownHeights += i.UpperBound - i.LowerBound + 1 } return &Tree{ - db: db, knownHeights: knownHeights, numKnownHeights: numKnownHeights, }, nil } -func (t *Tree) Add(height uint64) error { +func (t *Tree) Add(db database.KeyValueWriterDeleter, height uint64) error { var ( newInterval = &Interval{ LowerBound: height, @@ -70,30 +68,30 @@ func (t *Tree) Add(height uint64) error { switch { case adjacentToLowerBound && adjacentToUpperBound: // the upper and lower ranges should be merged - if err := DeleteInterval(t.db, lower.UpperBound); err != nil { + if err := DeleteInterval(db, lower.UpperBound); err != nil { return err } upper.LowerBound = lower.LowerBound t.knownHeights.Delete(lower) - return PutInterval(t.db, upper.UpperBound, lower.LowerBound) + return PutInterval(db, upper.UpperBound, lower.LowerBound) case adjacentToLowerBound: // the upper range should be extended by one on the lower side upper.LowerBound = height - return PutInterval(t.db, upper.UpperBound, height) + return PutInterval(db, upper.UpperBound, height) case adjacentToUpperBound: // the lower range should be extended by one on the upper side - if err := DeleteInterval(t.db, lower.UpperBound); err != nil { + if err := DeleteInterval(db, lower.UpperBound); err != nil { return err } lower.UpperBound = height - return PutInterval(t.db, height, lower.LowerBound) + return PutInterval(db, height, lower.LowerBound) default: t.knownHeights.ReplaceOrInsert(newInterval) - return PutInterval(t.db, height, height) + return PutInterval(db, height, height) } } -func (t *Tree) Remove(height uint64) error { +func (t *Tree) Remove(db database.KeyValueWriterDeleter, height uint64) error { var ( newInterval = &Interval{ LowerBound: height, @@ -115,26 +113,26 @@ func (t *Tree) Remove(height uint64) error { switch { case higher.LowerBound == higher.UpperBound: t.knownHeights.Delete(higher) - return DeleteInterval(t.db, higher.UpperBound) + return DeleteInterval(db, higher.UpperBound) case higher.LowerBound == height: higher.LowerBound++ - return PutInterval(t.db, higher.UpperBound, higher.LowerBound) + return PutInterval(db, higher.UpperBound, higher.LowerBound) case higher.UpperBound == height: - if err := DeleteInterval(t.db, higher.UpperBound); err != nil { + if err := DeleteInterval(db, higher.UpperBound); err != nil { return err } higher.UpperBound-- - return PutInterval(t.db, higher.UpperBound, higher.LowerBound) + return PutInterval(db, higher.UpperBound, higher.LowerBound) default: newInterval.LowerBound = higher.LowerBound newInterval.UpperBound = height - 1 t.knownHeights.ReplaceOrInsert(newInterval) - if err := PutInterval(t.db, newInterval.UpperBound, newInterval.LowerBound); err != nil { + if err := PutInterval(db, newInterval.UpperBound, newInterval.LowerBound); err != nil { return err } higher.LowerBound = height + 1 - return PutInterval(t.db, higher.UpperBound, higher.LowerBound) + return PutInterval(db, higher.UpperBound, higher.LowerBound) } } diff --git a/snow/engine/snowman/bootstrap/interval/tree_test.go b/snow/engine/snowman/bootstrap/interval/tree_test.go index 250943434843..b0a13d5ed59c 100644 --- a/snow/engine/snowman/bootstrap/interval/tree_test.go +++ b/snow/engine/snowman/bootstrap/interval/tree_test.go @@ -18,7 +18,7 @@ func newTree(require *require.Assertions, db database.Database, intervals []*Int for _, toAdd := range intervals { for i := toAdd.LowerBound; i <= toAdd.UpperBound; i++ { - require.NoError(tree.Add(i)) + require.NoError(tree.Add(db, i)) } } return tree @@ -270,7 +270,7 @@ func TestTreeRemove(t *testing.T) { treeFromModifications := newTree(require, db, test.toAdd) for _, toRemove := range test.toRemove { for i := toRemove.LowerBound; i <= toRemove.UpperBound; i++ { - require.NoError(treeFromModifications.Remove(i)) + require.NoError(treeFromModifications.Remove(db, i)) } } require.Equal(test.expected, treeFromModifications.Flatten()) From be64296df381b980399f935816f91934a30aed6d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 14:35:30 -0400 Subject: [PATCH 09/68] Add interval tests --- .../snowman/bootstrap/interval/interval.go | 22 +- .../bootstrap/interval/interval_test.go | 255 ++++++++++++++++++ 2 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 snow/engine/snowman/bootstrap/interval/interval_test.go diff --git a/snow/engine/snowman/bootstrap/interval/interval.go b/snow/engine/snowman/bootstrap/interval/interval.go index a79e3ce12369..35ae260e446a 100644 --- a/snow/engine/snowman/bootstrap/interval/interval.go +++ b/snow/engine/snowman/bootstrap/interval/interval.go @@ -3,6 +3,8 @@ package interval +import "math" + type Interval struct { LowerBound uint64 UpperBound uint64 @@ -13,15 +15,21 @@ func (i *Interval) Less(other *Interval) bool { } func (i *Interval) Contains(height uint64) bool { - return i != nil && i.LowerBound <= height && height <= i.UpperBound -} - -// AdjacentToUpperBound returns true if height is 1 greater than upperBound. -func (i *Interval) AdjacentToUpperBound(height uint64) bool { - return i != nil && i.UpperBound+1 == height + return i != nil && + i.LowerBound <= height && + height <= i.UpperBound } // AdjacentToLowerBound returns true if height is 1 less than lowerBound. func (i *Interval) AdjacentToLowerBound(height uint64) bool { - return i != nil && height+1 == i.LowerBound + return i != nil && + height < math.MaxUint64 && + height+1 == i.LowerBound +} + +// AdjacentToUpperBound returns true if height is 1 greater than upperBound. +func (i *Interval) AdjacentToUpperBound(height uint64) bool { + return i != nil && + i.UpperBound < math.MaxUint64 && + i.UpperBound+1 == height } diff --git a/snow/engine/snowman/bootstrap/interval/interval_test.go b/snow/engine/snowman/bootstrap/interval/interval_test.go new file mode 100644 index 000000000000..2213302925fd --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/interval_test.go @@ -0,0 +1,255 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIntervalLess(t *testing.T) { + tests := []struct { + name string + left *Interval + right *Interval + expected bool + }{ + { + name: "less", + left: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + right: &Interval{ + LowerBound: 11, + UpperBound: 11, + }, + expected: true, + }, + { + name: "greater", + left: &Interval{ + LowerBound: 11, + UpperBound: 11, + }, + right: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + expected: false, + }, + { + name: "equal", + left: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + right: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + less := test.left.Less(test.right) + require.Equal(t, test.expected, less) + }) + } +} + +func TestIntervalContains(t *testing.T) { + tests := []struct { + name string + interval *Interval + height uint64 + expected bool + }{ + { + name: "nil does not contain anything", + interval: nil, + height: 10, + expected: false, + }, + { + name: "too low", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 9, + expected: false, + }, + { + name: "inside", + interval: &Interval{ + LowerBound: 9, + UpperBound: 11, + }, + height: 10, + expected: true, + }, + { + name: "equal", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 10, + expected: true, + }, + { + name: "too high", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 11, + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + contains := test.interval.Contains(test.height) + require.Equal(t, test.expected, contains) + }) + } +} + +func TestIntervalAdjacentToLowerBound(t *testing.T) { + tests := []struct { + name string + interval *Interval + height uint64 + expected bool + }{ + { + name: "nil is not adjacent to anything", + interval: nil, + height: 10, + expected: false, + }, + { + name: "too low", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 8, + expected: false, + }, + { + name: "equal", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 10, + expected: false, + }, + { + name: "adjacent to both", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 9, + expected: true, + }, + { + name: "adjacent to lower", + interval: &Interval{ + LowerBound: 10, + UpperBound: 11, + }, + height: 9, + expected: true, + }, + { + name: "check for overflow", + interval: &Interval{ + LowerBound: 0, + UpperBound: math.MaxUint64 - 1, + }, + height: math.MaxUint64, + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + adjacent := test.interval.AdjacentToLowerBound(test.height) + require.Equal(t, test.expected, adjacent) + }) + } +} + +func TestIntervalAdjacentToUpperBound(t *testing.T) { + tests := []struct { + name string + interval *Interval + height uint64 + expected bool + }{ + { + name: "nil is not adjacent to anything", + interval: nil, + height: 10, + expected: false, + }, + { + name: "too low", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 8, + expected: false, + }, + { + name: "equal", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 10, + expected: false, + }, + { + name: "adjacent to both", + interval: &Interval{ + LowerBound: 10, + UpperBound: 10, + }, + height: 11, + expected: true, + }, + { + name: "adjacent to higher", + interval: &Interval{ + LowerBound: 9, + UpperBound: 10, + }, + height: 11, + expected: true, + }, + { + name: "check for overflow", + interval: &Interval{ + LowerBound: 1, + UpperBound: math.MaxUint64, + }, + height: 0, + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + adjacent := test.interval.AdjacentToUpperBound(test.height) + require.Equal(t, test.expected, adjacent) + }) + } +} From 884f39c4d2765a75c2908f106c83de263c97867b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 14:46:42 -0400 Subject: [PATCH 10/68] Fix too frequent iterator releases and batch writes --- snow/engine/snowman/bootstrap/interval/blocks.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index d6ab83382649..e330c593e4ac 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -102,7 +102,7 @@ func Execute( if processedSinceBatchWrite == 0 { return nil } - processedSinceBatchWrite %= batchWritePeriod + processedSinceBatchWrite = 0 if err := batch.Write(); err != nil { return err @@ -163,6 +163,7 @@ func Execute( return err } + processedSinceIteratorRelease = 0 iterator.Release() iterator = db.NewIteratorWithPrefix(blockPrefix) } From e4e92cca72d8098d8c41b3bb7c206d12866c98a3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 16:10:10 -0400 Subject: [PATCH 11/68] nit + support cancellation --- .../snowman/bootstrap/interval/blocks.go | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index e330c593e4ac..6c6ba26b8b05 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -60,7 +60,8 @@ func GetMissingBlockIDs( return missingBlocks, nil } -// Add the block to the tree and return if the parent block should be fetched. +// Add the block to the tree and return if the parent block should be fetched, +// but wasn't desired before. func Add( db database.KeyValueWriterDeleter, tree *Tree, @@ -126,7 +127,7 @@ func Execute( zap.Uint64("numToExecute", totalNumberToProcess), ) - for iterator.Next() { + for ctx.Err() == nil && iterator.Next() { blkBytes := iterator.Value() blk, err := parser.ParseBlock(ctx, blkBytes) if err != nil { @@ -170,8 +171,10 @@ func Execute( now := time.Now() if now.After(timeOfNextLog) { - numProcessed := totalNumberToProcess - tree.Len() - eta := timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) + var ( + numProcessed = totalNumberToProcess - tree.Len() + eta = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) + ) log.Info("executing blocks", zap.Duration("eta", eta), @@ -199,9 +202,15 @@ func Execute( return err } + var ( + numProcessed = totalNumberToProcess - tree.Len() + err = ctx.Err() + ) log.Info("executed blocks", - zap.Uint64("numExecuted", totalNumberToProcess), + zap.Uint64("numExecuted", numProcessed), + zap.Uint64("numToExecute", totalNumberToProcess), zap.Duration("duration", time.Since(startTime)), + zap.Error(err), ) - return nil + return err } From 72314d1b2d33ed53bf4cba3d4191a37eae337383 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 16:32:33 -0400 Subject: [PATCH 12/68] Add invariant comment + tests --- .../snowman/bootstrap/interval/blocks.go | 1 + .../snowman/bootstrap/interval/blocks_test.go | 450 ++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 snow/engine/snowman/bootstrap/interval/blocks_test.go diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 6c6ba26b8b05..613814868bfa 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -88,6 +88,7 @@ func Add( return height != lastHeightToFetch && !tree.Contains(height-1), nil } +// Invariant: Execute assumes that GetMissingBlockIDs would return an empty set. func Execute( ctx context.Context, log logging.Logger, diff --git a/snow/engine/snowman/bootstrap/interval/blocks_test.go b/snow/engine/snowman/bootstrap/interval/blocks_test.go new file mode 100644 index 000000000000..48b269d9b1fc --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -0,0 +1,450 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" +) + +func TestGetMissingBlockIDs(t *testing.T) { + blocks := generateBlockchain(7) + parser := makeParser(blocks) + + db := memdb.New() + tree, err := NewTree(db) + require.NoError(t, err) + lastAcceptedHeight := uint64(1) + + t.Run("initially empty", func(t *testing.T) { + require := require.New(t) + + missing, err := GetMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Empty(missing) + }) + + t.Run("adding first block", func(t *testing.T) { + require := require.New(t) + + _, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[5], + ) + require.NoError(err) + + missing, err := GetMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Equal( + set.Of( + blocks[4].ID(), + ), + missing, + ) + }) + + t.Run("adding second block", func(t *testing.T) { + require := require.New(t) + + _, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[3], + ) + require.NoError(err) + + missing, err := GetMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Equal( + set.Of( + blocks[2].ID(), + blocks[4].ID(), + ), + missing, + ) + }) + + t.Run("adding last desired block", func(t *testing.T) { + require := require.New(t) + + _, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[2], + ) + require.NoError(err) + + missing, err := GetMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Equal( + set.Of( + blocks[4].ID(), + ), + missing, + ) + }) + + t.Run("adding block with known parent", func(t *testing.T) { + require := require.New(t) + + _, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[6], + ) + require.NoError(err) + + missing, err := GetMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Equal( + set.Of( + blocks[4].ID(), + ), + missing, + ) + }) +} + +func TestAdd(t *testing.T) { + blocks := generateBlockchain(7) + + db := memdb.New() + tree, err := NewTree(db) + require.NoError(t, err) + lastAcceptedHeight := uint64(1) + + t.Run("adding first block", func(t *testing.T) { + require := require.New(t) + + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[5], + ) + require.NoError(err) + require.True(newlyWantsParent) + require.Equal(uint64(1), tree.Len()) + + bytes, err := GetBlock(db, blocks[5].Height()) + require.NoError(err) + require.Equal(blocks[5].Bytes(), bytes) + }) + + t.Run("adding duplicate block", func(t *testing.T) { + require := require.New(t) + + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[5], + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(1), tree.Len()) + }) + + t.Run("adding second block", func(t *testing.T) { + require := require.New(t) + + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[3], + ) + require.NoError(err) + require.True(newlyWantsParent) + require.Equal(uint64(2), tree.Len()) + + bytes, err := GetBlock(db, blocks[3].Height()) + require.NoError(err) + require.Equal(blocks[3].Bytes(), bytes) + }) + + t.Run("adding last desired block", func(t *testing.T) { + require := require.New(t) + + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[2], + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(3), tree.Len()) + + bytes, err := GetBlock(db, blocks[2].Height()) + require.NoError(err) + require.Equal(blocks[2].Bytes(), bytes) + }) + + t.Run("adding undesired block", func(t *testing.T) { + require := require.New(t) + + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[1], + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(3), tree.Len()) + + _, err = GetBlock(db, blocks[1].Height()) + require.ErrorIs(err, database.ErrNotFound) + }) + + t.Run("adding block with known parent", func(t *testing.T) { + require := require.New(t) + + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeight, + blocks[6], + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(4), tree.Len()) + + bytes, err := GetBlock(db, blocks[6].Height()) + require.NoError(err) + require.Equal(blocks[6].Bytes(), bytes) + }) +} + +func TestExecute(t *testing.T) { + require := require.New(t) + + const numBlocks = 2*max(batchWritePeriod, iteratorReleasePeriod) + 1 + blocks := generateBlockchain(numBlocks) + parser := makeParser(blocks) + + db := memdb.New() + tree, err := NewTree(db) + require.NoError(err) + const lastAcceptedHeight = 1 + + for i, block := range blocks[lastAcceptedHeight+1:] { + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeight, + block, + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(i+1), tree.Len()) + } + + err = Execute( + context.Background(), + logging.NoLog{}, + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + + for _, block := range blocks[lastAcceptedHeight+1:] { + require.Equal(choices.Accepted, block.Status()) + } + + size, err := database.Count(db) + require.NoError(err) + require.Zero(size) +} + +func TestExecuteExitsWhenCancelled(t *testing.T) { + require := require.New(t) + + blocks := generateBlockchain(7) + parser := makeParser(blocks) + + db := memdb.New() + tree, err := NewTree(db) + require.NoError(err) + const lastAcceptedHeight = 1 + + for i, block := range blocks[lastAcceptedHeight+1:] { + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeight, + block, + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(i+1), tree.Len()) + } + + startSize, err := database.Count(db) + require.NoError(err) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err = Execute( + ctx, + logging.NoLog{}, + db, + parser, + tree, + lastAcceptedHeight, + ) + require.ErrorIs(err, context.Canceled) + + for _, block := range blocks[lastAcceptedHeight+1:] { + require.Equal(choices.Processing, block.Status()) + } + + endSize, err := database.Count(db) + require.NoError(err) + require.Equal(startSize, endSize) +} + +func TestExecuteSkipsAcceptedBlocks(t *testing.T) { + require := require.New(t) + + blocks := generateBlockchain(7) + parser := makeParser(blocks) + + db := memdb.New() + tree, err := NewTree(db) + require.NoError(err) + const ( + lastAcceptedHeightWhenAdding = 1 + lastAcceptedHeightWhenExecuting = 3 + ) + + for i, block := range blocks[lastAcceptedHeightWhenAdding+1:] { + newlyWantsParent, err := Add( + db, + tree, + lastAcceptedHeightWhenAdding, + block, + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(i+1), tree.Len()) + } + + err = Execute( + context.Background(), + logging.NoLog{}, + db, + parser, + tree, + lastAcceptedHeightWhenExecuting, + ) + require.NoError(err) + + for _, block := range blocks[lastAcceptedHeightWhenAdding+1 : lastAcceptedHeightWhenExecuting] { + require.Equal(choices.Processing, block.Status()) + } + for _, block := range blocks[lastAcceptedHeightWhenExecuting+1:] { + require.Equal(choices.Accepted, block.Status()) + } + + size, err := database.Count(db) + require.NoError(err) + require.Zero(size) +} + +func generateBlockchain(length uint64) []snowman.Block { + if length == 0 { + return nil + } + + blocks := make([]snowman.Block, length) + blocks[0] = &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.GenerateTestID(), + StatusV: choices.Processing, + }, + ParentV: ids.Empty, + HeightV: 0, + BytesV: utils.RandomBytes(1024), + } + for height := uint64(1); height < length; height++ { + blocks[height] = &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.GenerateTestID(), + StatusV: choices.Processing, + }, + ParentV: blocks[height-1].ID(), + HeightV: height, + BytesV: utils.RandomBytes(1024), + } + } + return blocks +} + +type testParser func(context.Context, []byte) (snowman.Block, error) + +func (f testParser) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { + return f(ctx, bytes) +} + +func makeParser(blocks []snowman.Block) Parser { + return testParser(func(_ context.Context, b []byte) (snowman.Block, error) { + for _, block := range blocks { + if bytes.Equal(b, block.Bytes()) { + return block, nil + } + } + return nil, database.ErrNotFound + }) +} From a64b8639b63d8c4af51d1be964138d7c77e43633 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 19:56:41 -0400 Subject: [PATCH 13/68] wip --- chains/manager.go | 26 +-- snow/engine/snowman/bootstrap/block_job.go | 116 ----------- snow/engine/snowman/bootstrap/bootstrapper.go | 194 ++++++++---------- .../snowman/bootstrap/bootstrapper_test.go | 35 +--- snow/engine/snowman/bootstrap/config.go | 9 +- .../snowman/bootstrap/interval/blocks.go | 8 +- .../snowman/bootstrap/interval/blocks_test.go | 6 +- utils/logging/logger.go | 2 + 8 files changed, 117 insertions(+), 279 deletions(-) delete mode 100644 snow/engine/snowman/bootstrap/block_job.go diff --git a/chains/manager.go b/chains/manager.go index 6393f7ca58e8..c91a57e7cffc 100644 --- a/chains/manager.go +++ b/chains/manager.go @@ -82,10 +82,11 @@ var ( VMDBPrefix = []byte("vm") // Bootstrapping prefixes for LinearizableVMs - VertexDBPrefix = []byte("vertex") - VertexBootstrappingDBPrefix = []byte("vertex_bs") - TxBootstrappingDBPrefix = []byte("tx_bs") - BlockBootstrappingDBPrefix = []byte("block_bs") + VertexDBPrefix = []byte("vertex") + VertexBootstrappingDBPrefix = []byte("vertex_bs") + TxBootstrappingDBPrefix = []byte("tx_bs") + BlockBootstrappingDBPrefix = []byte("block_bs") + NewBlockBootstrappingDBPrefix = []byte("new_block_bs") // Bootstrapping prefixes for ChainVMs ChainBootstrappingDBPrefix = []byte("bs") @@ -569,7 +570,7 @@ func (m *manager) createAvalancheChain( vertexDB := prefixdb.New(VertexDBPrefix, prefixDB) vertexBootstrappingDB := prefixdb.New(VertexBootstrappingDBPrefix, prefixDB) txBootstrappingDB := prefixdb.New(TxBootstrappingDBPrefix, prefixDB) - blockBootstrappingDB := prefixdb.New(BlockBootstrappingDBPrefix, prefixDB) + newBlockBootstrappingDB := prefixdb.New(NewBlockBootstrappingDBPrefix, prefixDB) vtxBlocker, err := queue.NewWithMissing(vertexBootstrappingDB, "vtx", ctx.AvalancheRegisterer) if err != nil { @@ -579,10 +580,6 @@ func (m *manager) createAvalancheChain( if err != nil { return nil, err } - blockBlocker, err := queue.NewWithMissing(blockBootstrappingDB, "block", ctx.Registerer) - if err != nil { - return nil, err - } // Passes messages from the avalanche engines to the network avalancheMessageSender, err := sender.New( @@ -837,7 +834,7 @@ func (m *manager) createAvalancheChain( BootstrapTracker: sb, Timer: h, AncestorsMaxContainersReceived: m.BootstrapAncestorsMaxContainersReceived, - Blocked: blockBlocker, + DB: newBlockBootstrappingDB, VM: vmWrappingProposerVM, } var snowmanBootstrapper common.BootstrapableEngine @@ -950,12 +947,7 @@ func (m *manager) createSnowmanChain( } prefixDB := prefixdb.New(ctx.ChainID[:], meterDB) vmDB := prefixdb.New(VMDBPrefix, prefixDB) - bootstrappingDB := prefixdb.New(ChainBootstrappingDBPrefix, prefixDB) - - blocked, err := queue.NewWithMissing(bootstrappingDB, "block", ctx.Registerer) - if err != nil { - return nil, err - } + newBootstrappingDB := prefixdb.New(NewBlockBootstrappingDBPrefix, prefixDB) // Passes messages from the consensus engine to the network messageSender, err := sender.New( @@ -1175,7 +1167,7 @@ func (m *manager) createSnowmanChain( BootstrapTracker: sb, Timer: h, AncestorsMaxContainersReceived: m.BootstrapAncestorsMaxContainersReceived, - Blocked: blocked, + DB: newBootstrappingDB, VM: vm, Bootstrapped: bootstrapFunc, } diff --git a/snow/engine/snowman/bootstrap/block_job.go b/snow/engine/snowman/bootstrap/block_job.go deleted file mode 100644 index a9496316f1fb..000000000000 --- a/snow/engine/snowman/bootstrap/block_job.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package bootstrap - -import ( - "context" - "errors" - "fmt" - - "github.com/prometheus/client_golang/prometheus" - "go.uber.org/zap" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/choices" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" -) - -var errMissingDependenciesOnAccept = errors.New("attempting to accept a block with missing dependencies") - -type parser struct { - log logging.Logger - numAccepted, numDropped prometheus.Counter - vm block.ChainVM -} - -func (p *parser) Parse(ctx context.Context, blkBytes []byte) (queue.Job, error) { - blk, err := p.vm.ParseBlock(ctx, blkBytes) - if err != nil { - return nil, err - } - return &blockJob{ - log: p.log, - numAccepted: p.numAccepted, - numDropped: p.numDropped, - blk: blk, - vm: p.vm, - }, nil -} - -type blockJob struct { - log logging.Logger - numAccepted, numDropped prometheus.Counter - blk snowman.Block - vm block.Getter -} - -func (b *blockJob) ID() ids.ID { - return b.blk.ID() -} - -func (b *blockJob) MissingDependencies(ctx context.Context) (set.Set[ids.ID], error) { - missing := set.Set[ids.ID]{} - parentID := b.blk.Parent() - if parent, err := b.vm.GetBlock(ctx, parentID); err != nil || parent.Status() != choices.Accepted { - missing.Add(parentID) - } - return missing, nil -} - -func (b *blockJob) HasMissingDependencies(ctx context.Context) (bool, error) { - parentID := b.blk.Parent() - if parent, err := b.vm.GetBlock(ctx, parentID); err != nil || parent.Status() != choices.Accepted { - return true, nil - } - return false, nil -} - -func (b *blockJob) Execute(ctx context.Context) error { - hasMissingDeps, err := b.HasMissingDependencies(ctx) - if err != nil { - return err - } - if hasMissingDeps { - b.numDropped.Inc() - return errMissingDependenciesOnAccept - } - status := b.blk.Status() - switch status { - case choices.Unknown, choices.Rejected: - b.numDropped.Inc() - return fmt.Errorf("attempting to execute block with status %s", status) - case choices.Processing: - blkID := b.blk.ID() - if err := b.blk.Verify(ctx); err != nil { - b.log.Error("block failed verification during bootstrapping", - zap.Stringer("blkID", blkID), - zap.Error(err), - ) - return fmt.Errorf("failed to verify block in bootstrapping: %w", err) - } - - b.numAccepted.Inc() - b.log.Trace("accepting block in bootstrapping", - zap.Stringer("blkID", blkID), - zap.Uint64("height", b.blk.Height()), - zap.Time("timestamp", b.blk.Timestamp()), - ) - if err := b.blk.Accept(ctx); err != nil { - b.log.Debug("failed to accept block during bootstrapping", - zap.Stringer("blkID", blkID), - zap.Error(err), - ) - return fmt.Errorf("failed to accept block in bootstrapping: %w", err) - } - } - return nil -} - -func (b *blockJob) Bytes() []byte { - return b.blk.Bytes() -} diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 29754a24d734..ac452c72ed15 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -13,14 +13,15 @@ import ( "go.uber.org/zap" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/proto/pb/p2p" "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/consensus/snowman/bootstrapper" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" "github.com/ava-labs/avalanchego/utils/bimap" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer" @@ -95,13 +96,6 @@ type Bootstrapper struct { // tracks which validators were asked for which containers in which requests outstandingRequests *bimap.BiMap[common.Request, ids.ID] - // number of state transitions executed - executedStateTransitions int - - parser *parser - - awaitingTimeout bool - // fetchFrom is the set of nodes that we can fetch the next container from. // When a container is fetched, the nodeID is removed from [fetchFrom] to // attempt to limit a single request to a peer at any given time. When the @@ -111,6 +105,13 @@ type Bootstrapper struct { // again. fetchFrom set.Set[ids.NodeID] + // number of state transitions executed + executedStateTransitions uint64 + awaitingTimeout bool + + tree *interval.Tree + missingBlockIDs set.Set[ids.ID] + // bootstrappedOnce ensures that the [Bootstrapped] callback is only invoked // once, even if bootstrapping is retried. bootstrappedOnce sync.Once @@ -149,10 +150,11 @@ func (b *Bootstrapper) Clear(context.Context) error { b.Ctx.Lock.Lock() defer b.Ctx.Lock.Unlock() - if err := b.Config.Blocked.Clear(); err != nil { - return err - } - return b.Config.Blocked.Commit() + return database.AtomicClear(b.DB, b.DB) + // if err := b.Config.Blocked.Clear(); err != nil { + // return err + // } + // return b.Config.Blocked.Commit() } func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { @@ -163,18 +165,7 @@ func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { State: snow.Bootstrapping, }) if err := b.VM.SetState(ctx, snow.Bootstrapping); err != nil { - return fmt.Errorf("failed to notify VM that bootstrapping has started: %w", - err) - } - - b.parser = &parser{ - log: b.Ctx.Log, - numAccepted: b.numAccepted, - numDropped: b.numDropped, - vm: b.VM, - } - if err := b.Blocked.SetParser(ctx, b.parser); err != nil { - return err + return fmt.Errorf("failed to notify VM that bootstrapping has started: %w", err) } // Set the starting height @@ -189,6 +180,17 @@ func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { b.startingHeight = lastAccepted.Height() b.requestID = startReqID + tree, err := interval.NewTree(b.DB) + if err != nil { + return fmt.Errorf("failed to initialize interval tree: %w", err) + } + b.tree = tree + + b.missingBlockIDs, err = interval.GetMissingBlockIDs(ctx, b.DB, b.VM, tree, b.startingHeight) + if err != nil { + return fmt.Errorf("failed to initialize missing block IDs: %w", err) + } + return b.tryStartBootstrapping(ctx) } @@ -389,7 +391,7 @@ func (b *Bootstrapper) startSyncing(ctx context.Context, acceptedContainerIDs [] // Initialize the fetch from set to the currently preferred peers b.fetchFrom = b.StartupTracker.PreferredPeers() - pendingContainerIDs := b.Blocked.MissingIDs() + pendingContainerIDs := b.missingBlockIDs.List() // Append the list of accepted container IDs to pendingContainerIDs to ensure // we iterate over every container that must be traversed. pendingContainerIDs = append(pendingContainerIDs, acceptedContainerIDs...) @@ -400,7 +402,7 @@ func (b *Bootstrapper) startSyncing(ctx context.Context, acceptedContainerIDs [] toProcess := make([]snowman.Block, 0, len(pendingContainerIDs)) for _, blkID := range pendingContainerIDs { - b.Blocked.AddMissingID(blkID) + b.missingBlockIDs.Add(blkID) // TODO: if `GetBlock` returns an error other than // `database.ErrNotFound`, then the error should be propagated. @@ -414,7 +416,7 @@ func (b *Bootstrapper) startSyncing(ctx context.Context, acceptedContainerIDs [] toProcess = append(toProcess, blk) } - b.initiallyFetched = b.Blocked.PendingJobs() + b.initiallyFetched = b.tree.Len() b.startTime = time.Now() // Process received blocks @@ -434,11 +436,6 @@ func (b *Bootstrapper) fetch(ctx context.Context, blkID ids.ID) error { return nil } - // Make sure we don't already have this block - if _, err := b.VM.GetBlock(ctx, blkID); err == nil { - return b.tryStartExecuting(ctx) - } - validatorID, ok := b.fetchFrom.Peek() if !ok { return fmt.Errorf("dropping request for %s as there are no validators", blkID) @@ -532,7 +529,11 @@ func (b *Bootstrapper) Ancestors(ctx context.Context, nodeID ids.NodeID, request for _, block := range blocks[1:] { blockSet[block.ID()] = block } - return b.process(ctx, requestedBlock, blockSet) + if err := b.process(ctx, requestedBlock, blockSet); err != nil { + return err + } + + return b.tryStartExecuting(ctx) } func (b *Bootstrapper) GetAncestorsFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error { @@ -579,65 +580,38 @@ func (b *Bootstrapper) markUnavailable(nodeID ids.NodeID) { // If [blk]'s height is <= the last accepted height, then it will be removed // from the missingIDs set. func (b *Bootstrapper) process(ctx context.Context, blk snowman.Block, processingBlocks map[ids.ID]snowman.Block) error { + lastAcceptedID, err := b.VM.LastAccepted(ctx) + if err != nil { + return fmt.Errorf("couldn't get last accepted ID: %w", err) + } + lastAccepted, err := b.VM.GetBlock(ctx, lastAcceptedID) + if err != nil { + return fmt.Errorf("couldn't get last accepted block: %w", err) + } + lastAcceptedHeight := lastAccepted.Height() + + batch := b.DB.NewBatch() for { - blkID := blk.ID() if b.Halted() { - // We must add in [blkID] to the set of missing IDs so that we are - // guaranteed to continue processing from this state when the - // bootstrapper is restarted. - b.Blocked.AddMissingID(blkID) - return b.Blocked.Commit() - } - - b.Blocked.RemoveMissingID(blkID) - - status := blk.Status() - // The status should never be rejected here - but we check to fail as - // quickly as possible - if status == choices.Rejected { - return fmt.Errorf("bootstrapping wants to accept %s, however it was previously rejected", blkID) + return batch.Write() } - blkHeight := blk.Height() - if status == choices.Accepted || blkHeight <= b.startingHeight { - // We can stop traversing, as we have reached the accepted frontier - if err := b.Blocked.Commit(); err != nil { - return err - } - return b.tryStartExecuting(ctx) - } + blkID := blk.ID() + b.missingBlockIDs.Remove(blkID) - // If this block is going to be accepted, make sure to update the - // tipHeight for logging - if blkHeight > b.tipHeight { - b.tipHeight = blkHeight - } + height := blk.Height() + b.tipHeight = max(b.tipHeight, height) - pushed, err := b.Blocked.Push(ctx, &blockJob{ - log: b.Ctx.Log, - numAccepted: b.numAccepted, - numDropped: b.numDropped, - blk: blk, - vm: b.VM, - }) + wantsParent, err := interval.Add(batch, b.tree, lastAcceptedHeight, blk) if err != nil { return err } - if !pushed { - // We can stop traversing, as we have reached a block that we - // previously pushed onto the jobs queue - if err := b.Blocked.Commit(); err != nil { - return err - } - return b.tryStartExecuting(ctx) - } - // We added a new block to the queue, so track that it was fetched b.numFetched.Inc() // Periodically log progress - blocksFetchedSoFar := b.Blocked.Jobs.PendingJobs() + blocksFetchedSoFar := b.tree.Len() if blocksFetchedSoFar%statusUpdateFrequency == 0 { totalBlocksToFetch := b.tipHeight - b.startingHeight eta := timer.EstimateETA( @@ -662,8 +636,13 @@ func (b *Bootstrapper) process(ctx context.Context, blk snowman.Block, processin } } + if !wantsParent { + return batch.Write() + } + // Attempt to traverse to the next block parentID := blk.Parent() + b.missingBlockIDs.Add(parentID) // First check if the parent is in the processing blocks set parent, ok := processingBlocks[parentID] @@ -683,15 +662,11 @@ func (b *Bootstrapper) process(ctx context.Context, blk snowman.Block, processin // If the block wasn't able to be acquired immediately, attempt to fetch // it - b.Blocked.AddMissingID(parentID) if err := b.fetch(ctx, parentID); err != nil { return err } - if err := b.Blocked.Commit(); err != nil { - return err - } - return b.tryStartExecuting(ctx) + return batch.Write() } } @@ -699,7 +674,7 @@ func (b *Bootstrapper) process(ctx context.Context, blk snowman.Block, processin // being fetched. After executing all pending blocks it will either restart // bootstrapping, or transition into normal operations. func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { - if numPending := b.Blocked.NumMissingIDs(); numPending != 0 { + if numPending := b.missingBlockIDs.Len(); numPending != 0 { return nil } @@ -707,34 +682,45 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { return nil } - if !b.restarted { - b.Ctx.Log.Info("executing blocks", - zap.Uint64("numPendingJobs", b.Blocked.PendingJobs()), - ) - } else { - b.Ctx.Log.Debug("executing blocks", - zap.Uint64("numPendingJobs", b.Blocked.PendingJobs()), - ) + lastAcceptedID, err := b.VM.LastAccepted(ctx) + if err != nil { + return fmt.Errorf("couldn't get last accepted ID: %w", err) } + lastAccepted, err := b.VM.GetBlock(ctx, lastAcceptedID) + if err != nil { + return fmt.Errorf("couldn't get last accepted block: %w", err) + } + lastAcceptedHeight := lastAccepted.Height() - executedBlocks, err := b.Blocked.ExecuteAll( - ctx, - b.Config.Ctx, - b, - b.restarted, - b.Ctx.BlockAcceptor, - ) + // TODO: Remove after testing + { + expectedMissingIDs, err := interval.GetMissingBlockIDs(ctx, b.DB, b.VM, b.tree, lastAcceptedHeight) + if err != nil { + return err + } + if expectedMissingIDs.Len() > 0 { + return fmt.Errorf("unexpectedly had missing IDs: %d", expectedMissingIDs.Len()) + } + } + + log := b.Ctx.Log.Info + if b.restarted { + log = b.Ctx.Log.Debug + } + + numToExecute := b.tree.Len() + err = interval.Execute(ctx, log, b.DB, b.VM, b.tree, lastAcceptedHeight) if err != nil || b.Halted() { return err } previouslyExecuted := b.executedStateTransitions - b.executedStateTransitions = executedBlocks + b.executedStateTransitions = numToExecute // Note that executedBlocks < c*previouslyExecuted ( 0 <= c < 1 ) is enforced // so that the bootstrapping process will terminate even as new blocks are // being issued. - if executedBlocks > 0 && executedBlocks < previouslyExecuted/2 { + if numToExecute > 0 && numToExecute < previouslyExecuted/2 { return b.restartBootstrapping(ctx) } @@ -750,11 +736,7 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { // If the subnet hasn't finished bootstrapping, this chain should remain // syncing. if !b.Config.BootstrapTracker.IsBootstrapped() { - if !b.restarted { - b.Ctx.Log.Info("waiting for the remaining chains in this subnet to finish syncing") - } else { - b.Ctx.Log.Debug("waiting for the remaining chains in this subnet to finish syncing") - } + log("waiting for the remaining chains in this subnet to finish syncing") // Restart bootstrapping after [bootstrappingDelay] to keep up to date // on the latest tip. b.Config.Timer.RegisterTimeout(bootstrappingDelay) diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index d5cb9cc763fc..f146309b8e32 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/database" @@ -21,14 +20,13 @@ import ( "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" "github.com/ava-labs/avalanchego/snow/engine/snowman/getter" "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/version" ) @@ -79,7 +77,6 @@ func newConfig(t *testing.T) (Config, ids.NodeID, *common.SenderTest, *block.Tes snowGetHandler, err := getter.New(vm, sender, ctx.Log, time.Second, 2000, ctx.Registerer) require.NoError(err) - blocker, _ := queue.NewWithMissing(memdb.New(), "", prometheus.NewRegistry()) return Config{ AllGetsServer: snowGetHandler, Ctx: ctx, @@ -90,7 +87,7 @@ func newConfig(t *testing.T) (Config, ids.NodeID, *common.SenderTest, *block.Tes BootstrapTracker: bootstrapTracker, Timer: &common.TimerTest{}, AncestorsMaxContainersReceived: 2000, - Blocked: blocker, + DB: memdb.New(), VM: vm, }, peer, sender, vm } @@ -117,7 +114,6 @@ func TestBootstrapperStartsOnlyIfEnoughStakeIsConnected(t *testing.T) { startupTracker := tracker.NewStartup(peerTracker, startupAlpha) peers.RegisterCallbackListener(ctx.SubnetID, startupTracker) - blocker, _ := queue.NewWithMissing(memdb.New(), "", prometheus.NewRegistry()) snowGetHandler, err := getter.New(vm, sender, ctx.Log, time.Second, 2000, ctx.Registerer) require.NoError(err) cfg := Config{ @@ -130,7 +126,7 @@ func TestBootstrapperStartsOnlyIfEnoughStakeIsConnected(t *testing.T) { BootstrapTracker: &common.BootstrapTrackerTest{}, Timer: &common.TimerTest{}, AncestorsMaxContainersReceived: 2000, - Blocked: blocker, + DB: memdb.New(), VM: vm, } @@ -1310,7 +1306,7 @@ func TestBootstrapContinueAfterHalt(t *testing.T) { require.NoError(bs.startSyncing(context.Background(), []ids.ID{blk2.ID()})) - require.Equal(1, bs.Blocked.NumMissingIDs()) + require.Equal(1, bs.missingBlockIDs.Len()) } func TestBootstrapNoParseOnNew(t *testing.T) { @@ -1355,10 +1351,6 @@ func TestBootstrapNoParseOnNew(t *testing.T) { snowGetHandler, err := getter.New(vm, sender, ctx.Log, time.Second, 2000, ctx.Registerer) require.NoError(err) - queueDB := memdb.New() - blocker, err := queue.NewWithMissing(queueDB, "", prometheus.NewRegistry()) - require.NoError(err) - blk0 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: ids.GenerateTestID(), @@ -1383,23 +1375,14 @@ func TestBootstrapNoParseOnNew(t *testing.T) { return blk0, nil } - pushed, err := blocker.Push(context.Background(), &blockJob{ - log: logging.NoLog{}, - numAccepted: prometheus.NewCounter(prometheus.CounterOpts{}), - numDropped: prometheus.NewCounter(prometheus.CounterOpts{}), - blk: blk1, - vm: vm, - }) + intervalDB := memdb.New() + tree, err := interval.NewTree(intervalDB) + require.NoError(err) + _, err = interval.Add(intervalDB, tree, 0, blk1) require.NoError(err) - require.True(pushed) - - require.NoError(blocker.Commit()) vm.GetBlockF = nil - blocker, err = queue.NewWithMissing(queueDB, "", prometheus.NewRegistry()) - require.NoError(err) - config := Config{ AllGetsServer: snowGetHandler, Ctx: ctx, @@ -1410,7 +1393,7 @@ func TestBootstrapNoParseOnNew(t *testing.T) { BootstrapTracker: bootstrapTracker, Timer: &common.TimerTest{}, AncestorsMaxContainersReceived: 2000, - Blocked: blocker, + DB: intervalDB, VM: vm, } diff --git a/snow/engine/snowman/bootstrap/config.go b/snow/engine/snowman/bootstrap/config.go index 6fb8894db96f..9cb951e378b1 100644 --- a/snow/engine/snowman/bootstrap/config.go +++ b/snow/engine/snowman/bootstrap/config.go @@ -4,9 +4,9 @@ package bootstrap import ( + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/snow/validators" @@ -28,12 +28,7 @@ type Config struct { // containers in an ancestors message it receives. AncestorsMaxContainersReceived int - // Blocked tracks operations that are blocked on blocks - // - // It should be guaranteed that `MissingIDs` should contain all IDs - // referenced by the `MissingDependencies` that have not already been added - // to the queue. - Blocked *queue.JobsWithMissing + DB database.Database VM block.ChainVM diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 613814868bfa..5c8c20818a1e 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -91,7 +91,7 @@ func Add( // Invariant: Execute assumes that GetMissingBlockIDs would return an empty set. func Execute( ctx context.Context, - log logging.Logger, + log logging.Func, db database.Database, parser Parser, tree *Tree, @@ -124,7 +124,7 @@ func Execute( iterator.Release() }() - log.Info("executing blocks", + log("executing blocks", zap.Uint64("numToExecute", totalNumberToProcess), ) @@ -177,7 +177,7 @@ func Execute( eta = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) ) - log.Info("executing blocks", + log("executing blocks", zap.Duration("eta", eta), zap.Uint64("numExecuted", numProcessed), zap.Uint64("numToExecute", totalNumberToProcess), @@ -207,7 +207,7 @@ func Execute( numProcessed = totalNumberToProcess - tree.Len() err = ctx.Err() ) - log.Info("executed blocks", + log("executed blocks", zap.Uint64("numExecuted", numProcessed), zap.Uint64("numToExecute", totalNumberToProcess), zap.Duration("duration", time.Since(startTime)), diff --git a/snow/engine/snowman/bootstrap/interval/blocks_test.go b/snow/engine/snowman/bootstrap/interval/blocks_test.go index 48b269d9b1fc..6b27801d13be 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks_test.go +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -291,7 +291,7 @@ func TestExecute(t *testing.T) { err = Execute( context.Background(), - logging.NoLog{}, + logging.NoLog{}.Info, db, parser, tree, @@ -338,7 +338,7 @@ func TestExecuteExitsWhenCancelled(t *testing.T) { cancel() err = Execute( ctx, - logging.NoLog{}, + logging.NoLog{}.Info, db, parser, tree, @@ -383,7 +383,7 @@ func TestExecuteSkipsAcceptedBlocks(t *testing.T) { err = Execute( context.Background(), - logging.NoLog{}, + logging.NoLog{}.Info, db, parser, tree, diff --git a/utils/logging/logger.go b/utils/logging/logger.go index 2ca95bff104c..57ddedf42796 100644 --- a/utils/logging/logger.go +++ b/utils/logging/logger.go @@ -9,6 +9,8 @@ import ( "go.uber.org/zap" ) +type Func func(msg string, fields ...zap.Field) + // Logger defines the interface that is used to keep a record of all events that // happen to the program type Logger interface { From 782a02c11c14f3b9208b766a59d383581ac189c2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 20:24:11 -0400 Subject: [PATCH 14/68] wip --- chains/manager.go | 17 ++++++++++++++--- .../snowman/bootstrap/bootstrapper_test.go | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/chains/manager.go b/chains/manager.go index c91a57e7cffc..c5d64da71d54 100644 --- a/chains/manager.go +++ b/chains/manager.go @@ -85,11 +85,12 @@ var ( VertexDBPrefix = []byte("vertex") VertexBootstrappingDBPrefix = []byte("vertex_bs") TxBootstrappingDBPrefix = []byte("tx_bs") - BlockBootstrappingDBPrefix = []byte("block_bs") + OldBlockBootstrappingDBPrefix = []byte("block_bs") NewBlockBootstrappingDBPrefix = []byte("new_block_bs") // Bootstrapping prefixes for ChainVMs - ChainBootstrappingDBPrefix = []byte("bs") + OldChainBootstrappingDBPrefix = []byte("bs") + NewChainBootstrappingDBPrefix = []byte("new_bs") errUnknownVMType = errors.New("the vm should have type avalanche.DAGVM or snowman.ChainVM") errCreatePlatformVM = errors.New("attempted to create a chain running the PlatformVM") @@ -570,6 +571,11 @@ func (m *manager) createAvalancheChain( vertexDB := prefixdb.New(VertexDBPrefix, prefixDB) vertexBootstrappingDB := prefixdb.New(VertexBootstrappingDBPrefix, prefixDB) txBootstrappingDB := prefixdb.New(TxBootstrappingDBPrefix, prefixDB) + + oldBlockBootstrappingDB := prefixdb.New(OldBlockBootstrappingDBPrefix, prefixDB) + if err := database.AtomicClear(oldBlockBootstrappingDB, oldBlockBootstrappingDB); err != nil { + return nil, fmt.Errorf("failed to clear legacy bootstrapping database: %w", err) + } newBlockBootstrappingDB := prefixdb.New(NewBlockBootstrappingDBPrefix, prefixDB) vtxBlocker, err := queue.NewWithMissing(vertexBootstrappingDB, "vtx", ctx.AvalancheRegisterer) @@ -947,7 +953,12 @@ func (m *manager) createSnowmanChain( } prefixDB := prefixdb.New(ctx.ChainID[:], meterDB) vmDB := prefixdb.New(VMDBPrefix, prefixDB) - newBootstrappingDB := prefixdb.New(NewBlockBootstrappingDBPrefix, prefixDB) + + oldBootstrappingDB := prefixdb.New(OldChainBootstrappingDBPrefix, prefixDB) + if err := database.AtomicClear(oldBootstrappingDB, oldBootstrappingDB); err != nil { + return nil, fmt.Errorf("failed to clear legacy bootstrapping database: %w", err) + } + newBootstrappingDB := prefixdb.New(NewChainBootstrappingDBPrefix, prefixDB) // Passes messages from the consensus engine to the network messageSender, err := sender.New( diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index f146309b8e32..d8dca8f69122 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -270,7 +270,7 @@ func TestBootstrapperSingleFrontier(t *testing.T) { } require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) - require.Equal(snow.NormalOp, config.Ctx.State.Get().State) + require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) require.Equal(choices.Accepted, blk1.Status()) } From 5358587306d08830203b90b472ea67c02b62470b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 21:34:48 -0400 Subject: [PATCH 15/68] support cancellation --- snow/engine/snowman/bootstrap/bootstrapper.go | 17 +++++++++-- snow/engine/snowman/bootstrap/context.go | 29 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 snow/engine/snowman/bootstrap/context.go diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index ac452c72ed15..60c14f1cee89 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -709,8 +709,21 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { } numToExecute := b.tree.Len() - err = interval.Execute(ctx, log, b.DB, b.VM, b.tree, lastAcceptedHeight) - if err != nil || b.Halted() { + err = interval.Execute( + &haltableContext{ + Context: ctx, + Haltable: b, + }, + log, + b.DB, + b.VM, + b.tree, + lastAcceptedHeight, + ) + if errors.Is(err, errHalted) { + return nil + } + if err != nil { return err } diff --git a/snow/engine/snowman/bootstrap/context.go b/snow/engine/snowman/bootstrap/context.go new file mode 100644 index 000000000000..e87cadc9bcfb --- /dev/null +++ b/snow/engine/snowman/bootstrap/context.go @@ -0,0 +1,29 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrap + +import ( + "context" + "errors" + + "github.com/ava-labs/avalanchego/snow/engine/common" +) + +var ( + _ context.Context = (*haltableContext)(nil) + + errHalted = errors.New("halted") +) + +type haltableContext struct { + context.Context + common.Haltable +} + +func (c *haltableContext) Err() error { + if c.Halted() { + return context.Canceled + } + return c.Context.Err() +} From 9d24b20abd9ec2c0f16edeae0334afe2a442de0c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 21:40:24 -0400 Subject: [PATCH 16/68] save --- snow/engine/snowman/bootstrap/context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/context.go b/snow/engine/snowman/bootstrap/context.go index e87cadc9bcfb..3aa14bef5e98 100644 --- a/snow/engine/snowman/bootstrap/context.go +++ b/snow/engine/snowman/bootstrap/context.go @@ -23,7 +23,7 @@ type haltableContext struct { func (c *haltableContext) Err() error { if c.Halted() { - return context.Canceled + return errHalted } return c.Context.Err() } From 0de614228456f31866cb70bf38f8ac3fa9a85710 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 21:47:49 -0400 Subject: [PATCH 17/68] remove test code --- snow/engine/snowman/bootstrap/bootstrapper.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 60c14f1cee89..dea64ba4d880 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -692,17 +692,6 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { } lastAcceptedHeight := lastAccepted.Height() - // TODO: Remove after testing - { - expectedMissingIDs, err := interval.GetMissingBlockIDs(ctx, b.DB, b.VM, b.tree, lastAcceptedHeight) - if err != nil { - return err - } - if expectedMissingIDs.Len() > 0 { - return fmt.Errorf("unexpectedly had missing IDs: %d", expectedMissingIDs.Len()) - } - } - log := b.Ctx.Log.Info if b.restarted { log = b.Ctx.Log.Debug From e658404ff87ca5ab8a1d6d6d9724818f1bf0a243 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 22:09:35 -0400 Subject: [PATCH 18/68] keep acceptor callback --- snow/engine/snowman/bootstrap/acceptor.go | 46 +++++++++++++++++++ snow/engine/snowman/bootstrap/bootstrapper.go | 5 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 snow/engine/snowman/bootstrap/acceptor.go diff --git a/snow/engine/snowman/bootstrap/acceptor.go b/snow/engine/snowman/bootstrap/acceptor.go new file mode 100644 index 000000000000..30e75ad2c6a3 --- /dev/null +++ b/snow/engine/snowman/bootstrap/acceptor.go @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrap + +import ( + "context" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" +) + +var ( + _ interval.Parser = (*parseAcceptor)(nil) + _ snowman.Block = (*blockAcceptor)(nil) +) + +type parseAcceptor struct { + parser interval.Parser + ctx *snow.ConsensusContext +} + +func (p *parseAcceptor) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { + blk, err := p.parser.ParseBlock(ctx, bytes) + if err != nil { + return nil, err + } + return &blockAcceptor{ + Block: blk, + ctx: p.ctx, + }, nil +} + +type blockAcceptor struct { + snowman.Block + + ctx *snow.ConsensusContext +} + +func (b *blockAcceptor) Accept(ctx context.Context) error { + if err := b.ctx.BlockAcceptor.Accept(b.ctx, b.ID(), b.Bytes()); err != nil { + return err + } + return b.Block.Accept(ctx) +} diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index dea64ba4d880..2e02134f8943 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -705,7 +705,10 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { }, log, b.DB, - b.VM, + &parseAcceptor{ + parser: b.VM, + ctx: b.Ctx, + }, b.tree, lastAcceptedHeight, ) From 03f60c6418027bec6b6cdbd5c3d18afdb84b1796 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 22:18:07 -0400 Subject: [PATCH 19/68] lint --- snow/engine/snowman/bootstrap/interval/blocks_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks_test.go b/snow/engine/snowman/bootstrap/interval/blocks_test.go index 48b269d9b1fc..1673fffb1ae8 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks_test.go +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -289,15 +289,14 @@ func TestExecute(t *testing.T) { require.Equal(uint64(i+1), tree.Len()) } - err = Execute( + require.NoError(Execute( context.Background(), logging.NoLog{}, db, parser, tree, lastAcceptedHeight, - ) - require.NoError(err) + )) for _, block := range blocks[lastAcceptedHeight+1:] { require.Equal(choices.Accepted, block.Status()) @@ -381,15 +380,14 @@ func TestExecuteSkipsAcceptedBlocks(t *testing.T) { require.Equal(uint64(i+1), tree.Len()) } - err = Execute( + require.NoError(Execute( context.Background(), logging.NoLog{}, db, parser, tree, lastAcceptedHeightWhenExecuting, - ) - require.NoError(err) + )) for _, block := range blocks[lastAcceptedHeightWhenAdding+1 : lastAcceptedHeightWhenExecuting] { require.Equal(choices.Processing, block.Status()) From f81281a9cd5c8325d98c509b135ec22ba203abf8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 22:31:43 -0400 Subject: [PATCH 20/68] fix test --- vms/platformvm/vm_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index fdeb4a316047..2a54db8b55e4 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -24,7 +24,6 @@ import ( "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowball" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap" "github.com/ava-labs/avalanchego/snow/networking/benchlist" @@ -1253,9 +1252,7 @@ func TestBootstrapPartiallyAccepted(t *testing.T) { baseDB := memdb.New() vmDB := prefixdb.New(chains.VMDBPrefix, baseDB) - bootstrappingDB := prefixdb.New(chains.ChainBootstrappingDBPrefix, baseDB) - blocked, err := queue.NewWithMissing(bootstrappingDB, "", prometheus.NewRegistry()) - require.NoError(err) + bootstrappingDB := prefixdb.New(chains.NewChainBootstrappingDBPrefix, baseDB) vm := &VM{Config: config.Config{ Chains: chains.TestManager, @@ -1431,7 +1428,7 @@ func TestBootstrapPartiallyAccepted(t *testing.T) { Sender: sender, BootstrapTracker: bootstrapTracker, AncestorsMaxContainersReceived: 2000, - Blocked: blocked, + DB: bootstrappingDB, VM: vm, } From 984849c81d5c83c43e26760c2ba808b91790c744 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 22:37:23 -0400 Subject: [PATCH 21/68] remove incorrect db cleanup --- chains/manager.go | 30 +++++++++--------------------- vms/platformvm/vm_test.go | 2 +- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/chains/manager.go b/chains/manager.go index c5d64da71d54..c62519c757c9 100644 --- a/chains/manager.go +++ b/chains/manager.go @@ -82,15 +82,13 @@ var ( VMDBPrefix = []byte("vm") // Bootstrapping prefixes for LinearizableVMs - VertexDBPrefix = []byte("vertex") - VertexBootstrappingDBPrefix = []byte("vertex_bs") - TxBootstrappingDBPrefix = []byte("tx_bs") - OldBlockBootstrappingDBPrefix = []byte("block_bs") - NewBlockBootstrappingDBPrefix = []byte("new_block_bs") + VertexDBPrefix = []byte("vertex") + VertexBootstrappingDBPrefix = []byte("vertex_bs") + TxBootstrappingDBPrefix = []byte("tx_bs") + BlockBootstrappingDBPrefix = []byte("interval_block_bs") // Bootstrapping prefixes for ChainVMs - OldChainBootstrappingDBPrefix = []byte("bs") - NewChainBootstrappingDBPrefix = []byte("new_bs") + ChainBootstrappingDBPrefix = []byte("interval_bs") errUnknownVMType = errors.New("the vm should have type avalanche.DAGVM or snowman.ChainVM") errCreatePlatformVM = errors.New("attempted to create a chain running the PlatformVM") @@ -571,12 +569,7 @@ func (m *manager) createAvalancheChain( vertexDB := prefixdb.New(VertexDBPrefix, prefixDB) vertexBootstrappingDB := prefixdb.New(VertexBootstrappingDBPrefix, prefixDB) txBootstrappingDB := prefixdb.New(TxBootstrappingDBPrefix, prefixDB) - - oldBlockBootstrappingDB := prefixdb.New(OldBlockBootstrappingDBPrefix, prefixDB) - if err := database.AtomicClear(oldBlockBootstrappingDB, oldBlockBootstrappingDB); err != nil { - return nil, fmt.Errorf("failed to clear legacy bootstrapping database: %w", err) - } - newBlockBootstrappingDB := prefixdb.New(NewBlockBootstrappingDBPrefix, prefixDB) + blockBootstrappingDB := prefixdb.New(BlockBootstrappingDBPrefix, prefixDB) vtxBlocker, err := queue.NewWithMissing(vertexBootstrappingDB, "vtx", ctx.AvalancheRegisterer) if err != nil { @@ -840,7 +833,7 @@ func (m *manager) createAvalancheChain( BootstrapTracker: sb, Timer: h, AncestorsMaxContainersReceived: m.BootstrapAncestorsMaxContainersReceived, - DB: newBlockBootstrappingDB, + DB: blockBootstrappingDB, VM: vmWrappingProposerVM, } var snowmanBootstrapper common.BootstrapableEngine @@ -953,12 +946,7 @@ func (m *manager) createSnowmanChain( } prefixDB := prefixdb.New(ctx.ChainID[:], meterDB) vmDB := prefixdb.New(VMDBPrefix, prefixDB) - - oldBootstrappingDB := prefixdb.New(OldChainBootstrappingDBPrefix, prefixDB) - if err := database.AtomicClear(oldBootstrappingDB, oldBootstrappingDB); err != nil { - return nil, fmt.Errorf("failed to clear legacy bootstrapping database: %w", err) - } - newBootstrappingDB := prefixdb.New(NewChainBootstrappingDBPrefix, prefixDB) + bootstrappingDB := prefixdb.New(ChainBootstrappingDBPrefix, prefixDB) // Passes messages from the consensus engine to the network messageSender, err := sender.New( @@ -1178,7 +1166,7 @@ func (m *manager) createSnowmanChain( BootstrapTracker: sb, Timer: h, AncestorsMaxContainersReceived: m.BootstrapAncestorsMaxContainersReceived, - DB: newBootstrappingDB, + DB: bootstrappingDB, VM: vm, Bootstrapped: bootstrapFunc, } diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index 2a54db8b55e4..f6df4db96036 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -1252,7 +1252,7 @@ func TestBootstrapPartiallyAccepted(t *testing.T) { baseDB := memdb.New() vmDB := prefixdb.New(chains.VMDBPrefix, baseDB) - bootstrappingDB := prefixdb.New(chains.NewChainBootstrappingDBPrefix, baseDB) + bootstrappingDB := prefixdb.New(chains.ChainBootstrappingDBPrefix, baseDB) vm := &VM{Config: config.Config{ Chains: chains.TestManager, From f4a724af613c618c4cb873017420cbfb5ccf8927 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 23:23:02 -0400 Subject: [PATCH 22/68] nits --- snow/engine/snowman/bootstrap/bootstrapper.go | 4 ---- snow/engine/snowman/bootstrap/config.go | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 2e02134f8943..90c613106ef8 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -151,10 +151,6 @@ func (b *Bootstrapper) Clear(context.Context) error { defer b.Ctx.Lock.Unlock() return database.AtomicClear(b.DB, b.DB) - // if err := b.Config.Blocked.Clear(); err != nil { - // return err - // } - // return b.Config.Blocked.Commit() } func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { diff --git a/snow/engine/snowman/bootstrap/config.go b/snow/engine/snowman/bootstrap/config.go index 9cb951e378b1..5ddef07970d3 100644 --- a/snow/engine/snowman/bootstrap/config.go +++ b/snow/engine/snowman/bootstrap/config.go @@ -28,6 +28,8 @@ type Config struct { // containers in an ancestors message it receives. AncestorsMaxContainersReceived int + // Database used to track the fetched, but not yet executed, blocks during + // bootstrapping. DB database.Database VM block.ChainVM From f01b6a38222319e414997fff4556269bba526600 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 23:26:06 -0400 Subject: [PATCH 23/68] allow specifying the log level --- snow/engine/snowman/bootstrap/interval/blocks.go | 8 ++++---- snow/engine/snowman/bootstrap/interval/blocks_test.go | 6 +++--- utils/logging/logger.go | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 613814868bfa..5c8c20818a1e 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -91,7 +91,7 @@ func Add( // Invariant: Execute assumes that GetMissingBlockIDs would return an empty set. func Execute( ctx context.Context, - log logging.Logger, + log logging.Func, db database.Database, parser Parser, tree *Tree, @@ -124,7 +124,7 @@ func Execute( iterator.Release() }() - log.Info("executing blocks", + log("executing blocks", zap.Uint64("numToExecute", totalNumberToProcess), ) @@ -177,7 +177,7 @@ func Execute( eta = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) ) - log.Info("executing blocks", + log("executing blocks", zap.Duration("eta", eta), zap.Uint64("numExecuted", numProcessed), zap.Uint64("numToExecute", totalNumberToProcess), @@ -207,7 +207,7 @@ func Execute( numProcessed = totalNumberToProcess - tree.Len() err = ctx.Err() ) - log.Info("executed blocks", + log("executed blocks", zap.Uint64("numExecuted", numProcessed), zap.Uint64("numToExecute", totalNumberToProcess), zap.Duration("duration", time.Since(startTime)), diff --git a/snow/engine/snowman/bootstrap/interval/blocks_test.go b/snow/engine/snowman/bootstrap/interval/blocks_test.go index 1673fffb1ae8..4bfc8b0da64f 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks_test.go +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -291,7 +291,7 @@ func TestExecute(t *testing.T) { require.NoError(Execute( context.Background(), - logging.NoLog{}, + logging.NoLog{}.Info, db, parser, tree, @@ -337,7 +337,7 @@ func TestExecuteExitsWhenCancelled(t *testing.T) { cancel() err = Execute( ctx, - logging.NoLog{}, + logging.NoLog{}.Info, db, parser, tree, @@ -382,7 +382,7 @@ func TestExecuteSkipsAcceptedBlocks(t *testing.T) { require.NoError(Execute( context.Background(), - logging.NoLog{}, + logging.NoLog{}.Info, db, parser, tree, diff --git a/utils/logging/logger.go b/utils/logging/logger.go index 2ca95bff104c..f6b3b66a77b8 100644 --- a/utils/logging/logger.go +++ b/utils/logging/logger.go @@ -9,6 +9,10 @@ import ( "go.uber.org/zap" ) +// Func defines the method signature used for all logging methods on the Logger +// interface. +type Func func(msg string, fields ...zap.Field) + // Logger defines the interface that is used to keep a record of all events that // happen to the program type Logger interface { From 0021f3edc06e93b56b69910ab1aaa0f09864a729 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 19 Mar 2024 23:55:03 -0400 Subject: [PATCH 24/68] cleanup metrics --- snow/engine/snowman/bootstrap/acceptor.go | 15 ++++++++++----- snow/engine/snowman/bootstrap/bootstrapper.go | 5 +++-- snow/engine/snowman/bootstrap/metrics.go | 10 ++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/snow/engine/snowman/bootstrap/acceptor.go b/snow/engine/snowman/bootstrap/acceptor.go index 30e75ad2c6a3..4c2affe3f96c 100644 --- a/snow/engine/snowman/bootstrap/acceptor.go +++ b/snow/engine/snowman/bootstrap/acceptor.go @@ -9,6 +9,7 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" + "github.com/prometheus/client_golang/prometheus" ) var ( @@ -17,8 +18,9 @@ var ( ) type parseAcceptor struct { - parser interval.Parser - ctx *snow.ConsensusContext + parser interval.Parser + ctx *snow.ConsensusContext + numAccepted prometheus.Counter } func (p *parseAcceptor) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { @@ -27,18 +29,21 @@ func (p *parseAcceptor) ParseBlock(ctx context.Context, bytes []byte) (snowman.B return nil, err } return &blockAcceptor{ - Block: blk, - ctx: p.ctx, + Block: blk, + ctx: p.ctx, + numAccepted: p.numAccepted, }, nil } type blockAcceptor struct { snowman.Block - ctx *snow.ConsensusContext + ctx *snow.ConsensusContext + numAccepted prometheus.Counter } func (b *blockAcceptor) Accept(ctx context.Context) error { + b.numAccepted.Inc() if err := b.ctx.BlockAcceptor.Accept(b.ctx, b.ID(), b.Bytes()); err != nil { return err } diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 90c613106ef8..137887b3744c 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -702,8 +702,9 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { log, b.DB, &parseAcceptor{ - parser: b.VM, - ctx: b.Ctx, + parser: b.VM, + ctx: b.Ctx, + numAccepted: b.numAccepted, }, b.tree, lastAcceptedHeight, diff --git a/snow/engine/snowman/bootstrap/metrics.go b/snow/engine/snowman/bootstrap/metrics.go index f6ad90d16419..aea46d2a93e8 100644 --- a/snow/engine/snowman/bootstrap/metrics.go +++ b/snow/engine/snowman/bootstrap/metrics.go @@ -10,8 +10,8 @@ import ( ) type metrics struct { - numFetched, numDropped, numAccepted prometheus.Counter - fetchETA prometheus.Gauge + numFetched, numAccepted prometheus.Counter + fetchETA prometheus.Gauge } func newMetrics(namespace string, registerer prometheus.Registerer) (*metrics, error) { @@ -21,11 +21,6 @@ func newMetrics(namespace string, registerer prometheus.Registerer) (*metrics, e Name: "fetched", Help: "Number of blocks fetched during bootstrapping", }), - numDropped: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Name: "dropped", - Help: "Number of blocks dropped during bootstrapping", - }), numAccepted: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Name: "accepted", @@ -40,7 +35,6 @@ func newMetrics(namespace string, registerer prometheus.Registerer) (*metrics, e err := utils.Err( registerer.Register(m.numFetched), - registerer.Register(m.numDropped), registerer.Register(m.numAccepted), registerer.Register(m.fetchETA), ) From f1933d6593cf012a0ea3026a3924bbd088bb1ea9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Mar 2024 00:06:59 -0400 Subject: [PATCH 25/68] lint --- snow/engine/snowman/bootstrap/acceptor.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/acceptor.go b/snow/engine/snowman/bootstrap/acceptor.go index 4c2affe3f96c..9ca718f3ab52 100644 --- a/snow/engine/snowman/bootstrap/acceptor.go +++ b/snow/engine/snowman/bootstrap/acceptor.go @@ -6,10 +6,11 @@ package bootstrap import ( "context" + "github.com/prometheus/client_golang/prometheus" + "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" - "github.com/prometheus/client_golang/prometheus" ) var ( From dd4a57fc8b8b06117ffc5ec737e8b9673f9d468c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Mar 2024 00:41:08 -0400 Subject: [PATCH 26/68] add error information --- snow/engine/snowman/bootstrap/interval/blocks.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 5c8c20818a1e..4a7e11a1226b 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -5,6 +5,7 @@ package interval import ( "context" + "fmt" "time" "go.uber.org/zap" @@ -190,10 +191,18 @@ func Execute( } if err := blk.Verify(ctx); err != nil { - return err + return fmt.Errorf("failed to verify block %s (%d) in bootstrapping: %w", + blk.ID(), + height, + err, + ) } if err := blk.Accept(ctx); err != nil { - return err + return fmt.Errorf("failed to accept block %s (%d) in bootstrapping: %w", + blk.ID(), + height, + err, + ) } } if err := writeBatch(); err != nil { From d5da1e935b85d3836c25220ca841097ad4eea2ff Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Mar 2024 02:25:33 -0400 Subject: [PATCH 27/68] upstream --- snow/engine/snowman/bootstrap/interval/blocks.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 5c8c20818a1e..4a7e11a1226b 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -5,6 +5,7 @@ package interval import ( "context" + "fmt" "time" "go.uber.org/zap" @@ -190,10 +191,18 @@ func Execute( } if err := blk.Verify(ctx); err != nil { - return err + return fmt.Errorf("failed to verify block %s (%d) in bootstrapping: %w", + blk.ID(), + height, + err, + ) } if err := blk.Accept(ctx); err != nil { - return err + return fmt.Errorf("failed to accept block %s (%d) in bootstrapping: %w", + blk.ID(), + height, + err, + ) } } if err := writeBatch(); err != nil { From 25e28011015d29f6e15b13753acc460e01782fe9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Mar 2024 13:50:44 -0400 Subject: [PATCH 28/68] cleanup --- .../snowman/bootstrap/interval/blocks.go | 15 ++++- .../snowman/bootstrap/interval/state.go | 56 +++++++++---------- .../engine/snowman/bootstrap/interval/tree.go | 24 +++++++- 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 4a7e11a1226b..1adc6153da53 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -19,7 +19,7 @@ import ( ) const ( - batchWritePeriod = 1 + batchWritePeriod = 64 iteratorReleasePeriod = 1024 logPeriod = 5 * time.Second ) @@ -28,6 +28,13 @@ type Parser interface { ParseBlock(context.Context, []byte) (snowman.Block, error) } +// GetMissingBlockIDs returns the ID of the blocks that should be fetched to +// attempt to make a single continuous range from +// (lastAcceptedHeight, highestTrackedHeight]. +// +// For example, if the tree currently contains heights [1, 4, 6, 7] and the +// lastAcceptedHeight is 2, this function will return the IDs corresponding to +// blocks [3, 5]. func GetMissingBlockIDs( ctx context.Context, db database.KeyValueReader, @@ -89,7 +96,11 @@ func Add( return height != lastHeightToFetch && !tree.Contains(height-1), nil } -// Invariant: Execute assumes that GetMissingBlockIDs would return an empty set. +// Execute all the blocks tracked by the tree. If a block is in the tree but is +// already accepted based on the lastAcceptedHeight, it will be removed from the +// tree but not executed. +// +// Execute assumes that GetMissingBlockIDs would return an empty set. func Execute( ctx context.Context, log logging.Func, diff --git a/snow/engine/snowman/bootstrap/interval/state.go b/snow/engine/snowman/bootstrap/interval/state.go index 320d5c97105f..31bb5540c387 100644 --- a/snow/engine/snowman/bootstrap/interval/state.go +++ b/snow/engine/snowman/bootstrap/interval/state.go @@ -10,21 +10,21 @@ import ( ) const ( - rangePrefixByte byte = iota + intervalPrefixByte byte = iota blockPrefixByte prefixLen = 1 ) var ( - rangePrefix = []byte{rangePrefixByte} - blockPrefix = []byte{blockPrefixByte} + intervalPrefix = []byte{intervalPrefixByte} + blockPrefix = []byte{blockPrefixByte} errInvalidKeyLength = errors.New("invalid key length") ) func GetIntervals(db database.Iteratee) ([]*Interval, error) { - it := db.NewIteratorWithPrefix(rangePrefix) + it := db.NewIteratorWithPrefix(intervalPrefix) defer it.Release() var intervals []*Interval @@ -34,8 +34,8 @@ func GetIntervals(db database.Iteratee) ([]*Interval, error) { return nil, errInvalidKeyLength } - rangeKey := dbKey[prefixLen:] - upperBound, err := database.ParseUInt64(rangeKey) + intervalKey := dbKey[prefixLen:] + upperBound, err := database.ParseUInt64(intervalKey) if err != nil { return nil, err } @@ -55,39 +55,39 @@ func GetIntervals(db database.Iteratee) ([]*Interval, error) { } func PutInterval(db database.KeyValueWriter, upperBound uint64, lowerBound uint64) error { - rangeKey := database.PackUInt64(upperBound) - return database.PutUInt64( - db, - append(rangePrefix, rangeKey...), - lowerBound, - ) + return database.PutUInt64(db, makeIntervalKey(upperBound), lowerBound) } func DeleteInterval(db database.KeyValueDeleter, upperBound uint64) error { - rangeKey := database.PackUInt64(upperBound) - return db.Delete( - append(rangePrefix, rangeKey...), - ) + return db.Delete(makeIntervalKey(upperBound)) +} + +// makeIntervalKey uses the upperBound rather than the lowerBound because blocks +// are fetched from tip towards genesis. This means that it is more common for +// the lowerBound to change than the upperBound. Modifying the lowerBound only +// requires a single write rather than a write and a delete when modifying the +// upperBound. +func makeIntervalKey(upperBound uint64) []byte { + intervalKey := database.PackUInt64(upperBound) + return append(intervalPrefix, intervalKey...) } func GetBlock(db database.KeyValueReader, height uint64) ([]byte, error) { - blockKey := database.PackUInt64(height) - return db.Get( - append(blockPrefix, blockKey...), - ) + return db.Get(makeBlockKey(height)) } func PutBlock(db database.KeyValueWriter, height uint64, bytes []byte) error { - blockKey := database.PackUInt64(height) - return db.Put( - append(blockPrefix, blockKey...), - bytes, - ) + return db.Put(makeBlockKey(height), bytes) } func DeleteBlock(db database.KeyValueDeleter, height uint64) error { + return db.Delete(makeBlockKey(height)) +} + +// makeBlockKey ensures that the returned key maintains the same sorted order as +// the height. This ensures that database iteration of block keys will iterate +// from lower height to higher height. +func makeBlockKey(height uint64) []byte { blockKey := database.PackUInt64(height) - return db.Delete( - append(blockPrefix, blockKey...), - ) + return append(blockPrefix, blockKey...) } diff --git a/snow/engine/snowman/bootstrap/interval/tree.go b/snow/engine/snowman/bootstrap/interval/tree.go index a4f07102f04a..51d1083c1e21 100644 --- a/snow/engine/snowman/bootstrap/interval/tree.go +++ b/snow/engine/snowman/bootstrap/interval/tree.go @@ -9,13 +9,31 @@ import ( "github.com/ava-labs/avalanchego/database" ) +// TODO: Benchmark what degree to use. const treeDegree = 2 +// Tree implements a set of numbers by tracking intervals. It supports adding +// and removing new values. It also allows checking if a value is included in +// the set. +// +// Tree is more space efficient than a map implementation if the values that it +// contains are continuous. The tree takes O(n) space where n is the number of +// continuous ranges that have been inserted into the tree. +// +// Add, Remove, and Contains all run in O(log n) where n is the number of +// continuous ranges that have been inserted into the tree. type Tree struct { - knownHeights *btree.BTreeG[*Interval] + knownHeights *btree.BTreeG[*Interval] + // If knownHeights contains the full range [0, MaxUint64], then + // numKnownHeights overflows to 0. numKnownHeights uint64 } +// NewTree creates a new interval tree from the provided database. +// +// It is assumed that persisted intervals are non-overlapping. Providing a +// database with overlapping intervals will result in undefined behavior of the +// structure. func NewTree(db database.Iteratee) (*Tree, error) { intervals, err := GetIntervals(db) if err != nil { @@ -161,6 +179,10 @@ func (t *Tree) Flatten() []*Interval { } // Len returns the number of heights in the tree; not the number of intervals. +// +// Because Len returns a uint64 and is describing the number of values in the +// range of uint64s, it will return 0 if the tree contains the full interval +// [0, MaxUint64]. func (t *Tree) Len() uint64 { return t.numKnownHeights } From 5ca206b043d810447cf14b112a54413e59958ab7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Mar 2024 14:00:39 -0400 Subject: [PATCH 29/68] reduce copied code --- snow/engine/snowman/bootstrap/bootstrapper.go | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 137887b3744c..8f4ec62aaff6 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -165,15 +165,11 @@ func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { } // Set the starting height - lastAcceptedID, err := b.VM.LastAccepted(ctx) - if err != nil { - return fmt.Errorf("couldn't get last accepted ID: %w", err) - } - lastAccepted, err := b.VM.GetBlock(ctx, lastAcceptedID) + lastAcceptedHeight, err := b.getLastAcceptedHeight(ctx) if err != nil { - return fmt.Errorf("couldn't get last accepted block: %w", err) + return err } - b.startingHeight = lastAccepted.Height() + b.startingHeight = lastAcceptedHeight b.requestID = startReqID tree, err := interval.NewTree(b.DB) @@ -576,15 +572,10 @@ func (b *Bootstrapper) markUnavailable(nodeID ids.NodeID) { // If [blk]'s height is <= the last accepted height, then it will be removed // from the missingIDs set. func (b *Bootstrapper) process(ctx context.Context, blk snowman.Block, processingBlocks map[ids.ID]snowman.Block) error { - lastAcceptedID, err := b.VM.LastAccepted(ctx) + lastAcceptedHeight, err := b.getLastAcceptedHeight(ctx) if err != nil { - return fmt.Errorf("couldn't get last accepted ID: %w", err) - } - lastAccepted, err := b.VM.GetBlock(ctx, lastAcceptedID) - if err != nil { - return fmt.Errorf("couldn't get last accepted block: %w", err) + return err } - lastAcceptedHeight := lastAccepted.Height() batch := b.DB.NewBatch() for { @@ -678,15 +669,10 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { return nil } - lastAcceptedID, err := b.VM.LastAccepted(ctx) + lastAcceptedHeight, err := b.getLastAcceptedHeight(ctx) if err != nil { - return fmt.Errorf("couldn't get last accepted ID: %w", err) - } - lastAccepted, err := b.VM.GetBlock(ctx, lastAcceptedID) - if err != nil { - return fmt.Errorf("couldn't get last accepted block: %w", err) + return err } - lastAcceptedHeight := lastAccepted.Height() log := b.Ctx.Log.Info if b.restarted { @@ -749,6 +735,18 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { return b.onFinished(ctx, b.requestID) } +func (b *Bootstrapper) getLastAcceptedHeight(ctx context.Context) (uint64, error) { + lastAcceptedID, err := b.VM.LastAccepted(ctx) + if err != nil { + return 0, fmt.Errorf("couldn't get last accepted ID: %w", err) + } + lastAccepted, err := b.VM.GetBlock(ctx, lastAcceptedID) + if err != nil { + return 0, fmt.Errorf("couldn't get last accepted block: %w", err) + } + return lastAccepted.Height(), nil +} + func (b *Bootstrapper) Timeout(ctx context.Context) error { if !b.awaitingTimeout { return errUnexpectedTimeout From d9a7f8aa91aec8a4124c5a5d66c866468dc420d7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Mar 2024 14:07:45 -0400 Subject: [PATCH 30/68] nit cleanup --- snow/engine/snowman/bootstrap/bootstrapper.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 8f4ec62aaff6..fa9c759d86a0 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -383,19 +383,15 @@ func (b *Bootstrapper) startSyncing(ctx context.Context, acceptedContainerIDs [] // Initialize the fetch from set to the currently preferred peers b.fetchFrom = b.StartupTracker.PreferredPeers() - pendingContainerIDs := b.missingBlockIDs.List() - // Append the list of accepted container IDs to pendingContainerIDs to ensure - // we iterate over every container that must be traversed. - pendingContainerIDs = append(pendingContainerIDs, acceptedContainerIDs...) + b.missingBlockIDs.Add(acceptedContainerIDs...) + numMissingBlockIDs := b.missingBlockIDs.Len() b.Ctx.Log.Debug("starting bootstrapping", - zap.Int("numPendingBlocks", len(pendingContainerIDs)), + zap.Int("numMissingBlocks", numMissingBlockIDs), zap.Int("numAcceptedBlocks", len(acceptedContainerIDs)), ) - toProcess := make([]snowman.Block, 0, len(pendingContainerIDs)) - for _, blkID := range pendingContainerIDs { - b.missingBlockIDs.Add(blkID) - + toProcess := make([]snowman.Block, 0, numMissingBlockIDs) + for blkID := range b.missingBlockIDs { // TODO: if `GetBlock` returns an error other than // `database.ErrNotFound`, then the error should be propagated. blk, err := b.VM.GetBlock(ctx, blkID) From bd2b2d2d6ef64f6504666b2c45cb713827ca7ab8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 20 Mar 2024 14:12:42 -0400 Subject: [PATCH 31/68] nit cleanup --- snow/engine/snowman/bootstrap/bootstrapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index fa9c759d86a0..28d23b6e983e 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -657,7 +657,7 @@ func (b *Bootstrapper) process(ctx context.Context, blk snowman.Block, processin // being fetched. After executing all pending blocks it will either restart // bootstrapping, or transition into normal operations. func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { - if numPending := b.missingBlockIDs.Len(); numPending != 0 { + if numMissingBlockIDs := b.missingBlockIDs.Len(); numMissingBlockIDs != 0 { return nil } From 175ab8e7d123659626a31b524f5ef853305d3cd4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 21 Mar 2024 16:08:13 -0400 Subject: [PATCH 32/68] nit --- snow/engine/snowman/bootstrap/bootstrapper.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index a9ab472a1cc7..c4151a2d5aa8 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -172,13 +172,12 @@ func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { b.startingHeight = lastAcceptedHeight b.requestID = startReqID - tree, err := interval.NewTree(b.DB) + b.tree, err = interval.NewTree(b.DB) if err != nil { return fmt.Errorf("failed to initialize interval tree: %w", err) } - b.tree = tree - b.missingBlockIDs, err = interval.GetMissingBlockIDs(ctx, b.DB, b.VM, tree, b.startingHeight) + b.missingBlockIDs, err = interval.GetMissingBlockIDs(ctx, b.DB, b.VM, b.tree, b.startingHeight) if err != nil { return fmt.Errorf("failed to initialize missing block IDs: %w", err) } From c89d24d0a03046ed850f15a183cdae808ba9675d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 21 Mar 2024 16:37:57 -0400 Subject: [PATCH 33/68] Remove redundent interface --- snow/engine/snowman/bootstrap/interval/blocks.go | 9 +++------ snow/engine/snowman/bootstrap/interval/blocks_test.go | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 1adc6153da53..b825735a1b35 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer" @@ -24,10 +25,6 @@ const ( logPeriod = 5 * time.Second ) -type Parser interface { - ParseBlock(context.Context, []byte) (snowman.Block, error) -} - // GetMissingBlockIDs returns the ID of the blocks that should be fetched to // attempt to make a single continuous range from // (lastAcceptedHeight, highestTrackedHeight]. @@ -38,7 +35,7 @@ type Parser interface { func GetMissingBlockIDs( ctx context.Context, db database.KeyValueReader, - parser Parser, + parser block.Parser, tree *Tree, lastAcceptedHeight uint64, ) (set.Set[ids.ID], error) { @@ -105,7 +102,7 @@ func Execute( ctx context.Context, log logging.Func, db database.Database, - parser Parser, + parser block.Parser, tree *Tree, lastAcceptedHeight uint64, ) error { diff --git a/snow/engine/snowman/bootstrap/interval/blocks_test.go b/snow/engine/snowman/bootstrap/interval/blocks_test.go index 4bfc8b0da64f..03492698ecea 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks_test.go +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" @@ -436,7 +437,7 @@ func (f testParser) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block return f(ctx, bytes) } -func makeParser(blocks []snowman.Block) Parser { +func makeParser(blocks []snowman.Block) block.Parser { return testParser(func(_ context.Context, b []byte) (snowman.Block, error) { for _, block := range blocks { if bytes.Equal(b, block.Bytes()) { From a736e6c6eeb146d2d2e9c03d768ef3f76ee1aba9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 21 Mar 2024 17:45:06 -0400 Subject: [PATCH 34/68] nit --- snow/engine/snowman/bootstrap/acceptor.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/snow/engine/snowman/bootstrap/acceptor.go b/snow/engine/snowman/bootstrap/acceptor.go index 9ca718f3ab52..81db1decf388 100644 --- a/snow/engine/snowman/bootstrap/acceptor.go +++ b/snow/engine/snowman/bootstrap/acceptor.go @@ -10,16 +10,16 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" ) var ( - _ interval.Parser = (*parseAcceptor)(nil) - _ snowman.Block = (*blockAcceptor)(nil) + _ block.Parser = (*parseAcceptor)(nil) + _ snowman.Block = (*blockAcceptor)(nil) ) type parseAcceptor struct { - parser interval.Parser + parser block.Parser ctx *snow.ConsensusContext numAccepted prometheus.Counter } From faa5329557b7cec54f1b0ead0ca7ff345a3d39aa Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 21 Mar 2024 17:45:34 -0400 Subject: [PATCH 35/68] nit --- snow/engine/snowman/bootstrap/acceptor.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/snow/engine/snowman/bootstrap/acceptor.go b/snow/engine/snowman/bootstrap/acceptor.go index 81db1decf388..eae4be879afa 100644 --- a/snow/engine/snowman/bootstrap/acceptor.go +++ b/snow/engine/snowman/bootstrap/acceptor.go @@ -44,9 +44,10 @@ type blockAcceptor struct { } func (b *blockAcceptor) Accept(ctx context.Context) error { - b.numAccepted.Inc() if err := b.ctx.BlockAcceptor.Accept(b.ctx, b.ID(), b.Bytes()); err != nil { return err } - return b.Block.Accept(ctx) + err := b.Block.Accept(ctx) + b.numAccepted.Inc() + return err } From bc3fedfa992dba7f3f092bd0bbdfd4c7f8838620 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 25 Mar 2024 15:04:03 -0400 Subject: [PATCH 36/68] nit --- snow/engine/snowman/bootstrap/interval/blocks.go | 4 ++-- snow/engine/snowman/bootstrap/interval/state.go | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index b825735a1b35..8124f96e5cac 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -122,7 +122,7 @@ func Execute( return nil } - iterator = db.NewIteratorWithPrefix(blockPrefix) + iterator = GetBlockIterator(db) processedSinceIteratorRelease uint startTime = time.Now() @@ -176,7 +176,7 @@ func Execute( processedSinceIteratorRelease = 0 iterator.Release() - iterator = db.NewIteratorWithPrefix(blockPrefix) + iterator = GetBlockIterator(db) } now := time.Now() diff --git a/snow/engine/snowman/bootstrap/interval/state.go b/snow/engine/snowman/bootstrap/interval/state.go index 31bb5540c387..cf2e2bf3a2ef 100644 --- a/snow/engine/snowman/bootstrap/interval/state.go +++ b/snow/engine/snowman/bootstrap/interval/state.go @@ -72,6 +72,12 @@ func makeIntervalKey(upperBound uint64) []byte { return append(intervalPrefix, intervalKey...) } +// GetBlockIterator returns a block iterator that will produce values +// corresponding to persisted blocks in order of increasing height. +func GetBlockIterator(db database.Iteratee) database.Iterator { + return db.NewIteratorWithPrefix(blockPrefix) +} + func GetBlock(db database.KeyValueReader, height uint64) ([]byte, error) { return db.Get(makeBlockKey(height)) } From 8134e16af0e10253e073ad318a5d9ed843a1d90c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 25 Mar 2024 15:04:54 -0400 Subject: [PATCH 37/68] nit --- .../snowman/bootstrap/interval/blocks.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 8124f96e5cac..85f5d7994027 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -93,6 +93,18 @@ func Add( return height != lastHeightToFetch && !tree.Contains(height-1), nil } +// Remove the block from the tree. +func Remove( + db database.KeyValueWriterDeleter, + tree *Tree, + height uint64, +) error { + if err := DeleteBlock(db, height); err != nil { + return err + } + return tree.Remove(db, height) +} + // Execute all the blocks tracked by the tree. If a block is in the tree but is // already accepted based on the lastAcceptedHeight, it will be removed from the // tree but not executed. @@ -145,11 +157,7 @@ func Execute( } height := blk.Height() - if err := DeleteBlock(batch, height); err != nil { - return err - } - - if err := tree.Remove(batch, height); err != nil { + if err := Remove(batch, tree, height); err != nil { return err } From 9991e39e8f8129888251348021f6337677ca1b8c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 25 Mar 2024 15:25:18 -0400 Subject: [PATCH 38/68] Move GetMissingBlockIDs and Execute out of the interval package --- .../snowman/bootstrap/interval/blocks.go | 193 ---------- .../snowman/bootstrap/interval/blocks_test.go | 291 ++------------- snow/engine/snowman/bootstrap/storage.go | 202 +++++++++++ snow/engine/snowman/bootstrap/storage_test.go | 338 ++++++++++++++++++ 4 files changed, 561 insertions(+), 463 deletions(-) create mode 100644 snow/engine/snowman/bootstrap/storage.go create mode 100644 snow/engine/snowman/bootstrap/storage_test.go diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 85f5d7994027..5a00e96bafca 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -4,67 +4,10 @@ package interval import ( - "context" - "fmt" - "time" - - "go.uber.org/zap" - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/avalanchego/utils/timer" -) - -const ( - batchWritePeriod = 64 - iteratorReleasePeriod = 1024 - logPeriod = 5 * time.Second ) -// GetMissingBlockIDs returns the ID of the blocks that should be fetched to -// attempt to make a single continuous range from -// (lastAcceptedHeight, highestTrackedHeight]. -// -// For example, if the tree currently contains heights [1, 4, 6, 7] and the -// lastAcceptedHeight is 2, this function will return the IDs corresponding to -// blocks [3, 5]. -func GetMissingBlockIDs( - ctx context.Context, - db database.KeyValueReader, - parser block.Parser, - tree *Tree, - lastAcceptedHeight uint64, -) (set.Set[ids.ID], error) { - var ( - missingBlocks set.Set[ids.ID] - intervals = tree.Flatten() - lastHeightToFetch = lastAcceptedHeight + 1 - ) - for _, i := range intervals { - if i.LowerBound <= lastHeightToFetch { - continue - } - - blkBytes, err := GetBlock(db, i.LowerBound) - if err != nil { - return nil, err - } - - blk, err := parser.ParseBlock(ctx, blkBytes) - if err != nil { - return nil, err - } - - parentID := blk.Parent() - missingBlocks.Add(parentID) - } - return missingBlocks, nil -} - // Add the block to the tree and return if the parent block should be fetched, // but wasn't desired before. func Add( @@ -104,139 +47,3 @@ func Remove( } return tree.Remove(db, height) } - -// Execute all the blocks tracked by the tree. If a block is in the tree but is -// already accepted based on the lastAcceptedHeight, it will be removed from the -// tree but not executed. -// -// Execute assumes that GetMissingBlockIDs would return an empty set. -func Execute( - ctx context.Context, - log logging.Func, - db database.Database, - parser block.Parser, - tree *Tree, - lastAcceptedHeight uint64, -) error { - var ( - batch = db.NewBatch() - processedSinceBatchWrite uint - writeBatch = func() error { - if processedSinceBatchWrite == 0 { - return nil - } - processedSinceBatchWrite = 0 - - if err := batch.Write(); err != nil { - return err - } - batch.Reset() - return nil - } - - iterator = GetBlockIterator(db) - processedSinceIteratorRelease uint - - startTime = time.Now() - timeOfNextLog = startTime.Add(logPeriod) - totalNumberToProcess = tree.Len() - ) - defer func() { - iterator.Release() - }() - - log("executing blocks", - zap.Uint64("numToExecute", totalNumberToProcess), - ) - - for ctx.Err() == nil && iterator.Next() { - blkBytes := iterator.Value() - blk, err := parser.ParseBlock(ctx, blkBytes) - if err != nil { - return err - } - - height := blk.Height() - if err := Remove(batch, tree, height); err != nil { - return err - } - - // Periodically write the batch to disk to avoid memory pressure. - processedSinceBatchWrite++ - if processedSinceBatchWrite >= batchWritePeriod { - if err := writeBatch(); err != nil { - return err - } - } - - // Periodically release and re-grab the database iterator to avoid - // keeping a reference to an old database revision. - processedSinceIteratorRelease++ - if processedSinceIteratorRelease >= iteratorReleasePeriod { - if err := iterator.Error(); err != nil { - return err - } - - // The batch must be written here to avoid re-processing a block. - if err := writeBatch(); err != nil { - return err - } - - processedSinceIteratorRelease = 0 - iterator.Release() - iterator = GetBlockIterator(db) - } - - now := time.Now() - if now.After(timeOfNextLog) { - var ( - numProcessed = totalNumberToProcess - tree.Len() - eta = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) - ) - - log("executing blocks", - zap.Duration("eta", eta), - zap.Uint64("numExecuted", numProcessed), - zap.Uint64("numToExecute", totalNumberToProcess), - ) - timeOfNextLog = now.Add(logPeriod) - } - - if height <= lastAcceptedHeight { - continue - } - - if err := blk.Verify(ctx); err != nil { - return fmt.Errorf("failed to verify block %s (%d) in bootstrapping: %w", - blk.ID(), - height, - err, - ) - } - if err := blk.Accept(ctx); err != nil { - return fmt.Errorf("failed to accept block %s (%d) in bootstrapping: %w", - blk.ID(), - height, - err, - ) - } - } - if err := writeBatch(); err != nil { - return err - } - if err := iterator.Error(); err != nil { - return err - } - - var ( - numProcessed = totalNumberToProcess - tree.Len() - err = ctx.Err() - ) - log("executed blocks", - zap.Uint64("numExecuted", numProcessed), - zap.Uint64("numToExecute", totalNumberToProcess), - zap.Duration("duration", time.Since(startTime)), - zap.Error(err), - ) - return err -} diff --git a/snow/engine/snowman/bootstrap/interval/blocks_test.go b/snow/engine/snowman/bootstrap/interval/blocks_test.go index 03492698ecea..9741b29aff48 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks_test.go +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -4,8 +4,6 @@ package interval import ( - "bytes" - "context" "testing" "github.com/stretchr/testify/require" @@ -15,145 +13,9 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" ) -func TestGetMissingBlockIDs(t *testing.T) { - blocks := generateBlockchain(7) - parser := makeParser(blocks) - - db := memdb.New() - tree, err := NewTree(db) - require.NoError(t, err) - lastAcceptedHeight := uint64(1) - - t.Run("initially empty", func(t *testing.T) { - require := require.New(t) - - missing, err := GetMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Empty(missing) - }) - - t.Run("adding first block", func(t *testing.T) { - require := require.New(t) - - _, err := Add( - db, - tree, - lastAcceptedHeight, - blocks[5], - ) - require.NoError(err) - - missing, err := GetMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding second block", func(t *testing.T) { - require := require.New(t) - - _, err := Add( - db, - tree, - lastAcceptedHeight, - blocks[3], - ) - require.NoError(err) - - missing, err := GetMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[2].ID(), - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding last desired block", func(t *testing.T) { - require := require.New(t) - - _, err := Add( - db, - tree, - lastAcceptedHeight, - blocks[2], - ) - require.NoError(err) - - missing, err := GetMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding block with known parent", func(t *testing.T) { - require := require.New(t) - - _, err := Add( - db, - tree, - lastAcceptedHeight, - blocks[6], - ) - require.NoError(err) - - missing, err := GetMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) -} - func TestAdd(t *testing.T) { blocks := generateBlockchain(7) @@ -266,140 +128,46 @@ func TestAdd(t *testing.T) { }) } -func TestExecute(t *testing.T) { - require := require.New(t) - - const numBlocks = 2*max(batchWritePeriod, iteratorReleasePeriod) + 1 - blocks := generateBlockchain(numBlocks) - parser := makeParser(blocks) - - db := memdb.New() - tree, err := NewTree(db) - require.NoError(err) - const lastAcceptedHeight = 1 - - for i, block := range blocks[lastAcceptedHeight+1:] { - newlyWantsParent, err := Add( - db, - tree, - lastAcceptedHeight, - block, - ) - require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) - } - - require.NoError(Execute( - context.Background(), - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeight, - )) - - for _, block := range blocks[lastAcceptedHeight+1:] { - require.Equal(choices.Accepted, block.Status()) - } - - size, err := database.Count(db) - require.NoError(err) - require.Zero(size) -} - -func TestExecuteExitsWhenCancelled(t *testing.T) { - require := require.New(t) - +func TestRemove(t *testing.T) { blocks := generateBlockchain(7) - parser := makeParser(blocks) db := memdb.New() tree, err := NewTree(db) - require.NoError(err) - const lastAcceptedHeight = 1 + require.NoError(t, err) + lastAcceptedHeight := uint64(1) + + t.Run("adding a block", func(t *testing.T) { + require := require.New(t) - for i, block := range blocks[lastAcceptedHeight+1:] { newlyWantsParent, err := Add( db, tree, lastAcceptedHeight, - block, + blocks[5], ) require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) - } - - startSize, err := database.Count(db) - require.NoError(err) - - ctx, cancel := context.WithCancel(context.Background()) - cancel() - err = Execute( - ctx, - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeight, - ) - require.ErrorIs(err, context.Canceled) - - for _, block := range blocks[lastAcceptedHeight+1:] { - require.Equal(choices.Processing, block.Status()) - } - - endSize, err := database.Count(db) - require.NoError(err) - require.Equal(startSize, endSize) -} - -func TestExecuteSkipsAcceptedBlocks(t *testing.T) { - require := require.New(t) + require.True(newlyWantsParent) + require.Equal(uint64(1), tree.Len()) - blocks := generateBlockchain(7) - parser := makeParser(blocks) + bytes, err := GetBlock(db, blocks[5].Height()) + require.NoError(err) + require.Equal(blocks[5].Bytes(), bytes) + }) - db := memdb.New() - tree, err := NewTree(db) - require.NoError(err) - const ( - lastAcceptedHeightWhenAdding = 1 - lastAcceptedHeightWhenExecuting = 3 - ) + t.Run("removing a block", func(t *testing.T) { + require := require.New(t) - for i, block := range blocks[lastAcceptedHeightWhenAdding+1:] { - newlyWantsParent, err := Add( + err := Remove( db, tree, - lastAcceptedHeightWhenAdding, - block, + blocks[5].Height(), ) require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) - } - - require.NoError(Execute( - context.Background(), - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeightWhenExecuting, - )) - - for _, block := range blocks[lastAcceptedHeightWhenAdding+1 : lastAcceptedHeightWhenExecuting] { - require.Equal(choices.Processing, block.Status()) - } - for _, block := range blocks[lastAcceptedHeightWhenExecuting+1:] { - require.Equal(choices.Accepted, block.Status()) - } + require.Zero(tree.Len()) - size, err := database.Count(db) - require.NoError(err) - require.Zero(size) + _, err = GetBlock(db, blocks[5].Height()) + require.ErrorIs(err, database.ErrNotFound) + }) } func generateBlockchain(length uint64) []snowman.Block { @@ -430,20 +198,3 @@ func generateBlockchain(length uint64) []snowman.Block { } return blocks } - -type testParser func(context.Context, []byte) (snowman.Block, error) - -func (f testParser) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { - return f(ctx, bytes) -} - -func makeParser(blocks []snowman.Block) block.Parser { - return testParser(func(_ context.Context, b []byte) (snowman.Block, error) { - for _, block := range blocks { - if bytes.Equal(b, block.Bytes()) { - return block, nil - } - } - return nil, database.ErrNotFound - }) -} diff --git a/snow/engine/snowman/bootstrap/storage.go b/snow/engine/snowman/bootstrap/storage.go new file mode 100644 index 000000000000..47317abb1aff --- /dev/null +++ b/snow/engine/snowman/bootstrap/storage.go @@ -0,0 +1,202 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrap + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer" +) + +const ( + batchWritePeriod = 64 + iteratorReleasePeriod = 1024 + logPeriod = 5 * time.Second +) + +// getMissingBlockIDs returns the ID of the blocks that should be fetched to +// attempt to make a single continuous range from +// (lastAcceptedHeight, highestTrackedHeight]. +// +// For example, if the tree currently contains heights [1, 4, 6, 7] and the +// lastAcceptedHeight is 2, this function will return the IDs corresponding to +// blocks [3, 5]. +func getMissingBlockIDs( + ctx context.Context, + db database.KeyValueReader, + parser block.Parser, + tree *interval.Tree, + lastAcceptedHeight uint64, +) (set.Set[ids.ID], error) { + var ( + missingBlocks set.Set[ids.ID] + intervals = tree.Flatten() + lastHeightToFetch = lastAcceptedHeight + 1 + ) + for _, i := range intervals { + if i.LowerBound <= lastHeightToFetch { + continue + } + + blkBytes, err := interval.GetBlock(db, i.LowerBound) + if err != nil { + return nil, err + } + + blk, err := parser.ParseBlock(ctx, blkBytes) + if err != nil { + return nil, err + } + + parentID := blk.Parent() + missingBlocks.Add(parentID) + } + return missingBlocks, nil +} + +// execute all the blocks tracked by the tree. If a block is in the tree but is +// already accepted based on the lastAcceptedHeight, it will be removed from the +// tree but not executed. +// +// execute assumes that getMissingBlockIDs would return an empty set. +func execute( + ctx context.Context, + log logging.Func, + db database.Database, + parser block.Parser, + tree *interval.Tree, + lastAcceptedHeight uint64, +) error { + var ( + batch = db.NewBatch() + processedSinceBatchWrite uint + writeBatch = func() error { + if processedSinceBatchWrite == 0 { + return nil + } + processedSinceBatchWrite = 0 + + if err := batch.Write(); err != nil { + return err + } + batch.Reset() + return nil + } + + iterator = interval.GetBlockIterator(db) + processedSinceIteratorRelease uint + + startTime = time.Now() + timeOfNextLog = startTime.Add(logPeriod) + totalNumberToProcess = tree.Len() + ) + defer func() { + iterator.Release() + }() + + log("executing blocks", + zap.Uint64("numToExecute", totalNumberToProcess), + ) + + for ctx.Err() == nil && iterator.Next() { + blkBytes := iterator.Value() + blk, err := parser.ParseBlock(ctx, blkBytes) + if err != nil { + return err + } + + height := blk.Height() + if err := interval.Remove(batch, tree, height); err != nil { + return err + } + + // Periodically write the batch to disk to avoid memory pressure. + processedSinceBatchWrite++ + if processedSinceBatchWrite >= batchWritePeriod { + if err := writeBatch(); err != nil { + return err + } + } + + // Periodically release and re-grab the database iterator to avoid + // keeping a reference to an old database revision. + processedSinceIteratorRelease++ + if processedSinceIteratorRelease >= iteratorReleasePeriod { + if err := iterator.Error(); err != nil { + return err + } + + // The batch must be written here to avoid re-processing a block. + if err := writeBatch(); err != nil { + return err + } + + processedSinceIteratorRelease = 0 + iterator.Release() + iterator = interval.GetBlockIterator(db) + } + + now := time.Now() + if now.After(timeOfNextLog) { + var ( + numProcessed = totalNumberToProcess - tree.Len() + eta = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) + ) + + log("executing blocks", + zap.Duration("eta", eta), + zap.Uint64("numExecuted", numProcessed), + zap.Uint64("numToExecute", totalNumberToProcess), + ) + timeOfNextLog = now.Add(logPeriod) + } + + if height <= lastAcceptedHeight { + continue + } + + if err := blk.Verify(ctx); err != nil { + return fmt.Errorf("failed to verify block %s (%d) in bootstrapping: %w", + blk.ID(), + height, + err, + ) + } + if err := blk.Accept(ctx); err != nil { + return fmt.Errorf("failed to accept block %s (%d) in bootstrapping: %w", + blk.ID(), + height, + err, + ) + } + } + if err := writeBatch(); err != nil { + return err + } + if err := iterator.Error(); err != nil { + return err + } + + var ( + numProcessed = totalNumberToProcess - tree.Len() + err = ctx.Err() + ) + log("executed blocks", + zap.Uint64("numExecuted", numProcessed), + zap.Uint64("numToExecute", totalNumberToProcess), + zap.Duration("duration", time.Since(startTime)), + zap.Error(err), + ) + return err +} diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go new file mode 100644 index 000000000000..0dbb0eb3ed88 --- /dev/null +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -0,0 +1,338 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrap + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" +) + +func TestGetMissingBlockIDs(t *testing.T) { + blocks := generateBlockchain(7) + parser := makeParser(blocks) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(t, err) + lastAcceptedHeight := uint64(1) + + t.Run("initially empty", func(t *testing.T) { + require := require.New(t) + + missing, err := getMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Empty(missing) + }) + + t.Run("adding first block", func(t *testing.T) { + require := require.New(t) + + _, err := interval.Add( + db, + tree, + lastAcceptedHeight, + blocks[5], + ) + require.NoError(err) + + missing, err := getMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Equal( + set.Of( + blocks[4].ID(), + ), + missing, + ) + }) + + t.Run("adding second block", func(t *testing.T) { + require := require.New(t) + + _, err := interval.Add( + db, + tree, + lastAcceptedHeight, + blocks[3], + ) + require.NoError(err) + + missing, err := getMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Equal( + set.Of( + blocks[2].ID(), + blocks[4].ID(), + ), + missing, + ) + }) + + t.Run("adding last desired block", func(t *testing.T) { + require := require.New(t) + + _, err := interval.Add( + db, + tree, + lastAcceptedHeight, + blocks[2], + ) + require.NoError(err) + + missing, err := getMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Equal( + set.Of( + blocks[4].ID(), + ), + missing, + ) + }) + + t.Run("adding block with known parent", func(t *testing.T) { + require := require.New(t) + + _, err := interval.Add( + db, + tree, + lastAcceptedHeight, + blocks[6], + ) + require.NoError(err) + + missing, err := getMissingBlockIDs( + context.Background(), + db, + parser, + tree, + lastAcceptedHeight, + ) + require.NoError(err) + require.Equal( + set.Of( + blocks[4].ID(), + ), + missing, + ) + }) +} + +func TestExecute(t *testing.T) { + require := require.New(t) + + const numBlocks = 2*max(batchWritePeriod, iteratorReleasePeriod) + 1 + blocks := generateBlockchain(numBlocks) + parser := makeParser(blocks) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) + const lastAcceptedHeight = 1 + + for i, block := range blocks[lastAcceptedHeight+1:] { + newlyWantsParent, err := interval.Add( + db, + tree, + lastAcceptedHeight, + block, + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(i+1), tree.Len()) + } + + require.NoError(execute( + context.Background(), + logging.NoLog{}.Info, + db, + parser, + tree, + lastAcceptedHeight, + )) + + for _, block := range blocks[lastAcceptedHeight+1:] { + require.Equal(choices.Accepted, block.Status()) + } + + size, err := database.Count(db) + require.NoError(err) + require.Zero(size) +} + +func TestExecuteExitsWhenCancelled(t *testing.T) { + require := require.New(t) + + blocks := generateBlockchain(7) + parser := makeParser(blocks) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) + const lastAcceptedHeight = 1 + + for i, block := range blocks[lastAcceptedHeight+1:] { + newlyWantsParent, err := interval.Add( + db, + tree, + lastAcceptedHeight, + block, + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(i+1), tree.Len()) + } + + startSize, err := database.Count(db) + require.NoError(err) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err = execute( + ctx, + logging.NoLog{}.Info, + db, + parser, + tree, + lastAcceptedHeight, + ) + require.ErrorIs(err, context.Canceled) + + for _, block := range blocks[lastAcceptedHeight+1:] { + require.Equal(choices.Processing, block.Status()) + } + + endSize, err := database.Count(db) + require.NoError(err) + require.Equal(startSize, endSize) +} + +func TestExecuteSkipsAcceptedBlocks(t *testing.T) { + require := require.New(t) + + blocks := generateBlockchain(7) + parser := makeParser(blocks) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) + const ( + lastAcceptedHeightWhenAdding = 1 + lastAcceptedHeightWhenExecuting = 3 + ) + + for i, block := range blocks[lastAcceptedHeightWhenAdding+1:] { + newlyWantsParent, err := interval.Add( + db, + tree, + lastAcceptedHeightWhenAdding, + block, + ) + require.NoError(err) + require.False(newlyWantsParent) + require.Equal(uint64(i+1), tree.Len()) + } + + require.NoError(execute( + context.Background(), + logging.NoLog{}.Info, + db, + parser, + tree, + lastAcceptedHeightWhenExecuting, + )) + + for _, block := range blocks[lastAcceptedHeightWhenAdding+1 : lastAcceptedHeightWhenExecuting] { + require.Equal(choices.Processing, block.Status()) + } + for _, block := range blocks[lastAcceptedHeightWhenExecuting+1:] { + require.Equal(choices.Accepted, block.Status()) + } + + size, err := database.Count(db) + require.NoError(err) + require.Zero(size) +} + +func generateBlockchain(length uint64) []snowman.Block { + if length == 0 { + return nil + } + + blocks := make([]snowman.Block, length) + blocks[0] = &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.GenerateTestID(), + StatusV: choices.Processing, + }, + ParentV: ids.Empty, + HeightV: 0, + BytesV: utils.RandomBytes(1024), + } + for height := uint64(1); height < length; height++ { + blocks[height] = &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.GenerateTestID(), + StatusV: choices.Processing, + }, + ParentV: blocks[height-1].ID(), + HeightV: height, + BytesV: utils.RandomBytes(1024), + } + } + return blocks +} + +type testParser func(context.Context, []byte) (snowman.Block, error) + +func (f testParser) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { + return f(ctx, bytes) +} + +func makeParser(blocks []snowman.Block) block.Parser { + return testParser(func(_ context.Context, b []byte) (snowman.Block, error) { + for _, block := range blocks { + if bytes.Equal(b, block.Bytes()) { + return block, nil + } + } + return nil, database.ErrNotFound + }) +} From a7e24582883dcac182219e0e087cfa915156e7a9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 25 Mar 2024 16:02:20 -0400 Subject: [PATCH 39/68] wip --- snow/engine/snowman/bootstrap/bootstrapper.go | 98 +++++++------------ snow/engine/snowman/bootstrap/storage.go | 47 +++++++++ 2 files changed, 85 insertions(+), 60 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index c4151a2d5aa8..15c4a036121e 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -177,7 +177,7 @@ func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { return fmt.Errorf("failed to initialize interval tree: %w", err) } - b.missingBlockIDs, err = interval.GetMissingBlockIDs(ctx, b.DB, b.VM, b.tree, b.startingHeight) + b.missingBlockIDs, err = getMissingBlockIDs(ctx, b.DB, b.VM, b.tree, b.startingHeight) if err != nil { return fmt.Errorf("failed to initialize missing block IDs: %w", err) } @@ -555,96 +555,74 @@ func (b *Bootstrapper) markUnavailable(nodeID ids.NodeID) { // // - blk is a block that is assumed to have been marked as acceptable by the // bootstrapping engine. -// - processingBlocks is a set of blocks that can be used to lookup blocks. -// This enables the engine to process multiple blocks without relying on the -// VM to have stored blocks during `ParseBlock`. -// -// If [blk]'s height is <= the last accepted height, then it will be removed -// from the missingIDs set. -func (b *Bootstrapper) process(ctx context.Context, blk snowman.Block, processingBlocks map[ids.ID]snowman.Block) error { +// - ancestors is a set of blocks that can be used to optimisically lookup +// parent blocks. This enables the engine to process multiple blocks without +// relying on the VM to have stored blocks during `ParseBlock`. +func (b *Bootstrapper) process( + ctx context.Context, + blk snowman.Block, + ancestors map[ids.ID]snowman.Block, +) error { lastAcceptedHeight, err := b.getLastAcceptedHeight(ctx) if err != nil { return err } + numPreviouslyFetched := b.tree.Len() + batch := b.DB.NewBatch() - for { - if b.Halted() { - return batch.Write() - } + missingBlockID, foundNewMissingID, err := process( + batch, + b.tree, + blk, + ancestors, + b.missingBlockIDs, + lastAcceptedHeight, + ) + if err != nil { + return err + } - blkID := blk.ID() - b.missingBlockIDs.Remove(blkID) + // Update metrics and log statuses + { + numFetched := b.tree.Len() + b.numFetched.Add(float64(b.tree.Len() - numPreviouslyFetched)) height := blk.Height() b.tipHeight = max(b.tipHeight, height) - wantsParent, err := interval.Add(batch, b.tree, lastAcceptedHeight, blk) - if err != nil { - return err - } - - // We added a new block to the queue, so track that it was fetched - b.numFetched.Inc() - - // Periodically log progress - blocksFetchedSoFar := b.tree.Len() - if blocksFetchedSoFar%statusUpdateFrequency == 0 { + if numFetched%statusUpdateFrequency == 0 { totalBlocksToFetch := b.tipHeight - b.startingHeight eta := timer.EstimateETA( b.startTime, - blocksFetchedSoFar-b.initiallyFetched, // Number of blocks we have fetched during this run + numFetched-b.initiallyFetched, // Number of blocks we have fetched during this run totalBlocksToFetch-b.initiallyFetched, // Number of blocks we expect to fetch during this run ) b.fetchETA.Set(float64(eta)) if !b.restarted { b.Ctx.Log.Info("fetching blocks", - zap.Uint64("numFetchedBlocks", blocksFetchedSoFar), + zap.Uint64("numFetchedBlocks", numFetched), zap.Uint64("numTotalBlocks", totalBlocksToFetch), zap.Duration("eta", eta), ) } else { b.Ctx.Log.Debug("fetching blocks", - zap.Uint64("numFetchedBlocks", blocksFetchedSoFar), + zap.Uint64("numFetchedBlocks", numFetched), zap.Uint64("numTotalBlocks", totalBlocksToFetch), zap.Duration("eta", eta), ) } } + } - if !wantsParent { - return batch.Write() - } - - // Attempt to traverse to the next block - parentID := blk.Parent() - b.missingBlockIDs.Add(parentID) - - // First check if the parent is in the processing blocks set - parent, ok := processingBlocks[parentID] - if ok { - blk = parent - continue - } - - // If the parent is not available in processing blocks, attempt to get - // the block from the vm - parent, err = b.VM.GetBlock(ctx, parentID) - if err == nil { - blk = parent - continue - } - // TODO: report errors that aren't `database.ErrNotFound` - - // If the block wasn't able to be acquired immediately, attempt to fetch - // it - if err := b.fetch(ctx, parentID); err != nil { - return err - } - - return batch.Write() + if err := batch.Write(); err != nil || !foundNewMissingID { + return err } + + b.missingBlockIDs.Add(missingBlockID) + // Attempt to fetch the newly discovered block + return b.fetch(ctx, missingBlockID) } // tryStartExecuting executes all pending blocks if there are no more blocks @@ -670,7 +648,7 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { } numToExecute := b.tree.Len() - err = interval.Execute( + err = execute( &haltableContext{ Context: ctx, Haltable: b, diff --git a/snow/engine/snowman/bootstrap/storage.go b/snow/engine/snowman/bootstrap/storage.go index 47317abb1aff..b5b5c4971ddd 100644 --- a/snow/engine/snowman/bootstrap/storage.go +++ b/snow/engine/snowman/bootstrap/storage.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" "github.com/ava-labs/avalanchego/utils/logging" @@ -65,6 +66,52 @@ func getMissingBlockIDs( return missingBlocks, nil } +// process a series of consecutive blocks starting at [blk]. +// +// - blk is a block that is assumed to have been marked as acceptable by the +// bootstrapping engine. +// - ancestors is a set of blocks that can be used to lookup blocks. +// +// If [blk]'s height is <= the last accepted height, then it will be removed +// from the missingIDs set. +// +// Returns a newly discovered blockID that should be fetched. +func process( + db database.KeyValueWriterDeleter, + tree *interval.Tree, + blk snowman.Block, + ancestors map[ids.ID]snowman.Block, + missingBlockIDs set.Set[ids.ID], + lastAcceptedHeight uint64, +) (ids.ID, bool, error) { + for { + // It's possible that missingBlockIDs contain values contained inside of + // ancestors. So, it's important to remove IDs from the set for each + // iteration, not just the first block's ID. + blkID := blk.ID() + missingBlockIDs.Remove(blkID) + + wantsParent, err := interval.Add(db, tree, lastAcceptedHeight, blk) + if err != nil { + return ids.Empty, false, err + } + + if !wantsParent { + return ids.Empty, false, nil + } + + // If the parent was provided in the ancestors set, we can immediately + // process it. + parentID := blk.Parent() + parent, ok := ancestors[parentID] + if !ok { + return parentID, true, nil + } + + blk = parent + } +} + // execute all the blocks tracked by the tree. If a block is in the tree but is // already accepted based on the lastAcceptedHeight, it will be removed from the // tree but not executed. From f1dd0a60d0335ee85f9a367c8c55fea19827b268 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 25 Mar 2024 23:59:21 -0400 Subject: [PATCH 40/68] nit --- snow/engine/snowman/bootstrap/interval/blocks_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks_test.go b/snow/engine/snowman/bootstrap/interval/blocks_test.go index 9741b29aff48..9c04264e1845 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks_test.go +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -157,12 +157,11 @@ func TestRemove(t *testing.T) { t.Run("removing a block", func(t *testing.T) { require := require.New(t) - err := Remove( + require.NoError(Remove( db, tree, blocks[5].Height(), - ) - require.NoError(err) + )) require.Zero(tree.Len()) _, err = GetBlock(db, blocks[5].Height()) From bf29d869cb7a54e4ad64b3c4c9e3abd7253f833c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:01:11 -0400 Subject: [PATCH 41/68] remove storage --- snow/engine/snowman/bootstrap/storage.go | 202 ----------- snow/engine/snowman/bootstrap/storage_test.go | 338 ------------------ 2 files changed, 540 deletions(-) delete mode 100644 snow/engine/snowman/bootstrap/storage.go delete mode 100644 snow/engine/snowman/bootstrap/storage_test.go diff --git a/snow/engine/snowman/bootstrap/storage.go b/snow/engine/snowman/bootstrap/storage.go deleted file mode 100644 index 47317abb1aff..000000000000 --- a/snow/engine/snowman/bootstrap/storage.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package bootstrap - -import ( - "context" - "fmt" - "time" - - "go.uber.org/zap" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/avalanchego/utils/timer" -) - -const ( - batchWritePeriod = 64 - iteratorReleasePeriod = 1024 - logPeriod = 5 * time.Second -) - -// getMissingBlockIDs returns the ID of the blocks that should be fetched to -// attempt to make a single continuous range from -// (lastAcceptedHeight, highestTrackedHeight]. -// -// For example, if the tree currently contains heights [1, 4, 6, 7] and the -// lastAcceptedHeight is 2, this function will return the IDs corresponding to -// blocks [3, 5]. -func getMissingBlockIDs( - ctx context.Context, - db database.KeyValueReader, - parser block.Parser, - tree *interval.Tree, - lastAcceptedHeight uint64, -) (set.Set[ids.ID], error) { - var ( - missingBlocks set.Set[ids.ID] - intervals = tree.Flatten() - lastHeightToFetch = lastAcceptedHeight + 1 - ) - for _, i := range intervals { - if i.LowerBound <= lastHeightToFetch { - continue - } - - blkBytes, err := interval.GetBlock(db, i.LowerBound) - if err != nil { - return nil, err - } - - blk, err := parser.ParseBlock(ctx, blkBytes) - if err != nil { - return nil, err - } - - parentID := blk.Parent() - missingBlocks.Add(parentID) - } - return missingBlocks, nil -} - -// execute all the blocks tracked by the tree. If a block is in the tree but is -// already accepted based on the lastAcceptedHeight, it will be removed from the -// tree but not executed. -// -// execute assumes that getMissingBlockIDs would return an empty set. -func execute( - ctx context.Context, - log logging.Func, - db database.Database, - parser block.Parser, - tree *interval.Tree, - lastAcceptedHeight uint64, -) error { - var ( - batch = db.NewBatch() - processedSinceBatchWrite uint - writeBatch = func() error { - if processedSinceBatchWrite == 0 { - return nil - } - processedSinceBatchWrite = 0 - - if err := batch.Write(); err != nil { - return err - } - batch.Reset() - return nil - } - - iterator = interval.GetBlockIterator(db) - processedSinceIteratorRelease uint - - startTime = time.Now() - timeOfNextLog = startTime.Add(logPeriod) - totalNumberToProcess = tree.Len() - ) - defer func() { - iterator.Release() - }() - - log("executing blocks", - zap.Uint64("numToExecute", totalNumberToProcess), - ) - - for ctx.Err() == nil && iterator.Next() { - blkBytes := iterator.Value() - blk, err := parser.ParseBlock(ctx, blkBytes) - if err != nil { - return err - } - - height := blk.Height() - if err := interval.Remove(batch, tree, height); err != nil { - return err - } - - // Periodically write the batch to disk to avoid memory pressure. - processedSinceBatchWrite++ - if processedSinceBatchWrite >= batchWritePeriod { - if err := writeBatch(); err != nil { - return err - } - } - - // Periodically release and re-grab the database iterator to avoid - // keeping a reference to an old database revision. - processedSinceIteratorRelease++ - if processedSinceIteratorRelease >= iteratorReleasePeriod { - if err := iterator.Error(); err != nil { - return err - } - - // The batch must be written here to avoid re-processing a block. - if err := writeBatch(); err != nil { - return err - } - - processedSinceIteratorRelease = 0 - iterator.Release() - iterator = interval.GetBlockIterator(db) - } - - now := time.Now() - if now.After(timeOfNextLog) { - var ( - numProcessed = totalNumberToProcess - tree.Len() - eta = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) - ) - - log("executing blocks", - zap.Duration("eta", eta), - zap.Uint64("numExecuted", numProcessed), - zap.Uint64("numToExecute", totalNumberToProcess), - ) - timeOfNextLog = now.Add(logPeriod) - } - - if height <= lastAcceptedHeight { - continue - } - - if err := blk.Verify(ctx); err != nil { - return fmt.Errorf("failed to verify block %s (%d) in bootstrapping: %w", - blk.ID(), - height, - err, - ) - } - if err := blk.Accept(ctx); err != nil { - return fmt.Errorf("failed to accept block %s (%d) in bootstrapping: %w", - blk.ID(), - height, - err, - ) - } - } - if err := writeBatch(); err != nil { - return err - } - if err := iterator.Error(); err != nil { - return err - } - - var ( - numProcessed = totalNumberToProcess - tree.Len() - err = ctx.Err() - ) - log("executed blocks", - zap.Uint64("numExecuted", numProcessed), - zap.Uint64("numToExecute", totalNumberToProcess), - zap.Duration("duration", time.Since(startTime)), - zap.Error(err), - ) - return err -} diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go deleted file mode 100644 index 0dbb0eb3ed88..000000000000 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package bootstrap - -import ( - "bytes" - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/choices" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" - "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" -) - -func TestGetMissingBlockIDs(t *testing.T) { - blocks := generateBlockchain(7) - parser := makeParser(blocks) - - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(t, err) - lastAcceptedHeight := uint64(1) - - t.Run("initially empty", func(t *testing.T) { - require := require.New(t) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Empty(missing) - }) - - t.Run("adding first block", func(t *testing.T) { - require := require.New(t) - - _, err := interval.Add( - db, - tree, - lastAcceptedHeight, - blocks[5], - ) - require.NoError(err) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding second block", func(t *testing.T) { - require := require.New(t) - - _, err := interval.Add( - db, - tree, - lastAcceptedHeight, - blocks[3], - ) - require.NoError(err) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[2].ID(), - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding last desired block", func(t *testing.T) { - require := require.New(t) - - _, err := interval.Add( - db, - tree, - lastAcceptedHeight, - blocks[2], - ) - require.NoError(err) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding block with known parent", func(t *testing.T) { - require := require.New(t) - - _, err := interval.Add( - db, - tree, - lastAcceptedHeight, - blocks[6], - ) - require.NoError(err) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) -} - -func TestExecute(t *testing.T) { - require := require.New(t) - - const numBlocks = 2*max(batchWritePeriod, iteratorReleasePeriod) + 1 - blocks := generateBlockchain(numBlocks) - parser := makeParser(blocks) - - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(err) - const lastAcceptedHeight = 1 - - for i, block := range blocks[lastAcceptedHeight+1:] { - newlyWantsParent, err := interval.Add( - db, - tree, - lastAcceptedHeight, - block, - ) - require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) - } - - require.NoError(execute( - context.Background(), - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeight, - )) - - for _, block := range blocks[lastAcceptedHeight+1:] { - require.Equal(choices.Accepted, block.Status()) - } - - size, err := database.Count(db) - require.NoError(err) - require.Zero(size) -} - -func TestExecuteExitsWhenCancelled(t *testing.T) { - require := require.New(t) - - blocks := generateBlockchain(7) - parser := makeParser(blocks) - - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(err) - const lastAcceptedHeight = 1 - - for i, block := range blocks[lastAcceptedHeight+1:] { - newlyWantsParent, err := interval.Add( - db, - tree, - lastAcceptedHeight, - block, - ) - require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) - } - - startSize, err := database.Count(db) - require.NoError(err) - - ctx, cancel := context.WithCancel(context.Background()) - cancel() - err = execute( - ctx, - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeight, - ) - require.ErrorIs(err, context.Canceled) - - for _, block := range blocks[lastAcceptedHeight+1:] { - require.Equal(choices.Processing, block.Status()) - } - - endSize, err := database.Count(db) - require.NoError(err) - require.Equal(startSize, endSize) -} - -func TestExecuteSkipsAcceptedBlocks(t *testing.T) { - require := require.New(t) - - blocks := generateBlockchain(7) - parser := makeParser(blocks) - - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(err) - const ( - lastAcceptedHeightWhenAdding = 1 - lastAcceptedHeightWhenExecuting = 3 - ) - - for i, block := range blocks[lastAcceptedHeightWhenAdding+1:] { - newlyWantsParent, err := interval.Add( - db, - tree, - lastAcceptedHeightWhenAdding, - block, - ) - require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) - } - - require.NoError(execute( - context.Background(), - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeightWhenExecuting, - )) - - for _, block := range blocks[lastAcceptedHeightWhenAdding+1 : lastAcceptedHeightWhenExecuting] { - require.Equal(choices.Processing, block.Status()) - } - for _, block := range blocks[lastAcceptedHeightWhenExecuting+1:] { - require.Equal(choices.Accepted, block.Status()) - } - - size, err := database.Count(db) - require.NoError(err) - require.Zero(size) -} - -func generateBlockchain(length uint64) []snowman.Block { - if length == 0 { - return nil - } - - blocks := make([]snowman.Block, length) - blocks[0] = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: ids.Empty, - HeightV: 0, - BytesV: utils.RandomBytes(1024), - } - for height := uint64(1); height < length; height++ { - blocks[height] = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: blocks[height-1].ID(), - HeightV: height, - BytesV: utils.RandomBytes(1024), - } - } - return blocks -} - -type testParser func(context.Context, []byte) (snowman.Block, error) - -func (f testParser) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { - return f(ctx, bytes) -} - -func makeParser(blocks []snowman.Block) block.Parser { - return testParser(func(_ context.Context, b []byte) (snowman.Block, error) { - for _, block := range blocks { - if bytes.Equal(b, block.Bytes()) { - return block, nil - } - } - return nil, database.ErrNotFound - }) -} From 5a8fadcda784adb6f32d9f966b6567e856109973 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:16:40 -0400 Subject: [PATCH 42/68] nit --- .../snowman/bootstrap/interval/blocks.go | 15 ++--- .../snowman/bootstrap/interval/blocks_test.go | 65 +++++++------------ 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 5a00e96bafca..db953f569b68 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -5,7 +5,6 @@ package interval import ( "github.com/ava-labs/avalanchego/database" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" ) // Add the block to the tree and return if the parent block should be fetched, @@ -14,17 +13,13 @@ func Add( db database.KeyValueWriterDeleter, tree *Tree, lastAcceptedHeight uint64, - blk snowman.Block, + height uint64, + blkBytes []byte, ) (bool, error) { - var ( - height = blk.Height() - lastHeightToFetch = lastAcceptedHeight + 1 - ) - if height < lastHeightToFetch || tree.Contains(height) { + if height <= lastAcceptedHeight || tree.Contains(height) { return false, nil } - blkBytes := blk.Bytes() if err := PutBlock(db, height, blkBytes); err != nil { return false, err } @@ -33,7 +28,9 @@ func Add( return false, err } - return height != lastHeightToFetch && !tree.Contains(height-1), nil + // We know that height is greater than lastAcceptedHeight here, so + // lastAcceptedHeight+1 is guaranteed not to overflow. + return height != lastAcceptedHeight+1 && !tree.Contains(height-1), nil } // Remove the block from the tree. diff --git a/snow/engine/snowman/bootstrap/interval/blocks_test.go b/snow/engine/snowman/bootstrap/interval/blocks_test.go index 9c04264e1845..5bd0f9b050fd 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks_test.go +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -10,9 +10,6 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/choices" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/utils" ) @@ -31,15 +28,16 @@ func TestAdd(t *testing.T) { db, tree, lastAcceptedHeight, + 5, blocks[5], ) require.NoError(err) require.True(newlyWantsParent) require.Equal(uint64(1), tree.Len()) - bytes, err := GetBlock(db, blocks[5].Height()) + bytes, err := GetBlock(db, 5) require.NoError(err) - require.Equal(blocks[5].Bytes(), bytes) + require.Equal(blocks[5], bytes) }) t.Run("adding duplicate block", func(t *testing.T) { @@ -49,6 +47,7 @@ func TestAdd(t *testing.T) { db, tree, lastAcceptedHeight, + 5, blocks[5], ) require.NoError(err) @@ -63,15 +62,16 @@ func TestAdd(t *testing.T) { db, tree, lastAcceptedHeight, + 3, blocks[3], ) require.NoError(err) require.True(newlyWantsParent) require.Equal(uint64(2), tree.Len()) - bytes, err := GetBlock(db, blocks[3].Height()) + bytes, err := GetBlock(db, 3) require.NoError(err) - require.Equal(blocks[3].Bytes(), bytes) + require.Equal(blocks[3], bytes) }) t.Run("adding last desired block", func(t *testing.T) { @@ -81,15 +81,16 @@ func TestAdd(t *testing.T) { db, tree, lastAcceptedHeight, + 2, blocks[2], ) require.NoError(err) require.False(newlyWantsParent) require.Equal(uint64(3), tree.Len()) - bytes, err := GetBlock(db, blocks[2].Height()) + bytes, err := GetBlock(db, 2) require.NoError(err) - require.Equal(blocks[2].Bytes(), bytes) + require.Equal(blocks[2], bytes) }) t.Run("adding undesired block", func(t *testing.T) { @@ -99,13 +100,14 @@ func TestAdd(t *testing.T) { db, tree, lastAcceptedHeight, + 1, blocks[1], ) require.NoError(err) require.False(newlyWantsParent) require.Equal(uint64(3), tree.Len()) - _, err = GetBlock(db, blocks[1].Height()) + _, err = GetBlock(db, 1) require.ErrorIs(err, database.ErrNotFound) }) @@ -116,15 +118,16 @@ func TestAdd(t *testing.T) { db, tree, lastAcceptedHeight, + 6, blocks[6], ) require.NoError(err) require.False(newlyWantsParent) require.Equal(uint64(4), tree.Len()) - bytes, err := GetBlock(db, blocks[6].Height()) + bytes, err := GetBlock(db, 6) require.NoError(err) - require.Equal(blocks[6].Bytes(), bytes) + require.Equal(blocks[6], bytes) }) } @@ -143,15 +146,16 @@ func TestRemove(t *testing.T) { db, tree, lastAcceptedHeight, + 5, blocks[5], ) require.NoError(err) require.True(newlyWantsParent) require.Equal(uint64(1), tree.Len()) - bytes, err := GetBlock(db, blocks[5].Height()) + bytes, err := GetBlock(db, 5) require.NoError(err) - require.Equal(blocks[5].Bytes(), bytes) + require.Equal(blocks[5], bytes) }) t.Run("removing a block", func(t *testing.T) { @@ -160,40 +164,19 @@ func TestRemove(t *testing.T) { require.NoError(Remove( db, tree, - blocks[5].Height(), + 5, )) require.Zero(tree.Len()) - _, err = GetBlock(db, blocks[5].Height()) + _, err = GetBlock(db, 5) require.ErrorIs(err, database.ErrNotFound) }) } -func generateBlockchain(length uint64) []snowman.Block { - if length == 0 { - return nil - } - - blocks := make([]snowman.Block, length) - blocks[0] = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: ids.Empty, - HeightV: 0, - BytesV: utils.RandomBytes(1024), - } - for height := uint64(1); height < length; height++ { - blocks[height] = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: blocks[height-1].ID(), - HeightV: height, - BytesV: utils.RandomBytes(1024), - } +func generateBlockchain(length uint64) [][]byte { + blocks := make([][]byte, length) + for i := range blocks { + blocks[i] = utils.RandomBytes(1024) } return blocks } From 69ed897acb1115e80dada84f64e8ea2e22606b46 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:17:41 -0400 Subject: [PATCH 43/68] nit --- snow/engine/snowman/bootstrap/interval/blocks.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index db953f569b68..0a7ca95c0801 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -3,9 +3,7 @@ package interval -import ( - "github.com/ava-labs/avalanchego/database" -) +import "github.com/ava-labs/avalanchego/database" // Add the block to the tree and return if the parent block should be fetched, // but wasn't desired before. From 9815047188cfe49f942b23bb893de4e0978d2c78 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:19:06 -0400 Subject: [PATCH 44/68] nit --- snow/engine/snowman/bootstrap/interval/blocks.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 0a7ca95c0801..248e5b31ac92 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -26,9 +26,10 @@ func Add( return false, err } - // We know that height is greater than lastAcceptedHeight here, so - // lastAcceptedHeight+1 is guaranteed not to overflow. - return height != lastAcceptedHeight+1 && !tree.Contains(height-1), nil + // We know that height is greater than lastAcceptedHeight here, so height-1 + // is guaranteed not to underflow. + nextHeight := height - 1 + return nextHeight != lastAcceptedHeight && !tree.Contains(nextHeight), nil } // Remove the block from the tree. From 5bf4899d57079a038c1b15c9fb704aa765eb82e5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:19:29 -0400 Subject: [PATCH 45/68] nit --- snow/engine/snowman/bootstrap/interval/blocks.go | 1 - 1 file changed, 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go index 248e5b31ac92..d7d053c17876 100644 --- a/snow/engine/snowman/bootstrap/interval/blocks.go +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -21,7 +21,6 @@ func Add( if err := PutBlock(db, height, blkBytes); err != nil { return false, err } - if err := tree.Add(db, height); err != nil { return false, err } From 9da3bc1e3d4169ee2c39663d1dd90f5a2eab94db Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:25:15 -0400 Subject: [PATCH 46/68] nit --- .../snowman/bootstrap/interval/tree_test.go | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/snow/engine/snowman/bootstrap/interval/tree_test.go b/snow/engine/snowman/bootstrap/interval/tree_test.go index b0a13d5ed59c..396e4d281a91 100644 --- a/snow/engine/snowman/bootstrap/interval/tree_test.go +++ b/snow/engine/snowman/bootstrap/interval/tree_test.go @@ -4,6 +4,7 @@ package interval import ( + "math" "testing" "github.com/stretchr/testify/require" @@ -366,3 +367,23 @@ func TestTreeContains(t *testing.T) { }) } } + +func TestTreeLenOverflow(t *testing.T) { + require := require.New(t) + + db := memdb.New() + require.NoError(PutInterval(db, math.MaxUint64, 0)) + + tree, err := NewTree(db) + require.NoError(err) + require.Zero(tree.Len()) + require.True(tree.Contains(0)) + require.True(tree.Contains(math.MaxUint64 / 2)) + require.True(tree.Contains(math.MaxUint64)) + + require.NoError(tree.Remove(db, 5)) + require.Equal(uint64(math.MaxUint64), tree.Len()) + + require.NoError(tree.Add(db, 5)) + require.Zero(tree.Len()) +} From bd5687a163d6b778b7e529c12de460dc65fd99d9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:29:15 -0400 Subject: [PATCH 47/68] nit --- snow/engine/snowman/bootstrap/storage.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/snow/engine/snowman/bootstrap/storage.go b/snow/engine/snowman/bootstrap/storage.go index b5b5c4971ddd..4f6ae615cde5 100644 --- a/snow/engine/snowman/bootstrap/storage.go +++ b/snow/engine/snowman/bootstrap/storage.go @@ -91,15 +91,19 @@ func process( blkID := blk.ID() missingBlockIDs.Remove(blkID) - wantsParent, err := interval.Add(db, tree, lastAcceptedHeight, blk) - if err != nil { + height := blk.Height() + blkBytes := blk.Bytes() + wantsParent, err := interval.Add( + db, + tree, + lastAcceptedHeight, + height, + blkBytes, + ) + if err != nil || !wantsParent { return ids.Empty, false, err } - if !wantsParent { - return ids.Empty, false, nil - } - // If the parent was provided in the ancestors set, we can immediately // process it. parentID := blk.Parent() From b6e3ee14d6702d8c4d11f79d88e679f50b95f945 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:33:14 -0400 Subject: [PATCH 48/68] merged --- .../snowman/bootstrap/bootstrapper_test.go | 2 +- snow/engine/snowman/bootstrap/storage_test.go | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index d8dca8f69122..37853ae2c698 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -1378,7 +1378,7 @@ func TestBootstrapNoParseOnNew(t *testing.T) { intervalDB := memdb.New() tree, err := interval.NewTree(intervalDB) require.NoError(err) - _, err = interval.Add(intervalDB, tree, 0, blk1) + _, err = interval.Add(intervalDB, tree, 0, blk1.Height(), blk1.Bytes()) require.NoError(err) vm.GetBlockF = nil diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index 0dbb0eb3ed88..a0ceebaf1577 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -52,7 +52,8 @@ func TestGetMissingBlockIDs(t *testing.T) { db, tree, lastAcceptedHeight, - blocks[5], + 5, + blocks[5].Bytes(), ) require.NoError(err) @@ -79,7 +80,8 @@ func TestGetMissingBlockIDs(t *testing.T) { db, tree, lastAcceptedHeight, - blocks[3], + 3, + blocks[3].Bytes(), ) require.NoError(err) @@ -107,7 +109,8 @@ func TestGetMissingBlockIDs(t *testing.T) { db, tree, lastAcceptedHeight, - blocks[2], + 2, + blocks[2].Bytes(), ) require.NoError(err) @@ -134,7 +137,8 @@ func TestGetMissingBlockIDs(t *testing.T) { db, tree, lastAcceptedHeight, - blocks[6], + 6, + blocks[6].Bytes(), ) require.NoError(err) @@ -172,7 +176,8 @@ func TestExecute(t *testing.T) { db, tree, lastAcceptedHeight, - block, + block.Height(), + block.Bytes(), ) require.NoError(err) require.False(newlyWantsParent) @@ -213,7 +218,8 @@ func TestExecuteExitsWhenCancelled(t *testing.T) { db, tree, lastAcceptedHeight, - block, + block.Height(), + block.Bytes(), ) require.NoError(err) require.False(newlyWantsParent) @@ -263,7 +269,8 @@ func TestExecuteSkipsAcceptedBlocks(t *testing.T) { db, tree, lastAcceptedHeightWhenAdding, - block, + block.Height(), + block.Bytes(), ) require.NoError(err) require.False(newlyWantsParent) From c376df21bedc3f046f0d3fa9f8bdacc83bfc0b98 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:55:47 -0400 Subject: [PATCH 49/68] add process test --- snow/engine/snowman/bootstrap/storage_test.go | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index a0ceebaf1577..e36016ea30ae 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -159,6 +159,76 @@ func TestGetMissingBlockIDs(t *testing.T) { }) } +func TestProcess(t *testing.T) { + blocks := generateBlockchain(7) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(t, err) + lastAcceptedHeight := uint64(1) + + t.Run("adding first block", func(t *testing.T) { + require := require.New(t) + + missingIDs := set.Of(blocks[6].ID()) + parentID, shouldFetchParentID, err := process( + db, + tree, + blocks[6], + map[ids.ID]snowman.Block{ + blocks[2].ID(): blocks[2], + }, + missingIDs, + lastAcceptedHeight, + ) + require.NoError(err) + require.True(shouldFetchParentID) + require.Equal(blocks[5].ID(), parentID) + require.Equal(uint64(1), tree.Len()) + require.Empty(missingIDs) + }) + + t.Run("adding multiple blocks", func(t *testing.T) { + require := require.New(t) + + missingIDs := set.Of(blocks[5].ID()) + parentID, shouldFetchParentID, err := process( + db, + tree, + blocks[5], + map[ids.ID]snowman.Block{ + blocks[4].ID(): blocks[4], + blocks[3].ID(): blocks[3], + }, + missingIDs, + lastAcceptedHeight, + ) + require.NoError(err) + require.True(shouldFetchParentID) + require.Equal(blocks[2].ID(), parentID) + require.Equal(uint64(4), tree.Len()) + require.Empty(missingIDs) + }) + + t.Run("do not request last accepted block", func(t *testing.T) { + require := require.New(t) + + missingIDs := set.Of(blocks[2].ID()) + _, shouldFetchParentID, err := process( + db, + tree, + blocks[2], + nil, + missingIDs, + lastAcceptedHeight, + ) + require.NoError(err) + require.False(shouldFetchParentID) + require.Equal(uint64(5), tree.Len()) + require.Empty(missingIDs) + }) +} + func TestExecute(t *testing.T) { require := require.New(t) From 53bdb78305674d3108c17beacc3005c8d0dc16e5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:59:27 -0400 Subject: [PATCH 50/68] nit --- snow/engine/snowman/bootstrap/bootstrapper.go | 4 ++-- snow/engine/snowman/bootstrap/storage_test.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 15c4a036121e..43b0c0b723c0 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -574,10 +574,10 @@ func (b *Bootstrapper) process( missingBlockID, foundNewMissingID, err := process( batch, b.tree, - blk, - ancestors, b.missingBlockIDs, lastAcceptedHeight, + blk, + ancestors, ) if err != nil { return err diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index e36016ea30ae..92d927578437 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -174,12 +174,12 @@ func TestProcess(t *testing.T) { parentID, shouldFetchParentID, err := process( db, tree, + missingIDs, + lastAcceptedHeight, blocks[6], map[ids.ID]snowman.Block{ blocks[2].ID(): blocks[2], }, - missingIDs, - lastAcceptedHeight, ) require.NoError(err) require.True(shouldFetchParentID) @@ -195,13 +195,13 @@ func TestProcess(t *testing.T) { parentID, shouldFetchParentID, err := process( db, tree, + missingIDs, + lastAcceptedHeight, blocks[5], map[ids.ID]snowman.Block{ blocks[4].ID(): blocks[4], blocks[3].ID(): blocks[3], }, - missingIDs, - lastAcceptedHeight, ) require.NoError(err) require.True(shouldFetchParentID) @@ -217,10 +217,10 @@ func TestProcess(t *testing.T) { _, shouldFetchParentID, err := process( db, tree, - blocks[2], - nil, missingIDs, lastAcceptedHeight, + blocks[2], + nil, ) require.NoError(err) require.False(shouldFetchParentID) From 73108325db1f35131bc4011a8656d12060875fb8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 00:59:46 -0400 Subject: [PATCH 51/68] nit --- snow/engine/snowman/bootstrap/storage.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snow/engine/snowman/bootstrap/storage.go b/snow/engine/snowman/bootstrap/storage.go index 4f6ae615cde5..764d74a7cfcf 100644 --- a/snow/engine/snowman/bootstrap/storage.go +++ b/snow/engine/snowman/bootstrap/storage.go @@ -79,10 +79,10 @@ func getMissingBlockIDs( func process( db database.KeyValueWriterDeleter, tree *interval.Tree, - blk snowman.Block, - ancestors map[ids.ID]snowman.Block, missingBlockIDs set.Set[ids.ID], lastAcceptedHeight uint64, + blk snowman.Block, + ancestors map[ids.ID]snowman.Block, ) (ids.ID, bool, error) { for { // It's possible that missingBlockIDs contain values contained inside of From 7de549931fcdf7a09135665b1526cf6faca9f598 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 11:12:16 -0400 Subject: [PATCH 52/68] wip --- .../snowman/bootstrap/bootstrapper_test.go | 591 +++++++----------- 1 file changed, 243 insertions(+), 348 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index 37853ae2c698..24173ce9cf0b 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -220,14 +220,40 @@ func TestBootstrapperSingleFrontier(t *testing.T) { HeightV: 1, BytesV: blkBytes1, } + blks := map[ids.ID]snowman.Block{ + blkID0: blk0, + blkID1: blk1, + } - vm.CantLastAccepted = false + vm.CantSetState = false vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil + var ( + lastAcceptedID = blk0.ID() + lastAcceptedHeight = blk0.Height() + ) + for blkID, blk := range blks { + height := blk.Height() + if blk.Status() == choices.Accepted && height > lastAcceptedHeight { + lastAcceptedID = blkID + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil } vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil + if blk, ok := blks[blkID]; ok { + return blk, nil + } + return nil, database.ErrNotFound + } + vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { + for _, blk := range blks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } + } + require.FailNow(errUnknownBlock.Error()) + return nil, errUnknownBlock } bs, err := New( @@ -242,41 +268,15 @@ func TestBootstrapperSingleFrontier(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) acceptedIDs := []ids.ID{blkID1} - - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID1: - return blk1, nil - case blkID0: - return blk0, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes1): - return blk1, nil - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) require.Equal(choices.Accepted, blk1.Status()) } -// Requests the unknown block and gets back a Ancestors with unexpected request ID. -// Requests again and gets response from unexpected peer. -// Requests again and gets an unexpected block. +// Requests the unknown block and gets back a Ancestors with unexpected block. // Requests again and gets the expected block. func TestBootstrapperUnknownByzantineResponse(t *testing.T) { require := require.New(t) @@ -285,11 +285,9 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { blkID0 := ids.Empty.Prefix(0) blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) blkBytes0 := []byte{0} blkBytes1 := []byte{1} - blkBytes2 := []byte{2} blk0 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ @@ -302,30 +300,46 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { blk1 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID1, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk0.IDV, HeightV: 1, BytesV: blkBytes1, } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, + blks := map[ids.ID]snowman.Block{ + blkID0: blk0, + blkID1: blk1, } vm.CantSetState = false - vm.CantLastAccepted = false vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil + var ( + lastAcceptedID = blk0.ID() + lastAcceptedHeight = blk0.Height() + ) + for blkID, blk := range blks { + height := blk.Height() + if blk.Status() == choices.Accepted && height > lastAcceptedHeight { + lastAcceptedID = blkID + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil } vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil + if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { + return blk, nil + } + return nil, database.ErrNotFound + } + vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { + for _, blk := range blks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } + } + require.FailNow(errUnknownBlock.Error()) + return nil, errUnknownBlock } bs, err := New( @@ -342,38 +356,6 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { require.NoError(bs.Start(context.Background(), 0)) - parsedBlk1 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - return blk2, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - return blk2, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - var requestID uint32 sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { require.Equal(peerID, vdr) @@ -382,7 +364,7 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { } vm.CantSetState = false - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID2})) // should request blk1 + require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1})) // should request blk1 oldReqID := requestID require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes0})) // respond with wrong block @@ -393,9 +375,8 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) require.Equal(choices.Accepted, blk0.Status()) require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID2})) + require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1})) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } @@ -426,7 +407,7 @@ func TestBootstrapperPartialFetch(t *testing.T) { blk1 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID1, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk0.IDV, HeightV: 1, @@ -435,7 +416,7 @@ func TestBootstrapperPartialFetch(t *testing.T) { blk2 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID2, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk1.IDV, HeightV: 2, @@ -450,14 +431,42 @@ func TestBootstrapperPartialFetch(t *testing.T) { HeightV: 3, BytesV: blkBytes3, } + blks := map[ids.ID]snowman.Block{ + blkID0: blk0, + blkID1: blk1, + blkID2: blk2, + blkID3: blk3, + } - vm.CantLastAccepted = false + vm.CantSetState = false vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil + var ( + lastAcceptedID = blk0.ID() + lastAcceptedHeight = blk0.Height() + ) + for blkID, blk := range blks { + height := blk.Height() + if blk.Status() == choices.Accepted && height > lastAcceptedHeight { + lastAcceptedID = blkID + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil } vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil + if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { + return blk, nil + } + return nil, database.ErrNotFound + } + vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { + for _, blk := range blks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } + } + require.FailNow(errUnknownBlock.Error()) + return nil, errUnknownBlock } bs, err := New( @@ -472,69 +481,27 @@ func TestBootstrapperPartialFetch(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - acceptedIDs := []ids.ID{blkID3} - - parsedBlk1 := false - parsedBlk2 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - case blkID3: - return blk3, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - case bytes.Equal(blkBytes, blkBytes3): - return blk3, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - - requestID := new(uint32) - requested := ids.Empty - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) - require.Contains([]ids.ID{blkID1, blkID2}, blkID) - *requestID = reqID + var ( + requestID uint32 + requested ids.ID + ) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) + require.Contains([]ids.ID{blkID1, blkID2, blkID3}, blkID) + requestID = reqID requested = blkID } - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk2 + acceptedIDs := []ids.ID{blkID3} + require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk3 + require.Equal(blkID3, requested) - require.NoError(bs.Ancestors(context.Background(), peerID, *requestID, [][]byte{blkBytes2})) // respond with blk2 + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes3, blkBytes2})) // respond with blk3 and blk2 require.Equal(blkID1, requested) - require.NoError(bs.Ancestors(context.Background(), peerID, *requestID, [][]byte{blkBytes1})) // respond with blk1 - require.Equal(blkID1, requested) + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes1})) // respond with blk1 require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) require.Equal(choices.Accepted, blk0.Status()) @@ -545,8 +512,8 @@ func TestBootstrapperPartialFetch(t *testing.T) { require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } -// There are multiple needed blocks and some validators do not have all the blocks -// This test was modeled after TestBootstrapperPartialFetch. +// There are multiple needed blocks and some validators do not have all the +// blocks. func TestBootstrapperEmptyResponse(t *testing.T) { require := require.New(t) @@ -554,13 +521,9 @@ func TestBootstrapperEmptyResponse(t *testing.T) { blkID0 := ids.Empty.Prefix(0) blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - blkID3 := ids.Empty.Prefix(3) blkBytes0 := []byte{0} blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - blkBytes3 := []byte{3} blk0 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ @@ -573,38 +536,46 @@ func TestBootstrapperEmptyResponse(t *testing.T) { blk1 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID1, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk0.IDV, HeightV: 1, BytesV: blkBytes1, } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Unknown, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blk3 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID3, - StatusV: choices.Processing, - }, - ParentV: blk2.IDV, - HeightV: 3, - BytesV: blkBytes3, + blks := map[ids.ID]snowman.Block{ + blkID0: blk0, + blkID1: blk1, } - vm.CantLastAccepted = false + vm.CantSetState = false vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil + var ( + lastAcceptedID = blk0.ID() + lastAcceptedHeight = blk0.Height() + ) + for blkID, blk := range blks { + height := blk.Height() + if blk.Status() == choices.Accepted && height > lastAcceptedHeight { + lastAcceptedID = blkID + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil } vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil + if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { + return blk, nil + } + return nil, database.ErrNotFound + } + vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { + for _, blk := range blks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } + } + require.FailNow(errUnknownBlock.Error()) + return nil, errUnknownBlock } bs, err := New( @@ -619,93 +590,40 @@ func TestBootstrapperEmptyResponse(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - acceptedIDs := []ids.ID{blkID3} - - parsedBlk1 := false - parsedBlk2 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - case blkID3: - return blk3, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - case bytes.Equal(blkBytes, blkBytes3): - return blk3, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - - requestedVdr := ids.EmptyNodeID - requestID := uint32(0) - requestedBlock := ids.Empty - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - requestedVdr = vdr + var ( + requestedNodeID ids.NodeID + requestID uint32 + requestedBlockID ids.ID + ) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(blkID1, blkID) + requestedNodeID = nodeID requestID = reqID - requestedBlock = blkID + requestedBlockID = blkID } - // should request blk2 - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) - require.Equal(peerID, requestedVdr) - require.Equal(blkID2, requestedBlock) - - // add another two validators to the fetch set to test behavior on empty response - newPeerID := ids.GenerateTestNodeID() - bs.fetchFrom.Add(newPeerID) - - newPeerID = ids.GenerateTestNodeID() - bs.fetchFrom.Add(newPeerID) - - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes2})) - require.Equal(blkID1, requestedBlock) - - peerToBlacklist := requestedVdr + acceptedIDs := []ids.ID{blkID1} + require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk3 + require.Equal(requestedNodeID, peerID) + require.Equal(blkID1, requestedBlockID) - // respond with empty - require.NoError(bs.Ancestors(context.Background(), peerToBlacklist, requestID, nil)) - require.NotEqual(peerToBlacklist, requestedVdr) - require.Equal(blkID1, requestedBlock) + // add another 2 validators to the fetch set to test behavior on empty + // response + bs.fetchFrom.Add(ids.GenerateTestNodeID(), ids.GenerateTestNodeID()) - require.NoError(bs.Ancestors(context.Background(), requestedVdr, requestID, [][]byte{blkBytes1})) // respond with blk1 + require.NoError(bs.Ancestors(context.Background(), requestedNodeID, requestID, nil)) // respond with empty + require.NotEqual(requestedNodeID, peerID) + require.Equal(blkID1, requestedBlockID) + require.NoError(bs.Ancestors(context.Background(), requestedNodeID, requestID, [][]byte{blkBytes1})) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) require.Equal(choices.Accepted, blk0.Status()) require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) - // check peerToBlacklist was removed from the fetch set - require.NotContains(bs.fetchFrom, peerToBlacklist) + // check that peerID was removed from the fetch set + require.NotContains(bs.fetchFrom, peerID) } // There are multiple needed blocks and Ancestors returns all at once @@ -735,7 +653,7 @@ func TestBootstrapperAncestors(t *testing.T) { blk1 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID1, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk0.IDV, HeightV: 1, @@ -744,7 +662,7 @@ func TestBootstrapperAncestors(t *testing.T) { blk2 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID2, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk1.IDV, HeightV: 2, @@ -759,15 +677,42 @@ func TestBootstrapperAncestors(t *testing.T) { HeightV: 3, BytesV: blkBytes3, } + blks := map[ids.ID]snowman.Block{ + blkID0: blk0, + blkID1: blk1, + blkID2: blk2, + blkID3: blk3, + } vm.CantSetState = false - vm.CantLastAccepted = false vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil + var ( + lastAcceptedID = blk0.ID() + lastAcceptedHeight = blk0.Height() + ) + for blkID, blk := range blks { + height := blk.Height() + if blk.Status() == choices.Accepted && height > lastAcceptedHeight { + lastAcceptedID = blkID + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil } vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil + if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { + return blk, nil + } + return nil, database.ErrNotFound + } + vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { + for _, blk := range blks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } + } + require.FailNow(errUnknownBlock.Error()) + return nil, errUnknownBlock } bs, err := New( @@ -784,67 +729,28 @@ func TestBootstrapperAncestors(t *testing.T) { require.NoError(bs.Start(context.Background(), 0)) - acceptedIDs := []ids.ID{blkID3} - - parsedBlk1 := false - parsedBlk2 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - case blkID3: - return blk3, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - case bytes.Equal(blkBytes, blkBytes3): - return blk3, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - - requestID := new(uint32) - requested := ids.Empty + var ( + requestID uint32 + requested ids.ID + ) sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { require.Equal(peerID, vdr) - require.Contains([]ids.ID{blkID1, blkID2}, blkID) - *requestID = reqID + require.Equal(blkID3, blkID) + requestID = reqID requested = blkID } - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk2 - require.NoError(bs.Ancestors(context.Background(), peerID, *requestID, [][]byte{blkBytes2, blkBytes1})) // respond with blk2 and blk1 - require.Equal(blkID2, requested) + acceptedIDs := []ids.ID{blkID3} + require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk3 + require.Equal(blkID3, requested) + + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes3, blkBytes2, blkBytes1, blkBytes0})) // respond with all the blocks require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) require.Equal(choices.Accepted, blk0.Status()) require.Equal(choices.Accepted, blk1.Status()) require.Equal(choices.Accepted, blk2.Status()) + require.Equal(choices.Accepted, blk3.Status()) require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) @@ -874,7 +780,7 @@ func TestBootstrapperFinalized(t *testing.T) { blk1 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID1, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk0.IDV, HeightV: 1, @@ -883,21 +789,49 @@ func TestBootstrapperFinalized(t *testing.T) { blk2 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID2, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk1.IDV, HeightV: 2, BytesV: blkBytes2, } + blks := map[ids.ID]snowman.Block{ + blkID0: blk0, + blkID1: blk1, + blkID2: blk2, + } - vm.CantLastAccepted = false + vm.CantSetState = false vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil + var ( + lastAcceptedID = blk0.ID() + lastAcceptedHeight = blk0.Height() + ) + for blkID, blk := range blks { + height := blk.Height() + if blk.Status() == choices.Accepted && height > lastAcceptedHeight { + lastAcceptedID = blkID + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil } vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil + if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { + return blk, nil + } + return nil, database.ErrNotFound + } + vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { + for _, blk := range blks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } + } + require.FailNow(errUnknownBlock.Error()) + return nil, errUnknownBlock } + bs, err := New( config, func(context.Context, uint32) error { @@ -910,54 +844,15 @@ func TestBootstrapperFinalized(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - parsedBlk1 := false - parsedBlk2 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - requestIDs := map[ids.ID]uint32{} sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { require.Equal(peerID, vdr) requestIDs[blkID] = reqID } - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1, blkID2})) // should request blk2 and blk1 + require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1, blkID2})) // should request blk1 and blk2 reqIDBlk2, ok := requestIDs[blkID2] require.True(ok) From 4f88827f7845b2c5b6511eaa91ee577dfe94c837 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 11:21:56 -0400 Subject: [PATCH 53/68] wip --- .../snowman/bootstrap/bootstrapper_test.go | 105 +++++++----------- 1 file changed, 40 insertions(+), 65 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index 24173ce9cf0b..3c5fa925c853 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -363,7 +363,6 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { requestID = reqID } - vm.CantSetState = false require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1})) // should request blk1 oldReqID := requestID @@ -896,7 +895,7 @@ func TestRestartBootstrapping(t *testing.T) { blk1 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID1, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk0.IDV, HeightV: 1, @@ -905,7 +904,7 @@ func TestRestartBootstrapping(t *testing.T) { blk2 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID2, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk1.IDV, HeightV: 2, @@ -914,7 +913,7 @@ func TestRestartBootstrapping(t *testing.T) { blk3 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID3, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk2.IDV, HeightV: 3, @@ -923,70 +922,46 @@ func TestRestartBootstrapping(t *testing.T) { blk4 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: blkID4, - StatusV: choices.Unknown, + StatusV: choices.Processing, }, ParentV: blk3.IDV, HeightV: 4, BytesV: blkBytes4, } + blks := map[ids.ID]snowman.Block{ + blkID0: blk0, + blkID1: blk1, + blkID2: blk2, + blkID3: blk3, + blkID4: blk4, + } - vm.CantLastAccepted = false + vm.CantSetState = false vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil + var ( + lastAcceptedID = blk0.ID() + lastAcceptedHeight = blk0.Height() + ) + for blkID, blk := range blks { + height := blk.Height() + if blk.Status() == choices.Accepted && height > lastAcceptedHeight { + lastAcceptedID = blkID + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil } - parsedBlk1 := false - parsedBlk2 := false - parsedBlk3 := false - parsedBlk4 := false vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - case blkID3: - if parsedBlk3 { - return blk3, nil - } - return nil, database.ErrNotFound - case blkID4: - if parsedBlk4 { - return blk4, nil - } - return nil, database.ErrNotFound - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound + if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { + return blk, nil } + return nil, database.ErrNotFound } vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - case bytes.Equal(blkBytes, blkBytes3): - blk3.StatusV = choices.Processing - parsedBlk3 = true - return blk3, nil - case bytes.Equal(blkBytes, blkBytes4): - blk4.StatusV = choices.Processing - parsedBlk4 = true - return blk4, nil + for _, blk := range blks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } } require.FailNow(errUnknownBlock.Error()) return nil, errUnknownBlock @@ -1004,29 +979,26 @@ func TestRestartBootstrapping(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) requestIDs := map[ids.ID]uint32{} - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) requestIDs[blkID] = reqID } - // Force Accept blk3 require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID3})) // should request blk3 reqID, ok := requestIDs[blkID3] require.True(ok) require.NoError(bs.Ancestors(context.Background(), peerID, reqID, [][]byte{blkBytes3, blkBytes2})) - require.Contains(requestIDs, blkID1) // Remove request, so we can restart bootstrapping via startSyncing _, removed := bs.outstandingRequests.DeleteValue(blkID1) require.True(removed) - requestIDs = map[ids.ID]uint32{} + clear(requestIDs) require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID4})) @@ -1036,11 +1008,14 @@ func TestRestartBootstrapping(t *testing.T) { require.True(ok) require.NoError(bs.Ancestors(context.Background(), peerID, blk1RequestID, [][]byte{blkBytes1})) - - require.NotEqual(snow.NormalOp, config.Ctx.State.Get().State) + require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) + require.Equal(choices.Accepted, blk0.Status()) + require.Equal(choices.Processing, blk1.Status()) + require.Equal(choices.Processing, blk2.Status()) + require.Equal(choices.Processing, blk3.Status()) + require.Equal(choices.Processing, blk4.Status()) require.NoError(bs.Ancestors(context.Background(), peerID, blk4RequestID, [][]byte{blkBytes4})) - require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) require.Equal(choices.Accepted, blk0.Status()) require.Equal(choices.Accepted, blk1.Status()) From 21d1fd185c0640cc759e7962e6d0b3d3f5c630e4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 12:55:03 -0400 Subject: [PATCH 54/68] remove duplicate code --- .../snowman/bootstrap/bootstrapper_test.go | 938 ++++-------------- snow/engine/snowman/bootstrap/storage_test.go | 30 - 2 files changed, 178 insertions(+), 790 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index 3c5fa925c853..989dc0774ac4 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -197,64 +197,8 @@ func TestBootstrapperSingleFrontier(t *testing.T) { config, _, _, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blks := map[ids.ID]snowman.Block{ - blkID0: blk0, - blkID1: blk1, - } - - vm.CantSetState = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - var ( - lastAcceptedID = blk0.ID() - lastAcceptedHeight = blk0.Height() - ) - for blkID, blk := range blks { - height := blk.Height() - if blk.Status() == choices.Accepted && height > lastAcceptedHeight { - lastAcceptedID = blkID - lastAcceptedHeight = height - } - } - return lastAcceptedID, nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if blk, ok := blks[blkID]; ok { - return blk, nil - } - return nil, database.ErrNotFound - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - for _, blk := range blks { - if bytes.Equal(blk.Bytes(), blkBytes) { - return blk, nil - } - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks := generateBlockchain(1) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -270,10 +214,8 @@ func TestBootstrapperSingleFrontier(t *testing.T) { require.NoError(bs.Start(context.Background(), 0)) - acceptedIDs := []ids.ID{blkID1} - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) - require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk1.Status()) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[0:1]))) + require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } // Requests the unknown block and gets back a Ancestors with unexpected block. @@ -283,64 +225,8 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blks := map[ids.ID]snowman.Block{ - blkID0: blk0, - blkID1: blk1, - } - - vm.CantSetState = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - var ( - lastAcceptedID = blk0.ID() - lastAcceptedHeight = blk0.Height() - ) - for blkID, blk := range blks { - height := blk.Height() - if blk.Status() == choices.Accepted && height > lastAcceptedHeight { - lastAcceptedID = blkID - lastAcceptedHeight = height - } - } - return lastAcceptedID, nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { - return blk, nil - } - return nil, database.ErrNotFound - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - for _, blk := range blks { - if bytes.Equal(blk.Bytes(), blkBytes) { - return blk, nil - } - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks := generateBlockchain(2) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -357,116 +243,35 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { require.NoError(bs.Start(context.Background(), 0)) var requestID uint32 - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) - require.Equal(blkID1, blkID) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) + require.Equal(blks[1].ID(), blkID) requestID = reqID } - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1})) // should request blk1 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:2]))) // should request blk1 oldReqID := requestID - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes0})) // respond with wrong block + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks[0:1]))) // respond with wrong block require.NotEqual(oldReqID, requestID) - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes1})) + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks[1:2]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1})) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:2]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } -// There are multiple needed blocks and Ancestors returns one at a time +// There are multiple needed blocks and multiple Ancestors are required func TestBootstrapperPartialFetch(t *testing.T) { require := require.New(t) config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - blkID3 := ids.Empty.Prefix(3) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - blkBytes3 := []byte{3} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blk3 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID3, - StatusV: choices.Processing, - }, - ParentV: blk2.IDV, - HeightV: 3, - BytesV: blkBytes3, - } - blks := map[ids.ID]snowman.Block{ - blkID0: blk0, - blkID1: blk1, - blkID2: blk2, - blkID3: blk3, - } - - vm.CantSetState = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - var ( - lastAcceptedID = blk0.ID() - lastAcceptedHeight = blk0.Height() - ) - for blkID, blk := range blks { - height := blk.Height() - if blk.Status() == choices.Accepted && height > lastAcceptedHeight { - lastAcceptedID = blkID - lastAcceptedHeight = height - } - } - return lastAcceptedID, nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { - return blk, nil - } - return nil, database.ErrNotFound - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - for _, blk := range blks { - if bytes.Equal(blk.Bytes(), blkBytes) { - return blk, nil - } - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks := generateBlockchain(4) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -488,26 +293,23 @@ func TestBootstrapperPartialFetch(t *testing.T) { ) sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { require.Equal(peerID, nodeID) - require.Contains([]ids.ID{blkID1, blkID2, blkID3}, blkID) + require.Contains([]ids.ID{blks[1].ID(), blks[3].ID()}, blkID) requestID = reqID requested = blkID } - acceptedIDs := []ids.ID{blkID3} - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk3 - require.Equal(blkID3, requested) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) // should request blk3 + require.Equal(blks[3].ID(), requested) - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes3, blkBytes2})) // respond with blk3 and blk2 - require.Equal(blkID1, requested) + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks[2:4]))) // respond with blk3 and blk2 + require.Equal(blks[1].ID(), requested) - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes1})) // respond with blk1 + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks[1:2]))) // respond with blk1 require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } @@ -518,64 +320,8 @@ func TestBootstrapperEmptyResponse(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blks := map[ids.ID]snowman.Block{ - blkID0: blk0, - blkID1: blk1, - } - - vm.CantSetState = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - var ( - lastAcceptedID = blk0.ID() - lastAcceptedHeight = blk0.Height() - ) - for blkID, blk := range blks { - height := blk.Height() - if blk.Status() == choices.Accepted && height > lastAcceptedHeight { - lastAcceptedID = blkID - lastAcceptedHeight = height - } - } - return lastAcceptedID, nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { - return blk, nil - } - return nil, database.ErrNotFound - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - for _, blk := range blks { - if bytes.Equal(blk.Bytes(), blkBytes) { - return blk, nil - } - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks := generateBlockchain(2) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -592,21 +338,17 @@ func TestBootstrapperEmptyResponse(t *testing.T) { require.NoError(bs.Start(context.Background(), 0)) var ( - requestedNodeID ids.NodeID - requestID uint32 - requestedBlockID ids.ID + requestedNodeID ids.NodeID + requestID uint32 ) sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(blkID1, blkID) + require.Equal(blks[1].ID(), blkID) requestedNodeID = nodeID requestID = reqID - requestedBlockID = blkID } - acceptedIDs := []ids.ID{blkID1} - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk3 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:2]))) require.Equal(requestedNodeID, peerID) - require.Equal(blkID1, requestedBlockID) // add another 2 validators to the fetch set to test behavior on empty // response @@ -614,12 +356,10 @@ func TestBootstrapperEmptyResponse(t *testing.T) { require.NoError(bs.Ancestors(context.Background(), requestedNodeID, requestID, nil)) // respond with empty require.NotEqual(requestedNodeID, peerID) - require.Equal(blkID1, requestedBlockID) - require.NoError(bs.Ancestors(context.Background(), requestedNodeID, requestID, [][]byte{blkBytes1})) + require.NoError(bs.Ancestors(context.Background(), requestedNodeID, requestID, blocksToBytes(blks[1:2]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) + requireStatusIs(require, blks, choices.Accepted) // check that peerID was removed from the fetch set require.NotContains(bs.fetchFrom, peerID) @@ -631,88 +371,8 @@ func TestBootstrapperAncestors(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - blkID3 := ids.Empty.Prefix(3) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - blkBytes3 := []byte{3} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blk3 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID3, - StatusV: choices.Processing, - }, - ParentV: blk2.IDV, - HeightV: 3, - BytesV: blkBytes3, - } - blks := map[ids.ID]snowman.Block{ - blkID0: blk0, - blkID1: blk1, - blkID2: blk2, - blkID3: blk3, - } - - vm.CantSetState = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - var ( - lastAcceptedID = blk0.ID() - lastAcceptedHeight = blk0.Height() - ) - for blkID, blk := range blks { - height := blk.Height() - if blk.Status() == choices.Accepted && height > lastAcceptedHeight { - lastAcceptedID = blkID - lastAcceptedHeight = height - } - } - return lastAcceptedID, nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { - return blk, nil - } - return nil, database.ErrNotFound - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - for _, blk := range blks { - if bytes.Equal(blk.Bytes(), blkBytes) { - return blk, nil - } - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks := generateBlockchain(4) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -734,24 +394,20 @@ func TestBootstrapperAncestors(t *testing.T) { ) sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { require.Equal(peerID, vdr) - require.Equal(blkID3, blkID) + require.Equal(blks[3].ID(), blkID) requestID = reqID requested = blkID } - acceptedIDs := []ids.ID{blkID3} - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk3 - require.Equal(blkID3, requested) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) // should request blk3 + require.Equal(blks[3].ID(), requested) - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes3, blkBytes2, blkBytes1, blkBytes0})) // respond with all the blocks + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks))) // respond with all the blocks require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) - require.Equal(choices.Accepted, blk3.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } @@ -760,76 +416,8 @@ func TestBootstrapperFinalized(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blks := map[ids.ID]snowman.Block{ - blkID0: blk0, - blkID1: blk1, - blkID2: blk2, - } - - vm.CantSetState = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - var ( - lastAcceptedID = blk0.ID() - lastAcceptedHeight = blk0.Height() - ) - for blkID, blk := range blks { - height := blk.Height() - if blk.Status() == choices.Accepted && height > lastAcceptedHeight { - lastAcceptedID = blkID - lastAcceptedHeight = height - } - } - return lastAcceptedID, nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { - return blk, nil - } - return nil, database.ErrNotFound - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - for _, blk := range blks { - if bytes.Equal(blk.Bytes(), blkBytes) { - return blk, nil - } - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks := generateBlockchain(3) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -851,19 +439,17 @@ func TestBootstrapperFinalized(t *testing.T) { requestIDs[blkID] = reqID } - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1, blkID2})) // should request blk1 and blk2 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:3]))) // should request blk1 and blk2 - reqIDBlk2, ok := requestIDs[blkID2] + reqIDBlk2, ok := requestIDs[blks[2].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk2, [][]byte{blkBytes2, blkBytes1})) + require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk2, blocksToBytes(blks[1:3]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID2})) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[2:3]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } @@ -872,100 +458,8 @@ func TestRestartBootstrapping(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - blkID3 := ids.Empty.Prefix(3) - blkID4 := ids.Empty.Prefix(4) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - blkBytes3 := []byte{3} - blkBytes4 := []byte{4} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blk3 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID3, - StatusV: choices.Processing, - }, - ParentV: blk2.IDV, - HeightV: 3, - BytesV: blkBytes3, - } - blk4 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID4, - StatusV: choices.Processing, - }, - ParentV: blk3.IDV, - HeightV: 4, - BytesV: blkBytes4, - } - blks := map[ids.ID]snowman.Block{ - blkID0: blk0, - blkID1: blk1, - blkID2: blk2, - blkID3: blk3, - blkID4: blk4, - } - - vm.CantSetState = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - var ( - lastAcceptedID = blk0.ID() - lastAcceptedHeight = blk0.Height() - ) - for blkID, blk := range blks { - height := blk.Height() - if blk.Status() == choices.Accepted && height > lastAcceptedHeight { - lastAcceptedID = blkID - lastAcceptedHeight = height - } - } - return lastAcceptedID, nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - if blk, ok := blks[blkID]; ok && blk.Status() == choices.Accepted { - return blk, nil - } - return nil, database.ErrNotFound - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - for _, blk := range blks { - if bytes.Equal(blk.Bytes(), blkBytes) { - return blk, nil - } - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks := generateBlockchain(5) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -987,43 +481,36 @@ func TestRestartBootstrapping(t *testing.T) { requestIDs[blkID] = reqID } - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID3})) // should request blk3 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) // should request blk3 - reqID, ok := requestIDs[blkID3] + reqID, ok := requestIDs[blks[3].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, reqID, [][]byte{blkBytes3, blkBytes2})) - require.Contains(requestIDs, blkID1) + require.NoError(bs.Ancestors(context.Background(), peerID, reqID, blocksToBytes(blks[2:4]))) + require.Contains(requestIDs, blks[1].ID()) // Remove request, so we can restart bootstrapping via startSyncing - _, removed := bs.outstandingRequests.DeleteValue(blkID1) + _, removed := bs.outstandingRequests.DeleteValue(blks[1].ID()) require.True(removed) clear(requestIDs) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID4})) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[4:5]))) - blk1RequestID, ok := requestIDs[blkID1] + blk1RequestID, ok := requestIDs[blks[1].ID()] require.True(ok) - blk4RequestID, ok := requestIDs[blkID4] + blk4RequestID, ok := requestIDs[blks[4].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, blk1RequestID, [][]byte{blkBytes1})) + require.NoError(bs.Ancestors(context.Background(), peerID, blk1RequestID, blocksToBytes(blks[1:2]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Processing, blk1.Status()) - require.Equal(choices.Processing, blk2.Status()) - require.Equal(choices.Processing, blk3.Status()) - require.Equal(choices.Processing, blk4.Status()) + require.Equal(choices.Accepted, blks[0].Status()) + requireStatusIs(require, blks[1:], choices.Processing) - require.NoError(bs.Ancestors(context.Background(), peerID, blk4RequestID, [][]byte{blkBytes4})) + require.NoError(bs.Ancestors(context.Background(), peerID, blk4RequestID, blocksToBytes(blks[4:5]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) - require.Equal(choices.Accepted, blk3.Status()) - require.Equal(choices.Accepted, blk4.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID4})) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[4:5]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } @@ -1032,48 +519,11 @@ func TestBootstrapOldBlockAfterStateSync(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - HeightV: 0, - BytesV: utils.RandomBytes(32), - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Accepted, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: utils.RandomBytes(32), - } + blks := generateBlockchain(2) + initializeVMWithBlockchain(vm, blks) - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk1.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blk0.ID(): - return nil, database.ErrNotFound - case blk1.ID(): - return blk1, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blk0.Bytes()): - return blk0, nil - case bytes.Equal(blkBytes, blk1.Bytes()): - return blk1, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks[0].(*snowman.TestBlock).StatusV = choices.Processing + require.NoError(blks[1].Accept(context.Background())) bs, err := New( config, @@ -1087,25 +537,24 @@ func TestBootstrapOldBlockAfterStateSync(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) requestIDs := map[ids.ID]uint32{} - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) requestIDs[blkID] = reqID } // Force Accept, the already transitively accepted, blk0 - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blk0.ID()})) // should request blk0 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[0:1]))) // should request blk0 - reqID, ok := requestIDs[blk0.ID()] + reqID, ok := requestIDs[blks[0].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, reqID, [][]byte{blk0.Bytes()})) + require.NoError(bs.Ancestors(context.Background(), peerID, reqID, blocksToBytes(blks[0:1]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) - require.Equal(choices.Processing, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) + require.Equal(choices.Processing, blks[0].Status()) + require.Equal(choices.Accepted, blks[1].Status()) } func TestBootstrapContinueAfterHalt(t *testing.T) { @@ -1113,36 +562,8 @@ func TestBootstrapContinueAfterHalt(t *testing.T) { config, _, _, vm := newConfig(t) - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: utils.RandomBytes(32), - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: utils.RandomBytes(32), - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: utils.RandomBytes(32), - } - - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } + blks := generateBlockchain(2) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -1156,26 +577,15 @@ func TestBootstrapContinueAfterHalt(t *testing.T) { ) require.NoError(err) - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blk0.ID(): - return blk0, nil - case blk1.ID(): - bs.Halt(context.Background()) - return blk1, nil - case blk2.ID(): - return blk2, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } + getBlockF := vm.GetBlockF + vm.GetBlockF = func(ctx context.Context, blkID ids.ID) (snowman.Block, error) { + bs.Halt(ctx) + return getBlockF(ctx, blkID) } - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blk2.ID()})) - + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:2]))) require.Equal(1, bs.missingBlockIDs.Len()) } @@ -1285,50 +695,9 @@ func TestBootstrapperReceiveStaleAncestorsMessage(t *testing.T) { config, peerID, sender, vm := newConfig(t) - var ( - blkID0 = ids.GenerateTestID() - blkBytes0 = utils.RandomBytes(1024) - blk0 = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } + blks := generateBlockchain(3) + initializeVMWithBlockchain(vm, blks) - blkID1 = ids.GenerateTestID() - blkBytes1 = utils.RandomBytes(1024) - blk1 = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: blk0.HeightV + 1, - BytesV: blkBytes1, - } - - blkID2 = ids.GenerateTestID() - blkBytes2 = utils.RandomBytes(1024) - blk2 = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: blk1.HeightV + 1, - BytesV: blkBytes2, - } - ) - - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blkID0, blkID) - return blk0, nil - } bs, err := New( config, func(context.Context, uint32) error { @@ -1341,62 +710,111 @@ func TestBootstrapperReceiveStaleAncestorsMessage(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if blk1.StatusV == choices.Accepted { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if blk2.StatusV == choices.Accepted { - return blk2, nil - } - return nil, database.ErrNotFound - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - return blk2, nil - default: - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - } - requestIDs := map[ids.ID]uint32{} - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) requestIDs[blkID] = reqID } - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1, blkID2})) // should request blk2 and blk1 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:3]))) // should request blk1 and blk2 - reqIDBlk1, ok := requestIDs[blkID1] + reqIDBlk1, ok := requestIDs[blks[1].ID()] require.True(ok) - reqIDBlk2, ok := requestIDs[blkID2] + reqIDBlk2, ok := requestIDs[blks[2].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk2, [][]byte{blkBytes2, blkBytes1})) - + require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk2, blocksToBytes(blks[1:3]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk1, [][]byte{blkBytes1})) + require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk1, blocksToBytes(blks[1:2]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) } + +func generateBlockchain(length uint64) []snowman.Block { + if length == 0 { + return nil + } + + blocks := make([]snowman.Block, length) + blocks[0] = &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.GenerateTestID(), + StatusV: choices.Accepted, + }, + ParentV: ids.Empty, + HeightV: 0, + BytesV: utils.RandomBytes(1024), + } + for height := uint64(1); height < length; height++ { + blocks[height] = &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.GenerateTestID(), + StatusV: choices.Processing, + }, + ParentV: blocks[height-1].ID(), + HeightV: height, + BytesV: utils.RandomBytes(1024), + } + } + return blocks +} + +func initializeVMWithBlockchain(vm *block.TestVM, blocks []snowman.Block) { + vm.CantSetState = false + vm.LastAcceptedF = func(context.Context) (ids.ID, error) { + var ( + lastAcceptedID ids.ID + lastAcceptedHeight uint64 + ) + for _, blk := range blocks { + height := blk.Height() + if blk.Status() == choices.Accepted && height >= lastAcceptedHeight { + lastAcceptedID = blk.ID() + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil + } + vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { + for _, blk := range blocks { + if blk.Status() == choices.Accepted && blk.ID() == blkID { + return blk, nil + } + } + return nil, database.ErrNotFound + } + vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { + for _, blk := range blocks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } + } + return nil, errUnknownBlock + } +} + +func requireStatusIs(require *require.Assertions, blocks []snowman.Block, status choices.Status) { + for i, blk := range blocks { + require.Equal(status, blk.Status(), i) + } +} + +func blocksToIDs(blocks []snowman.Block) []ids.ID { + blkIDs := make([]ids.ID, len(blocks)) + for i, blk := range blocks { + blkIDs[i] = blk.ID() + } + return blkIDs +} + +func blocksToBytes(blocks []snowman.Block) [][]byte { + numBlocks := len(blocks) + blkBytes := make([][]byte, numBlocks) + for i, blk := range blocks { + blkBytes[numBlocks-i-1] = blk.Bytes() + } + return blkBytes +} diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index 92d927578437..a0c88c4f623b 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -17,7 +17,6 @@ import ( "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" - "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" ) @@ -368,35 +367,6 @@ func TestExecuteSkipsAcceptedBlocks(t *testing.T) { require.Zero(size) } -func generateBlockchain(length uint64) []snowman.Block { - if length == 0 { - return nil - } - - blocks := make([]snowman.Block, length) - blocks[0] = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: ids.Empty, - HeightV: 0, - BytesV: utils.RandomBytes(1024), - } - for height := uint64(1); height < length; height++ { - blocks[height] = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: blocks[height-1].ID(), - HeightV: height, - BytesV: utils.RandomBytes(1024), - } - } - return blocks -} - type testParser func(context.Context, []byte) (snowman.Block, error) func (f testParser) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { From dd98c10edb6fa03fdcc28e1addf69bf73c7fec11 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 12:58:34 -0400 Subject: [PATCH 55/68] nit --- snow/engine/snowman/bootstrap/bootstrapper_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index 989dc0774ac4..45ef9435b3c2 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -392,8 +392,8 @@ func TestBootstrapperAncestors(t *testing.T) { requestID uint32 requested ids.ID ) - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) require.Equal(blks[3].ID(), blkID) requestID = reqID requested = blkID @@ -434,8 +434,8 @@ func TestBootstrapperFinalized(t *testing.T) { require.NoError(bs.Start(context.Background(), 0)) requestIDs := map[ids.ID]uint32{} - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) requestIDs[blkID] = reqID } From 81f4b67efe056d19997e75fc286e8d327a538b1f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 13:40:54 -0400 Subject: [PATCH 56/68] nit fix log check --- snow/engine/snowman/bootstrap/bootstrapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 43b0c0b723c0..9f38dd59bfc0 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -591,7 +591,7 @@ func (b *Bootstrapper) process( height := blk.Height() b.tipHeight = max(b.tipHeight, height) - if numFetched%statusUpdateFrequency == 0 { + if numPreviouslyFetched/statusUpdateFrequency != numFetched/statusUpdateFrequency { totalBlocksToFetch := b.tipHeight - b.startingHeight eta := timer.EstimateETA( b.startTime, From 03a22b804d0e0b7bac02123697a152d71116cd4c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 13:54:40 -0400 Subject: [PATCH 57/68] nit --- snow/engine/snowman/bootstrap/storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/storage.go b/snow/engine/snowman/bootstrap/storage.go index 764d74a7cfcf..4c35772bd082 100644 --- a/snow/engine/snowman/bootstrap/storage.go +++ b/snow/engine/snowman/bootstrap/storage.go @@ -206,9 +206,9 @@ func execute( ) log("executing blocks", - zap.Duration("eta", eta), zap.Uint64("numExecuted", numProcessed), zap.Uint64("numToExecute", totalNumberToProcess), + zap.Duration("eta", eta), ) timeOfNextLog = now.Add(logPeriod) } From d753cb552f6d2f544ce791160be504965058b177 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 16:14:58 -0400 Subject: [PATCH 58/68] nit --- snow/engine/snowman/bootstrap/bootstrapper_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index 45ef9435b3c2..d5db4be5ae93 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -6,6 +6,7 @@ package bootstrap import ( "bytes" "context" + "encoding/binary" "errors" "testing" "time" @@ -746,7 +747,7 @@ func generateBlockchain(length uint64) []snowman.Block { }, ParentV: ids.Empty, HeightV: 0, - BytesV: utils.RandomBytes(1024), + BytesV: binary.AppendUvarint(nil, 0), } for height := uint64(1); height < length; height++ { blocks[height] = &snowman.TestBlock{ @@ -756,7 +757,7 @@ func generateBlockchain(length uint64) []snowman.Block { }, ParentV: blocks[height-1].ID(), HeightV: height, - BytesV: utils.RandomBytes(1024), + BytesV: binary.AppendUvarint(nil, height), } } return blocks From aa8d2489b96f1fe4ff271cc3cba684c5a5183efb Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 16:29:54 -0400 Subject: [PATCH 59/68] Remove haltableContext --- snow/engine/snowman/bootstrap/bootstrapper.go | 11 ++----- snow/engine/snowman/bootstrap/context.go | 29 ------------------- snow/engine/snowman/bootstrap/storage.go | 19 ++++++------ 3 files changed, 12 insertions(+), 47 deletions(-) delete mode 100644 snow/engine/snowman/bootstrap/context.go diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 9f38dd59bfc0..aa4e42a24219 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -649,10 +649,8 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { numToExecute := b.tree.Len() err = execute( - &haltableContext{ - Context: ctx, - Haltable: b, - }, + ctx, + b, log, b.DB, &parseAcceptor{ @@ -663,10 +661,7 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { b.tree, lastAcceptedHeight, ) - if errors.Is(err, errHalted) { - return nil - } - if err != nil { + if err != nil || b.Halted() { return err } diff --git a/snow/engine/snowman/bootstrap/context.go b/snow/engine/snowman/bootstrap/context.go deleted file mode 100644 index 3aa14bef5e98..000000000000 --- a/snow/engine/snowman/bootstrap/context.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package bootstrap - -import ( - "context" - "errors" - - "github.com/ava-labs/avalanchego/snow/engine/common" -) - -var ( - _ context.Context = (*haltableContext)(nil) - - errHalted = errors.New("halted") -) - -type haltableContext struct { - context.Context - common.Haltable -} - -func (c *haltableContext) Err() error { - if c.Halted() { - return errHalted - } - return c.Context.Err() -} diff --git a/snow/engine/snowman/bootstrap/storage.go b/snow/engine/snowman/bootstrap/storage.go index 4c35772bd082..ee266e578692 100644 --- a/snow/engine/snowman/bootstrap/storage.go +++ b/snow/engine/snowman/bootstrap/storage.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" "github.com/ava-labs/avalanchego/utils/logging" @@ -121,8 +122,11 @@ func process( // tree but not executed. // // execute assumes that getMissingBlockIDs would return an empty set. +// +// TODO: Replace usage of haltable with context cancellation. func execute( ctx context.Context, + haltable common.Haltable, log logging.Func, db database.Database, parser block.Parser, @@ -160,7 +164,7 @@ func execute( zap.Uint64("numToExecute", totalNumberToProcess), ) - for ctx.Err() == nil && iterator.Next() { + for !haltable.Halted() && iterator.Next() { blkBytes := iterator.Value() blk, err := parser.ParseBlock(ctx, blkBytes) if err != nil { @@ -198,13 +202,11 @@ func execute( iterator = interval.GetBlockIterator(db) } - now := time.Now() - if now.After(timeOfNextLog) { + if now := time.Now(); now.After(timeOfNextLog) { var ( numProcessed = totalNumberToProcess - tree.Len() eta = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) ) - log("executing blocks", zap.Uint64("numExecuted", numProcessed), zap.Uint64("numToExecute", totalNumberToProcess), @@ -239,15 +241,12 @@ func execute( return err } - var ( - numProcessed = totalNumberToProcess - tree.Len() - err = ctx.Err() - ) + numProcessed := totalNumberToProcess - tree.Len() log("executed blocks", zap.Uint64("numExecuted", numProcessed), zap.Uint64("numToExecute", totalNumberToProcess), + zap.Bool("halted", haltable.Halted()), zap.Duration("duration", time.Since(startTime)), - zap.Error(err), ) - return err + return nil } From ef09bd2a756ceb74f648207bc15626891afa7fb2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Mar 2024 18:01:09 -0400 Subject: [PATCH 60/68] nit --- snow/engine/snowman/bootstrap/storage_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index 48cb0677f5c1..e1b660925657 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -302,7 +302,7 @@ func TestExecuteExitsWhenHalted(t *testing.T) { halter := common.Halter{} halter.Halt(context.Background()) - err = execute( + require.NoError(execute( context.Background(), &halter, logging.NoLog{}.Info, @@ -310,8 +310,7 @@ func TestExecuteExitsWhenHalted(t *testing.T) { parser, tree, lastAcceptedHeight, - ) - require.NoError(err) + )) for _, block := range blocks[lastAcceptedHeight+1:] { require.Equal(choices.Processing, block.Status()) From db1c8af25b76f05aa5780d0d17fc46345eb94fde Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 27 Mar 2024 16:37:44 -0400 Subject: [PATCH 61/68] Cleanup getMissingBlockIDs tests --- snow/engine/snowman/bootstrap/storage_test.go | 201 +++++++----------- 1 file changed, 71 insertions(+), 130 deletions(-) diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index e1b660925657..fce932459cb4 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -26,137 +26,78 @@ func TestGetMissingBlockIDs(t *testing.T) { blocks := generateBlockchain(7) parser := makeParser(blocks) - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(t, err) - lastAcceptedHeight := uint64(1) - - t.Run("initially empty", func(t *testing.T) { - require := require.New(t) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Empty(missing) - }) - - t.Run("adding first block", func(t *testing.T) { - require := require.New(t) - - _, err := interval.Add( - db, - tree, - lastAcceptedHeight, - 5, - blocks[5].Bytes(), - ) - require.NoError(err) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding second block", func(t *testing.T) { - require := require.New(t) - - _, err := interval.Add( - db, - tree, - lastAcceptedHeight, - 3, - blocks[3].Bytes(), - ) - require.NoError(err) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[2].ID(), - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding last desired block", func(t *testing.T) { - require := require.New(t) - - _, err := interval.Add( - db, - tree, - lastAcceptedHeight, - 2, - blocks[2].Bytes(), - ) - require.NoError(err) - - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) - - t.Run("adding block with known parent", func(t *testing.T) { - require := require.New(t) - - _, err := interval.Add( - db, - tree, - lastAcceptedHeight, - 6, - blocks[6].Bytes(), - ) - require.NoError(err) + tests := []struct { + name string + blocks []snowman.Block + lastAcceptedHeight uint64 + expected set.Set[ids.ID] + }{ + { + name: "initially empty", + blocks: nil, + lastAcceptedHeight: 0, + expected: nil, + }, + { + name: "wants one block", + blocks: []snowman.Block{blocks[4]}, + lastAcceptedHeight: 0, + expected: set.Of(blocks[3].ID()), + }, + { + name: "wants multiple blocks", + blocks: []snowman.Block{blocks[2], blocks[4]}, + lastAcceptedHeight: 0, + expected: set.Of(blocks[1].ID(), blocks[3].ID()), + }, + { + name: "doesn't want last accepted block", + blocks: []snowman.Block{blocks[1]}, + lastAcceptedHeight: 0, + expected: nil, + }, + { + name: "doesn't want known block", + blocks: []snowman.Block{blocks[2], blocks[3]}, + lastAcceptedHeight: 0, + expected: set.Of(blocks[1].ID()), + }, + { + name: "doesn't want already accepted block", + blocks: []snowman.Block{blocks[1]}, + lastAcceptedHeight: 4, + expected: nil, + }, + { + name: "doesn't underflow", + blocks: []snowman.Block{blocks[0]}, + lastAcceptedHeight: 0, + expected: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) + for _, blk := range test.blocks { + _, err := interval.Add(db, tree, 0, blk.Height(), blk.Bytes()) + require.NoError(err) + } - missing, err := getMissingBlockIDs( - context.Background(), - db, - parser, - tree, - lastAcceptedHeight, - ) - require.NoError(err) - require.Equal( - set.Of( - blocks[4].ID(), - ), - missing, - ) - }) + missingBlockIDs, err := getMissingBlockIDs( + context.Background(), + db, + parser, + tree, + test.lastAcceptedHeight, + ) + require.NoError(err) + require.Equal(test.expected, missingBlockIDs) + }) + } } func TestProcess(t *testing.T) { From 8d6f491021af0a71273706e473ee41f07136dc8c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 27 Mar 2024 17:08:56 -0400 Subject: [PATCH 62/68] refactor process --- snow/engine/snowman/bootstrap/storage_test.go | 152 +++++++++++------- 1 file changed, 92 insertions(+), 60 deletions(-) diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index fce932459cb4..702b98087897 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -103,71 +103,103 @@ func TestGetMissingBlockIDs(t *testing.T) { func TestProcess(t *testing.T) { blocks := generateBlockchain(7) - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(t, err) - lastAcceptedHeight := uint64(1) - - t.Run("adding first block", func(t *testing.T) { - require := require.New(t) - - missingIDs := set.Of(blocks[6].ID()) - parentID, shouldFetchParentID, err := process( - db, - tree, - missingIDs, - lastAcceptedHeight, - blocks[6], - map[ids.ID]snowman.Block{ - blocks[2].ID(): blocks[2], - }, - ) - require.NoError(err) - require.True(shouldFetchParentID) - require.Equal(blocks[5].ID(), parentID) - require.Equal(uint64(1), tree.Len()) - require.Empty(missingIDs) - }) - - t.Run("adding multiple blocks", func(t *testing.T) { - require := require.New(t) - - missingIDs := set.Of(blocks[5].ID()) - parentID, shouldFetchParentID, err := process( - db, - tree, - missingIDs, - lastAcceptedHeight, - blocks[5], - map[ids.ID]snowman.Block{ + tests := []struct { + name string + initialBlocks []snowman.Block + lastAcceptedHeight uint64 + missingBlockIDs set.Set[ids.ID] + blk snowman.Block + ancestors map[ids.ID]snowman.Block + expectedParentID ids.ID + expectedShouldFetchParentID bool + expectedMissingBlockIDs set.Set[ids.ID] + }{ + { + name: "add single block", + initialBlocks: nil, + lastAcceptedHeight: 0, + missingBlockIDs: set.Of(blocks[5].ID()), + blk: blocks[5], + ancestors: nil, + expectedParentID: blocks[4].ID(), + expectedShouldFetchParentID: true, + expectedMissingBlockIDs: set.Set[ids.ID]{}, + }, + { + name: "add multiple blocks", + initialBlocks: nil, + lastAcceptedHeight: 0, + missingBlockIDs: set.Of(blocks[5].ID()), + blk: blocks[5], + ancestors: map[ids.ID]snowman.Block{ blocks[4].ID(): blocks[4], + }, + expectedParentID: blocks[3].ID(), + expectedShouldFetchParentID: true, + expectedMissingBlockIDs: set.Set[ids.ID]{}, + }, + { + name: "ignore non-consecutive blocks", + initialBlocks: nil, + lastAcceptedHeight: 0, + missingBlockIDs: set.Of(blocks[3].ID(), blocks[5].ID()), + blk: blocks[5], + ancestors: map[ids.ID]snowman.Block{ blocks[3].ID(): blocks[3], }, - ) - require.NoError(err) - require.True(shouldFetchParentID) - require.Equal(blocks[2].ID(), parentID) - require.Equal(uint64(4), tree.Len()) - require.Empty(missingIDs) - }) + expectedParentID: blocks[4].ID(), + expectedShouldFetchParentID: true, + expectedMissingBlockIDs: set.Of(blocks[3].ID()), + }, + { + name: "do not request the last accepted block", + initialBlocks: nil, + lastAcceptedHeight: 2, + missingBlockIDs: set.Of(blocks[3].ID()), + blk: blocks[3], + ancestors: nil, + expectedParentID: ids.Empty, + expectedShouldFetchParentID: false, + expectedMissingBlockIDs: set.Set[ids.ID]{}, + }, + { + name: "do not request already known block", + initialBlocks: []snowman.Block{blocks[2]}, + lastAcceptedHeight: 0, + missingBlockIDs: set.Of(blocks[1].ID(), blocks[3].ID()), + blk: blocks[3], + ancestors: nil, + expectedParentID: ids.Empty, + expectedShouldFetchParentID: false, + expectedMissingBlockIDs: set.Of(blocks[1].ID()), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) - t.Run("do not request last accepted block", func(t *testing.T) { - require := require.New(t) + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) + for _, blk := range test.initialBlocks { + _, err := interval.Add(db, tree, 0, blk.Height(), blk.Bytes()) + require.NoError(err) + } - missingIDs := set.Of(blocks[2].ID()) - _, shouldFetchParentID, err := process( - db, - tree, - missingIDs, - lastAcceptedHeight, - blocks[2], - nil, - ) - require.NoError(err) - require.False(shouldFetchParentID) - require.Equal(uint64(5), tree.Len()) - require.Empty(missingIDs) - }) + parentID, shouldFetchParentID, err := process( + db, + tree, + test.missingBlockIDs, + test.lastAcceptedHeight, + test.blk, + test.ancestors, + ) + require.NoError(err) + require.Equal(test.expectedShouldFetchParentID, shouldFetchParentID) + require.Equal(test.expectedParentID, parentID) + require.Equal(test.expectedMissingBlockIDs, test.missingBlockIDs) + }) + } } func TestExecute(t *testing.T) { From 699fae323e9c21627c5775286186ec0e54ba1898 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 27 Mar 2024 17:11:55 -0400 Subject: [PATCH 63/68] nit --- snow/engine/snowman/bootstrap/storage_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index 702b98087897..b18726608bd7 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -113,6 +113,7 @@ func TestProcess(t *testing.T) { expectedParentID ids.ID expectedShouldFetchParentID bool expectedMissingBlockIDs set.Set[ids.ID] + expectedTrackedHeights []uint64 }{ { name: "add single block", @@ -124,6 +125,7 @@ func TestProcess(t *testing.T) { expectedParentID: blocks[4].ID(), expectedShouldFetchParentID: true, expectedMissingBlockIDs: set.Set[ids.ID]{}, + expectedTrackedHeights: []uint64{5}, }, { name: "add multiple blocks", @@ -137,6 +139,7 @@ func TestProcess(t *testing.T) { expectedParentID: blocks[3].ID(), expectedShouldFetchParentID: true, expectedMissingBlockIDs: set.Set[ids.ID]{}, + expectedTrackedHeights: []uint64{4, 5}, }, { name: "ignore non-consecutive blocks", @@ -150,6 +153,7 @@ func TestProcess(t *testing.T) { expectedParentID: blocks[4].ID(), expectedShouldFetchParentID: true, expectedMissingBlockIDs: set.Of(blocks[3].ID()), + expectedTrackedHeights: []uint64{5}, }, { name: "do not request the last accepted block", @@ -161,6 +165,7 @@ func TestProcess(t *testing.T) { expectedParentID: ids.Empty, expectedShouldFetchParentID: false, expectedMissingBlockIDs: set.Set[ids.ID]{}, + expectedTrackedHeights: []uint64{3}, }, { name: "do not request already known block", @@ -172,6 +177,7 @@ func TestProcess(t *testing.T) { expectedParentID: ids.Empty, expectedShouldFetchParentID: false, expectedMissingBlockIDs: set.Of(blocks[1].ID()), + expectedTrackedHeights: []uint64{2, 3}, }, } for _, test := range tests { @@ -198,6 +204,11 @@ func TestProcess(t *testing.T) { require.Equal(test.expectedShouldFetchParentID, shouldFetchParentID) require.Equal(test.expectedParentID, parentID) require.Equal(test.expectedMissingBlockIDs, test.missingBlockIDs) + + require.Equal(uint64(len(test.expectedTrackedHeights)), tree.Len()) + for _, height := range test.expectedTrackedHeights { + require.True(tree.Contains(height)) + } }) } } From e5e8dffd0d225ad276708fac95256eca728f9312 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 27 Mar 2024 17:29:55 -0400 Subject: [PATCH 64/68] cleanup --- snow/engine/snowman/bootstrap/storage_test.go | 73 +++---------------- 1 file changed, 11 insertions(+), 62 deletions(-) diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index b18726608bd7..5e2c393b9824 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -223,13 +223,16 @@ func TestExecute(t *testing.T) { db := memdb.New() tree, err := interval.NewTree(db) require.NoError(err) - const lastAcceptedHeight = 1 - for i, block := range blocks[lastAcceptedHeight+1:] { + const lastAcceptedHeight = numBlocks / 2 + blocksNotToExecute := blocks[1 : lastAcceptedHeight+1] + blocksToExecute := blocks[lastAcceptedHeight+1:] + + for i, block := range blocks[1:] { newlyWantsParent, err := interval.Add( db, tree, - lastAcceptedHeight, + 0, block.Height(), block.Bytes(), ) @@ -247,10 +250,8 @@ func TestExecute(t *testing.T) { tree, lastAcceptedHeight, )) - - for _, block := range blocks[lastAcceptedHeight+1:] { - require.Equal(choices.Accepted, block.Status()) - } + requireStatusIs(require, blocksNotToExecute, choices.Processing) + requireStatusIs(require, blocksToExecute, choices.Accepted) size, err := database.Count(db) require.NoError(err) @@ -266,9 +267,9 @@ func TestExecuteExitsWhenHalted(t *testing.T) { db := memdb.New() tree, err := interval.NewTree(db) require.NoError(err) - const lastAcceptedHeight = 1 - for i, block := range blocks[lastAcceptedHeight+1:] { + const lastAcceptedHeight = 1 + for i, block := range blocks[1:] { newlyWantsParent, err := interval.Add( db, tree, @@ -295,65 +296,13 @@ func TestExecuteExitsWhenHalted(t *testing.T) { tree, lastAcceptedHeight, )) - - for _, block := range blocks[lastAcceptedHeight+1:] { - require.Equal(choices.Processing, block.Status()) - } + requireStatusIs(require, blocks[1:], choices.Processing) endSize, err := database.Count(db) require.NoError(err) require.Equal(startSize, endSize) } -func TestExecuteSkipsAcceptedBlocks(t *testing.T) { - require := require.New(t) - - blocks := generateBlockchain(7) - parser := makeParser(blocks) - - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(err) - const ( - lastAcceptedHeightWhenAdding = 1 - lastAcceptedHeightWhenExecuting = 3 - ) - - for i, block := range blocks[lastAcceptedHeightWhenAdding+1:] { - newlyWantsParent, err := interval.Add( - db, - tree, - lastAcceptedHeightWhenAdding, - block.Height(), - block.Bytes(), - ) - require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) - } - - require.NoError(execute( - context.Background(), - &common.Halter{}, - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeightWhenExecuting, - )) - - for _, block := range blocks[lastAcceptedHeightWhenAdding+1 : lastAcceptedHeightWhenExecuting] { - require.Equal(choices.Processing, block.Status()) - } - for _, block := range blocks[lastAcceptedHeightWhenExecuting+1:] { - require.Equal(choices.Accepted, block.Status()) - } - - size, err := database.Count(db) - require.NoError(err) - require.Zero(size) -} - type testParser func(context.Context, []byte) (snowman.Block, error) func (f testParser) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { From 0c2b01a33de6b30db0dc14fbff09bdcf2b77aa87 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 28 Mar 2024 12:14:01 -0400 Subject: [PATCH 65/68] fix test --- snow/engine/snowman/bootstrap/storage_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index 5e2c393b9824..73cfe60f6689 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -273,7 +273,7 @@ func TestExecuteExitsWhenHalted(t *testing.T) { newlyWantsParent, err := interval.Add( db, tree, - lastAcceptedHeight, + 0, block.Height(), block.Bytes(), ) From 8ce7bfbbaf690fb35f471acda4f630076274b6e2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 28 Mar 2024 12:17:33 -0400 Subject: [PATCH 66/68] Remove fetch eta metric --- snow/engine/snowman/bootstrap/bootstrapper.go | 3 --- snow/engine/snowman/bootstrap/metrics.go | 7 ------- 2 files changed, 10 deletions(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index aa4e42a24219..9547341a685c 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -598,7 +598,6 @@ func (b *Bootstrapper) process( numFetched-b.initiallyFetched, // Number of blocks we have fetched during this run totalBlocksToFetch-b.initiallyFetched, // Number of blocks we expect to fetch during this run ) - b.fetchETA.Set(float64(eta)) if !b.restarted { b.Ctx.Log.Info("fetching blocks", @@ -694,7 +693,6 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { b.awaitingTimeout = true return nil } - b.fetchETA.Set(0) return b.onFinished(ctx, b.requestID) } @@ -719,7 +717,6 @@ func (b *Bootstrapper) Timeout(ctx context.Context) error { if !b.Config.BootstrapTracker.IsBootstrapped() { return b.restartBootstrapping(ctx) } - b.fetchETA.Set(0) return b.onFinished(ctx, b.requestID) } diff --git a/snow/engine/snowman/bootstrap/metrics.go b/snow/engine/snowman/bootstrap/metrics.go index aea46d2a93e8..311ed05f136d 100644 --- a/snow/engine/snowman/bootstrap/metrics.go +++ b/snow/engine/snowman/bootstrap/metrics.go @@ -11,7 +11,6 @@ import ( type metrics struct { numFetched, numAccepted prometheus.Counter - fetchETA prometheus.Gauge } func newMetrics(namespace string, registerer prometheus.Registerer) (*metrics, error) { @@ -26,17 +25,11 @@ func newMetrics(namespace string, registerer prometheus.Registerer) (*metrics, e Name: "accepted", Help: "Number of blocks accepted during bootstrapping", }), - fetchETA: prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Name: "eta_fetching_complete", - Help: "ETA in nanoseconds until fetching phase of bootstrapping finishes", - }), } err := utils.Err( registerer.Register(m.numFetched), registerer.Register(m.numAccepted), - registerer.Register(m.fetchETA), ) return m, err } From 3b32eec0f0f4bbd38aa56f02e84e22f5d1c57924 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 28 Mar 2024 13:26:17 -0400 Subject: [PATCH 67/68] cleanup execution test --- snow/engine/snowman/bootstrap/storage_test.go | 144 ++++++++---------- 1 file changed, 66 insertions(+), 78 deletions(-) diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index 73cfe60f6689..7d568c4e8e83 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -214,93 +214,81 @@ func TestProcess(t *testing.T) { } func TestExecute(t *testing.T) { - require := require.New(t) + const numBlocks = 7 - const numBlocks = 2*max(batchWritePeriod, iteratorReleasePeriod) + 1 - blocks := generateBlockchain(numBlocks) - parser := makeParser(blocks) - - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(err) - - const lastAcceptedHeight = numBlocks / 2 - blocksNotToExecute := blocks[1 : lastAcceptedHeight+1] - blocksToExecute := blocks[lastAcceptedHeight+1:] + unhalted := &common.Halter{} + halted := &common.Halter{} + halted.Halt(context.Background()) - for i, block := range blocks[1:] { - newlyWantsParent, err := interval.Add( - db, - tree, - 0, - block.Height(), - block.Bytes(), - ) - require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) + tests := []struct { + name string + haltable common.Haltable + lastAcceptedHeight uint64 + expectedProcessingHeights []uint64 + expectedAcceptedHeights []uint64 + }{ + { + name: "execute everything", + haltable: unhalted, + lastAcceptedHeight: 0, + expectedProcessingHeights: nil, + expectedAcceptedHeights: []uint64{0, 1, 2, 3, 4, 5, 6}, + }, + { + name: "do not execute blocks accepted by height", + haltable: unhalted, + lastAcceptedHeight: 3, + expectedProcessingHeights: []uint64{1, 2, 3}, + expectedAcceptedHeights: []uint64{0, 4, 5, 6}, + }, + { + name: "do not execute blocks when halted", + haltable: halted, + lastAcceptedHeight: 0, + expectedProcessingHeights: []uint64{1, 2, 3, 4, 5, 6}, + expectedAcceptedHeights: []uint64{0}, + }, } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) - require.NoError(execute( - context.Background(), - &common.Halter{}, - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeight, - )) - requireStatusIs(require, blocksNotToExecute, choices.Processing) - requireStatusIs(require, blocksToExecute, choices.Accepted) - - size, err := database.Count(db) - require.NoError(err) - require.Zero(size) -} + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) -func TestExecuteExitsWhenHalted(t *testing.T) { - require := require.New(t) + blocks := generateBlockchain(numBlocks) + parser := makeParser(blocks) + for _, blk := range blocks { + _, err := interval.Add(db, tree, 0, blk.Height(), blk.Bytes()) + require.NoError(err) + } - blocks := generateBlockchain(7) - parser := makeParser(blocks) + require.NoError(execute( + context.Background(), + test.haltable, + logging.NoLog{}.Info, + db, + parser, + tree, + test.lastAcceptedHeight, + )) + for _, height := range test.expectedProcessingHeights { + require.Equal(choices.Processing, blocks[height].Status()) + } + for _, height := range test.expectedAcceptedHeights { + require.Equal(choices.Accepted, blocks[height].Status()) + } - db := memdb.New() - tree, err := interval.NewTree(db) - require.NoError(err) + if test.haltable.Halted() { + return + } - const lastAcceptedHeight = 1 - for i, block := range blocks[1:] { - newlyWantsParent, err := interval.Add( - db, - tree, - 0, - block.Height(), - block.Bytes(), - ) - require.NoError(err) - require.False(newlyWantsParent) - require.Equal(uint64(i+1), tree.Len()) + size, err := database.Count(db) + require.NoError(err) + require.Zero(size) + }) } - - startSize, err := database.Count(db) - require.NoError(err) - - halter := common.Halter{} - halter.Halt(context.Background()) - require.NoError(execute( - context.Background(), - &halter, - logging.NoLog{}.Info, - db, - parser, - tree, - lastAcceptedHeight, - )) - requireStatusIs(require, blocks[1:], choices.Processing) - - endSize, err := database.Count(db) - require.NoError(err) - require.Equal(startSize, endSize) } type testParser func(context.Context, []byte) (snowman.Block, error) From f4b612765a79012e6c7db54198d6c5acf9938ef7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 29 Mar 2024 10:58:50 -0400 Subject: [PATCH 68/68] nits --- snow/engine/snowman/bootstrap/bootstrapper.go | 2 +- snow/engine/snowman/bootstrap/storage_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 9547341a685c..ca0584128269 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -555,7 +555,7 @@ func (b *Bootstrapper) markUnavailable(nodeID ids.NodeID) { // // - blk is a block that is assumed to have been marked as acceptable by the // bootstrapping engine. -// - ancestors is a set of blocks that can be used to optimisically lookup +// - ancestors is a set of blocks that can be used to optimistically lookup // parent blocks. This enables the engine to process multiple blocks without // relying on the VM to have stored blocks during `ParseBlock`. func (b *Bootstrapper) process( diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go index 7d568c4e8e83..6ac2761f8cd7 100644 --- a/snow/engine/snowman/bootstrap/storage_test.go +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -22,6 +22,8 @@ import ( "github.com/ava-labs/avalanchego/utils/set" ) +var _ block.Parser = testParser(nil) + func TestGetMissingBlockIDs(t *testing.T) { blocks := generateBlockchain(7) parser := makeParser(blocks)