Skip to content

Commit 0766e0b

Browse files
holimangzliudan
authored andcommitted
trie: stacktrie fixes (ethereum#21799)
* trie: fix error in stacktrie not committing small roots * trie: improved tests * trie: fix error in stacktrie with small nodes * trie: add (skipped) testcase for stacktrie * trie: fix docs in stacktrie
1 parent d697675 commit 0766e0b

3 files changed

Lines changed: 112 additions & 12 deletions

File tree

trie/stacktrie.go

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -315,19 +315,22 @@ func (st *StackTrie) hash() {
315315
panic(err)
316316
}
317317
case extNode:
318+
st.children[0].hash()
318319
h = newHasher(false)
319320
defer returnHasherToPool(h)
320321
h.tmp.Reset()
321-
st.children[0].hash()
322-
// This is also possible:
323-
//sz := hexToCompactInPlace(st.key)
324-
//n := [][]byte{
325-
// st.key[:sz],
326-
// st.children[0].val,
327-
//}
328-
n := [][]byte{
329-
hexToCompact(st.key),
330-
st.children[0].val,
322+
var valuenode Node
323+
if len(st.children[0].val) < 32 {
324+
valuenode = rawNode(st.children[0].val)
325+
} else {
326+
valuenode = HashNode(st.children[0].val)
327+
}
328+
n := struct {
329+
Key []byte
330+
Val Node
331+
}{
332+
Key: hexToCompact(st.key),
333+
Val: valuenode,
331334
}
332335
if err := rlp.Encode(&h.tmp, n); err != nil {
333336
panic(err)
@@ -407,6 +410,18 @@ func (st *StackTrie) Commit() (common.Hash, error) {
407410
return common.Hash{}, ErrCommitDisabled
408411
}
409412
st.hash()
410-
h := common.BytesToHash(st.val)
411-
return h, nil
413+
if len(st.val) != 32 {
414+
// If the node's RLP isn't 32 bytes long, the node will not
415+
// be hashed (and committed), and instead contain the rlp-encoding of the
416+
// node. For the top level node, we need to force the hashing+commit.
417+
ret := make([]byte, 32)
418+
h := newHasher(false)
419+
defer returnHasherToPool(h)
420+
h.sha.Reset()
421+
h.sha.Write(st.val)
422+
h.sha.Read(ret)
423+
st.db.Put(ret, st.val)
424+
return common.BytesToHash(ret), nil
425+
}
426+
return common.BytesToHash(st.val), nil
412427
}

trie/stacktrie_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,52 @@ func TestDerivableList(t *testing.T) {
240240
}
241241
}
242242
}
243+
244+
// TestUpdateSmallNodes tests a case where the leaves are small (both key and value),
245+
// which causes a lot of node-within-node. This case was found via fuzzing.
246+
func TestUpdateSmallNodes(t *testing.T) {
247+
st := NewStackTrie(nil)
248+
nt, _ := New(common.Hash{}, NewDatabase(memorydb.New()))
249+
kvs := []struct {
250+
K string
251+
V string
252+
}{
253+
{"63303030", "3041"}, // stacktrie.Update
254+
{"65", "3000"}, // stacktrie.Update
255+
}
256+
for _, kv := range kvs {
257+
nt.TryUpdate(common.FromHex(kv.K), common.FromHex(kv.V))
258+
st.TryUpdate(common.FromHex(kv.K), common.FromHex(kv.V))
259+
}
260+
if nt.Hash() != st.Hash() {
261+
t.Fatalf("error %x != %x", st.Hash(), nt.Hash())
262+
}
263+
}
264+
265+
// TestUpdateVariableKeys contains a case which stacktrie fails: when keys of different
266+
// sizes are used, and the second one has the same prefix as the first, then the
267+
// stacktrie fails, since it's unable to 'expand' on an already added leaf.
268+
// For all practical purposes, this is fine, since keys are fixed-size length
269+
// in account and storage tries.
270+
//
271+
// The test is marked as 'skipped', and exists just to have the behaviour documented.
272+
// This case was found via fuzzing.
273+
func TestUpdateVariableKeys(t *testing.T) {
274+
t.SkipNow()
275+
st := NewStackTrie(nil)
276+
nt, _ := New(common.Hash{}, NewDatabase(memorydb.New()))
277+
kvs := []struct {
278+
K string
279+
V string
280+
}{
281+
{"0x33303534636532393561313031676174", "303030"},
282+
{"0x3330353463653239356131303167617430", "313131"},
283+
}
284+
for _, kv := range kvs {
285+
nt.TryUpdate(common.FromHex(kv.K), common.FromHex(kv.V))
286+
st.TryUpdate(common.FromHex(kv.K), common.FromHex(kv.V))
287+
}
288+
if nt.Hash() != st.Hash() {
289+
t.Fatalf("error %x != %x", st.Hash(), nt.Hash())
290+
}
291+
}

trie/trie_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,42 @@ func TestCommitSequenceStackTrie(t *testing.T) {
605605
}
606606
}
607607

608+
// TestCommitSequenceSmallRoot tests that a trie which is essentially only a
609+
// small (<32 byte) shortnode with an included value is properly committed to a
610+
// database.
611+
// This case might not matter, since in practice, all keys are 32 bytes, which means
612+
// that even a small trie which contains a leaf will have an extension making it
613+
// not fit into 32 bytes, rlp-encoded. However, it's still the correct thing to do.
614+
func TestCommitSequenceSmallRoot(t *testing.T) {
615+
s := &spongeDb{sponge: sha3.NewLegacyKeccak256(), id: "a"}
616+
db := NewDatabase(s)
617+
trie, _ := New(common.Hash{}, db)
618+
// Another sponge is used for the stacktrie commits
619+
stackTrieSponge := &spongeDb{sponge: sha3.NewLegacyKeccak256(), id: "b"}
620+
stTrie := NewStackTrie(stackTrieSponge)
621+
// Add a single small-element to the trie(s)
622+
key := make([]byte, 5)
623+
key[0] = 1
624+
trie.TryUpdate(key, []byte{0x1})
625+
stTrie.TryUpdate(key, []byte{0x1})
626+
// Flush trie -> database
627+
root, _ := trie.Commit(nil)
628+
// Flush memdb -> disk (sponge)
629+
db.Commit(root, false)
630+
// And flush stacktrie -> disk
631+
stRoot, err := stTrie.Commit()
632+
if err != nil {
633+
t.Fatalf("Failed to commit stack trie %v", err)
634+
}
635+
if stRoot != root {
636+
t.Fatalf("root wrong, got %x exp %x", stRoot, root)
637+
}
638+
fmt.Printf("root: %x\n", stRoot)
639+
if got, exp := stackTrieSponge.sponge.Sum(nil), s.sponge.Sum(nil); !bytes.Equal(got, exp) {
640+
t.Fatalf("test, disk write sequence wrong:\ngot %x exp %x\n", got, exp)
641+
}
642+
}
643+
608644
// BenchmarkCommitAfterHashFixedSize benchmarks the Commit (after Hash) of a fixed number of updates to a trie.
609645
// This benchmark is meant to capture the difference on efficiency of small versus large changes. Typically,
610646
// storage tries are small (a couple of entries), whereas the full post-block account trie update is large (a couple

0 commit comments

Comments
 (0)