Skip to content

Commit

Permalink
merkledb -- use Maybe for start bounds (#1872)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Laine authored Aug 18, 2023
1 parent 7d970c3 commit 2bd58d1
Show file tree
Hide file tree
Showing 23 changed files with 838 additions and 630 deletions.
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 {
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

0 comments on commit 2bd58d1

Please sign in to comment.