Skip to content

Commit

Permalink
chore(lib/trie): add internal/trie/tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 committed Nov 4, 2022
1 parent 2d2cf27 commit 1700e64
Show file tree
Hide file tree
Showing 10 changed files with 685 additions and 343 deletions.
56 changes: 56 additions & 0 deletions internal/trie/tracking/deltas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2022 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package tracking

import "github.com/ChainSafe/gossamer/lib/common"

// Deltas tracks the trie deltas, for example deleted node hashes.
type Deltas struct {
deletedNodeHashes map[common.Hash]struct{}
}

// New returns a new Deltas struct.
func New() *Deltas {
return &Deltas{
deletedNodeHashes: make(map[common.Hash]struct{}),
}
}

// RecordDeleted records a node hash as deleted.
func (d *Deltas) RecordDeleted(nodeHash common.Hash) {
d.deletedNodeHashes[nodeHash] = struct{}{}
}

// Deleted returns a set (map) of all the recorded deleted
// node hashes. Note the map returned is not deep copied for
// performance reasons and so it's not safe for mutation.
func (d *Deltas) Deleted() (nodeHashes map[common.Hash]struct{}) {
return d.deletedNodeHashes
}

// MergeWith merges the deltas given as argument in the receiving
// deltas struct.
func (d *Deltas) MergeWith(deltas DeletedGetter) {
for nodeHash := range deltas.Deleted() {
d.RecordDeleted(nodeHash)
}
}

// DeepCopy returns a deep copy of the deltas.
func (d *Deltas) DeepCopy() (deepCopy *Deltas) {
if d == nil {
return nil
}

deepCopy = &Deltas{}

if d.deletedNodeHashes != nil {
deepCopy.deletedNodeHashes = make(map[common.Hash]struct{}, len(d.deletedNodeHashes))
for nodeHash := range d.deletedNodeHashes {
deepCopy.deletedNodeHashes[nodeHash] = struct{}{}
}
}

return deepCopy
}
182 changes: 182 additions & 0 deletions internal/trie/tracking/deltas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2022 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package tracking

import (
"testing"

"github.com/ChainSafe/gossamer/lib/common"
"github.com/stretchr/testify/assert"
)

func Test_New(t *testing.T) {
t.Parallel()

deltas := New()

expectedDeltas := &Deltas{
deletedNodeHashes: make(map[common.Hash]struct{}),
}
assert.Equal(t, expectedDeltas, deltas)
}

func Test_Deltas_RecordDeleted(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
deltas Deltas
nodeHash common.Hash
expectedDeltas Deltas
}{
"set in empty deltas": {
deltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{},
},
nodeHash: common.Hash{1},
expectedDeltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
},
"set in non empty deltas": {
deltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
nodeHash: common.Hash{2},
expectedDeltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{
{1}: {}, {2}: {},
},
},
},
"override in deltas": {
deltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
nodeHash: common.Hash{1},
expectedDeltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
},
}

for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()

testCase.deltas.RecordDeleted(testCase.nodeHash)
assert.Equal(t, testCase.expectedDeltas, testCase.deltas)
})
}
}

func Test_Deltas_Deleted(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
deltas Deltas
nodeHashes map[common.Hash]struct{}
}{
"empty deltas": {},
"non empty deltas": {
deltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
nodeHashes: map[common.Hash]struct{}{{1}: {}},
},
}

for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()

nodeHashes := testCase.deltas.Deleted()
assert.Equal(t, testCase.nodeHashes, nodeHashes)
})
}
}

func Test_Deltas_MergeWith(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
deltas Deltas
deltasArg DeletedGetter
expectedDeltas Deltas
}{
"merge empty deltas": {
deltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
deltasArg: &Deltas{},
expectedDeltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
},
"merge deltas": {
deltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
deltasArg: &Deltas{
deletedNodeHashes: map[common.Hash]struct{}{
{1}: {}, {2}: {},
},
},
expectedDeltas: Deltas{
deletedNodeHashes: map[common.Hash]struct{}{
{1}: {}, {2}: {},
},
},
},
}

for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()

testCase.deltas.MergeWith(testCase.deltasArg)
assert.Equal(t, testCase.expectedDeltas, testCase.deltas)
})
}
}

func Test_Deltas_DeepCopy(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
deltasOriginal *Deltas
deltasCopy *Deltas
}{
"nil deltas": {},
"empty deltas": {
deltasOriginal: &Deltas{},
deltasCopy: &Deltas{},
},
"filled deltas": {
deltasOriginal: &Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
deltasCopy: &Deltas{
deletedNodeHashes: map[common.Hash]struct{}{{1}: {}},
},
},
}

for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()

deepCopy := testCase.deltasOriginal.DeepCopy()

