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

Initial SMT store type #12

Closed
wants to merge 14 commits into from
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway v1.16.0
github.com/hashicorp/golang-lru v0.5.4
github.com/lazyledger/lazyledger-core v0.0.0-20210115223437-eff282ad2592
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 @@ -357,6 +357,8 @@ github.com/lazyledger/nmt v0.0.0-20201231142002-4b4c58a9e7c0 h1:me5dUDcHct/jHL+9
github.com/lazyledger/nmt v0.0.0-20201231142002-4b4c58a9e7c0/go.mod h1:tY7ypPX26Sbkt6F8EbPl3AT33B5N0BJe4OVPbq849YI=
github.com/lazyledger/rsmt2d v0.0.0-20200626141417-ea94438fa457 h1:mXl0oKmdYegd8HaP++leltErwDnvcR2v+nttbRA8pk8=
github.com/lazyledger/rsmt2d v0.0.0-20200626141417-ea94438fa457/go.mod h1:X2BD0VWh4t2V361JujYpGpYKwjsyp8vgr4iFvYo+Ojg=
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/lazyledger/lazyledger-core/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
)
Comment on lines +14 to +17
Copy link
Member

Choose a reason for hiding this comment

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

This isn't really self-explanatory. Add a brief explanation (even if it is unexported).

Nit: I'd prefer using string constants instead and wrap them with []byte() where they are used.


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

func plainKey(key []byte) []byte {
return key[len(indexPrefix):]
}

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 }{
Copy link
Member Author

Choose a reason for hiding this comment

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

Table driven tests

{[]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())
iter.Next()
}
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/lazyledger-core/crypto/merkle"
tmmerkle "github.com/lazyledger/lazyledger-core/proto/tendermint/crypto"
"github.com/lazyledger/smt"
)

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",
liamsi marked this conversation as resolved.
Show resolved Hide resolved
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