-
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
Merged
robert-zaremba
merged 15 commits into
cosmos:store/ll-smt
from
celestiaorg:tzdybal/upsteam/ll-smt
May 14, 2021
Merged
Add SMT store type #8507
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
6e4934c
Initial SMT store type
tzdybal fa8bc8e
Add iteration support to SMT
tzdybal e208a1e
Migrate to smt v0.1.1
tzdybal cd06819
Extra test for SMT iterator
tzdybal 65a40a4
CommitStore implementation for SMT store
tzdybal d22f575
Use interface instead of concrete type
tzdybal 95b1073
Add telemetry to SMT store
tzdybal b2086e7
SMT: version->root mapping, cleanup
tzdybal 04bb530
SMT proofs - initial code
tzdybal c1e928b
Tests for SMT store ProofOp implementation
tzdybal 6fd860d
Fix linter errors
tzdybal c192976
Use simple 1 byte KV-store prefixes
tzdybal 6a43af8
Improve assertions in tests
tzdybal 549c414
Use mutex properly
tzdybal e965573
Store data in ADR-040-compatible way
tzdybal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package smt | ||
|
||
import ( | ||
"bytes" | ||
|
||
dbm "github.com/tendermint/tm-db" | ||
) | ||
|
||
type Iterator struct { | ||
store *Store | ||
iter dbm.Iterator | ||
} | ||
|
||
func indexKey(key []byte) []byte { | ||
return append(indexPrefix, key...) | ||
} | ||
|
||
func plainKey(key []byte) []byte { | ||
return key[prefixLen:] | ||
} | ||
|
||
func startKey(key []byte) []byte { | ||
if key == nil { | ||
return dataPrefix | ||
} | ||
return dataKey(key) | ||
} | ||
|
||
func endKey(key []byte) []byte { | ||
if key == nil { | ||
return indexPrefix | ||
} | ||
return dataKey(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, dataPrefix) { | ||
start = nil | ||
} else { | ||
start = plainKey(start) | ||
} | ||
if bytes.Equal(end, indexPrefix) { | ||
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
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) { | ||
assert := assert.New(t) | ||
require := require.New(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(iter.Valid()) | ||
require.Equal(p.key, iter.Key()) | ||
require.Equal(p.val, iter.Value()) | ||
iter.Next() | ||
} | ||
assert.False(iter.Valid()) | ||
assert.NoError(iter.Error()) | ||
assert.NoError(iter.Close()) | ||
|
||
iter = s.Iterator(nil, nil) | ||
for _, p := range pairs { | ||
require.True(iter.Valid()) | ||
require.Equal(p.key, iter.Key()) | ||
require.Equal(p.val, iter.Value()) | ||
iter.Next() | ||
} | ||
assert.False(iter.Valid()) | ||
assert.NoError(iter.Error()) | ||
assert.NoError(iter.Close()) | ||
|
||
iter = s.Iterator([]byte("epsilon"), []byte("gamma")) | ||
for _, p := range pairs[1:4] { | ||
require.True(iter.Valid()) | ||
require.Equal(p.key, iter.Key()) | ||
require.Equal(p.val, iter.Value()) | ||
iter.Next() | ||
} | ||
assert.False(iter.Valid()) | ||
assert.NoError(iter.Error()) | ||
assert.NoError(iter.Close()) | ||
|
||
rIter := s.ReverseIterator(nil, nil) | ||
for i := len(pairs) - 1; i >= 0; i-- { | ||
require.True(rIter.Valid()) | ||
require.Equal(pairs[i].key, rIter.Key()) | ||
require.Equal(pairs[i].val, rIter.Value()) | ||
rIter.Next() | ||
} | ||
assert.False(rIter.Valid()) | ||
assert.NoError(rIter.Error()) | ||
assert.NoError(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(iter.Valid()) | ||
require.Equal(p.key, iter.Key()) | ||
require.Equal(p.val, iter.Value()) | ||
iter.Next() | ||
} | ||
} | ||
assert.False(iter.Valid()) | ||
assert.NoError(iter.Error()) | ||
assert.NoError(iter.Close()) | ||
} | ||
|
||
func TestDomain(t *testing.T) { | ||
assert := assert.New(t) | ||
s := smt.NewStore(dbm.NewMemDB()) | ||
|
||
iter := s.Iterator(nil, nil) | ||
start, end := iter.Domain() | ||
assert.Nil(start) | ||
assert.Nil(end) | ||
|
||
iter = s.Iterator([]byte("foo"), []byte("bar")) | ||
start, end = iter.Domain() | ||
assert.Equal([]byte("foo"), start) | ||
assert.Equal([]byte("bar"), end) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 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 comment
The 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).