light, trie: use NodeSet and NodeList for Merkle proofs

This commit is contained in:
Zsolt Felfoldi 2017-08-12 14:02:08 +02:00
parent bb030e08f3
commit 6016610f7d
7 changed files with 273 additions and 59 deletions

View file

@ -35,6 +35,7 @@ import (
"github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/light"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
@ -690,9 +691,10 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
} }
} }
if tr != nil { if tr != nil {
proof := tr.Prove(req.Key) var proof light.NodeList
tr.Prove(req.Key, 0, &proof)
proofs = append(proofs, proof) proofs = append(proofs, proof)
bytes += len(proof) bytes += proof.DataSize()
} }
} }
} }
@ -751,9 +753,10 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
if tr, _ := trie.New(root, pm.chainDb); tr != nil { if tr, _ := trie.New(root, pm.chainDb); tr != nil {
var encNumber [8]byte var encNumber [8]byte
binary.BigEndian.PutUint64(encNumber[:], req.BlockNum) binary.BigEndian.PutUint64(encNumber[:], req.BlockNum)
proof := tr.Prove(encNumber[:]) var proof light.NodeList
tr.Prove(encNumber[:], 0, &proof)
proofs = append(proofs, ChtResp{Header: header, Proof: proof}) proofs = append(proofs, ChtResp{Header: header, Proof: proof})
bytes += len(proof) + estHeaderRlpSize bytes += proof.DataSize() + estHeaderRlpSize
} }
} }
} }

View file

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/light"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
@ -331,7 +332,8 @@ func testGetProofs(t *testing.T, protocol int) {
} }
proofreqs = append(proofreqs, req) proofreqs = append(proofreqs, req)
proof := trie.Prove(crypto.Keccak256(acc[:])) var proof light.NodeList
trie.Prove(crypto.Keccak256(acc[:]), 0, &proof)
proofs = append(proofs, proof) proofs = append(proofs, proof)
} }
} }

View file

