feat: Remove the store/internal module (#18944)

Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com>
This commit is contained in:
cool-developer 2024-01-04 13:10:16 -05:00 committed by GitHub
parent 7fbca40059
commit 014cbbcb8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 184 additions and 1005 deletions

View File

@ -2,9 +2,8 @@ package store
import (
"fmt"
"sort"
"time"
"cosmossdk.io/store/v2/internal/maps"
)
type (
@ -14,6 +13,7 @@ type (
Version uint64
StoreInfos []StoreInfo
Timestamp time.Time
CommitHash []byte
}
// StoreInfo defines store-specific commit information. It contains a reference
@ -37,25 +37,44 @@ func (si StoreInfo) GetHash() []byte {
// Hash returns the root hash of all committed stores represented by CommitInfo,
// sorted by store name/key.
func (ci CommitInfo) Hash() []byte {
func (ci *CommitInfo) Hash() []byte {
if len(ci.StoreInfos) == 0 {
return nil
}
rootHash, _, _ := maps.ProofsFromMap(ci.toMap())
if len(ci.CommitHash) != 0 {
return ci.CommitHash
}
rootHash, _, _ := ci.GetStoreProof("")
return rootHash
}
func (ci CommitInfo) toMap() map[string][]byte {
m := make(map[string][]byte, len(ci.StoreInfos))
for _, storeInfo := range ci.StoreInfos {
m[storeInfo.Name] = storeInfo.GetHash()
func (ci *CommitInfo) GetStoreProof(storeKey string) ([]byte, *CommitmentOp, error) {
sort.Slice(ci.StoreInfos, func(i, j int) bool {
return ci.StoreInfos[i].Name < ci.StoreInfos[j].Name
})
index := 0
leaves := make([][]byte, len(ci.StoreInfos))
for i, si := range ci.StoreInfos {
var err error
leaves[i], err = LeafHash([]byte(si.Name), si.GetHash())
if err != nil {
return nil, nil, err
}
if si.Name == storeKey {
index = i
}
}
return m
rootHash, inners := ProofFromByteSlices(leaves, index)
commitmentOp := ConvertCommitmentOp(inners, []byte(storeKey), ci.StoreInfos[index].GetHash())
return rootHash, &commitmentOp, nil
}
func (ci CommitInfo) CommitID() CommitID {
func (ci *CommitInfo) CommitID() CommitID {
return CommitID{
Version: ci.Version,
Hash: ci.Hash(),

View File

@ -6,7 +6,6 @@ require (
cosmossdk.io/core v0.12.0
cosmossdk.io/errors v1.0.0
cosmossdk.io/log v1.2.1
cosmossdk.io/math v1.2.0
github.com/cockroachdb/errors v1.11.1
github.com/cockroachdb/pebble v1.0.0
github.com/cometbft/cometbft v0.38.2

View File

@ -4,8 +4,6 @@ cosmossdk.io/errors v1.0.0 h1:nxF07lmlBbB8NKQhtJ+sJm6ef5uV1XkvPXG2bUntb04=
cosmossdk.io/errors v1.0.0/go.mod h1:+hJZLuhdDE0pYN8HkOrVNwrIOYvUGnn6+4fjnJs/oV0=
cosmossdk.io/log v1.2.1 h1:Xc1GgTCicniwmMiKwDxUjO4eLhPxoVdI9vtMW8Ti/uk=
cosmossdk.io/log v1.2.1/go.mod h1:GNSCc/6+DhFIj1aLn/j7Id7PaO8DzNylUZoOYBL9+I4=
cosmossdk.io/math v1.2.0 h1:8gudhTkkD3NxOP2YyyJIYYmt6dQ55ZfJkDOaxXpy7Ig=
cosmossdk.io/math v1.2.0/go.mod h1:l2Gnda87F0su8a/7FEKJfFdJrM0JZRXQaohlgJeyQh0=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=

View File

@ -1,17 +0,0 @@
package kv
import "fmt"
// AssertKeyAtLeastLength panics when store key length is less than the given length.
func AssertKeyAtLeastLength(bz []byte, length int) {
if len(bz) < length {
panic(fmt.Sprintf("expected key of length at least %d, got %d", length, len(bz)))
}
}
// AssertKeyLength panics when store key length is not equal to the given length.
func AssertKeyLength(bz []byte, length int) {
if len(bz) != length {
panic(fmt.Sprintf("unexpected key length; got: %d, expected: %d", len(bz), length))
}
}

View File

@ -1,39 +0,0 @@
package kv
import (
"bytes"
"sort"
)
type (
Pair struct {
Key []byte
Value []byte
}
Pairs struct {
Pairs []Pair
}
)
func (kvs Pairs) Len() int { return len(kvs.Pairs) }
func (kvs Pairs) Less(i, j int) bool {
switch bytes.Compare(kvs.Pairs[i].Key, kvs.Pairs[j].Key) {
case -1:
return true
case 0:
return bytes.Compare(kvs.Pairs[i].Value, kvs.Pairs[j].Value) < 0
case 1:
return false
default:
panic("invalid comparison result")
}
}
func (kvs Pairs) Swap(i, j int) { kvs.Pairs[i], kvs.Pairs[j] = kvs.Pairs[j], kvs.Pairs[i] }
// Sort invokes sort.Sort on kvs.
func (kvs Pairs) Sort() { sort.Sort(kvs) }

View File

@ -1,13 +0,0 @@
package maps
import "testing"
func BenchmarkKVPairBytes(b *testing.B) {
kvp := NewKVPair(make([]byte, 128), make([]byte, 1e6))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
b.SetBytes(int64(len(kvp.Bytes())))
}
}

View File

@ -1,216 +0,0 @@
package maps
import (
"encoding/binary"
"github.com/cometbft/cometbft/crypto/merkle"
"github.com/cometbft/cometbft/crypto/tmhash"
cmtprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto"
"cosmossdk.io/store/v2/internal/kv"
"cosmossdk.io/store/v2/internal/tree"
)
// merkleMap defines a merkle-ized tree from a map. Leave values are treated as
// hash(key) | hash(value). Leaves are sorted before Merkle hashing.
type merkleMap struct {
kvs kv.Pairs
sorted bool
}
func newMerkleMap() *merkleMap {
return &merkleMap{
kvs: kv.Pairs{},
sorted: false,
}
}
// Set creates a kv.Pair from the provided key and value. The value is hashed prior
// to creating a kv.Pair. The created kv.Pair is appended to the MerkleMap's slice
// of kv.Pairs. Whenever called, the MerkleMap must be resorted.
func (sm *merkleMap) set(key string, value []byte) {
byteKey := []byte(key)
assertValidKey(byteKey)
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.Pairs = append(sm.kvs.Pairs, kv.Pair{
Key: byteKey,
Value: vhash,
})
}
// Hash returns the merkle root of items sorted by key. Note, it is unstable.
func (sm *merkleMap) hash() []byte {
sm.sort()
return hashKVPairs(sm.kvs)
}
func (sm *merkleMap) sort() {
if sm.sorted {
return
}
sm.kvs.Sort()
sm.sorted = true
}
// hashKVPairs hashes a kvPair and creates a merkle tree where the leaves are
// byte slices.
func hashKVPairs(kvs kv.Pairs) []byte {
kvsH := make([][]byte, len(kvs.Pairs))
for i, kvp := range kvs.Pairs {
kvsH[i] = KVPair(kvp).Bytes()
}
return tree.HashFromByteSlices(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: kv.Pairs{},
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) {
byteKey := []byte(key)
assertValidKey(byteKey)
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.Pairs = append(sm.Kvs.Pairs, kv.Pair{
Key: byteKey,
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 := kv.Pairs{
Pairs: make([]kv.Pair, len(sm.Kvs.Pairs)),
}
copy(kvs.Pairs, sm.Kvs.Pairs)
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 {
// In the worst case:
// * 8 bytes to Uvarint encode the length of the key
// * 8 bytes to Uvarint encode the length of the value
// So preallocate for the worst case, which will in total
// be a maximum of 14 bytes wasted, if len(key)=1, len(value)=1,
// but that's going to rare.
buf := make([]byte, 8+len(kv.Key)+8+len(kv.Value))
// Encode the key, prefixed with its length.
nlk := binary.PutUvarint(buf, uint64(len(kv.Key)))
nk := copy(buf[nlk:], kv.Key)
// Encode the value, prefixing with its length.
nlv := binary.PutUvarint(buf[nlk+nk:], uint64(len(kv.Value)))
nv := copy(buf[nlk+nk+nlv:], kv.Value)
return buf[:nlk+nk+nlv+nv]
}
// HashFromMap computes a merkle tree from sorted map and returns the merkle
// root.
func HashFromMap(m map[string][]byte) []byte {
mm := newMerkleMap()
for k, v := range m {
mm.set(k, v)
}
return mm.hash()
}
// ProofsFromMap 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 ProofsFromMap(m map[string][]byte) ([]byte, map[string]*cmtprotocrypto.Proof, []string) {
sm := newSimpleMap()
for k, v := range m {
sm.Set(k, v)
}
sm.Sort()
kvs := sm.Kvs
kvsBytes := make([][]byte, len(kvs.Pairs))
for i, kvp := range kvs.Pairs {
kvsBytes[i] = KVPair(kvp).Bytes()
}
rootHash, proofList := merkle.ProofsFromByteSlices(kvsBytes)
proofs := make(map[string]*cmtprotocrypto.Proof)
keys := make([]string, len(proofList))
for i, kvp := range kvs.Pairs {
proofs[string(kvp.Key)] = proofList[i].ToProto()
keys[i] = string(kvp.Key)
}
return rootHash, proofs, keys
}
func assertValidKey(key []byte) {
if len(key) == 0 {
panic("key is nil")
}
}

View File

@ -1,104 +0,0 @@
package maps
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEmptyKeyMerkleMap(t *testing.T) {
db := newMerkleMap()
require.Panics(t, func() { db.set("", []byte("value")) }, "setting an empty key should panic")
}
func TestMerkleMap(t *testing.T) {
tests := []struct {
keys []string
values []string // each string gets converted to []byte in test
want string
}{
{[]string{}, []string{}, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
{[]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 := newMerkleMap()
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)
}
}
func TestEmptyKeySimpleMap(t *testing.T) {
db := newSimpleMap()
require.Panics(t, func() { db.Set("", []byte("value")) }, "setting an empty key should panic")
}
func TestSimpleMap(t *testing.T) {
tests := []struct {
keys []string
values []string // each string gets converted to []byte in test
want string
}{
{[]string{}, []string{}, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
{[]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)
}
}

View File

@ -1,98 +0,0 @@
package proofs
import (
"fmt"
"math/bits"
cmtprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto"
ics23 "github.com/cosmos/ics23/go"
)
// ConvertExistenceProof will convert the given proof into a valid
// existence proof, if that's what it is.
//
// This is the simplest case of the range proof and we will focus on
// demoing compatibility here
func ConvertExistenceProof(p *cmtprotocrypto.Proof, key, value []byte) (*ics23.ExistenceProof, error) {
path, err := convertInnerOps(p)
if err != nil {
return nil, err
}
proof := &ics23.ExistenceProof{
Key: key,
Value: value,
Leaf: convertLeafOp(),
Path: path,
}
return proof, nil
}
// this is adapted from merkle/hash.go:leafHash()
// and merkle/simple_map.go:KVPair.Bytes()
func convertLeafOp() *ics23.LeafOp {
prefix := []byte{0}
return &ics23.LeafOp{
Hash: ics23.HashOp_SHA256,
PrehashKey: ics23.HashOp_NO_HASH,
PrehashValue: ics23.HashOp_SHA256,
Length: ics23.LengthOp_VAR_PROTO,
Prefix: prefix,
}
}
func convertInnerOps(p *cmtprotocrypto.Proof) ([]*ics23.InnerOp, error) {
inners := make([]*ics23.InnerOp, 0, len(p.Aunts))
path := buildPath(p.Index, p.Total)
if len(p.Aunts) != len(path) {
return nil, fmt.Errorf("calculated a path different length (%d) than provided by SimpleProof (%d)", len(path), len(p.Aunts))
}
for i, aunt := range p.Aunts {
auntRight := path[i]
// combine with: 0x01 || lefthash || righthash
inner := &ics23.InnerOp{Hash: ics23.HashOp_SHA256}
if auntRight {
inner.Prefix = []byte{1}
inner.Suffix = aunt
} else {
inner.Prefix = append([]byte{1}, aunt...)
}
inners = append(inners, inner)
}
return inners, nil
}
// buildPath returns a list of steps from leaf to root
// in each step, true means index is left side, false index is right side
// code adapted from merkle/simple_proof.go:computeHashFromAunts
func buildPath(idx, total int64) []bool {
if total < 2 {
return nil
}
numLeft := getSplitPoint(total)
goLeft := idx < numLeft
// we put goLeft at the end of the array, as we recurse from top to bottom,
// and want the leaf to be first in array, root last
if goLeft {
return append(buildPath(idx, numLeft), goLeft)
}
return append(buildPath(idx-numLeft, total-numLeft), goLeft)
}
func getSplitPoint(length int64) int64 {
if length < 1 {
panic("Trying to split a tree with size < 1")
}
uLength := uint(length)
bitlen := bits.Len(uLength)
k := int64(1 << uint(bitlen-1))
if k == length {
k >>= 1
}
return k
}

View File

@ -1,105 +0,0 @@
package proofs
import (
"bytes"
"fmt"
"testing"
)
func TestLeafOp(t *testing.T) {
proof := GenerateRangeProof(20, Middle)
converted, err := ConvertExistenceProof(proof.Proof, proof.Key, proof.Value)
if err != nil {
t.Fatal(err)
}
leaf := converted.GetLeaf()
if leaf == nil {
t.Fatalf("Missing leaf node")
}
hash, err := leaf.Apply(converted.Key, converted.Value)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(hash, proof.Proof.LeafHash) {
t.Errorf("Calculated: %X\nExpected: %X", hash, proof.Proof.LeafHash)
}
}
func TestBuildPath(t *testing.T) {
cases := map[string]struct {
idx int64
total int64
expected []bool
}{
"pair left": {
idx: 0,
total: 2,
expected: []bool{true},
},
"pair right": {
idx: 1,
total: 2,
expected: []bool{false},
},
"power of 2": {
idx: 3,
total: 8,
expected: []bool{false, false, true},
},
"size of 7 right most": {
idx: 6,
total: 7,
expected: []bool{false, false},
},
"size of 6 right-left (from top)": {
idx: 4,
total: 6,
expected: []bool{true, false},
},
"size of 6 left-right-left (from top)": {
idx: 2,
total: 7,
expected: []bool{true, false, true},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
path := buildPath(tc.idx, tc.total)
if len(path) != len(tc.expected) {
t.Fatalf("Got %v\nExpected %v", path, tc.expected)
}
for i := range path {
if path[i] != tc.expected[i] {
t.Fatalf("Differ at %d\nGot %v\nExpected %v", i, path, tc.expected)
}
}
})
}
}
func TestConvertProof(t *testing.T) {
for i := 0; i < 100; i++ {
t.Run(fmt.Sprintf("Run %d", i), func(t *testing.T) {
proof := GenerateRangeProof(57, Left)
converted, err := ConvertExistenceProof(proof.Proof, proof.Key, proof.Value)
if err != nil {
t.Fatal(err)
}
calc, err := converted.Calculate()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(calc, proof.RootHash) {
t.Errorf("Calculated: %X\nExpected: %X", calc, proof.RootHash)
}
})
}
}

View File

@ -1,103 +0,0 @@
package proofs
import (
"errors"
"sort"
ics23 "github.com/cosmos/ics23/go"
"cosmossdk.io/store/v2/internal/maps"
)
var (
ErrEmptyKey = errors.New("key is empty")
ErrEmptyKeyInData = errors.New("data contains empty key")
)
/*
CreateMembershipProof will produce a CommitmentProof that the given key (and queries value) exists in the map.
If the key doesn't exist in the tree, this will return an error.
*/
func CreateMembershipProof(data map[string][]byte, key []byte) (*ics23.CommitmentProof, error) {
if len(key) == 0 {
return nil, ErrEmptyKey
}
exist, err := createExistenceProof(data, key)
if err != nil {
return nil, err
}
proof := &ics23.CommitmentProof{
Proof: &ics23.CommitmentProof_Exist{
Exist: exist,
},
}
return proof, nil
}
/*
CreateNonMembershipProof will produce a CommitmentProof that the given key doesn't exist in the map.
If the key exists in the tree, this will return an error.
*/
func CreateNonMembershipProof(data map[string][]byte, key []byte) (*ics23.CommitmentProof, error) {
if len(key) == 0 {
return nil, ErrEmptyKey
}
// ensure this key is not in the store
if _, ok := data[string(key)]; ok {
return nil, errors.New("cannot create non-membership proof if key is in map")
}
keys := SortedKeys(data)
rightidx := sort.SearchStrings(keys, string(key))
var err error
nonexist := &ics23.NonExistenceProof{
Key: key,
}
// include left proof unless key is left of entire map
if rightidx >= 1 {
leftkey := keys[rightidx-1]
nonexist.Left, err = createExistenceProof(data, []byte(leftkey))
if err != nil {
return nil, err
}
}
// include right proof unless key is right of entire map
if rightidx < len(keys) {
rightkey := keys[rightidx]
nonexist.Right, err = createExistenceProof(data, []byte(rightkey))
if err != nil {
return nil, err
}
}
proof := &ics23.CommitmentProof{
Proof: &ics23.CommitmentProof_Nonexist{
Nonexist: nonexist,
},
}
return proof, nil
}
func createExistenceProof(data map[string][]byte, key []byte) (*ics23.ExistenceProof, error) {
for k := range data {
if k == "" {
return nil, ErrEmptyKeyInData
}
}
value, ok := data[string(key)]
if !ok {
return nil, errors.New("cannot make existence proof if key is not in map")
}
_, proofs, _ := maps.ProofsFromMap(data)
proof := proofs[string(key)]
if proof == nil {
return nil, errors.New("returned no proof for key")
}
return ConvertExistenceProof(proof, key, value)
}

View File

@ -1,125 +0,0 @@
package proofs
import (
"errors"
"testing"
ics23 "github.com/cosmos/ics23/go"
"github.com/stretchr/testify/assert"
)
func TestCreateMembership(t *testing.T) {
cases := map[string]struct {
size int
loc Where
}{
"small left": {size: 100, loc: Left},
"small middle": {size: 100, loc: Middle},
"small right": {size: 100, loc: Right},
"big left": {size: 5431, loc: Left},
"big middle": {size: 5431, loc: Middle},
"big right": {size: 5431, loc: Right},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
data := BuildMap(tc.size)
allkeys := SortedKeys(data)
key := GetKey(allkeys, tc.loc)
nonKey := GetNonKey(allkeys, tc.loc)
// error if the key does not exist
proof, err := CreateMembershipProof(data, []byte(nonKey))
assert.EqualError(t, err, "cannot make existence proof if key is not in map")
assert.Nil(t, proof)
val := data[key]
proof, err = CreateMembershipProof(data, []byte(key))
if err != nil {
t.Fatalf("Creating Proof: %+v", err)
}
if proof.GetExist() == nil {
t.Fatal("Unexpected proof format")
}
root := CalcRoot(data)
err = proof.GetExist().Verify(ics23.TendermintSpec, root, []byte(key), val)
if err != nil {
t.Fatalf("Verifying Proof: %+v", err)
}
valid := ics23.VerifyMembership(ics23.TendermintSpec, root, proof, []byte(key), val)
if !valid {
t.Fatalf("Membership Proof Invalid")
}
})
}
}
func TestCreateNonMembership(t *testing.T) {
cases := map[string]struct {
size int
loc Where
}{
"small left": {size: 100, loc: Left},
"small middle": {size: 100, loc: Middle},
"small right": {size: 100, loc: Right},
"big left": {size: 5431, loc: Left},
"big middle": {size: 5431, loc: Middle},
"big right": {size: 5431, loc: Right},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
data := BuildMap(tc.size)
allkeys := SortedKeys(data)
nonKey := GetNonKey(allkeys, tc.loc)
key := GetKey(allkeys, tc.loc)
// error if the key exists
proof, err := CreateNonMembershipProof(data, []byte(key))
assert.EqualError(t, err, "cannot create non-membership proof if key is in map")
assert.Nil(t, proof)
proof, err = CreateNonMembershipProof(data, []byte(nonKey))
if err != nil {
t.Fatalf("Creating Proof: %+v", err)
}
if proof.GetNonexist() == nil {
t.Fatal("Unexpected proof format")
}
root := CalcRoot(data)
err = proof.GetNonexist().Verify(ics23.TendermintSpec, root, []byte(nonKey))
if err != nil {
t.Fatalf("Verifying Proof: %+v", err)
}
valid := ics23.VerifyNonMembership(ics23.TendermintSpec, root, proof, []byte(nonKey))
if !valid {
t.Fatalf("Non Membership Proof Invalid")
}
})
}
}
func TestInvalidKey(t *testing.T) {
tests := []struct {
name string
f func(data map[string][]byte, key []byte) (*ics23.CommitmentProof, error)
data map[string][]byte
key []byte
err error
}{
{"CreateMembershipProof empty key", CreateMembershipProof, map[string][]byte{"": nil}, []byte(""), ErrEmptyKey},
{"CreateMembershipProof empty key in data", CreateMembershipProof, map[string][]byte{"": nil, " ": nil}, []byte(" "), ErrEmptyKeyInData},
{"CreateNonMembershipProof empty key", CreateNonMembershipProof, map[string][]byte{" ": nil}, []byte(""), ErrEmptyKey},
{"CreateNonMembershipProof empty key in data", CreateNonMembershipProof, map[string][]byte{"": nil}, []byte(" "), ErrEmptyKeyInData},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.f(tc.data, tc.key)
assert.True(t, errors.Is(err, tc.err))
})
}
}

View File

@ -1,101 +0,0 @@
package proofs
import (
"sort"
cmtprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto"
"golang.org/x/exp/maps"
"cosmossdk.io/math/unsafe"
internalmaps "cosmossdk.io/store/v2/internal/maps"
)
// SimpleResult contains a merkle.SimpleProof along with all data needed to build the confio/proof
type SimpleResult struct {
Key []byte
Value []byte
Proof *cmtprotocrypto.Proof
RootHash []byte
}
// GenerateRangeProof makes a tree of size and returns a range proof for one random element
//
// returns a range proof and the root hash of the tree
func GenerateRangeProof(size int, loc Where) *SimpleResult {
data := BuildMap(size)
root, proofs, allkeys := internalmaps.ProofsFromMap(data)
key := GetKey(allkeys, loc)
proof := proofs[key]
res := &SimpleResult{
Key: []byte(key),
Value: toValue(key),
Proof: proof,
RootHash: root,
}
return res
}
// Where selects a location for a key - Left, Right, or Middle
type Where int
const (
Left Where = iota
Right
Middle
)
func SortedKeys(data map[string][]byte) []string {
keys := maps.Keys(data)
sort.Strings(keys)
return keys
}
func CalcRoot(data map[string][]byte) []byte {
root, _, _ := internalmaps.ProofsFromMap(data)
return root
}
// GetKey this returns a key, on Left/Right/Middle
func GetKey(allkeys []string, loc Where) string {
if loc == Left {
return allkeys[0]
}
if loc == Right {
return allkeys[len(allkeys)-1]
}
// select a random index between 1 and allkeys-2
idx := unsafe.NewRand().Int()%(len(allkeys)-2) + 1
return allkeys[idx]
}
// GetNonKey returns a missing key - Left of all, Right of all, or in the Middle
func GetNonKey(allkeys []string, loc Where) string {
if loc == Left {
return string([]byte{1, 1, 1, 1})
}
if loc == Right {
return string([]byte{0xff, 0xff, 0xff, 0xff})
}
// otherwise, next to an existing key (copy before mod)
key := GetKey(allkeys, loc)
key = key[:len(key)-2] + string([]byte{255, 255})
return key
}
func toValue(key string) []byte {
return []byte("value_for_" + key)
}
// BuildMap creates random key/values and stores in a map,
// returns a list of all keys in sorted order
func BuildMap(size int) map[string][]byte {
data := make(map[string][]byte)
// insert lots of info and store the bytes
for i := 0; i < size; i++ {
key := unsafe.Str(20)
data[key] = toValue(key)
}
return data
}

View File

@ -1,68 +0,0 @@
package tree
import (
"crypto/sha256"
"hash"
"math/bits"
)
var (
leafPrefix = []byte{0}
innerPrefix = []byte{1}
)
// HashFromByteSlices computes a Merkle tree where the leaves are the byte slice,
// in the provided order. It follows RFC-6962.
func HashFromByteSlices(items [][]byte) []byte {
return hashFromByteSlices(sha256.New(), items)
}
func hashFromByteSlices(sha hash.Hash, items [][]byte) []byte {
switch len(items) {
case 0:
return emptyHash()
case 1:
return leafHashOpt(sha, items[0])
default:
k := getSplitPoint(int64(len(items)))
left := hashFromByteSlices(sha, items[:k])
right := hashFromByteSlices(sha, items[k:])
return innerHashOpt(sha, left, right)
}
}
// returns tmhash(0x00 || leaf)
func leafHashOpt(s hash.Hash, leaf []byte) []byte {
s.Reset()
s.Write(leafPrefix)
s.Write(leaf)
return s.Sum(nil)
}
func innerHashOpt(s hash.Hash, left, right []byte) []byte {
s.Reset()
s.Write(innerPrefix)
s.Write(left)
s.Write(right)
return s.Sum(nil)
}
// returns tmhash(<empty>)
func emptyHash() []byte {
h := sha256.Sum256([]byte{})
return h[:]
}
// getSplitPoint returns the largest power of 2 less than length
func getSplitPoint(length int64) int64 {
if length < 1 {
panic("Trying to split a tree with size < 1")
}
uLength := uint(length)
bitlen := bits.Len(uLength)
k := int64(1 << uint(bitlen-1))
if k == length {
k >>= 1
}
return k
}

View File

@ -1,6 +1,8 @@
package store
import (
"crypto/sha256"
ics23 "github.com/cosmos/ics23/go"
errorsmod "cosmossdk.io/errors"
@ -13,6 +15,29 @@ const (
ProofOpSMTCommitment = "ics23:smt"
)
var (
leafPrefix = []byte{0}
innerPrefix = []byte{1}
// SimpleMerkleSpec is the ics23 proof spec for simple merkle proofs.
SimpleMerkleSpec = &ics23.ProofSpec{
LeafSpec: &ics23.LeafOp{
Prefix: leafPrefix,
PrehashKey: ics23.HashOp_NO_HASH,
PrehashValue: ics23.HashOp_NO_HASH,
Hash: ics23.HashOp_SHA256,
Length: ics23.LengthOp_VAR_PROTO,
},
InnerSpec: &ics23.InnerSpec{
ChildOrder: []int32{0, 1},
MinPrefixLength: 1,
MaxPrefixLength: 1,
ChildSize: 32,
Hash: ics23.HashOp_SHA256,
},
}
)
// CommitmentOp implements merkle.ProofOperator by wrapping an ics23 CommitmentProof.
// It also contains a Key field to determine which key the proof is proving.
// NOTE: CommitmentProof currently can either be ExistenceProof or NonexistenceProof
@ -39,7 +64,7 @@ func NewIAVLCommitmentOp(key []byte, proof *ics23.CommitmentProof) CommitmentOp
func NewSimpleMerkleCommitmentOp(key []byte, proof *ics23.CommitmentProof) CommitmentOp {
return CommitmentOp{
Type: ProofOpSimpleMerkleCommitment,
Spec: ics23.TendermintSpec,
Spec: SimpleMerkleSpec,
Key: key,
Proof: proof,
}
@ -96,3 +121,69 @@ func (op CommitmentOp) Run(args [][]byte) ([][]byte, error) {
return [][]byte{root}, nil
}
// ProofFromByteSlices computes the proof from the given leaves.
func ProofFromByteSlices(leaves [][]byte, index int) (rootHash []byte, inners []*ics23.InnerOp) {
if len(leaves) == 0 {
return emptyHash(), nil
}
n := len(leaves)
for n > 1 {
if index < n-1 || index&1 == 1 {
inner := &ics23.InnerOp{Hash: ics23.HashOp_SHA256}
if index&1 == 0 {
inner.Prefix = innerPrefix
inner.Suffix = leaves[index^1]
} else {
inner.Prefix = append(innerPrefix, leaves[index^1]...)
}
inners = append(inners, inner)
}
for i := 0; i < n/2; i++ {
leaves[i] = InnerHash(leaves[2*i], leaves[2*i+1])
}
if n&1 == 1 {
leaves[n/2] = leaves[n-1]
}
n = (n + 1) / 2
index /= 2
}
rootHash = leaves[0]
return
}
// ConvertCommitmentOp converts the given merkle proof into an CommitmentOp.
func ConvertCommitmentOp(inners []*ics23.InnerOp, key, value []byte) CommitmentOp {
return NewSimpleMerkleCommitmentOp(key, &ics23.CommitmentProof{
Proof: &ics23.CommitmentProof_Exist{
Exist: &ics23.ExistenceProof{
Key: key,
Value: value,
Leaf: SimpleMerkleSpec.LeafSpec,
Path: inners,
},
},
})
}
func emptyHash() []byte {
h := sha256.Sum256([]byte{})
return h[:]
}
// LeafHash computes the hash of a leaf node.
func LeafHash(key, value []byte) ([]byte, error) {
return SimpleMerkleSpec.LeafSpec.Apply(key, value)
}
// InnerHash computes the hash of an inner node.
func InnerHash(left, right []byte) []byte {
data := make([]byte, len(innerPrefix)+len(left)+len(right))
n := copy(data, innerPrefix)
n += copy(data[n:], left)
copy(data[n:], right)
h := sha256.Sum256(data)
return h[:]
}

61
store/proof_test.go Normal file
View File

@ -0,0 +1,61 @@
package store
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestProofFromBytesSlices(t *testing.T) {
tests := []struct {
keys []string
values []string
want string
}{
{[]string{}, []string{}, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
{[]string{"key1"}, []string{"value1"}, "09c468a07fe9bc1f14e754cff0acbad4faf9449449288be8e1d5d1199a247034"},
{[]string{"key1"}, []string{"value2"}, "2131d85de3a8ded5d3a72bfc657f7324138540c520de7401ac8594785a3082fb"},
// swap order with 2 keys
{
[]string{"key1", "key2"},
[]string{"value1", "value2"},
"017788f37362dd0687beb59c0b3bfcc17a955120a4cb63dbdd4a0fdf9e07730e",
},
{
[]string{"key2", "key1"},
[]string{"value2", "value1"},
"ad2b0c23dbd3376440a5347fba02ff35cfad7930daa5e733930315b6fbb03b26",
},
// swap order with 3 keys
{
[]string{"key1", "key2", "key3"},
[]string{"value1", "value2", "value3"},
"68f41a8a3508cb5f8eb3f1c7534a86fea9f59aa4898a5aac2f1bb92834ae2a36",
},
{
[]string{"key1", "key3", "key2"},
[]string{"value1", "value3", "value2"},
"92cd50420c22d0c79f64dd1b04bfd5f5d73265f7ac37e65cf622f3cf8b963805",
},
}
for i, tc := range tests {
var err error
leaves := make([][]byte, len(tc.keys))
for j, key := range tc.keys {
leaves[j], err = LeafHash([]byte(key), []byte(tc.values[j]))
require.NoError(t, err)
}
for j := range leaves {
buf := make([][]byte, len(leaves))
copy(buf, leaves)
rootHash, inners := ProofFromByteSlices(buf, j)
require.Equal(t, tc.want, fmt.Sprintf("%x", rootHash), "test case %d", i)
commitmentOp := ConvertCommitmentOp(inners, []byte(tc.keys[j]), []byte(tc.values[j]))
expRoots, err := commitmentOp.Run([][]byte{[]byte(tc.values[j])})
require.NoError(t, err)
require.Equal(t, tc.want, fmt.Sprintf("%x", expRoots[0]), "test case %d", i)
}
}
}

View File

@ -420,10 +420,10 @@ func (s *Store) commitSC() error {
return fmt.Errorf("failed to commit SC store: %w", err)
}
commitHash := store.CommitInfo{
commitHash := (&store.CommitInfo{
Version: s.lastCommitInfo.Version,
StoreInfos: commitStoreInfos,
}.Hash()
}).Hash()
workingHash, err := s.WorkingHash()
if err != nil {