From 86201ae6bf1d3ba7119d2464aff64c3a5325a44f Mon Sep 17 00:00:00 2001 From: David Boehm <91908103+dboehm-avalabs@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:08:10 -0500 Subject: [PATCH] Remove sentinel node from MerkleDB proofs (#2106) Signed-off-by: David Boehm <91908103+dboehm-avalabs@users.noreply.github.com> Co-authored-by: Stephen Buttolph Co-authored-by: Dan Laine --- x/merkledb/db.go | 60 ++++++++++++++++----------- x/merkledb/history_test.go | 17 +++++--- x/merkledb/proof_test.go | 49 +++++++++++----------- x/merkledb/trie_test.go | 18 ++++---- x/merkledb/trieview.go | 85 +++++++++++++++++++++++++++++--------- x/sync/manager.go | 24 ++++++++++- 6 files changed, 170 insertions(+), 83 deletions(-) diff --git a/x/merkledb/db.go b/x/merkledb/db.go index 7e52e1fa9ecf..a2e71a5176da 100644 --- a/x/merkledb/db.go +++ b/x/merkledb/db.go @@ -41,8 +41,7 @@ const ( ) var ( - rootKey []byte - _ MerkleDB = (*merkleDB)(nil) + _ MerkleDB = (*merkleDB)(nil) codec = newCodec() @@ -54,8 +53,8 @@ var ( hadCleanShutdown = []byte{1} didNotHaveCleanShutdown = []byte{0} - errSameRoot = errors.New("start and end root are the same") - errNoNewRoot = errors.New("there was no updated root in change list") + errSameRoot = errors.New("start and end root are the same") + errNoNewSentinel = errors.New("there was no updated sentinel node in change list") ) type ChangeProofer interface { @@ -194,8 +193,10 @@ type merkleDB struct { debugTracer trace.Tracer infoTracer trace.Tracer - // The root of this trie. - root *node + // The sentinel node of this trie. + // It is the node with a nil key and is the ancestor of all nodes in the trie. + // If it has a value or has multiple children, it is also the root of the trie. + sentinelNode *node // Valid children of this trie. childViews []*trieView @@ -286,7 +287,7 @@ func newDatabase( // Deletes every intermediate node and rebuilds them by re-adding every key/value. // TODO: make this more efficient by only clearing out the stale portions of the trie. func (db *merkleDB) rebuild(ctx context.Context, cacheSize int) error { - db.root = newNode(Key{}) + db.sentinelNode = newNode(Key{}) // Delete intermediate nodes. if err := database.ClearPrefix(db.baseDB, intermediateNodePrefix, rebuildIntermediateDeletionWriteSize); err != nil { @@ -569,7 +570,20 @@ func (db *merkleDB) GetMerkleRoot(ctx context.Context) (ids.ID, error) { // Assumes [db.lock] is read locked. func (db *merkleDB) getMerkleRoot() ids.ID { - return db.root.id + if !isSentinelNodeTheRoot(db.sentinelNode) { + // if the sentinel node should be skipped, the trie's root is the nil key node's only child + for _, childEntry := range db.sentinelNode.children { + return childEntry.id + } + } + return db.sentinelNode.id +} + +// isSentinelNodeTheRoot returns true if the passed in sentinel node has a value and or multiple child nodes +// When this is true, the root of the trie is the sentinel node +// When this is false, the root of the trie is the sentinel node's single child +func isSentinelNodeTheRoot(sentinel *node) bool { + return sentinel.valueDigest.HasValue() || len(sentinel.children) != 1 } func (db *merkleDB) GetProof(ctx context.Context, key []byte) (*Proof, error) { @@ -915,9 +929,9 @@ func (db *merkleDB) commitChanges(ctx context.Context, trieToCommit *trieView) e return nil } - rootChange, ok := changes.nodes[Key{}] + sentinelChange, ok := changes.nodes[Key{}] if !ok { - return errNoNewRoot + return errNoNewSentinel } currentValueNodeBatch := db.valueNodeDB.NewBatch() @@ -959,7 +973,7 @@ func (db *merkleDB) commitChanges(ctx context.Context, trieToCommit *trieView) e // Only modify in-memory state after the commit succeeds // so that we don't need to clean up on error. - db.root = rootChange.after + db.sentinelNode = sentinelChange.after db.history.record(changes) return nil } @@ -1140,33 +1154,33 @@ func (db *merkleDB) invalidateChildrenExcept(exception *trieView) { } func (db *merkleDB) initializeRootIfNeeded() (ids.ID, error) { - // not sure if the root exists or had a value or not + // not sure if the sentinel node exists or if it had a value // check under both prefixes var err error - db.root, err = db.intermediateNodeDB.Get(Key{}) + db.sentinelNode, err = db.intermediateNodeDB.Get(Key{}) if errors.Is(err, database.ErrNotFound) { - db.root, err = db.valueNodeDB.Get(Key{}) + db.sentinelNode, err = db.valueNodeDB.Get(Key{}) } if err == nil { - // Root already exists, so calculate its id - db.root.calculateID(db.metrics) - return db.root.id, nil + // sentinel node already exists, so calculate the root ID of the trie + db.sentinelNode.calculateID(db.metrics) + return db.getMerkleRoot(), nil } if !errors.Is(err, database.ErrNotFound) { return ids.Empty, err } - // Root doesn't exist; make a new one. - db.root = newNode(Key{}) + // sentinel node doesn't exist; make a new one. + db.sentinelNode = newNode(Key{}) // update its ID - db.root.calculateID(db.metrics) + db.sentinelNode.calculateID(db.metrics) - if err := db.intermediateNodeDB.Put(Key{}, db.root); err != nil { + if err := db.intermediateNodeDB.Put(Key{}, db.sentinelNode); err != nil { return ids.Empty, err } - return db.root.id, nil + return db.sentinelNode.id, nil } // Returns a view of the trie as it was when it had root [rootID] for keys within range [start, end]. @@ -1243,7 +1257,7 @@ func (db *merkleDB) getNode(key Key, hasValue bool) (*node, error) { case db.closed: return nil, database.ErrClosed case key == Key{}: - return db.root, nil + return db.sentinelNode, nil case hasValue: return db.valueNodeDB.Get(key) } diff --git a/x/merkledb/history_test.go b/x/merkledb/history_test.go index f27c1293cde0..2ee1e5f4b31b 100644 --- a/x/merkledb/history_test.go +++ b/x/merkledb/history_test.go @@ -36,7 +36,8 @@ func Test_History_Simple(t *testing.T) { origProof, err := db.GetRangeProof(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), 10) require.NoError(err) require.NotNil(origProof) - origRootID := db.root.id + + origRootID := db.getMerkleRoot() require.NoError(origProof.Verify(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), origRootID, db.tokenSize)) batch = db.NewBatch() @@ -338,7 +339,8 @@ func Test_History_RepeatedRoot(t *testing.T) { origProof, err := db.GetRangeProof(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), 10) require.NoError(err) require.NotNil(origProof) - origRootID := db.root.id + + origRootID := db.getMerkleRoot() require.NoError(origProof.Verify(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), origRootID, db.tokenSize)) batch = db.NewBatch() @@ -380,7 +382,8 @@ func Test_History_ExcessDeletes(t *testing.T) { origProof, err := db.GetRangeProof(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), 10) require.NoError(err) require.NotNil(origProof) - origRootID := db.root.id + + origRootID := db.getMerkleRoot() require.NoError(origProof.Verify(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), origRootID, db.tokenSize)) batch = db.NewBatch() @@ -412,7 +415,8 @@ func Test_History_DontIncludeAllNodes(t *testing.T) { origProof, err := db.GetRangeProof(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), 10) require.NoError(err) require.NotNil(origProof) - origRootID := db.root.id + + origRootID := db.getMerkleRoot() require.NoError(origProof.Verify(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), origRootID, db.tokenSize)) batch = db.NewBatch() @@ -440,7 +444,7 @@ func Test_History_Branching2Nodes(t *testing.T) { origProof, err := db.GetRangeProof(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), 10) require.NoError(err) require.NotNil(origProof) - origRootID := db.root.id + origRootID := db.getMerkleRoot() require.NoError(origProof.Verify(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), origRootID, db.tokenSize)) batch = db.NewBatch() @@ -468,7 +472,8 @@ func Test_History_Branching3Nodes(t *testing.T) { origProof, err := db.GetRangeProof(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), 10) require.NoError(err) require.NotNil(origProof) - origRootID := db.root.id + + origRootID := db.getMerkleRoot() require.NoError(origProof.Verify(context.Background(), maybe.Some([]byte("k")), maybe.Some([]byte("key3")), origRootID, db.tokenSize)) batch = db.NewBatch() diff --git a/x/merkledb/proof_test.go b/x/merkledb/proof_test.go index 508e3d545d76..b22b80ffd09d 100644 --- a/x/merkledb/proof_test.go +++ b/x/merkledb/proof_test.go @@ -60,9 +60,9 @@ func Test_Proof_Verify_Bad_Data(t *testing.T) { expectedErr: nil, }, { - name: "odd length key with value", + name: "odd length key path with value", malform: func(proof *Proof) { - proof.Path[1].ValueOrHash = maybe.Some([]byte{1, 2}) + proof.Path[0].ValueOrHash = maybe.Some([]byte{1, 2}) }, expectedErr: ErrPartialByteLengthWithValue, }, @@ -150,7 +150,7 @@ func Test_RangeProof_Extra_Value(t *testing.T) { context.Background(), maybe.Some([]byte{1}), maybe.Some([]byte{5, 5}), - db.root.id, + db.getMerkleRoot(), db.tokenSize, )) @@ -160,7 +160,7 @@ func Test_RangeProof_Extra_Value(t *testing.T) { context.Background(), maybe.Some([]byte{1}), maybe.Some([]byte{5, 5}), - db.root.id, + db.getMerkleRoot(), db.tokenSize, ) require.ErrorIs(err, ErrInvalidProof) @@ -187,9 +187,9 @@ func Test_RangeProof_Verify_Bad_Data(t *testing.T) { expectedErr: ErrProofValueDoesntMatch, }, { - name: "EndProof: odd length key with value", + name: "EndProof: odd length key path with value", malform: func(proof *RangeProof) { - proof.EndProof[1].ValueOrHash = maybe.Some([]byte{1, 2}) + proof.EndProof[0].ValueOrHash = maybe.Some([]byte{1, 2}) }, expectedErr: ErrPartialByteLengthWithValue, }, @@ -255,6 +255,7 @@ func Test_Proof(t *testing.T) { context.Background(), ViewChanges{ BatchOps: []database.BatchOp{ + {Key: []byte("key"), Value: []byte("value")}, {Key: []byte("key0"), Value: []byte("value0")}, {Key: []byte("key1"), Value: []byte("value1")}, {Key: []byte("key2"), Value: []byte("value2")}, @@ -273,12 +274,11 @@ func Test_Proof(t *testing.T) { require.Len(proof.Path, 3) + require.Equal(ToKey([]byte("key")), proof.Path[0].Key) + require.Equal(maybe.Some([]byte("value")), proof.Path[0].ValueOrHash) require.Equal(ToKey([]byte("key1")), proof.Path[2].Key) require.Equal(maybe.Some([]byte("value1")), proof.Path[2].ValueOrHash) - require.Equal(ToKey([]byte{}), proof.Path[0].Key) - require.True(proof.Path[0].ValueOrHash.IsNothing()) - expectedRootID, err := trie.GetMerkleRoot(context.Background()) require.NoError(err) require.NoError(proof.Verify(context.Background(), expectedRootID, dbTrie.tokenSize)) @@ -501,9 +501,8 @@ func Test_RangeProof(t *testing.T) { require.Equal([]byte{2}, proof.KeyValues[1].Value) require.Equal([]byte{3}, proof.KeyValues[2].Value) - require.Nil(proof.EndProof[0].Key.Bytes()) - require.Equal([]byte{0}, proof.EndProof[1].Key.Bytes()) - require.Equal([]byte{3}, proof.EndProof[2].Key.Bytes()) + require.Equal([]byte{0}, proof.EndProof[0].Key.Bytes()) + require.Equal([]byte{3}, proof.EndProof[1].Key.Bytes()) // only a single node here since others are duplicates in endproof require.Equal([]byte{1}, proof.StartProof[0].Key.Bytes()) @@ -512,7 +511,7 @@ func Test_RangeProof(t *testing.T) { context.Background(), maybe.Some([]byte{1}), maybe.Some([]byte{3, 5}), - db.root.id, + db.getMerkleRoot(), db.tokenSize, )) } @@ -557,15 +556,14 @@ func Test_RangeProof_NilStart(t *testing.T) { require.Equal([]byte("value1"), proof.KeyValues[0].Value) require.Equal([]byte("value2"), proof.KeyValues[1].Value) - require.Equal(ToKey([]byte("key2")), proof.EndProof[2].Key) - require.Equal(ToKey([]byte("key2")).Take(28), proof.EndProof[1].Key) - require.Equal(ToKey([]byte("")), proof.EndProof[0].Key) + require.Equal(ToKey([]byte("key2")), proof.EndProof[1].Key) + require.Equal(ToKey([]byte("key2")).Take(28), proof.EndProof[0].Key) require.NoError(proof.Verify( context.Background(), maybe.Nothing[[]byte](), maybe.Some([]byte("key35")), - db.root.id, + db.getMerkleRoot(), db.tokenSize, )) } @@ -592,15 +590,14 @@ func Test_RangeProof_NilEnd(t *testing.T) { require.Equal([]byte{1}, proof.StartProof[0].Key.Bytes()) - require.Nil(proof.EndProof[0].Key.Bytes()) - require.Equal([]byte{0}, proof.EndProof[1].Key.Bytes()) - require.Equal([]byte{2}, proof.EndProof[2].Key.Bytes()) + require.Equal([]byte{0}, proof.EndProof[0].Key.Bytes()) + require.Equal([]byte{2}, proof.EndProof[1].Key.Bytes()) require.NoError(proof.Verify( context.Background(), maybe.Some([]byte{1}), maybe.Nothing[[]byte](), - db.root.id, + db.getMerkleRoot(), db.tokenSize, )) } @@ -635,15 +632,15 @@ func Test_RangeProof_EmptyValues(t *testing.T) { require.Len(proof.StartProof, 1) require.Equal(ToKey([]byte("key1")), proof.StartProof[0].Key) - require.Len(proof.EndProof, 3) - require.Equal(ToKey([]byte("key2")), proof.EndProof[2].Key) - require.Equal(ToKey([]byte{}), proof.EndProof[0].Key) + require.Len(proof.EndProof, 2) + require.Equal(ToKey([]byte("key2")), proof.EndProof[1].Key) + require.Equal(ToKey([]byte("key2")).Take(28), proof.EndProof[0].Key) require.NoError(proof.Verify( context.Background(), maybe.Some([]byte("key1")), maybe.Some([]byte("key2")), - db.root.id, + db.getMerkleRoot(), db.tokenSize, )) } @@ -779,7 +776,7 @@ func Test_ChangeProof_Verify_Bad_Data(t *testing.T) { { name: "odd length key path with value", malform: func(proof *ChangeProof) { - proof.EndProof[1].ValueOrHash = maybe.Some([]byte{1, 2}) + proof.EndProof[0].ValueOrHash = maybe.Some([]byte{1, 2}) }, expectedErr: ErrPartialByteLengthWithValue, }, diff --git a/x/merkledb/trie_test.go b/x/merkledb/trie_test.go index 9bce417a7b1a..a431dd6b254d 100644 --- a/x/merkledb/trie_test.go +++ b/x/merkledb/trie_test.go @@ -126,7 +126,7 @@ func TestTrieViewVisitPathToKey(t *testing.T) { // Just the root require.Len(nodePath, 1) - require.Equal(trie.root, nodePath[0]) + require.Equal(trie.sentinelNode, nodePath[0]) // Insert a key key1 := []byte{0} @@ -151,7 +151,8 @@ func TestTrieViewVisitPathToKey(t *testing.T) { // Root and 1 value require.Len(nodePath, 2) - require.Equal(trie.root, nodePath[0]) + + require.Equal(trie.sentinelNode, nodePath[0]) require.Equal(ToKey(key1), nodePath[1].key) // Insert another key which is a child of the first @@ -175,7 +176,8 @@ func TestTrieViewVisitPathToKey(t *testing.T) { return nil })) require.Len(nodePath, 3) - require.Equal(trie.root, nodePath[0]) + + require.Equal(trie.sentinelNode, nodePath[0]) require.Equal(ToKey(key1), nodePath[1].key) require.Equal(ToKey(key2), nodePath[2].key) @@ -201,7 +203,8 @@ func TestTrieViewVisitPathToKey(t *testing.T) { })) require.Len(nodePath, 2) - require.Equal(trie.root, nodePath[0]) + + require.Equal(trie.sentinelNode, nodePath[0]) require.Equal(ToKey(key3), nodePath[1].key) // Other key path not affected @@ -211,7 +214,8 @@ func TestTrieViewVisitPathToKey(t *testing.T) { return nil })) require.Len(nodePath, 3) - require.Equal(trie.root, nodePath[0]) + + require.Equal(trie.sentinelNode, nodePath[0]) require.Equal(ToKey(key1), nodePath[1].key) require.Equal(ToKey(key2), nodePath[2].key) @@ -224,7 +228,7 @@ func TestTrieViewVisitPathToKey(t *testing.T) { })) require.Len(nodePath, 3) - require.Equal(trie.root, nodePath[0]) + require.Equal(trie.sentinelNode, nodePath[0]) require.Equal(ToKey(key1), nodePath[1].key) require.Equal(ToKey(key2), nodePath[2].key) @@ -236,7 +240,7 @@ func TestTrieViewVisitPathToKey(t *testing.T) { return nil })) require.Len(nodePath, 1) - require.Equal(trie.root, nodePath[0]) + require.Equal(trie.sentinelNode, nodePath[0]) } func Test_Trie_ViewOnCommitedView(t *testing.T) { diff --git a/x/merkledb/trieview.go b/x/merkledb/trieview.go index d8d9cfbdeb28..622bfcb11207 100644 --- a/x/merkledb/trieview.go +++ b/x/merkledb/trieview.go @@ -96,8 +96,9 @@ type trieView struct { db *merkleDB - // The root of the trie represented by this view. - root *node + // The nil key node + // It is either the root of the trie or the root of the trie is its single child node + sentinelNode *node tokenSize int } @@ -147,7 +148,7 @@ func newTrieView( parentTrie TrieView, changes ViewChanges, ) (*trieView, error) { - root, err := parentTrie.getEditableNode(Key{}, false /* hasValue */) + sentinelNode, err := parentTrie.getEditableNode(Key{}, false /* hasValue */) if err != nil { if errors.Is(err, database.ErrNotFound) { return nil, ErrNoValidRoot @@ -156,11 +157,11 @@ func newTrieView( } newView := &trieView{ - root: root, - db: db, - parentTrie: parentTrie, - changes: newChangeSummary(len(changes.BatchOps) + len(changes.MapOps)), - tokenSize: db.tokenSize, + sentinelNode: sentinelNode, + db: db, + parentTrie: parentTrie, + changes: newChangeSummary(len(changes.BatchOps) + len(changes.MapOps)), + tokenSize: db.tokenSize, } for _, op := range changes.BatchOps { @@ -200,17 +201,17 @@ func newHistoricalTrieView( return nil, ErrNoValidRoot } - passedRootChange, ok := changes.nodes[Key{}] + passedSentinelChange, ok := changes.nodes[Key{}] if !ok { return nil, ErrNoValidRoot } newView := &trieView{ - root: passedRootChange.after, - db: db, - parentTrie: db, - changes: changes, - tokenSize: db.tokenSize, + sentinelNode: passedSentinelChange.after, + db: db, + parentTrie: db, + changes: changes, + tokenSize: db.tokenSize, } // since this is a set of historical changes, all nodes have already been calculated // since no new changes have occurred, no new calculations need to be done @@ -250,9 +251,9 @@ func (t *trieView) calculateNodeIDs(ctx context.Context) error { } _ = t.db.calculateNodeIDsSema.Acquire(context.Background(), 1) - t.calculateNodeIDsHelper(t.root) + t.calculateNodeIDsHelper(t.sentinelNode) t.db.calculateNodeIDsSema.Release(1) - t.changes.rootID = t.root.id + t.changes.rootID = t.getMerkleRoot() // ensure no ancestor changes occurred during execution if t.isInvalid() { @@ -348,6 +349,22 @@ func (t *trieView) getProof(ctx context.Context, key []byte) (*Proof, error) { }); err != nil { return nil, err } + root, err := t.getRoot() + if err != nil { + return nil, err + } + + // The sentinel node is always the first node in the path. + // If the sentinel node is not the root, remove it from the proofPath. + if root != t.sentinelNode { + proof.Path = proof.Path[1:] + + // if there are no nodes in the proof path, add the root to serve as an exclusion proof + if len(proof.Path) == 0 { + proof.Path = []ProofNode{root.asProofNode()} + return proof, nil + } + } if closestNode.key == proof.Key { // There is a node with the given [key]. @@ -460,7 +477,11 @@ func (t *trieView) GetRangeProof( if len(result.StartProof) == 0 && len(result.EndProof) == 0 && len(result.KeyValues) == 0 { // If the range is empty, return the root proof. - rootProof, err := t.getProof(ctx, rootKey) + root, err := t.getRoot() + if err != nil { + return nil, err + } + rootProof, err := t.getProof(ctx, root.key.Bytes()) if err != nil { return nil, err } @@ -547,7 +568,17 @@ func (t *trieView) GetMerkleRoot(ctx context.Context) (ids.ID, error) { if err := t.calculateNodeIDs(ctx); err != nil { return ids.Empty, err } - return t.root.id, nil + return t.getMerkleRoot(), nil +} + +func (t *trieView) getMerkleRoot() ids.ID { + if !isSentinelNodeTheRoot(t.sentinelNode) { + for _, childEntry := range t.sentinelNode.children { + return childEntry.id + } + } + + return t.sentinelNode.id } func (t *trieView) GetValues(ctx context.Context, keys [][]byte) ([][]byte, []error) { @@ -717,8 +748,8 @@ func (t *trieView) compressNodePath(parent, node *node) error { // Always returns at least the root node. func (t *trieView) visitPathToKey(key Key, visitNode func(*node) error) error { var ( - // all node paths start at the root - currentNode = t.root + // all node paths start at the sentinelNode since its nil key is a prefix of all keys + currentNode = t.sentinelNode err error ) if err := visitNode(currentNode); err != nil { @@ -889,6 +920,20 @@ func (t *trieView) recordNodeDeleted(after *node) error { return t.recordKeyChange(after.key, nil, after.hasValue(), false /* newNode */) } +func (t *trieView) getRoot() (*node, error) { + if !isSentinelNodeTheRoot(t.sentinelNode) { + // sentinelNode has one child, which is the root + for index, childEntry := range t.sentinelNode.children { + return t.getNodeWithID( + childEntry.id, + t.sentinelNode.key.Extend(ToToken(index, t.tokenSize), childEntry.compressedKey), + childEntry.hasValue) + } + } + + return t.sentinelNode, nil +} + // Records that the node associated with the given key has been changed. // If it is an existing node, record what its value was before it was changed. // Must not be called after [calculateNodeIDs] has returned. diff --git a/x/sync/manager.go b/x/sync/manager.go index 6bd81e847aee..a7a6858d5122 100644 --- a/x/sync/manager.go +++ b/x/sync/manager.go @@ -434,10 +434,32 @@ func (m *Manager) findNextKey( nextKey := maybe.Nothing[[]byte]() + // Add sentinel node back into the localProofNodes, if it is missing. + // Required to ensure that a common node exists in both proofs + if len(localProofNodes) > 0 && localProofNodes[0].Key.Length() != 0 { + sentinel := merkledb.ProofNode{ + Children: map[byte]ids.ID{ + localProofNodes[0].Key.Token(0, m.tokenSize): ids.Empty, + }, + } + localProofNodes = append([]merkledb.ProofNode{sentinel}, localProofNodes...) + } + + // Add sentinel node back into the endProof, if it is missing. + // Required to ensure that a common node exists in both proofs + if len(endProof) > 0 && endProof[0].Key.Length() != 0 { + sentinel := merkledb.ProofNode{ + Children: map[byte]ids.ID{ + endProof[0].Key.Token(0, m.tokenSize): ids.Empty, + }, + } + endProof = append([]merkledb.ProofNode{sentinel}, endProof...) + } + localProofNodeIndex := len(localProofNodes) - 1 receivedProofNodeIndex := len(endProof) - 1 - // traverse the two proofs from the deepest nodes up to the root until a difference is found + // traverse the two proofs from the deepest nodes up to the sentinel node until a difference is found for localProofNodeIndex >= 0 && receivedProofNodeIndex >= 0 && nextKey.IsNothing() { localProofNode := localProofNodes[localProofNodeIndex] receivedProofNode := endProof[receivedProofNodeIndex]