trie: fix delete bug for values contained in fullNode
Delete crashed if a fullNode contained a valueNode directly. This bug is very unlikely to occur with SecureTrie, but can happen with regular tries. This commit also introduces a randomised test which triggers all trie operations, which should prevent such bugs in the future. Credit for finding this bug goes to Github user @rjl493456442.
This commit is contained in:
parent
ba8c4c6b1a
commit
c3a77d6268
@ -377,6 +377,9 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) {
|
||||
// n still contains at least two values and cannot be reduced.
|
||||
return true, n, nil
|
||||
|
||||
case valueNode:
|
||||
return true, nil, nil
|
||||
|
||||
case nil:
|
||||
return false, nil, nil
|
||||
|
||||
|
@ -21,8 +21,11 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
@ -297,41 +300,6 @@ func TestReplication(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func paranoiaCheck(t1 *Trie) (bool, *Trie) {
|
||||
t2 := new(Trie)
|
||||
it := NewIterator(t1)
|
||||
for it.Next() {
|
||||
t2.Update(it.Key, it.Value)
|
||||
}
|
||||
return t2.Hash() == t1.Hash(), t2
|
||||
}
|
||||
|
||||
func TestParanoia(t *testing.T) {
|
||||
t.Skip()
|
||||
trie := newEmpty()
|
||||
|
||||
vals := []struct{ k, v string }{
|
||||
{"do", "verb"},
|
||||
{"ether", "wookiedoo"},
|
||||
{"horse", "stallion"},
|
||||
{"shaman", "horse"},
|
||||
{"doge", "coin"},
|
||||
{"ether", ""},
|
||||
{"dog", "puppy"},
|
||||
{"shaman", ""},
|
||||
{"somethingveryoddindeedthis is", "myothernodedata"},
|
||||
}
|
||||
for _, val := range vals {
|
||||
updateString(trie, val.k, val.v)
|
||||
}
|
||||
trie.Commit()
|
||||
|
||||
ok, t2 := paranoiaCheck(trie)
|
||||
if !ok {
|
||||
t.Errorf("trie paranoia check failed %x %x", trie.Hash(), t2.Hash())
|
||||
}
|
||||
}
|
||||
|
||||
// Not an actual test
|
||||
func TestOutput(t *testing.T) {
|
||||
t.Skip()
|
||||
@ -356,7 +324,128 @@ func TestLargeValue(t *testing.T) {
|
||||
trie.Update([]byte("key1"), []byte{99, 99, 99, 99})
|
||||
trie.Update([]byte("key2"), bytes.Repeat([]byte{1}, 32))
|
||||
trie.Hash()
|
||||
}
|
||||
|
||||
type randTestStep struct {
|
||||
op int
|
||||
key []byte // for opUpdate, opDelete, opGet
|
||||
value []byte // for opUpdate
|
||||
}
|
||||
|
||||
type randTest []randTestStep
|
||||
|
||||
const (
|
||||
opUpdate = iota
|
||||
opDelete
|
||||
opGet
|
||||
opCommit
|
||||
opHash
|
||||
opReset
|
||||
opItercheckhash
|
||||
opMax // boundary value, not an actual op
|
||||
)
|
||||
|
||||
func (randTest) Generate(r *rand.Rand, size int) reflect.Value {
|
||||
var allKeys [][]byte
|
||||
genKey := func() []byte {
|
||||
if len(allKeys) < 2 || r.Intn(100) < 10 {
|
||||
// new key
|
||||
key := make([]byte, r.Intn(50))
|
||||
randRead(r, key)
|
||||
allKeys = append(allKeys, key)
|
||||
return key
|
||||
}
|
||||
// use existing key
|
||||
return allKeys[r.Intn(len(allKeys))]
|
||||
}
|
||||
|
||||
var steps randTest
|
||||
for i := 0; i < size; i++ {
|
||||
step := randTestStep{op: r.Intn(opMax)}
|
||||
switch step.op {
|
||||
case opUpdate:
|
||||
step.key = genKey()
|
||||
step.value = make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(step.value, uint64(i))
|
||||
case opGet, opDelete:
|
||||
step.key = genKey()
|
||||
}
|
||||
steps = append(steps, step)
|
||||
}
|
||||
return reflect.ValueOf(steps)
|
||||
}
|
||||
|
||||
// rand.Rand provides a Read method in Go 1.7 and later, but
|
||||
// we can't use it yet.
|
||||
func randRead(r *rand.Rand, b []byte) {
|
||||
pos := 0
|
||||
val := 0
|
||||
for n := 0; n < len(b); n++ {
|
||||
if pos == 0 {
|
||||
val = r.Int()
|
||||
pos = 7
|
||||
}
|
||||
b[n] = byte(val)
|
||||
val >>= 8
|
||||
pos--
|
||||
}
|
||||
}
|
||||
|
||||
func runRandTest(rt randTest) bool {
|
||||
db, _ := ethdb.NewMemDatabase()
|
||||
tr, _ := New(common.Hash{}, db)
|
||||
values := make(map[string]string) // tracks content of the trie
|
||||
|
||||
for _, step := range rt {
|
||||
switch step.op {
|
||||
case opUpdate:
|
||||
tr.Update(step.key, step.value)
|
||||
values[string(step.key)] = string(step.value)
|
||||
case opDelete:
|
||||
tr.Delete(step.key)
|
||||
delete(values, string(step.key))
|
||||
case opGet:
|
||||
v := tr.Get(step.key)
|
||||
want := values[string(step.key)]
|
||||
if string(v) != want {
|
||||
fmt.Printf("mismatch for key 0x%x, got 0x%x want 0x%x", step.key, v, want)
|
||||
return false
|
||||
}
|
||||
case opCommit:
|
||||
if _, err := tr.Commit(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case opHash:
|
||||
tr.Hash()
|
||||
case opReset:
|
||||
hash, err := tr.Commit()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newtr, err := New(hash, db)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tr = newtr
|
||||
case opItercheckhash:
|
||||
checktr, _ := New(common.Hash{}, nil)
|
||||
it := tr.Iterator()
|
||||
for it.Next() {
|
||||
checktr.Update(it.Key, it.Value)
|
||||
}
|
||||
if tr.Hash() != checktr.Hash() {
|
||||
fmt.Println("hashes not equal")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestRandom(t *testing.T) {
|
||||
if err := quick.Check(runRandTest, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGet(b *testing.B) { benchGet(b, false) }
|
||||
|
Loading…
Reference in New Issue
Block a user