@ -220,7 +220,7 @@ func (r *TrieRequest) Validate(db ethdb.Database, msg *Msg) error {
return errMultipleEntries return errMultipleEntries
} }
// Verify the proof and store if checks out // Verify the proof and store if checks out
if _, err := trie.VerifyProof(r.Id.Root, r.Key, proofs[0]); err != nil { if _, err, _ := trie.VerifyProof(r.Id.Root, r.Key, light.NodeList(proofs[0]).NodeSet()); err != nil {
return fmt.Errorf("merkle proof verification failed: %v", err) return fmt.Errorf("merkle proof verification failed: %v", err)
} }
r.Proof = proofs[0] r.Proof = proofs[0]
@ -336,7 +336,7 @@ func (r *ChtRequest) Validate(db ethdb.Database, msg *Msg) error {
var encNumber [8]byte var encNumber [8]byte
binary.BigEndian.PutUint64(encNumber[:], r.BlockNum) binary.BigEndian.PutUint64(encNumber[:], r.BlockNum)
value, err := trie.VerifyProof(r.ChtRoot, encNumber[:], proof.Proof) value, err, _ := trie.VerifyProof(r.ChtRoot, encNumber[:], light.NodeList(proof.Proof).NodeSet())
if err != nil { if err != nil {
return err return err
} }

173
light/nodeset.go Normal file
View file

@ -0,0 +1,173 @@
// Copyright 2014 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package light
import (
"errors"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
)
// NodeSet stores a set of trie nodes. It implements trie.Database and can also
// act as a cache for another trie.Database.
type NodeSet struct {
db map[string][]byte
dataSize int
lock sync.RWMutex
fallback trie.Database
copyFromFallback, writeToFallback bool
}
// NewNodeSet creates an empty node set
func NewNodeSet() *NodeSet {
return &NodeSet{
db: make(map[string][]byte),
}
}
// SetFallback will add a fallback database, making this node set a cache for the backing database.
// If copyFromFallback is true, it keeps any node it fetches from the fallback database.
// If writeToFallback is true, it writes stored nodes to the fallback database too.
func (db *NodeSet) SetFallback(fallback trie.Database, copyFromFallback, writeToFallback bool) {
db.lock.Lock()
defer db.lock.Unlock()
db.fallback = fallback
db.copyFromFallback = copyFromFallback
db.writeToFallback = writeToFallback
}
// ReadCache returns a new read cache (copyFromFallback=true) for this node set
func (db *NodeSet) ReadCache() *NodeSet {
cdb := NewNodeSet()
cdb.SetFallback(db, true, false)
return cdb
}
// Put stores a new node in the set
func (db *NodeSet) Put(key []byte, value []byte) error {
db.lock.Lock()
defer db.lock.Unlock()
if _, ok := db.db[string(key)]; !ok {
db.db[string(key)] = common.CopyBytes(value)
db.dataSize += len(value)
if db.writeToFallback && db.fallback != nil {
db.fallback.Put(key, value)
}
}
return nil
}
// Get returns a stored node
func (db *NodeSet) Get(key []byte) ([]byte, error) {
db.lock.RLock()
defer db.lock.RUnlock()
if entry, ok := db.db[string(key)]; ok {
return entry, nil
}
if db.fallback != nil {
value, err := db.fallback.Get(key)
if db.copyFromFallback && err == nil {
db.db[string(key)] = value
db.dataSize += len(value)
}
return value, err
}
return nil, errors.New("not found")
}
// Has returns true if the node set contains the given key
func (db *NodeSet) Has(key []byte) (bool, error) {
_, err := db.Get(key)
return err == nil, nil
}
// KeyCount returns the number of nodes in the set
func (db *NodeSet) KeyCount() int {
db.lock.RLock()
defer db.lock.RUnlock()
return len(db.db)
}
// DataSize returns the aggregated data size of nodes in the set
func (db *NodeSet) DataSize() int {
db.lock.RLock()
defer db.lock.RUnlock()
return db.dataSize
}
// NodeList converts the node set to a NodeList
func (db *NodeSet) NodeList() NodeList {
db.lock.RLock()
defer db.lock.RUnlock()
var values NodeList
for _, value := range db.db {
values = append(values, value)
}
return values
}
// Store writes the contents of the set to the given database
func (db *NodeSet) Store(target trie.Database) {
db.lock.RLock()
defer db.lock.RUnlock()
for key, value := range db.db {
target.Put([]byte(key), value)
}
}
// NodeList stores an ordered list of trie nodes. It implements trie.DatabaseWriter.
type NodeList []rlp.RawValue
// Store writes the contents of the list to the given database
func (n NodeList) Store(db trie.Database) {
for _, node := range n {
db.Put(crypto.Keccak256(node), node)
}
}
// NodeSet converts the node list to a NodeSet
func (n NodeList) NodeSet() *NodeSet {
db := NewNodeSet()
n.Store(db)
return db
}
// Put stores a new node at the end of the list
func (n *NodeList) Put(key []byte, value []byte) error {
*n = append(*n, value)
return nil
}
// DataSize returns the aggregated data size of nodes in the list
func (n NodeList) DataSize() int {
var size int
for _, node := range n {
size += len(node)
}
return size
}

View file

@ -77,7 +77,9 @@ func (odr *testOdr) Retrieve(ctx context.Context, req OdrRequest) error {
req.Receipts = core.GetBlockReceipts(odr.sdb, req.Hash, core.GetBlockNumber(odr.sdb, req.Hash)) req.Receipts = core.GetBlockReceipts(odr.sdb, req.Hash, core.GetBlockNumber(odr.sdb, req.Hash))
case *TrieRequest: case *TrieRequest:
t, _ := trie.New(req.Id.Root, odr.sdb) t, _ := trie.New(req.Id.Root, odr.sdb)
req.Proof = t.Prove(req.Key) nodes := NewNodeSet()
t.Prove(req.Key, 0, nodes)
req.Proof = nodes.NodeList()
case *CodeRequest: case *CodeRequest:
req.Data, _ = odr.sdb.Get(req.Hash[:]) req.Data, _ = odr.sdb.Get(req.Hash[:])
} }

View file

