Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merkledb -- use Maybe for start bounds #1872

Merged
merged 13 commits into from
Aug 18, 2023
471 changes: 243 additions & 228 deletions proto/pb/sync/sync.pb.go

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions proto/sync/sync.proto
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ message Proof {
message SyncGetChangeProofRequest {
bytes start_root_hash = 1;
bytes end_root_hash = 2;
bytes start_key = 3;
MaybeBytes start_key = 3;
MaybeBytes end_key = 4;
uint32 key_limit = 5;
uint32 bytes_limit = 6;
Expand All @@ -70,7 +70,7 @@ message SyncGetChangeProofResponse {
message GetChangeProofRequest {
bytes start_root_hash = 1;
bytes end_root_hash = 2;
bytes start_key = 3;
MaybeBytes start_key = 3;
MaybeBytes end_key = 4;
uint32 key_limit = 5;
}
Expand All @@ -85,8 +85,8 @@ message GetChangeProofResponse {

message VerifyChangeProofRequest {
ChangeProof proof = 1;
bytes start_key = 2;
bytes end_key = 3;
MaybeBytes start_key = 2;
MaybeBytes end_key = 3;
bytes expected_root_hash = 4;
}

Expand All @@ -103,15 +103,15 @@ message CommitChangeProofRequest {
// the response. GetRangeProof in the DB service doesn't.
message SyncGetRangeProofRequest {
bytes root_hash = 1;
bytes start_key = 2;
MaybeBytes start_key = 2;
MaybeBytes end_key = 3;
uint32 key_limit = 4;
uint32 bytes_limit = 5;
}

message GetRangeProofRequest {
bytes root_hash = 1;
bytes start_key = 2;
MaybeBytes start_key = 2;
MaybeBytes end_key = 3;
uint32 key_limit = 4;
}
Expand All @@ -121,7 +121,7 @@ message GetRangeProofResponse {
}

message CommitRangeProofRequest {
bytes start_key = 1;
MaybeBytes start_key = 1;
RangeProof range_proof = 2;
}

Expand Down
14 changes: 13 additions & 1 deletion utils/maybe/maybe.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@

package maybe

import "fmt"

// Maybe T = Some T | Nothing.
// A data wrapper that allows values to be something [Some T] or nothing [Nothing].
// Invariant: If [hasValue] is false, then [value] is the zero value of type T.
// Maybe is used to wrap types:
// * That can't be represented by nil.
// * That use nil as a valid value instead of an indicator of a missing value.
// For more info see https://en.wikipedia.org/wiki/Option_type
type Maybe[T any] struct {
hasValue bool
value T
// If [hasValue] is false, [value] is the zero value of type T.
value T
}

// Some returns a new Maybe[T] with the value val.
// If m.IsNothing(), returns the zero value of type T.
func Some[T any](val T) Maybe[T] {
return Maybe[T]{
value: val,
Expand Down Expand Up @@ -42,6 +47,13 @@ func (m Maybe[T]) Value() T {
return m.value
}

func (m Maybe[T]) String() string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this used anywhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes in manager.go:

		zap.Stringer("start", work.start),
		zap.Stringer("end", largestHandledKey),

if !m.hasValue {
return fmt.Sprintf("Nothing[%T]", m.value)
}
return fmt.Sprintf("Some[%T]{%v}", m.value, m.value)
}

// Bind returns Nothing iff [m] is Nothing.
// Otherwise applies [f] to the value of [m] and returns the result as a Some.
func Bind[T, U any](m Maybe[T], f func(T) U) Maybe[U] {
Expand Down
17 changes: 17 additions & 0 deletions utils/maybe/maybe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ func TestMaybeClone(t *testing.T) {
}
}

func TestMaybeString(t *testing.T) {
require := require.New(t)

// Case: Value is maybe
{
val := []int{1, 2, 3}
m := Some(val)
require.Equal("Some[[]int]{[1 2 3]}", m.String())
}

// Case: Value is nothing
{
m := Nothing[int]()
require.Equal("Nothing[int]", m.String())
}
}

func TestMaybeEquality(t *testing.T) {
require := require.New(t)
require.True(Equal(Nothing[int](), Nothing[int](), func(i int, i2 int) bool {
Expand Down
47 changes: 24 additions & 23 deletions x/merkledb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type ChangeProofer interface {
ctx context.Context,
startRootID ids.ID,
endRootID ids.ID,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
maxLength int,
) (*ChangeProof, error)
Expand All @@ -72,7 +72,7 @@ type ChangeProofer interface {
// - [start] <= [end].
// - [proof] is non-empty.
// - All keys in [proof.KeyValues] and [proof.DeletedKeys] are in [start, end].
// If [start] is empty, all keys are considered > [start].
// If [start] is nothing, all keys are considered > [start].
// If [end] is nothing, all keys are considered < [end].
// - [proof.KeyValues] and [proof.DeletedKeys] are sorted in order of increasing key.
// - [proof.StartProof] and [proof.EndProof] are well-formed.
Expand All @@ -84,7 +84,7 @@ type ChangeProofer interface {
VerifyChangeProof(
ctx context.Context,
proof *ChangeProof,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
expectedEndRootID ids.ID,
) error
Expand All @@ -96,17 +96,19 @@ type ChangeProofer interface {
type RangeProofer interface {
// GetRangeProofAtRoot returns a proof for the key/value pairs in this trie within the range
// [start, end] when the root of the trie was [rootID].
// If [start] is Nothing, there's no lower bound on the range.
// If [end] is Nothing, there's no upper bound on the range.
GetRangeProofAtRoot(
ctx context.Context,
rootID ids.ID,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
maxLength int,
) (*RangeProof, error)

// CommitRangeProof commits the key/value pairs within the [proof] to the db.
// [start] is the smallest key in the range this [proof] covers.
CommitRangeProof(ctx context.Context, start []byte, proof *RangeProof) error
CommitRangeProof(ctx context.Context, start maybe.Maybe[[]byte], proof *RangeProof) error
}

type MerkleDB interface {
Expand Down Expand Up @@ -314,7 +316,7 @@ func (db *merkleDB) CommitChangeProof(ctx context.Context, proof *ChangeProof) e
return view.commitToDB(ctx)
}

func (db *merkleDB) CommitRangeProof(ctx context.Context, start []byte, proof *RangeProof) error {
func (db *merkleDB) CommitRangeProof(ctx context.Context, start maybe.Maybe[[]byte], proof *RangeProof) error {
db.commitLock.Lock()
defer db.commitLock.Unlock()

Expand All @@ -336,7 +338,7 @@ func (db *merkleDB) CommitRangeProof(ctx context.Context, start []byte, proof *R
if len(proof.KeyValues) > 0 {
largestKey = proof.KeyValues[len(proof.KeyValues)-1].Key
}
keysToDelete, err := db.getKeysNotInSet(start, largestKey, keys)
keysToDelete, err := db.getKeysNotInSet(start.Value(), largestKey, keys)
if err != nil {
return err
}
Expand Down Expand Up @@ -509,11 +511,9 @@ func (db *merkleDB) getProof(ctx context.Context, key []byte) (*Proof, error) {
return view.getProof(ctx, key)
}

// GetRangeProof returns a proof for the key/value pairs in this trie within the range
// [start, end].
func (db *merkleDB) GetRangeProof(
ctx context.Context,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
maxLength int,
) (*RangeProof, error) {
Expand All @@ -523,12 +523,10 @@ func (db *merkleDB) GetRangeProof(
return db.getRangeProofAtRoot(ctx, db.getMerkleRoot(), start, end, maxLength)
}

// GetRangeProofAtRoot returns a proof for the key/value pairs in this trie within the range
// [start, end] when the root of the trie was [rootID].
func (db *merkleDB) GetRangeProofAtRoot(
ctx context.Context,
rootID ids.ID,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
maxLength int,
) (*RangeProof, error) {
Expand All @@ -542,7 +540,7 @@ func (db *merkleDB) GetRangeProofAtRoot(
func (db *merkleDB) getRangeProofAtRoot(
ctx context.Context,
rootID ids.ID,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
maxLength int,
) (*RangeProof, error) {
Expand All @@ -564,11 +562,11 @@ func (db *merkleDB) GetChangeProof(
ctx context.Context,
startRootID ids.ID,
endRootID ids.ID,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
maxLength int,
) (*ChangeProof, error) {
if end.HasValue() && bytes.Compare(start, end.Value()) == 1 {
if start.HasValue() && end.HasValue() && bytes.Compare(start.Value(), end.Value()) == 1 {
return nil, ErrStartAfterEnd
}
if startRootID == endRootID {
Expand Down Expand Up @@ -628,8 +626,8 @@ func (db *merkleDB) GetChangeProof(
result.EndProof = endProof.Path
}

if len(start) > 0 {
startProof, err := historicalView.getProof(ctx, start)
if start.HasValue() {
startProof, err := historicalView.getProof(ctx, start.Value())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -988,19 +986,19 @@ func (*merkleDB) CommitToDB(context.Context) error {
func (db *merkleDB) VerifyChangeProof(
ctx context.Context,
proof *ChangeProof,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
expectedEndRootID ids.ID,
) error {
switch {
case end.HasValue() && bytes.Compare(start, end.Value()) > 0:
case start.HasValue() && end.HasValue() && bytes.Compare(start.Value(), end.Value()) > 0:
return ErrStartAfterEnd
case proof.Empty():
return ErrNoMerkleProof
case end.HasValue() && len(proof.KeyChanges) == 0 && len(proof.EndProof) == 0:
// We requested an end proof but didn't get one.
return ErrNoEndProof
case len(start) > 0 && len(proof.StartProof) == 0 && len(proof.EndProof) == 0:
case start.HasValue() && len(proof.StartProof) == 0 && len(proof.EndProof) == 0:
// We requested a start proof but didn't get one.
// Note that we also have to check that [proof.EndProof] is empty
// to handle the case that the start proof is empty because all
Expand All @@ -1013,7 +1011,8 @@ func (db *merkleDB) VerifyChangeProof(
return err
}

smallestPath := newPath(start)
// Note that if [start] is Nothing, smallestPath is the empty path.
smallestPath := newPath(start.Value())

// Make sure the start proof, if given, is well-formed.
if err := verifyProofPath(proof.StartProof, smallestPath); err != nil {
Expand Down Expand Up @@ -1179,10 +1178,12 @@ func (db *merkleDB) initializeRootIfNeeded() (ids.ID, error) {
}

// Returns a view of the trie as it was when it had root [rootID] for keys within range [start, end].
// If [start] is Nothing, there's no lower bound on the range.
// If [end] is Nothing, there's no upper bound on the range.
// Assumes [db.commitLock] is read locked.
func (db *merkleDB) getHistoricalViewForRange(
rootID ids.ID,
start []byte,
start maybe.Maybe[[]byte],
end maybe.Maybe[[]byte],
) (*trieView, error) {
currentRootID := db.getMerkleRoot()
Expand Down
24 changes: 16 additions & 8 deletions x/merkledb/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,13 @@ func Test_MerkleDB_Commit_Proof_To_Empty_Trie(t *testing.T) {
require.NoError(batch.Put([]byte("key3"), []byte("3")))
require.NoError(batch.Write())

proof, err := db.GetRangeProof(context.Background(), []byte("key1"), maybe.Some([]byte("key3")), 10)
proof, err := db.GetRangeProof(context.Background(), maybe.Some([]byte("key1")), maybe.Some([]byte("key3")), 10)
require.NoError(err)

freshDB, err := getBasicDB()
require.NoError(err)

require.NoError(freshDB.CommitRangeProof(context.Background(), []byte("key1"), proof))
require.NoError(freshDB.CommitRangeProof(context.Background(), maybe.Some([]byte("key1")), proof))

value, err := freshDB.Get([]byte("key2"))
require.NoError(err)
Expand All @@ -296,7 +296,7 @@ func Test_MerkleDB_Commit_Proof_To_Filled_Trie(t *testing.T) {
require.NoError(batch.Put([]byte("key3"), []byte("3")))
require.NoError(batch.Write())

proof, err := db.GetRangeProof(context.Background(), []byte("key1"), maybe.Some([]byte("key3")), 10)
proof, err := db.GetRangeProof(context.Background(), maybe.Some([]byte("key1")), maybe.Some([]byte("key3")), 10)
require.NoError(err)

freshDB, err := getBasicDB()
Expand All @@ -308,7 +308,7 @@ func Test_MerkleDB_Commit_Proof_To_Filled_Trie(t *testing.T) {
require.NoError(batch.Put([]byte("key25"), []byte("5")))
require.NoError(batch.Write())

require.NoError(freshDB.CommitRangeProof(context.Background(), []byte("key1"), proof))
require.NoError(freshDB.CommitRangeProof(context.Background(), maybe.Some([]byte("key1")), proof))

value, err := freshDB.Get([]byte("key2"))
require.NoError(err)
Expand Down Expand Up @@ -747,16 +747,20 @@ func runRandDBTest(require *require.Assertions, r *rand.Rand, rt randTest) {
if len(pastRoots) > 0 {
root = pastRoots[r.Intn(len(pastRoots))]
}
start := maybe.Nothing[[]byte]()
if len(step.key) > 0 {
start = maybe.Some(step.key)
}
end := maybe.Nothing[[]byte]()
if len(step.value) > 0 {
end = maybe.Some(step.value)
}

rangeProof, err := db.GetRangeProofAtRoot(context.Background(), root, step.key, end, 100)
rangeProof, err := db.GetRangeProofAtRoot(context.Background(), root, start, end, 100)
require.NoError(err)
require.NoError(rangeProof.Verify(
context.Background(),
step.key,
start,
end,
root,
))
Expand All @@ -771,8 +775,12 @@ func runRandDBTest(require *require.Assertions, r *rand.Rand, rt randTest) {
if len(step.value) > 0 {
end = maybe.Some(step.value)
}
start := maybe.Nothing[[]byte]()
if len(step.key) > 0 {
start = maybe.Some(step.key)
}

changeProof, err := db.GetChangeProof(context.Background(), startRoot, root, step.key, end, 100)
changeProof, err := db.GetChangeProof(context.Background(), startRoot, root, start, end, 100)
if startRoot == root {
require.ErrorIs(err, errSameRoot)
continue
Expand All @@ -784,7 +792,7 @@ func runRandDBTest(require *require.Assertions, r *rand.Rand, rt randTest) {
require.NoError(changeProofDB.VerifyChangeProof(
context.Background(),
changeProof,
step.key,
start,
end,
root,
))
Expand Down
Loading
Loading