Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SMT store type #8507

Merged
merged 15 commits into from
May 14, 2021
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway v1.16.0
github.com/hashicorp/golang-lru v0.5.4
github.com/improbable-eng/grpc-web v0.13.0
github.com/lazyledger/smt v0.1.1
github.com/magiconair/properties v1.8.4
github.com/mattn/go-isatty v0.0.12
github.com/otiai10/copy v1.4.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lazyledger/smt v0.1.1 h1:EiZZnov3ixjvqBYvlPqBqkunarm0wU0tJhWdJxCgbpA=
github.com/lazyledger/smt v0.1.1/go.mod h1:9+Pb2/tg1PvEgW7aFx4bFhDE4bvbI03zuJ8kb7nJ9Jc=
github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs=
github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
Expand Down
7 changes: 7 additions & 0 deletions store/rootmulti/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rootmulti
import (
"github.com/tendermint/tendermint/crypto/merkle"

"github.com/cosmos/cosmos-sdk/store/smt"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
)

Expand All @@ -25,3 +26,9 @@ func DefaultProofRuntime() (prt *merkle.ProofRuntime) {
prt.RegisterOpDecoder(storetypes.ProofOpSimpleMerkleCommitment, storetypes.CommitmentOpDecoder)
return
}

func SMTProofRuntime() (prt *merkle.ProofRuntime) {
prt = merkle.NewProofRuntime()
prt.RegisterOpDecoder(smt.ProofType, smt.ProofDecoder)
return prt
}
2 changes: 1 addition & 1 deletion store/rootmulti/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ func (rs *Store) SetInitialVersion(version int64) error {
// If the store is wrapped with an inter-block cache, we must first unwrap
// it to get the underlying IAVL store.
store = rs.GetCommitKVStore(key)
store.(*iavl.Store).SetInitialVersion(version)
store.(types.StoreWithInitialVersion).SetInitialVersion(version)
}
}

Expand Down
106 changes: 106 additions & 0 deletions store/smt/iterator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package smt

import (
"bytes"

dbm "github.com/tendermint/tm-db"
)

type Iterator struct {
store *Store
iter dbm.Iterator
}

var (
indexPrefix = []byte("smt-ordering-idx-")
afterIndex = []byte("smt-ordering-idx.") // '.' is next after '-' in ASCII
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be more beneficial to have the prefix keys shorter?

Copy link
Contributor Author

@tzdybal tzdybal Mar 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect this to be compressed away by DB.
Single-byte, numerical value would be much cleaner.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, let's use single byte values.

)

func indexKey(key []byte) []byte {
return append(indexPrefix, key...)
}

func plainKey(key []byte) []byte {
return key[len(indexPrefix):]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's store cache len(indexPrefix) in a module scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If prefix is single-byte, we can even hardcode this ;)

}

func startKey(key []byte) []byte {
if key == nil {
return indexPrefix
}
return indexKey(key)
}

func endKey(key []byte) []byte {
if key == nil {
return afterIndex
}
return indexKey(key)
}

func newIterator(s *Store, start, end []byte, reverse bool) (*Iterator, error) {
start = startKey(start)
end = endKey(end)
var i dbm.Iterator
var err error
if reverse {
i, err = s.db.ReverseIterator(start, end)
} else {
i, err = s.db.Iterator(start, end)
}
if err != nil {
return nil, err
}
return &Iterator{store: s, iter: i}, nil
}

// Domain returns the start (inclusive) and end (exclusive) limits of the iterator.
// CONTRACT: start, end readonly []byte
func (i *Iterator) Domain() (start []byte, end []byte) {
start, end = i.iter.Domain()
if bytes.Equal(start, indexPrefix) {
start = nil
} else {
start = plainKey(start)
}
if bytes.Equal(end, afterIndex) {
end = nil
} else {
end = plainKey(end)
}
return start, end
}

// Valid returns whether the current iterator is valid. Once invalid, the Iterator remains
// invalid forever.
func (i *Iterator) Valid() bool {
return i.iter.Valid()
}

// Next moves the iterator to the next key in the database, as defined by order of iteration.
// If Valid returns false, this method will panic.
func (i *Iterator) Next() {
i.iter.Next()
}

// Key returns the key at the current position. Panics if the iterator is invalid.
// CONTRACT: key readonly []byte
func (i *Iterator) Key() (key []byte) {
return plainKey(i.iter.Key())
}

// Value returns the value at the current position. Panics if the iterator is invalid.
// CONTRACT: value readonly []byte
func (i *Iterator) Value() (value []byte) {
return i.store.Get(i.Key())
}

// Error returns the last error encountered by the iterator, if any.
func (i *Iterator) Error() error {
return i.iter.Error()
}

// Close closes the iterator, relasing any allocated resources.
func (i *Iterator) Close() error {
return i.iter.Close()
}
109 changes: 109 additions & 0 deletions store/smt/iterator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package smt_test

import (
"bytes"
"sort"
"testing"

"github.com/cosmos/cosmos-sdk/store/smt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
dbm "github.com/tendermint/tm-db"
)

