Skip to content

sync -- change proof request can return range proof #1772

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

Merged
merged 41 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e914ebc
GetChangeProof errors on insufficient history
Jul 21, 2023
22d794d
WIP remove HadRootsInHistory
Jul 21, 2023
8466754
remove HadRootsInHistory from proto
Jul 21, 2023
413aa0e
remove dead code
Jul 21, 2023
217c73a
WIP handle range proof response to change proof request
Jul 27, 2023
a6b52af
WIP handle range proof response to change proof request
Jul 27, 2023
2dab7ee
WIP handle range proof response to change proof request
Jul 27, 2023
300b925
appease linter
Jul 27, 2023
4ece13b
fix sync_test.go
Jul 27, 2023
bbc16e6
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Jul 27, 2023
672ec5d
remove todo
Jul 27, 2023
54c9543
cleanup
Jul 27, 2023
4f68799
WIP handle range proof response to change proof request
Jul 27, 2023
fd5c413
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Jul 31, 2023
3c18f47
handle ErrInsufficientHistory in DBServer
Jul 31, 2023
14fa8b3
formatting nit
Jul 31, 2023
f165af5
formatting nit
Jul 31, 2023
284d137
formatting nits
Jul 31, 2023
bdb3fee
comment nits
Jul 31, 2023
eeb5c95
WIP add test; WIP fix change proof handling for sending range proofs
Jul 31, 2023
4b3c2d6
factor out common range proof generation
Jul 31, 2023
c117790
appease linter
Jul 31, 2023
151494f
add test
Jul 31, 2023
65e46bd
appease linter
Jul 31, 2023
3cc0831
remove todos; add tests
Aug 1, 2023
81d4ca0
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Aug 1, 2023
0545d98
use errors.Is instead of checking for error equality
Aug 1, 2023
73d2f25
fix error message
Aug 1, 2023
71c9fb7
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Aug 3, 2023
a6ec21c
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Aug 7, 2023
eb8aaee
Merge branch 'dev' into sync-handle-insufficient-history
Aug 8, 2023
30ab6ea
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Aug 11, 2023
90b7bbc
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Aug 11, 2023
4572aa0
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Aug 14, 2023
ccd141b
address PR comments
Aug 15, 2023
4c89e03
Merge branch 'dev' into sync-handle-insufficient-history
Aug 15, 2023
34574b8
Merge branch 'dev' into sync-handle-insufficient-history
Aug 15, 2023
2a8fbe5
Merge remote-tracking branch 'upstream/dev' into sync-handle-insuffic…
Aug 16, 2023
81e74e7
Merge branch 'dev' into sync-handle-insufficient-history
Aug 16, 2023
565f8f8
comment
Aug 16, 2023
3fa86f3
Merge branch 'dev' into sync-handle-insufficient-history
Aug 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
623 changes: 361 additions & 262 deletions proto/pb/sync/sync.pb.go

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions proto/pb/sync/sync_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 12 additions & 5 deletions proto/sync/sync.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ service DB {

rpc GetProof(GetProofRequest) returns (GetProofResponse);

rpc GetChangeProof(GetChangeProofRequest) returns (ChangeProof);
rpc GetChangeProof(GetChangeProofRequest) returns (GetChangeProofResponse);
rpc VerifyChangeProof(VerifyChangeProofRequest) returns (VerifyChangeProofResponse);
rpc CommitChangeProof(CommitChangeProofRequest) returns (google.protobuf.Empty);

Expand Down Expand Up @@ -75,6 +75,14 @@ message GetChangeProofRequest {
uint32 key_limit = 5;
}

message GetChangeProofResponse {
oneof response {
ChangeProof change_proof = 1;
// True iff server errored with merkledb.ErrInsufficientHistory.
bool root_not_present = 2;
}
}

message VerifyChangeProofRequest {
ChangeProof proof = 1;
bytes start_key = 2;
Expand Down Expand Up @@ -118,10 +126,9 @@ message CommitRangeProofRequest {
}

message ChangeProof {
bool had_roots_in_history = 1; // TODO remove
repeated ProofNode start_proof = 2;
repeated ProofNode end_proof = 3;
repeated KeyChange key_changes = 4;
repeated ProofNode start_proof = 1;
repeated ProofNode end_proof = 2;
repeated KeyChange key_changes = 3;
}

message RangeProof {
Expand Down
33 changes: 9 additions & 24 deletions x/merkledb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type ChangeProofer interface {
// GetChangeProof returns a proof for a subset of the key/value changes in key range
// [start, end] that occurred between [startRootID] and [endRootID].
// Returns at most [maxLength] key/value pairs.
// Returns [ErrInsufficientHistory] if this node has insufficient history
// to generate the proof.
GetChangeProof(
ctx context.Context,
startRootID ids.ID,
Expand All @@ -68,7 +70,7 @@ type ChangeProofer interface {

// Returns nil iff all of the following hold:
// - [start] <= [end].
// - [proof] is non-empty iff [proof.HadRootsInHistory].
// - [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 [end] is nothing, all keys are considered < [end].
Expand All @@ -77,7 +79,6 @@ type ChangeProofer interface {
// - When the keys in [proof.KeyValues] are added to [db] and the keys in [proof.DeletedKeys]
// are removed from [db], the root ID of [db] is [expectedEndRootID].
//
// Assumes [db.lock] isn't held.
// This is defined on Database instead of ChangeProof because it accesses
// database internals.
VerifyChangeProof(
Expand Down Expand Up @@ -581,14 +582,7 @@ func (db *merkleDB) GetChangeProof(
return nil, database.ErrClosed
}

result := &ChangeProof{
HadRootsInHistory: true,
}
changes, err := db.history.getValueChanges(startRootID, endRootID, start, end, maxLength)
if err == ErrRootIDNotPresent {
result.HadRootsInHistory = false
return result, nil
}
if err != nil {
return nil, err
}
Expand All @@ -599,7 +593,9 @@ func (db *merkleDB) GetChangeProof(
changedKeys := maps.Keys(changes.values)
utils.Sort(changedKeys)

result.KeyChanges = make([]KeyChange, 0, len(changedKeys))
result := &ChangeProof{
KeyChanges: make([]KeyChange, 0, len(changedKeys)),
}

for _, key := range changedKeys {
change := changes.values[key]
Expand Down Expand Up @@ -988,28 +984,17 @@ func (*merkleDB) CommitToDB(context.Context) error {
return nil
}

// Assumes [db.lock] isn't held.
func (db *merkleDB) VerifyChangeProof(
ctx context.Context,
proof *ChangeProof,
start []byte,
end maybe.Maybe[[]byte],
expectedEndRootID ids.ID,
) error {
if end.HasValue() && bytes.Compare(start, end.Value()) > 0 {
return ErrStartAfterEnd
}

if !proof.HadRootsInHistory {
// The node we requested the proof from didn't have sufficient
// history to fulfill this request.
if !proof.Empty() {
// cannot have any changes if the root was missing
return ErrDataInMissingRootProof
}
return nil
}

switch {
case end.HasValue() && bytes.Compare(start, end.Value()) > 0:
return ErrStartAfterEnd
case proof.Empty():
return ErrNoMerkleProof
case end.HasValue() && len(proof.KeyChanges) == 0 && len(proof.EndProof) == 0:
Expand Down
34 changes: 23 additions & 11 deletions x/merkledb/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ import (
"github.com/ava-labs/avalanchego/utils/set"
)

var (
ErrStartRootNotFound = errors.New("start root is not before end root in history")
ErrRootIDNotPresent = errors.New("root id is not present in history")
)
var ErrInsufficientHistory = errors.New("insufficient history to generate proof")

// stores previous trie states
type trieHistory struct {
Expand Down Expand Up @@ -74,9 +71,17 @@ func newTrieHistory(maxHistoryLookback int) *trieHistory {
}
}

// Returns up to [maxLength] key-value pair changes with keys in [start, end] that
// occurred between [startRoot] and [endRoot].
func (th *trieHistory) getValueChanges(startRoot, endRoot ids.ID, start []byte, end maybe.Maybe[[]byte], maxLength int) (*changeSummary, error) {
// Returns up to [maxLength] key-value pair changes with keys in
// [start, end] that occurred between [startRoot] and [endRoot].
// Returns [ErrInsufficientHistory] if the history is insufficient
// to generate the proof.
func (th *trieHistory) getValueChanges(
startRoot ids.ID,
endRoot ids.ID,
start []byte,
end maybe.Maybe[[]byte],
maxLength int,
) (*changeSummary, error) {
if maxLength <= 0 {
return nil, fmt.Errorf("%w but was %d", ErrInvalidMaxLength, maxLength)
}
Expand All @@ -86,17 +91,21 @@ func (th *trieHistory) getValueChanges(startRoot, endRoot ids.ID, start []byte,
}

// [endRootChanges] is the last change in the history resulting in [endRoot].
// TODO when we update to minimum go version 1.20.X, make this return another
// wrapped error ErrNoEndRoot. In NetworkServer.HandleChangeProofRequest, if we return
// that error, we know we shouldn't try to generate a range proof since we
// lack the necessary history.
endRootChanges, ok := th.lastChanges[endRoot]
if !ok {
return nil, ErrRootIDNotPresent
return nil, fmt.Errorf("%w: end root %s not found", ErrInsufficientHistory, endRoot)
}

// Confirm there's a change resulting in [startRoot] before
// a change resulting in [endRoot] in the history.
// [startRootChanges] is the last appearance of [startRoot].
startRootChanges, ok := th.lastChanges[startRoot]
if !ok {
return nil, ErrStartRootNotFound
return nil, fmt.Errorf("%w: start root %s not found", ErrInsufficientHistory, startRoot)
}

var (
Expand Down Expand Up @@ -131,7 +140,10 @@ func (th *trieHistory) getValueChanges(startRoot, endRoot ids.ID, start []byte,
}

if i == 0 {
return nil, ErrStartRootNotFound
return nil, fmt.Errorf(
"%w: start root %s not found before end root %s",
ErrInsufficientHistory, startRoot, endRoot,
)
}
}
}
Expand Down Expand Up @@ -216,7 +228,7 @@ func (th *trieHistory) getChangesToGetToRoot(rootID ids.ID, start []byte, end ma
// [lastRootChange] is the last change in the history resulting in [rootID].
lastRootChange, ok := th.lastChanges[rootID]
if !ok {
return nil, ErrRootIDNotPresent
return nil, ErrInsufficientHistory
}

var (
Expand Down
8 changes: 4 additions & 4 deletions x/merkledb/history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func Test_History_Bad_GetValueChanges_Input(t *testing.T) {
require.ErrorIs(err, ErrInvalidMaxLength)

_, err = db.history.getValueChanges(endRoot, startRoot, nil, maybe.Nothing[[]byte](), 1)
require.ErrorIs(err, ErrStartRootNotFound)
require.ErrorIs(err, ErrInsufficientHistory)

// trigger the first root to be deleted by exiting the lookback window
batch = db.NewBatch()
Expand All @@ -201,7 +201,7 @@ func Test_History_Bad_GetValueChanges_Input(t *testing.T) {

// now this root should no longer be present
_, err = db.history.getValueChanges(toBeDeletedRoot, endRoot, nil, maybe.Nothing[[]byte](), 1)
require.ErrorIs(err, ErrStartRootNotFound)
require.ErrorIs(err, ErrInsufficientHistory)

// same start/end roots should yield an empty changelist
changes, err := db.history.getValueChanges(endRoot, endRoot, nil, maybe.Nothing[[]byte](), 10)
Expand Down Expand Up @@ -261,7 +261,7 @@ func Test_History_Trigger_History_Queue_Looping(t *testing.T) {

// proof from first root shouldn't be generatable since it should have been removed from the history
_, err = db.GetRangeProofAtRoot(context.Background(), origRootID, []byte("k"), maybe.Some([]byte("key3")), 10)
require.ErrorIs(err, ErrRootIDNotPresent)
require.ErrorIs(err, ErrInsufficientHistory)
}

func Test_History_Values_Lookup_Over_Queue_Break(t *testing.T) {
Expand Down Expand Up @@ -661,7 +661,7 @@ func TestHistoryGetChangesToRoot(t *testing.T) {
{
name: "unknown root ID",
rootID: ids.GenerateTestID(),
expectedErr: ErrRootIDNotPresent,
expectedErr: ErrInsufficientHistory,
},
{
name: "most recent change",
Expand Down
21 changes: 9 additions & 12 deletions x/merkledb/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,12 +499,6 @@ type ChangeProof struct {
// Invariant: At least one of [StartProof], [EndProof], or
// [KeyChanges] is non-empty.

// If false, the node that created this doesn't have
// sufficient history to generate a change proof and
// all other fields must be empty.
// Otherwise at least one other field is non-empty.
HadRootsInHistory bool

// A proof that the smallest key in the requested range does/doesn't
// exist in the trie with the requested start root.
// Empty if no lower bound on the requested range was given.
Expand Down Expand Up @@ -592,10 +586,9 @@ func (proof *ChangeProof) ToProto() *pb.ChangeProof {
}

return &pb.ChangeProof{
HadRootsInHistory: proof.HadRootsInHistory,
StartProof: startProof,
EndProof: endProof,
KeyChanges: keyChanges,
StartProof: startProof,
EndProof: endProof,
KeyChanges: keyChanges,
}
}

Expand All @@ -604,8 +597,6 @@ func (proof *ChangeProof) UnmarshalProto(pbProof *pb.ChangeProof) error {
return ErrNilChangeProof
}

proof.HadRootsInHistory = pbProof.HadRootsInHistory

proof.StartProof = make([]ProofNode, len(pbProof.StartProof))
for i, protoNode := range pbProof.StartProof {
if err := proof.StartProof[i].UnmarshalProto(protoNode); err != nil {
Expand Down Expand Up @@ -692,6 +683,12 @@ func (proof *ChangeProof) Empty() bool {
len(proof.StartProof) == 0 && len(proof.EndProof) == 0
}

// Exactly one of [ChangeProof] or [RangeProof] is non-nil.
type ChangeOrRangeProof struct {
ChangeProof *ChangeProof
RangeProof *RangeProof
}

// Returns nil iff both hold:
// 1. [kvs] is sorted by key in increasing order.
// 2. All keys in [kvs] are in the range [start, end].
Expand Down
Loading