diff --git a/trie/proof_test.go b/trie/proof_test.go index 59ae201cea..68d11ec752 100644 --- a/trie/proof_test.go +++ b/trie/proof_test.go @@ -195,6 +195,10 @@ func TestRangeProof(t *testing.T) { if err != nil { t.Fatalf("Case %d(%d->%d) expect no error, got %v", i, start, end-1, err) } + _, err = VerifyRangeProofWithStack(trie.Hash(), keys[0], keys, vals, proof) + if err != nil { + t.Fatalf("Case %d(%d->%d) expect no error, got %v", i, start, end-1, err) + } } } @@ -237,6 +241,10 @@ func TestRangeProofWithNonExistentProof(t *testing.T) { if err != nil { t.Fatalf("Case %d(%d->%d) expect no error, got %v", i, start, end-1, err) } + _, err = VerifyRangeProofWithStack(trie.Hash(), first, keys, vals, proof) + if err != nil { + t.Fatalf("Case %d(%d->%d) expect no error, got %v", i, start, end-1, err) + } } } @@ -872,16 +880,17 @@ func BenchmarkVerifyRangeProof10(b *testing.B) { benchmarkVerifyRangeProof(b, func BenchmarkVerifyRangeProof100(b *testing.B) { benchmarkVerifyRangeProof(b, 100) } func BenchmarkVerifyRangeProof1000(b *testing.B) { benchmarkVerifyRangeProof(b, 1000) } func BenchmarkVerifyRangeProof5000(b *testing.B) { benchmarkVerifyRangeProof(b, 5000) } +func BenchmarkVerifyRangeProof10K(b *testing.B) { benchmarkVerifyRangeProof(b, 10000) } func benchmarkVerifyRangeProof(b *testing.B, size int) { - trie, vals := randomTrie(8192) + trie, vals := randomTrie(size * 3) var entries []*kv for _, kv := range vals { entries = append(entries, kv) } slices.SortFunc(entries, (*kv).cmp) - start := 2 + start := size end := start + size proof := memorydb.New() if err := trie.Prove(entries[start].k, proof); err != nil { @@ -896,8 +905,8 @@ func benchmarkVerifyRangeProof(b *testing.B, size int) { keys = append(keys, entries[i].k) values = append(values, entries[i].v) } - b.ResetTimer() + b.ReportAllocs() for i := 0; i < b.N; i++ { _, err := VerifyRangeProof(trie.Hash(), keys[0], keys, values, proof) if err != nil { diff --git a/trie/stackproof.go b/trie/stackproof.go index e8abfa3326..c84dd59631 100644 --- a/trie/stackproof.go +++ b/trie/stackproof.go @@ -1,11 +1,13 @@ package trie import ( + "bytes" "errors" "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethdb" + "golang.org/x/exp/slices" ) // nodeToStNode converts from `node` to `*stNode`. @@ -222,3 +224,117 @@ func iterateProof(rootHash common.Hash, path []byte, ascending bool, proof ethdb } return paths, nil } + +// RootFromLeafs calculates the trie root for the trie built up with the key/values +// given as input. +// This method errors if +// 1. The keys/values are not of equal length +// 2. The keys are not monotonically increasing +func RootFromLeafs(keys [][]byte, values [][]byte) (common.Hash, error) { + var ( + tr = NewStackTrie(nil) + pKey []byte + ) + for i, key := range keys { + // Ensure the received batch is monotonic increasing and contains no deletions + if bytes.Compare(pKey, key) >= 0 { + return common.Hash{}, errors.New("range is not monotonically increasing") + } + if len(values[i]) == 0 { + return common.Hash{}, errors.New("range contains deletion") + } + tr.Update(key, values[i]) + pKey = key + } + return tr.Hash(), nil +} + +func VerifyRootFromLeafs(root common.Hash, keys [][]byte, values [][]byte) error { + have, err := RootFromLeafs(keys, values) + if err != nil { + return err + } + if have != root { + return fmt.Errorf("want root %x, have %x", root, have) + } + return nil +} + +// TODO @holiman make this handle proofs-of-nonexistence +func VerifyRangeProofWithStack(rootHash common.Hash, firstKey []byte, keys [][]byte, values [][]byte, proof ethdb.KeyValueReader) (bool, error) { + if len(keys) != len(values) { + return false, fmt.Errorf("inconsistent proof data, keys: %d, values: %d", len(keys), len(values)) + } + // Special case, there is no edge proof at all. The given range is expected + // to be the whole leaf-set in the trie. + if proof == nil { + return false, VerifyRootFromLeafs(rootHash, keys, values) + } + // Special case, there is a provided edge proof but zero key/value + // pairs, ensure there are no more accounts / slots in the trie. + if len(keys) == 0 { + root, val, err := proofToPath(rootHash, nil, firstKey, proof, true) + if err != nil { + return false, err + } + if val != nil || hasRightElement(root, firstKey) { + return false, errors.New("more entries available") + } + return false, nil + } + lastKey := keys[len(keys)-1] + // Special case, there is only one element and two edge keys are same. + // In this case, we can't construct two edge paths. So handle it here. + if len(keys) == 1 && bytes.Equal(firstKey, lastKey) { + root, val, err := proofToPath(rootHash, nil, firstKey, proof, false) + if err != nil { + return false, err + } + if !bytes.Equal(firstKey, keys[0]) { + return false, errors.New("correct proof but invalid key") + } + if !bytes.Equal(val, values[0]) { + return false, errors.New("correct proof but invalid data") + } + return hasRightElement(root, firstKey), nil + } + // Ok, in all other cases, we require two edge paths available. + // First check the validity of edge keys. + if bytes.Compare(firstKey, lastKey) >= 0 { + return false, errors.New("invalid edge keys") + } + // todo(rjl493456442) different length edge keys should be supported + if len(firstKey) != len(lastKey) { + return false, errors.New("inconsistent edge keys") + } + // Use the proof to initiate a stacktrie along the first path/value + stTrie, err := newStackTrieFromProof(rootHash, firstKey, proof, nil) + if err != nil { + return false, fmt.Errorf("could not initate stacktrie: %v", err) + } + // Feed in the values, starting from 1 (do not re-add the proof-leaf) + for i := 1; i < len(keys); i++ { + if bytes.Compare(keys[i-1], keys[i]) >= 0 { + return false, errors.New("range is not monotonically increasing") + } + if len(values[i]) == 0 { + return false, errors.New("range contains deletion") + } + stTrie.Update(keys[i], values[i]) + } + // For the right-hand-side, we need a list of hashes ot inject + hps, err := iterateProof(rootHash, lastKey, false, proof) + if err != nil { + return false, fmt.Errorf("proof iteration failed: %v", err) + } + slices.Reverse(hps) + // Insert into stacktrie + for _, hp := range hps { + stTrie.insert(stTrie.root, hp.path, hp.hash[:], nil, newHashed) + } + if have := stTrie.Hash(); have != rootHash { + return false, fmt.Errorf("invalid proof, want hash %x, got %x", rootHash, have) + } + // hasRightElement is true if the hashes we inserted are non-0 + return len(hps) > 0, nil +} diff --git a/trie/stackproof_test.go b/trie/stackproof_test.go index 86864afa50..8b7d4fd084 100644 --- a/trie/stackproof_test.go +++ b/trie/stackproof_test.go @@ -2,10 +2,12 @@ package trie import ( "bytes" + "fmt" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/memorydb" "golang.org/x/crypto/sha3" "golang.org/x/exp/slices" @@ -55,8 +57,8 @@ func testStRangeProofLeftside(t *testing.T, trie *Trie, vals map[string]*kv) { t.Fatalf("Failed to prove the first node %v", err) } // Initiate the stacktrie with the proof - stTrie, err := newStackTrieFromProof(trie.Hash(), entries[start].k, proof, func(owner common.Hash, path []byte, hash common.Hash, blob []byte) { - rawdb.WriteTrieNode(haveSponge, owner, path, hash, blob, "path") + stTrie, err := newStackTrieFromProof(trie.Hash(), entries[start].k, proof, func(path []byte, hash common.Hash, blob []byte) { + rawdb.WriteTrieNode(haveSponge, common.Hash{}, path, hash, blob, "path") }) if err != nil { t.Fatal(err) @@ -67,8 +69,8 @@ func testStRangeProofLeftside(t *testing.T, trie *Trie, vals map[string]*kv) { k, v := common.CopyBytes(entries[i].k), common.CopyBytes(entries[i].v) refTrie.Update(k, v) } - refTrie.writeFn = func(owner common.Hash, path []byte, hash common.Hash, blob []byte) { - rawdb.WriteTrieNode(wantSponge, owner, path, hash, blob, "path") + refTrie.writeFn = func(path []byte, hash common.Hash, blob []byte) { + rawdb.WriteTrieNode(wantSponge, common.Hash{}, path, hash, blob, "path") } // Feed the remaining values into them both for i := start + 1; i < len(vals); i++ { @@ -142,3 +144,66 @@ func testStackInsertHash(t *testing.T, trie *Trie, vals map[string]*kv) { } } } + +func TestStackRangeProof(t *testing.T) { + trie, vals := randomTrie(4096) + var entries []*kv + for _, kv := range vals { + entries = append(entries, kv) + } + slices.SortFunc(entries, (*kv).cmp) + proof := memorydb.New() + entries = entries[1000 : len(entries)-1000] // We snip off 1000 entries on either side + // Provide the proof for both first and last entry + if err := trie.Prove(entries[0].k, proof); err != nil { + t.Fatalf("Failed to prove the first node %v", err) + } + if err := trie.Prove(entries[len(entries)-1].k, proof); err != nil { + t.Fatalf("Failed to prove the last node %v", err) + } + testStackRangeProof(t, trie.Hash(), proof, entries) +} + +func testStackRangeProof( + t *testing.T, rootHash common.Hash, + proof ethdb.KeyValueReader, entries []*kv) { + + var leftBorder = keybytesToHex(entries[0].k) + var rightBorder = keybytesToHex(entries[len(entries)-1].k) + writeFn := func(_ common.Hash, path []byte, hash common.Hash, blob []byte) { + if bytes.HasPrefix(leftBorder, path) { + fmt.Printf("path %x tainted left (parent to %x)\n", path, leftBorder) + return + } + if bytes.HasPrefix(rightBorder, path) { + fmt.Printf("path %x tainted right (parent to %x)\n", path, rightBorder) + return + } + //fmt.Printf("Committing path %x\n", path) + } + + // Use the proof initiate the stacktrie with the first entry + stTrie, err := newStackTrieFromProof(rootHash, entries[0].k, proof, writeFn) + if err != nil { + t.Fatal(err) + } + // Feed in the standalone values + for i := 1; i < len(entries); i++ { + stTrie.Update(entries[i].k, common.CopyBytes(entries[i].v)) + } + // For the right-hand-side, we need a list of hashes ot inject + // Obtain the hashes + hps, err := iterateProof(rootHash, entries[len(entries)-1].k, false, proof) + if err != nil { + t.Fatal(err) + } + slices.Reverse(hps) + // Insert into stacktrie + for _, hp := range hps { + stTrie.insert(stTrie.root, hp.path, hp.hash[:], nil, newHashed) + } + have := stTrie.Hash() + if have != rootHash { + t.Fatalf("have %v want %v", have, rootHash) + } +}