@ -18,11 +18,10 @@ package trie
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto/sha3" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
@ -36,7 +35,7 @@ import (
// contains all nodes of the longest existing prefix of the key // contains all nodes of the longest existing prefix of the key
// (at least the root node), ending with the node that proves the // (at least the root node), ending with the node that proves the
// absence of the key. // absence of the key.
func (t *Trie) Prove(key []byte) []rlp.RawValue { func (t *Trie) Prove(key []byte, fromLevel uint, proofDb DatabaseWriter) error {
// Collect all nodes on the path to key. // Collect all nodes on the path to key.
key = keybytesToHex(key) key = keybytesToHex(key)
nodes := []node{} nodes := []node{}
@ -61,67 +60,63 @@ func (t *Trie) Prove(key []byte) []rlp.RawValue {
tn, err = t.resolveHash(n, nil) tn, err = t.resolveHash(n, nil)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("Unhandled trie error: %v", err)) log.Error(fmt.Sprintf("Unhandled trie error: %v", err))
return nil return err
} }
default: default:
panic(fmt.Sprintf("%T: invalid node: %v", tn, tn)) panic(fmt.Sprintf("%T: invalid node: %v", tn, tn))
} }
} }
hasher := newHasher(0, 0) hasher := newHasher(0, 0)
proof := make([]rlp.RawValue, 0, len(nodes))
for i, n := range nodes { for i, n := range nodes {
// Don't bother checking for errors here since hasher panics // Don't bother checking for errors here since hasher panics
// if encoding doesn't work and we're not writing to any database. // if encoding doesn't work and we're not writing to any database.
n, _, _ = hasher.hashChildren(n, nil) n, _, _ = hasher.hashChildren(n, nil)
hn, _ := hasher.store(n, nil, false) hn, _ := hasher.store(n, nil, false)
if _, ok := hn.(hashNode); ok || i == 0 { if hash, ok := hn.(hashNode); ok || i == 0 {
// If the node's database encoding is a hash (or is the // If the node's database encoding is a hash (or is the
// root node), it becomes a proof element. // root node), it becomes a proof element.
enc, _ := rlp.EncodeToBytes(n) if fromLevel > 0 {
proof = append(proof, enc) fromLevel--
} else {
enc, _ := rlp.EncodeToBytes(n)
if !ok {
hash = crypto.Keccak256(enc)
}
proofDb.Put(hash, enc)
}
} }
} }
return proof return nil
} }
// VerifyProof checks merkle proofs. The given proof must contain the // VerifyProof checks merkle proofs. The given proof must contain the
// value for key in a trie with the given root hash. VerifyProof // value for key in a trie with the given root hash. VerifyProof
// returns an error if the proof contains invalid trie nodes or the // returns an error if the proof contains invalid trie nodes or the
// wrong value. // wrong value.
func VerifyProof(rootHash common.Hash, key []byte, proof []rlp.RawValue) (value []byte, err error) { func VerifyProof(rootHash common.Hash, key []byte, proofDb DatabaseReader) (value []byte, err error, nodes int) {
key = keybytesToHex(key) key = keybytesToHex(key)
sha := sha3.NewKeccak256() wantHash := rootHash[:]
wantHash := rootHash.Bytes() for i := 0; ; i++ {
for i, buf := range proof { buf, _ := proofDb.Get(wantHash)
sha.Reset() if buf == nil {
sha.Write(buf) return nil, fmt.Errorf("proof node %d (hash %064x) missing", i, wantHash[:]), i
if !bytes.Equal(sha.Sum(nil), wantHash) {
return nil, fmt.Errorf("bad proof node %d: hash mismatch", i)
} }
n, err := decodeNode(wantHash, buf, 0) n, err := decodeNode(wantHash, buf, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("bad proof node %d: %v", i, err) return nil, fmt.Errorf("bad proof node %d: %v", i, err), i
} }
keyrest, cld := get(n, key) keyrest, cld := get(n, key)
switch cld := cld.(type) { switch cld := cld.(type) {
case nil: case nil:
if i != len(proof)-1 { // The trie doesn't contain the key.
return nil, fmt.Errorf("key mismatch at proof node %d", i) return nil, nil, i
} else {
// The trie doesn't contain the key.
return nil, nil
}
case hashNode: case hashNode:
key = keyrest key = keyrest
wantHash = cld wantHash = cld
case valueNode: case valueNode:
if i != len(proof)-1 { return cld, nil, i + 1
return nil, errors.New("additional nodes at end of proof")
}
return cld, nil
} }
} }
return nil, errors.New("unexpected end of proof")
} }
func get(tn node, key []byte) ([]byte, node) { func get(tn node, key []byte) ([]byte, node) {

View file

@ -19,12 +19,13 @@ package trie
import ( import (
"bytes" "bytes"
crand "crypto/rand" crand "crypto/rand"
"errors"
mrand "math/rand" mrand "math/rand"
"testing" "testing"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/crypto"
) )
func init() { func init() {
@ -35,13 +36,13 @@ func TestProof(t *testing.T) {
trie, vals := randomTrie(500) trie, vals := randomTrie(500)
root := trie.Hash() root := trie.Hash()
for _, kv := range vals { for _, kv := range vals {
proof := trie.Prove(kv.k) proofs := newTestProofDb()
if proof == nil { if trie.Prove(kv.k, 0, proofs) != nil {
t.Fatalf("missing key %x while constructing proof", kv.k) t.Fatalf("missing key %x while constructing proof", kv.k)
} }
val, err := VerifyProof(root, kv.k, proof) val, err, _ := VerifyProof(root, kv.k, proofs)
if err != nil { if err != nil {
t.Fatalf("VerifyProof error for key %x: %v\nraw proof: %x", kv.k, err, proof) t.Fatalf("VerifyProof error for key %x: %v\nraw proof: %v", kv.k, err, proofs)
} }
if !bytes.Equal(val, kv.v) { if !bytes.Equal(val, kv.v) {
t.Fatalf("VerifyProof returned wrong value for key %x: got %x, want %x", kv.k, val, kv.v) t.Fatalf("VerifyProof returned wrong value for key %x: got %x, want %x", kv.k, val, kv.v)
@ -52,16 +53,14 @@ func TestProof(t *testing.T) {
func TestOneElementProof(t *testing.T) { func TestOneElementProof(t *testing.T) {
trie := new(Trie) trie := new(Trie)
updateString(trie, "k", "v") updateString(trie, "k", "v")
proof := trie.Prove([]byte("k")) proofs := newTestProofDb()
if proof == nil { trie.Prove([]byte("k"), 0, proofs)
t.Fatal("nil proof") if len(proofs.db) != 1 {
}
if len(proof) != 1 {
t.Error("proof should have one element") t.Error("proof should have one element")
} }
val, err := VerifyProof(trie.Hash(), []byte("k"), proof) val, err, _ := VerifyProof(trie.Hash(), []byte("k"), proofs)
if err != nil { if err != nil {
t.Fatalf("VerifyProof error: %v\nraw proof: %x", err, proof) t.Fatalf("VerifyProof error: %v\nraw proof: %v", err, proofs.db)
} }
if !bytes.Equal(val, []byte("v")) { if !bytes.Equal(val, []byte("v")) {
t.Fatalf("VerifyProof returned wrong value: got %x, want 'k'", val) t.Fatalf("VerifyProof returned wrong value: got %x, want 'k'", val)
@ -72,12 +71,22 @@ func TestVerifyBadProof(t *testing.T) {
trie, vals := randomTrie(800) trie, vals := randomTrie(800)
root := trie.Hash() root := trie.Hash()
for _, kv := range vals { for _, kv := range vals {
proof := trie.Prove(kv.k) proofs := newTestProofDb()
if proof == nil { trie.Prove(kv.k, 0, proofs)
t.Fatal("nil proof") if len(proofs.db) == 0 {
t.Fatal("zero length proof")
} }
mutateByte(proof[mrand.Intn(len(proof))]) idx := mrand.Intn(len(proofs.db))
if _, err := VerifyProof(root, kv.k, proof); err == nil { for key, node := range proofs.db {
if idx == 0 {
delete(proofs.db, key)
mutateByte(node)
proofs.Put(crypto.Keccak256(node), node)
break
}
idx--
}
if _, err, _ := VerifyProof(root, kv.k, proofs); err == nil {
t.Fatalf("expected proof to fail for key %x", kv.k) t.Fatalf("expected proof to fail for key %x", kv.k)
} }
} }
@ -104,8 +113,9 @@ func BenchmarkProve(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
kv := vals[keys[i%len(keys)]] kv := vals[keys[i%len(keys)]]
if trie.Prove(kv.k) == nil { proofs := newTestProofDb()
b.Fatalf("nil proof for %x", kv.k) if trie.Prove(kv.k, 0, proofs); len(proofs.db) == 0 {
b.Fatalf("zero length proof for %x", kv.k)
} }
} }
} }
@ -114,16 +124,18 @@ func BenchmarkVerifyProof(b *testing.B) {
trie, vals := randomTrie(100) trie, vals := randomTrie(100)
root := trie.Hash() root := trie.Hash()
var keys []string var keys []string
var proofs [][]rlp.RawValue var proofs []*testProofDb
for k := range vals { for k := range vals {
keys = append(keys, k) keys = append(keys, k)
proofs = append(proofs, trie.Prove([]byte(k))) proof := newTestProofDb()
trie.Prove([]byte(k), 0, proof)
proofs = append(proofs, proof)
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
im := i % len(keys) im := i % len(keys)
if _, err := VerifyProof(root, []byte(keys[im]), proofs[im]); err != nil { if _, err, _ := VerifyProof(root, []byte(keys[im]), proofs[im]); err != nil {
b.Fatalf("key %x: %v", keys[im], err) b.Fatalf("key %x: %v", keys[im], err)
} }
} }
@ -153,3 +165,30 @@ func randBytes(n int) []byte {
crand.Read(r) crand.Read(r)
return r return r
} }
type testProofDb struct {
db map[string][]byte
}
func newTestProofDb() *testProofDb {
return &testProofDb{
db: make(map[string][]byte),
}
}
func (db *testProofDb) Put(key []byte, value []byte) error {
db.db[string(key)] = common.CopyBytes(value)
return nil
}
func (db *testProofDb) Get(key []byte) ([]byte, error) {
if entry, ok := db.db[string(key)]; ok {
return entry, nil
}
return nil, errors.New("not found")
}
func (db *testProofDb) Has(key []byte) (bool, error) {
_, err := db.Get(key)
return err == nil, nil
}