-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Add SMT store type #8507
Changes from 11 commits
6e4934c
fa8bc8e
e208a1e
cd06819
65a40a4
d22f575
95b1073
b2086e7
04bb530
c1e928b
6fd860d
c192976
6a43af8
549c414
e965573
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
) | ||
|
||
func indexKey(key []byte) []byte { | ||
return append(indexPrefix, key...) | ||
} | ||
|
||
func plainKey(key []byte) []byte { | ||
return key[len(indexPrefix):] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's store cache There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
} |
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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should use
Then in the function I don't need to pass |
||
iter.Next() | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, we're testing cosmos-sdk SMT store, not LL SMT data structure. I'm using ordering from key->value mapping (https://github.com/cosmos/cosmos-sdk/pull/8507/files/6fd860d514daa4c9caf55df383d81b1a1e671ede#diff-0d018f820578ed2dd08d3bd1cb6f854aead4e2b32333b15105743eecb21bba77R41). |
||
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) | ||
} |
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 | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.