From 67d079e68ad0f62a05d8d57575edffcc4209f76c Mon Sep 17 00:00:00 2001 From: Marko Date: Tue, 23 Jun 2020 10:05:36 +0200 Subject: [PATCH] store/rootmulti: add SimpleProofsFromMap (#6481) * add sipleproofs from map to sdk * bring changes from tendermint pr * bring back tmhash * add changelog * Update CHANGELOG.md Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com> --- CHANGELOG.md | 1 + store/rootmulti/{merkle_map.go => map.go} | 88 +++++++++++++++++++ .../{merkle_map_test.go => map_test.go} | 43 ++++++++- store/rootmulti/store.go | 29 +++++- 4 files changed, 158 insertions(+), 3 deletions(-) rename store/rootmulti/{merkle_map.go => map.go} (54%) rename store/rootmulti/{merkle_map_test.go => map_test.go} (51%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3977ec86aff4..fae116339612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -296,6 +296,7 @@ pagination. * (types) [\#6128](https://github.com/cosmos/cosmos-sdk/pull/6137) Add `String()` method to `GasMeter`. * (types) [\#6195](https://github.com/cosmos/cosmos-sdk/pull/6195) Add codespace to broadcast(sync/async) response. * (baseapp) [\#6053](https://github.com/cosmos/cosmos-sdk/pull/6053) Customizable panic recovery handling added for `app.runTx()` method (as proposed in the [ADR 22](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-022-custom-panic-handling.md)). Adds ability for developers to register custom panic handlers extending standard ones. +* (store) [\#6481](https://github.com/cosmos/cosmos-sdk/pull/6481) Move `SimpleProofsFromMap` from Tendermint into the SDK. ## [v0.38.4] - 2020-05-21 diff --git a/store/rootmulti/merkle_map.go b/store/rootmulti/map.go similarity index 54% rename from store/rootmulti/merkle_map.go rename to store/rootmulti/map.go index 615739990ce7..9d41d5656ca3 100644 --- a/store/rootmulti/merkle_map.go +++ b/store/rootmulti/map.go @@ -100,3 +100,91 @@ func hashKVPairs(kvs kv.Pairs) []byte { return merkle.SimpleHashFromByteSlices(kvsH) } + +// --------------------------------------------- + +// Merkle tree from a map. +// Leaves are `hash(key) | hash(value)`. +// Leaves are sorted before Merkle hashing. +type simpleMap struct { + kvs kv.Pairs + sorted bool +} + +func newSimpleMap() *simpleMap { + return &simpleMap{ + kvs: nil, + sorted: false, + } +} + +// Set creates a kv pair of the key and the hash of the value, +// and then appends it to simpleMap's kv pairs. +func (sm *simpleMap) Set(key string, value []byte) { + sm.sorted = false + + // The value is hashed, so you can + // check for equality with a cached value (say) + // and make a determination to fetch or not. + vhash := tmhash.Sum(value) + + sm.kvs = append(sm.kvs, kv.Pair{ + Key: []byte(key), + Value: vhash, + }) +} + +// Hash Merkle root hash of items sorted by key +// (UNSTABLE: and by value too if duplicate key). +func (sm *simpleMap) Hash() []byte { + sm.Sort() + return hashKVPairs(sm.kvs) +} + +func (sm *simpleMap) Sort() { + if sm.sorted { + return + } + sm.kvs.Sort() + sm.sorted = true +} + +// Returns a copy of sorted KVPairs. +// NOTE these contain the hashed key and value. +func (sm *simpleMap) KVPairs() kv.Pairs { + sm.Sort() + kvs := make(kv.Pairs, len(sm.kvs)) + copy(kvs, sm.kvs) + return kvs +} + +//---------------------------------------- + +// A local extension to KVPair that can be hashed. +// Key and value are length prefixed and concatenated, +// then hashed. +type KVPair kv.Pair + +// NewKVPair takes in a key and value and creates a kv.Pair +// wrapped in the local extension KVPair +func NewKVPair(key, value []byte) KVPair { + return KVPair(kv.Pair{ + Key: key, + Value: value, + }) +} + +// Bytes returns key || value, with both the +// key and value length prefixed. +func (kv KVPair) Bytes() []byte { + var b bytes.Buffer + err := encodeByteSlice(&b, kv.Key) + if err != nil { + panic(err) + } + err = encodeByteSlice(&b, kv.Value) + if err != nil { + panic(err) + } + return b.Bytes() +} diff --git a/store/rootmulti/merkle_map_test.go b/store/rootmulti/map_test.go similarity index 51% rename from store/rootmulti/merkle_map_test.go rename to store/rootmulti/map_test.go index 06f02a62bc5b..7b10c045f1d6 100644 --- a/store/rootmulti/merkle_map_test.go +++ b/store/rootmulti/map_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSimpleMap(t *testing.T) { +func TestMerkleMap(t *testing.T) { tests := []struct { keys []string values []string // each string gets converted to []byte in test @@ -48,3 +48,44 @@ func TestSimpleMap(t *testing.T) { assert.Equal(t, tc.want, fmt.Sprintf("%x", got), "Hash didn't match on tc %d", i) } } + +func TestSimpleMap(t *testing.T) { + tests := []struct { + keys []string + values []string // each string gets converted to []byte in test + want string + }{ + {[]string{"key1"}, []string{"value1"}, "a44d3cc7daba1a4600b00a2434b30f8b970652169810d6dfa9fb1793a2189324"}, + {[]string{"key1"}, []string{"value2"}, "0638e99b3445caec9d95c05e1a3fc1487b4ddec6a952ff337080360b0dcc078c"}, + // swap order with 2 keys + { + []string{"key1", "key2"}, + []string{"value1", "value2"}, + "8fd19b19e7bb3f2b3ee0574027d8a5a4cec370464ea2db2fbfa5c7d35bb0cff3", + }, + { + []string{"key2", "key1"}, + []string{"value2", "value1"}, + "8fd19b19e7bb3f2b3ee0574027d8a5a4cec370464ea2db2fbfa5c7d35bb0cff3", + }, + // swap order with 3 keys + { + []string{"key1", "key2", "key3"}, + []string{"value1", "value2", "value3"}, + "1dd674ec6782a0d586a903c9c63326a41cbe56b3bba33ed6ff5b527af6efb3dc", + }, + { + []string{"key1", "key3", "key2"}, + []string{"value1", "value3", "value2"}, + "1dd674ec6782a0d586a903c9c63326a41cbe56b3bba33ed6ff5b527af6efb3dc", + }, + } + for i, tc := range tests { + db := newSimpleMap() + for i := 0; i < len(tc.keys); i++ { + db.Set(tc.keys[i], []byte(tc.values[i])) + } + got := db.Hash() + assert.Equal(t, tc.want, fmt.Sprintf("%x", got), "Hash didn't match on tc %d", i) + } +} diff --git a/store/rootmulti/store.go b/store/rootmulti/store.go index 06b87372371a..3934d991d05b 100644 --- a/store/rootmulti/store.go +++ b/store/rootmulti/store.go @@ -613,13 +613,13 @@ func (ci commitInfo) Hash() []byte { if len(ci.StoreInfos) == 0 { return nil } - rootHash, _, _ := merkle.SimpleProofsFromMap(ci.toMap()) + rootHash, _, _ := SimpleProofsFromMap(ci.toMap()) return rootHash } func (ci commitInfo) ProofOp(storeName string) merkle.ProofOp { cmap := ci.toMap() - _, proofs, _ := merkle.SimpleProofsFromMap(cmap) + _, proofs, _ := SimpleProofsFromMap(cmap) proof := proofs[storeName] if proof == nil { panic(fmt.Sprintf("ProofOp for %s but not registered store name", storeName)) @@ -793,3 +793,28 @@ func SimpleHashFromMap(m map[string][]byte) []byte { return mm.hash() } + +// SimpleProofsFromMap generates proofs from a map. The keys/values of the map will be used as the keys/values +// in the underlying key-value pairs. +// The keys are sorted before the proofs are computed. +func SimpleProofsFromMap(m map[string][]byte) (rootHash []byte, proofs map[string]*merkle.SimpleProof, keys []string) { + sm := newSimpleMap() + for k, v := range m { + sm.Set(k, v) + } + sm.Sort() + kvs := sm.kvs + kvsBytes := make([][]byte, len(kvs)) + for i, kvp := range kvs { + kvsBytes[i] = KVPair(kvp).Bytes() + } + + rootHash, proofList := merkle.SimpleProofsFromByteSlices(kvsBytes) + proofs = make(map[string]*merkle.SimpleProof) + keys = make([]string, len(proofList)) + for i, kvp := range kvs { + proofs[string(kvp.Key)] = proofList[i] + keys[i] = string(kvp.Key) + } + return +}