func TestIteration(t *testing.T) {
pairs := []struct{ key, val []byte }{
{[]byte("foo"), []byte("bar")},
{[]byte("lorem"), []byte("ipsum")},
{[]byte("alpha"), []byte("beta")},
{[]byte("gamma"), []byte("delta")},
{[]byte("epsilon"), []byte("zeta")},
{[]byte("eta"), []byte("theta")},
{[]byte("iota"), []byte("kappa")},
}

s := smt.NewStore(dbm.NewMemDB())

for _, p := range pairs {
s.Set(p.key, p.val)
}

// sort test data by key, to get "expected" ordering
sort.Slice(pairs, func(i, j int) bool {
return bytes.Compare(pairs[i].key, pairs[j].key) < 0
})

iter := s.Iterator([]byte("alpha"), []byte("omega"))
for _, p := range pairs {
require.True(t, iter.Valid())
assert.Equal(t, p.key, iter.Key())
assert.Equal(t, p.val, iter.Value())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use require here. It will make lot of error messages if something is wrong and it will be messy. Assert doesn't stop the exectuion.
Tip: I'm usually declaring at the top of the function:

require := require.New(t)  // works with assert as well

Then in the function I don't need to pass t any more: require.Equal(a, b).

iter.Next()
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that LL SMT is hashing the keys before inserting, so how possible it preserves a pre-image order?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert.False(t, iter.Valid())
assert.NoError(t, iter.Error())
assert.NoError(t, iter.Close())

iter = s.Iterator(nil, nil)
for _, p := range pairs {
require.True(t, iter.Valid())
assert.Equal(t, p.key, iter.Key())
assert.Equal(t, p.val, iter.Value())
iter.Next()
}
assert.False(t, iter.Valid())
assert.NoError(t, iter.Error())
assert.NoError(t, iter.Close())

iter = s.Iterator([]byte("epsilon"), []byte("gamma"))
for _, p := range pairs[1:4] {
require.True(t, iter.Valid())
assert.Equal(t, p.key, iter.Key())
assert.Equal(t, p.val, iter.Value())
iter.Next()
}
assert.False(t, iter.Valid())
assert.NoError(t, iter.Error())
assert.NoError(t, iter.Close())

rIter := s.ReverseIterator(nil, nil)
for i := len(pairs) - 1; i >= 0; i-- {
require.True(t, rIter.Valid())
assert.Equal(t, pairs[i].key, rIter.Key())
assert.Equal(t, pairs[i].val, rIter.Value())
rIter.Next()
}
assert.False(t, rIter.Valid())
assert.NoError(t, rIter.Error())
assert.NoError(t, rIter.Close())

// delete something, and ensure that iteration still works
s.Delete([]byte("eta"))

iter = s.Iterator(nil, nil)
for _, p := range pairs {
if !bytes.Equal([]byte("eta"), p.key) {
require.True(t, iter.Valid())
assert.Equal(t, p.key, iter.Key())
assert.Equal(t, p.val, iter.Value())
iter.Next()
}
}
assert.False(t, iter.Valid())
assert.NoError(t, iter.Error())
assert.NoError(t, iter.Close())
}

func TestDomain(t *testing.T) {
s := smt.NewStore(dbm.NewMemDB())

iter := s.Iterator(nil, nil)
start, end := iter.Domain()
assert.Nil(t, start)
assert.Nil(t, end)

iter = s.Iterator([]byte("foo"), []byte("bar"))
start, end = iter.Domain()
assert.Equal(t, []byte("foo"), start)
assert.Equal(t, []byte("bar"), end)
}
92 changes: 92 additions & 0 deletions store/smt/proof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package smt

import (
"bytes"
"crypto/sha256"
"encoding/gob"
"hash"

"github.com/cosmos/cosmos-sdk/store/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/lazyledger/smt"
"github.com/tendermint/tendermint/crypto/merkle"
tmmerkle "github.com/tendermint/tendermint/proto/tendermint/crypto"
)

type HasherType byte

const (
SHA256 HasherType = iota
)

const (
ProofType = "smt"
)

type ProofOp struct {
Root []byte
Key []byte
Hasher HasherType
Proof smt.SparseMerkleProof
}

var _ merkle.ProofOperator = &ProofOp{}

func NewProofOp(root, key []byte, hasher HasherType, proof smt.SparseMerkleProof) *ProofOp {
return &ProofOp{
Root: root,
Key: key,
Hasher: hasher,
Proof: proof,
}
}

func (p *ProofOp) Run(args [][]byte) ([][]byte, error) {
switch len(args) {
case 0: // non-membership proof
if !smt.VerifyProof(p.Proof, p.Root, p.Key, []byte{}, getHasher(p.Hasher)) {
return nil, sdkerrors.Wrapf(types.ErrInvalidProof, "proof did not verify absence of key: %s", p.Key)
}
case 1: // membership proof
if !smt.VerifyProof(p.Proof, p.Root, p.Key, args[0], getHasher(p.Hasher)) {
return nil, sdkerrors.Wrapf(types.ErrInvalidProof, "proof did not verify existence of key %s with given value %x", p.Key, args[0])
}
default:
return nil, sdkerrors.Wrapf(types.ErrInvalidProof, "args must be length 0 or 1, got: %d", len(args))
}
return [][]byte{p.Root}, nil
}

func (p *ProofOp) GetKey() []byte {
return p.Key
}

func (p *ProofOp) ProofOp() tmmerkle.ProofOp {
var data bytes.Buffer
enc := gob.NewEncoder(&data)
enc.Encode(p)
return tmmerkle.ProofOp{
Type: "smt",
Key: p.Key,
Data: data.Bytes(),
}
}

func ProofDecoder(pop tmmerkle.ProofOp) (merkle.ProofOperator, error) {
dec := gob.NewDecoder(bytes.NewBuffer(pop.Data))
var proof ProofOp
err := dec.Decode(&proof)
if err != nil {
return nil, err
}
return &proof, nil
}

func getHasher(hasher HasherType) hash.Hash {
switch hasher {
case SHA256:
return sha256.New()
default:
return nil
}
}
Loading