forked from TrueCloudLab/neoneo-go
6a04880b49
Some kind of marker is needed to check whether node has been collapsed or not. So introduce (HashNode).Collapsed
211 lines
7.4 KiB
Go
211 lines
7.4 KiB
Go
package mpt
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestBillet_RestoreHashNode(t *testing.T) {
|
|
check := func(t *testing.T, tr *Billet, expectedRoot Node, expectedNode Node, expectedRefCount uint32) {
|
|
_ = expectedRoot.Hash()
|
|
_ = tr.root.Hash()
|
|
require.Equal(t, expectedRoot, tr.root)
|
|
expectedBytes, err := tr.Store.Get(makeStorageKey(expectedNode.Hash().BytesBE()))
|
|
if expectedRefCount != 0 {
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedRefCount, binary.LittleEndian.Uint32(expectedBytes[len(expectedBytes)-4:]))
|
|
} else {
|
|
require.True(t, errors.Is(err, storage.ErrKeyNotFound))
|
|
}
|
|
}
|
|
|
|
t.Run("parent is Extension", func(t *testing.T) {
|
|
t.Run("restore Branch", func(t *testing.T) {
|
|
b := NewBranchNode()
|
|
b.Children[0] = NewExtensionNode([]byte{0x01}, NewLeafNode([]byte{0xAB, 0xCD}))
|
|
b.Children[5] = NewExtensionNode([]byte{0x01}, NewLeafNode([]byte{0xAB, 0xDE}))
|
|
path := toNibbles([]byte{0xAC})
|
|
e := NewExtensionNode(path, NewHashNode(b.Hash()))
|
|
tr := NewBillet(e.Hash(), true, newTestStore())
|
|
tr.root = e
|
|
|
|
// OK
|
|
n := new(NodeObject)
|
|
n.DecodeBinary(io.NewBinReaderFromBuf(b.Bytes()))
|
|
require.NoError(t, tr.RestoreHashNode(path, n.Node))
|
|
expected := NewExtensionNode(path, n.Node)
|
|
check(t, tr, expected, n.Node, 1)
|
|
|
|
// One more time (already restored) => panic expected, no refcount changes
|
|
require.Panics(t, func() {
|
|
_ = tr.RestoreHashNode(path, n.Node)
|
|
})
|
|
check(t, tr, expected, n.Node, 1)
|
|
|
|
// Same path, but wrong hash => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode(path, NewBranchNode()), ErrRestoreFailed))
|
|
check(t, tr, expected, n.Node, 1)
|
|
|
|
// New path (changes in the MPT structure are not allowed) => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode(toNibbles([]byte{0xAB}), n.Node), ErrRestoreFailed))
|
|
check(t, tr, expected, n.Node, 1)
|
|
})
|
|
|
|
t.Run("restore Leaf", func(t *testing.T) {
|
|
l := NewLeafNode([]byte{0xAB, 0xCD})
|
|
path := toNibbles([]byte{0xAC})
|
|
e := NewExtensionNode(path, NewHashNode(l.Hash()))
|
|
tr := NewBillet(e.Hash(), true, newTestStore())
|
|
tr.root = e
|
|
|
|
// OK
|
|
require.NoError(t, tr.RestoreHashNode(path, l))
|
|
expected := NewHashNode(e.Hash()) // leaf should be collapsed immediately => extension should also be collapsed
|
|
expected.Collapsed = true
|
|
check(t, tr, expected, l, 1)
|
|
|
|
// One more time (already restored and collapsed) => error expected, no refcount changes
|
|
require.Error(t, tr.RestoreHashNode(path, l))
|
|
check(t, tr, expected, l, 1)
|
|
|
|
// Same path, but wrong hash => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode(path, NewLeafNode([]byte{0xAB, 0xEF})), ErrRestoreFailed))
|
|
check(t, tr, expected, l, 1)
|
|
|
|
// New path (changes in the MPT structure are not allowed) => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode(toNibbles([]byte{0xAB}), l), ErrRestoreFailed))
|
|
check(t, tr, expected, l, 1)
|
|
})
|
|
|
|
t.Run("restore Hash", func(t *testing.T) {
|
|
h := NewHashNode(util.Uint256{1, 2, 3})
|
|
path := toNibbles([]byte{0xAC})
|
|
e := NewExtensionNode(path, h)
|
|
tr := NewBillet(e.Hash(), true, newTestStore())
|
|
tr.root = e
|
|
|
|
// no-op
|
|
require.True(t, errors.Is(tr.RestoreHashNode(path, h), ErrRestoreFailed))
|
|
check(t, tr, e, h, 0)
|
|
})
|
|
})
|
|
|
|
t.Run("parent is Leaf", func(t *testing.T) {
|
|
l := NewLeafNode([]byte{0xAB, 0xCD})
|
|
path := []byte{}
|
|
tr := NewBillet(l.Hash(), true, newTestStore())
|
|
tr.root = l
|
|
|
|
// Already restored => panic expected
|
|
require.Panics(t, func() {
|
|
_ = tr.RestoreHashNode(path, l)
|
|
})
|
|
|
|
// Same path, but wrong hash => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode(path, NewLeafNode([]byte{0xAB, 0xEF})), ErrRestoreFailed))
|
|
|
|
// Non-nil path, but MPT structure can't be changed => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode(toNibbles([]byte{0xAC}), NewLeafNode([]byte{0xAB, 0xEF})), ErrRestoreFailed))
|
|
})
|
|
|
|
t.Run("parent is Branch", func(t *testing.T) {
|
|
t.Run("middle child", func(t *testing.T) {
|
|
l1 := NewLeafNode([]byte{0xAB, 0xCD})
|
|
l2 := NewLeafNode([]byte{0xAB, 0xDE})
|
|
b := NewBranchNode()
|
|
b.Children[5] = NewHashNode(l1.Hash())
|
|
b.Children[lastChild] = NewHashNode(l2.Hash())
|
|
tr := NewBillet(b.Hash(), true, newTestStore())
|
|
tr.root = b
|
|
|
|
// OK
|
|
path := []byte{0x05}
|
|
require.NoError(t, tr.RestoreHashNode(path, l1))
|
|
check(t, tr, b, l1, 1)
|
|
|
|
// One more time (already restored) => panic expected.
|
|
// It's an MPT pool duty to avoid such situations during real restore process.
|
|
require.Panics(t, func() {
|
|
_ = tr.RestoreHashNode(path, l1)
|
|
})
|
|
// No refcount changes expected.
|
|
check(t, tr, b, l1, 1)
|
|
|
|
// Same path, but wrong hash => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode(path, NewLeafNode([]byte{0xAD})), ErrRestoreFailed))
|
|
check(t, tr, b, l1, 1)
|
|
|
|
// New path pointing to the empty HashNode (changes in the MPT structure are not allowed) => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode([]byte{0x01}, l1), ErrRestoreFailed))
|
|
check(t, tr, b, l1, 1)
|
|
})
|
|
|
|
t.Run("last child", func(t *testing.T) {
|
|
l1 := NewLeafNode([]byte{0xAB, 0xCD})
|
|
l2 := NewLeafNode([]byte{0xAB, 0xDE})
|
|
b := NewBranchNode()
|
|
b.Children[5] = NewHashNode(l1.Hash())
|
|
b.Children[lastChild] = NewHashNode(l2.Hash())
|
|
tr := NewBillet(b.Hash(), true, newTestStore())
|
|
tr.root = b
|
|
|
|
// OK
|
|
path := []byte{}
|
|
require.NoError(t, tr.RestoreHashNode(path, l2))
|
|
check(t, tr, b, l2, 1)
|
|
|
|
// One more time (already restored) => panic expected.
|
|
// It's an MPT pool duty to avoid such situations during real restore process.
|
|
require.Panics(t, func() {
|
|
_ = tr.RestoreHashNode(path, l2)
|
|
})
|
|
// No refcount changes expected.
|
|
check(t, tr, b, l2, 1)
|
|
|
|
// Same path, but wrong hash => error expected, no refcount changes
|
|
require.True(t, errors.Is(tr.RestoreHashNode(path, NewLeafNode([]byte{0xAD})), ErrRestoreFailed))
|
|
check(t, tr, b, l2, 1)
|
|
})
|
|
|
|
t.Run("two children with same hash", func(t *testing.T) {
|
|
l := NewLeafNode([]byte{0xAB, 0xCD})
|
|
b := NewBranchNode()
|
|
// two same hashnodes => leaf's refcount expected to be 2 in the end.
|
|
b.Children[3] = NewHashNode(l.Hash())
|
|
b.Children[4] = NewHashNode(l.Hash())
|
|
tr := NewBillet(b.Hash(), true, newTestStore())
|
|
tr.root = b
|
|
|
|
// OK
|
|
require.NoError(t, tr.RestoreHashNode([]byte{0x03}, l))
|
|
expected := b
|
|
expected.Children[3].(*HashNode).Collapsed = true
|
|
check(t, tr, b, l, 1)
|
|
|
|
// Restore another node with the same hash => no error expected, refcount should be incremented.
|
|
// Branch node should be collapsed.
|
|
require.NoError(t, tr.RestoreHashNode([]byte{0x04}, l))
|
|
res := NewHashNode(b.Hash())
|
|
res.Collapsed = true
|
|
check(t, tr, res, l, 2)
|
|
})
|
|
})
|
|
|
|
t.Run("parent is Hash", func(t *testing.T) {
|
|
l := NewLeafNode([]byte{0xAB, 0xCD})
|
|
b := NewBranchNode()
|
|
b.Children[3] = NewHashNode(l.Hash())
|
|
b.Children[4] = NewHashNode(l.Hash())
|
|
tr := NewBillet(b.Hash(), true, newTestStore())
|
|
|
|
// Should fail, because if it's a hash node with non-empty path, then the node
|
|
// has already been collapsed.
|
|
require.Error(t, tr.RestoreHashNode([]byte{0x03}, l))
|
|
})
|
|
}
|