assert.Equal(t, testCase.deltasCopy, deepCopy)
assertPointersNotEqual(t, testCase.deltasOriginal, deepCopy)
if testCase.deltasOriginal != nil {
assertPointersNotEqual(t, testCase.deltasOriginal.deletedNodeHashes, deepCopy.deletedNodeHashes)
}
})
}
}
37 changes: 37 additions & 0 deletions internal/trie/tracking/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2022 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package tracking

import (
"reflect"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func getPointer(x interface{}) (pointer uintptr, ok bool) {
func() {
defer func() {
ok = recover() == nil
}()
valueOfX := reflect.ValueOf(x)
pointer = valueOfX.Pointer()
}()
return pointer, ok
}

func assertPointersNotEqual(t *testing.T, a, b interface{}) {
t.Helper()
pointerA, okA := getPointer(a)
pointerB, okB := getPointer(b)
require.Equal(t, okA, okB)

switch {
case pointerA == 0 && pointerB == 0: // nil and nil
case okA:
assert.NotEqual(t, pointerA, pointerB)
default: // values like `int`
}
}
11 changes: 11 additions & 0 deletions internal/trie/tracking/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2022 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package tracking

import "github.com/ChainSafe/gossamer/lib/common"

// DeletedGetter gets deleted node hashes.
type DeletedGetter interface {
Deleted() (nodeHashes map[common.Hash]struct{})
}
39 changes: 36 additions & 3 deletions lib/trie/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,37 @@ func PopulateNodeHashes(n *Node, nodeHashes map[string]struct{}) {
}
}

// recordAllDeleted records the node hashes of the given node and all its descendants.
// Note it does not record inlined nodes.
// It is assumed the node and its descendant nodes have their Merkle value already
// computed, or the function will panic.
func recordAllDeleted(n *Node, recorder DeltaRecorder) {
if n == nil {
return
}

if len(n.MerkleValue) == 0 {
panic(fmt.Sprintf("node with key 0x%x has no Merkle value computed", n.Key))
}

isInlined := len(n.MerkleValue) < 32
if isInlined {
return
}

nodeHash := common.NewHash(n.MerkleValue)
recorder.RecordDeleted(nodeHash)

if n.Kind() == node.Leaf {
return
}

branch := n
for _, child := range branch.Children {
recordAllDeleted(child, recorder)
}
}

// GetFromDB retrieves a value at the given key from the trie using the database.
// It recursively descends into the trie using the database starting
// from the root node until it reaches the node with the given key.
Expand Down Expand Up @@ -419,9 +450,11 @@ func (t *Trie) getInsertedNodeHashesAtNode(n *Node, merkleValues map[string]stru
// node that was deleted from the trie since the last snapshot was made.
// The returned set is a copy of the internal set to prevent data corruption.
func (t *Trie) GetDeletedMerkleValues() (merkleValues map[string]struct{}) {
merkleValues = make(map[string]struct{}, len(t.deletedMerkleValues))
for k := range t.deletedMerkleValues {
merkleValues[k] = struct{}{}
deletedNodeHashes := t.deltas.Deleted()
// TODO return deletedNodeHashes directly after changing MerkleValue -> NodeHash
merkleValues = make(map[string]struct{}, len(deletedNodeHashes))
for nodeHash := range deletedNodeHashes {
merkleValues[string(nodeHash[:])] = struct{}{}
}
return merkleValues
}
10 changes: 10 additions & 0 deletions lib/trie/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"time"

"github.com/ChainSafe/gossamer/internal/trie/node"
"github.com/ChainSafe/gossamer/internal/trie/tracking"
"github.com/ChainSafe/gossamer/lib/common"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -144,3 +146,11 @@ func checkMerkleValuesAreSet(t *testing.T, n *Node) {
checkMerkleValuesAreSet(t, child)
}
}

func newDeltas(deleted []common.Hash) (deltas *tracking.Deltas) {
deltas = tracking.New()
for _, hash := range deleted {
deltas.RecordDeleted(hash)
}
return deltas
}
32 changes: 32 additions & 0 deletions lib/trie/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2022 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package trie

import (
"github.com/ChainSafe/gossamer/internal/trie/tracking"
"github.com/ChainSafe/gossamer/lib/common"
)

// Deltas is the interface for the trie local deltas since
// the last snapshot.
type Deltas interface {
DeltaMerger
DeltaDeletedGetter
}

// DeltaMerger merges the given deltas into the current
// deltas.
type DeltaMerger interface {
MergeWith(deltas tracking.DeletedGetter)
}

// DeltaDeletedGetter returns the deleted node hashes recorded so far.
type DeltaDeletedGetter interface {
Deleted() (nodeHashes map[common.Hash]struct{})
}

// DeltaRecorder records deltas done in a ongoing trie operation.
type DeltaRecorder interface {
RecordDeleted(nodeHash common.Hash)
}
Loading

0 comments on commit 1700e64

Please sign in to comment.