diff --git a/snow/engine/snowman/bootstrap/interval/blocks.go b/snow/engine/snowman/bootstrap/interval/blocks.go new file mode 100644 index 00000000000..d7d053c1787 --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/blocks.go @@ -0,0 +1,44 @@ +// 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" + +// 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, + lastAcceptedHeight uint64, + height uint64, + blkBytes []byte, +) (bool, error) { + if height <= lastAcceptedHeight || tree.Contains(height) { + return false, nil + } + + if err := PutBlock(db, height, blkBytes); err != nil { + return false, err + } + if err := tree.Add(db, height); err != nil { + return false, err + } + + // 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. +func Remove( + db database.KeyValueWriterDeleter, + tree *Tree, + height uint64, +) error { + if err := DeleteBlock(db, height); err != nil { + return err + } + return tree.Remove(db, height) +} 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 00000000000..d11a6fe434a --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/blocks_test.go @@ -0,0 +1,137 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" +) + +func TestAdd(t *testing.T) { + tests := []struct { + name string + existing []uint64 + lastAcceptedHeight uint64 + height uint64 + blkBytes []byte + expectedToPersist bool + expectedToWantParent bool + }{ + { + name: "height already accepted", + lastAcceptedHeight: 1, + height: 1, + blkBytes: []byte{1}, + expectedToPersist: false, + expectedToWantParent: false, + }, + { + name: "height already added", + existing: []uint64{1}, + lastAcceptedHeight: 0, + height: 1, + blkBytes: []byte{1}, + expectedToPersist: false, + expectedToWantParent: false, + }, + { + name: "next block is desired", + lastAcceptedHeight: 0, + height: 2, + blkBytes: []byte{2}, + expectedToPersist: true, + expectedToWantParent: true, + }, + { + name: "next block is accepted", + lastAcceptedHeight: 0, + height: 1, + blkBytes: []byte{1}, + expectedToPersist: true, + expectedToWantParent: false, + }, + { + name: "next block already added", + existing: []uint64{1}, + lastAcceptedHeight: 0, + height: 2, + blkBytes: []byte{2}, + expectedToPersist: true, + expectedToWantParent: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + db := memdb.New() + tree, err := NewTree(db) + require.NoError(err) + for _, add := range test.existing { + require.NoError(tree.Add(db, add)) + } + + wantsParent, err := Add( + db, + tree, + test.lastAcceptedHeight, + test.height, + test.blkBytes, + ) + require.NoError(err) + require.Equal(test.expectedToWantParent, wantsParent) + + blkBytes, err := GetBlock(db, test.height) + if test.expectedToPersist { + require.NoError(err) + require.Equal(test.blkBytes, blkBytes) + require.True(tree.Contains(test.height)) + } else { + require.ErrorIs(err, database.ErrNotFound) + } + }) + } +} + +func TestRemove(t *testing.T) { + require := require.New(t) + + db := memdb.New() + tree, err := NewTree(db) + require.NoError(err) + lastAcceptedHeight := uint64(1) + height := uint64(5) + blkBytes := []byte{5} + + _, err = Add( + db, + tree, + lastAcceptedHeight, + height, + blkBytes, + ) + require.NoError(err) + + // Verify that the database has the block. + storedBlkBytes, err := GetBlock(db, height) + require.NoError(err) + require.Equal(blkBytes, storedBlkBytes) + require.Equal(uint64(1), tree.Len()) + + require.NoError(Remove( + db, + tree, + height, + )) + require.Zero(tree.Len()) + + // Verify that the database no longer contains the block. + _, err = GetBlock(db, height) + require.ErrorIs(err, database.ErrNotFound) + require.Zero(tree.Len()) +} diff --git a/snow/engine/snowman/bootstrap/interval/interval.go b/snow/engine/snowman/bootstrap/interval/interval.go new file mode 100644 index 00000000000..35ae260e446 --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/interval.go @@ -0,0 +1,35 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import "math" + +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 +} + +// AdjacentToLowerBound returns true if height is 1 less than lowerBound. +func (i *Interval) AdjacentToLowerBound(height uint64) bool { + 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 00000000000..2213302925f --- /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) + }) + } +} diff --git a/snow/engine/snowman/bootstrap/interval/state.go b/snow/engine/snowman/bootstrap/interval/state.go new file mode 100644 index 00000000000..cf2e2bf3a2e --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/state.go @@ -0,0 +1,99 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import ( + "errors" + + "github.com/ava-labs/avalanchego/database" +) + +const ( + intervalPrefixByte byte = iota + blockPrefixByte + + prefixLen = 1 +) + +var ( + intervalPrefix = []byte{intervalPrefixByte} + blockPrefix = []byte{blockPrefixByte} + + errInvalidKeyLength = errors.New("invalid key length") +) + +func GetIntervals(db database.Iteratee) ([]*Interval, error) { + it := db.NewIteratorWithPrefix(intervalPrefix) + defer it.Release() + + var intervals []*Interval + for it.Next() { + dbKey := it.Key() + if len(dbKey) < prefixLen { + return nil, errInvalidKeyLength + } + + intervalKey := dbKey[prefixLen:] + upperBound, err := database.ParseUInt64(intervalKey) + 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 { + return database.PutUInt64(db, makeIntervalKey(upperBound), lowerBound) +} + +func DeleteInterval(db database.KeyValueDeleter, upperBound uint64) error { + 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...) +} + +// 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)) +} + +func PutBlock(db database.KeyValueWriter, height uint64, bytes []byte) error { + 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 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 00000000000..51d1083c1e2 --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/tree.go @@ -0,0 +1,188 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interval + +import ( + "github.com/google/btree" + + "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] + // 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 { + return nil, err + } + + var ( + knownHeights = btree.NewG(treeDegree, (*Interval).Less) + numKnownHeights uint64 + ) + for _, i := range intervals { + knownHeights.ReplaceOrInsert(i) + numKnownHeights += i.UpperBound - i.LowerBound + 1 + } + return &Tree{ + knownHeights: knownHeights, + numKnownHeights: numKnownHeights, + }, nil +} + +func (t *Tree) Add(db database.KeyValueWriterDeleter, height uint64) error { + var ( + newInterval = &Interval{ + LowerBound: height, + UpperBound: height, + } + upper *Interval + lower *Interval + ) + t.knownHeights.AscendGreaterOrEqual(newInterval, func(item *Interval) bool { + upper = item + return false + }) + if upper.Contains(height) { + // height is already in the tree + return nil + } + + t.knownHeights.DescendLessOrEqual(newInterval, func(item *Interval) bool { + lower = item + return false + }) + + t.numKnownHeights++ + + var ( + adjacentToLowerBound = upper.AdjacentToLowerBound(height) + adjacentToUpperBound = lower.AdjacentToUpperBound(height) + ) + switch { + case adjacentToLowerBound && adjacentToUpperBound: + // the upper and lower ranges should be merged + if err := DeleteInterval(db, lower.UpperBound); err != nil { + return err + } + upper.LowerBound = lower.LowerBound + t.knownHeights.Delete(lower) + 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(db, upper.UpperBound, height) + case adjacentToUpperBound: + // the lower range should be extended by one on the upper side + if err := DeleteInterval(db, lower.UpperBound); err != nil { + return err + } + lower.UpperBound = height + return PutInterval(db, height, lower.LowerBound) + default: + t.knownHeights.ReplaceOrInsert(newInterval) + return PutInterval(db, height, height) + } +} + +func (t *Tree) Remove(db database.KeyValueWriterDeleter, 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 + } + + t.numKnownHeights-- + + switch { + case higher.LowerBound == higher.UpperBound: + t.knownHeights.Delete(higher) + return DeleteInterval(db, higher.UpperBound) + case higher.LowerBound == height: + higher.LowerBound++ + return PutInterval(db, higher.UpperBound, higher.LowerBound) + case higher.UpperBound == height: + if err := DeleteInterval(db, higher.UpperBound); err != nil { + return err + } + higher.UpperBound-- + return PutInterval(db, higher.UpperBound, higher.LowerBound) + default: + newInterval.LowerBound = higher.LowerBound + newInterval.UpperBound = height - 1 + t.knownHeights.ReplaceOrInsert(newInterval) + if err := PutInterval(db, newInterval.UpperBound, newInterval.LowerBound); err != nil { + return err + } + + higher.LowerBound = height + 1 + return PutInterval(db, higher.UpperBound, higher.LowerBound) + } +} + +func (t *Tree) Contains(height uint64) bool { + var ( + i = &Interval{ + LowerBound: height, + UpperBound: height, + } + higher *Interval + ) + 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 +} + +// 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 +} 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 00000000000..396e4d281a9 --- /dev/null +++ b/snow/engine/snowman/bootstrap/interval/tree_test.go @@ -0,0 +1,389 @@ +// 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" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" +) + +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++ { + require.NoError(tree.Add(db, i)) + } + } + return tree +} + +func TestTreeAdd(t *testing.T) { + tests := []struct { + name string + toAdd []*Interval + expected []*Interval + expectedLen uint64 + }{ + { + name: "single addition", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expectedLen: 1, + }, + { + name: "extend above", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + }, + expectedLen: 2, + }, + { + name: "extend below", + toAdd: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + }, + expectedLen: 2, + }, + { + name: "merge", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + { + LowerBound: 12, + UpperBound: 12, + }, + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 12, + }, + }, + expectedLen: 3, + }, + { + name: "ignore duplicate", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + }, + expectedLen: 2, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(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 + expectedLen uint64 + }{ + { + name: "single removal", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expected: []*Interval{}, + expectedLen: 0, + }, + { + name: "reduce above", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expectedLen: 1, + }, + { + name: "reduce below", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 11, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expected: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expectedLen: 1, + }, + { + name: "split", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 12, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + { + LowerBound: 12, + UpperBound: 12, + }, + }, + expectedLen: 2, + }, + { + name: "ignore missing", + toAdd: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + toRemove: []*Interval{ + { + LowerBound: 11, + UpperBound: 11, + }, + }, + expected: []*Interval{ + { + LowerBound: 10, + UpperBound: 10, + }, + }, + expectedLen: 1, + }, + } + 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(db, i)) + } + } + 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()) + }) + } +} + +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) { + require := require.New(t) + + tree := newTree(require, memdb.New(), test.tree) + require.Equal(test.expected, tree.Contains(test.height)) + }) + } +} + +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()) +} diff --git a/utils/logging/logger.go b/utils/logging/logger.go index 2ca95bff104..f6b3b66a77b 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 {