From b428f18b7284c5bf6df7dc0c29a358fd0fb7cecd Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 11 Oct 2023 10:06:13 -0700 Subject: [PATCH] [MemIAVL] Initial commit for memIAVL db and store --- .gitignore | 9 + benchmark/README.md | 0 proto/memiavl/commit_info.proto | 28 + proto/memiavl/wal.proto | 36 + sc/memiavl/README.md | 0 sc/memiavl/config/config.go | 33 + sc/memiavl/config/toml.go | 32 + sc/memiavl/db/benchmark_test.go | 239 +++++ sc/memiavl/db/commit_info.pb.go | 804 ++++++++++++++++ sc/memiavl/db/db.go | 1046 ++++++++++++++++++++ sc/memiavl/db/db_test.go | 508 ++++++++++ sc/memiavl/db/export.go | 146 +++ sc/memiavl/db/filelock.go | 27 + sc/memiavl/db/import.go | 260 +++++ sc/memiavl/db/iterator.go | 114 +++ sc/memiavl/db/iterator_test.go | 60 ++ sc/memiavl/db/layout_little_endian.go | 85 ++ sc/memiavl/db/layout_native.go | 125 +++ sc/memiavl/db/mem_node.go | 218 +++++ sc/memiavl/db/mmap.go | 60 ++ sc/memiavl/db/multitree.go | 459 +++++++++ sc/memiavl/db/node.go | 201 ++++ sc/memiavl/db/persisted_node.go | 248 +++++ sc/memiavl/db/proof.go | 194 ++++ sc/memiavl/db/proof_test.go | 59 ++ sc/memiavl/db/snapshot.go | 568 +++++++++++ sc/memiavl/db/snapshot_test.go | 193 ++++ sc/memiavl/db/tree.go | 300 ++++++ sc/memiavl/db/tree_test.go | 272 ++++++ sc/memiavl/db/types.go | 34 + sc/memiavl/db/wal.go | 100 ++ sc/memiavl/db/wal.pb.go | 1110 ++++++++++++++++++++++ sc/memiavl/db/wal_test.go | 45 + sc/memiavl/go.mod | 70 ++ sc/memiavl/go.sum | 342 +++++++ sc/memiavl/store/cachemulti/store.go | 37 + sc/memiavl/store/memiavlstore/store.go | 210 ++++ sc/memiavl/store/rootmulti/export.go | 66 ++ sc/memiavl/store/rootmulti/import.go | 91 ++ sc/memiavl/store/rootmulti/store.go | 652 +++++++++++++ sc/memiavl/store/rootmulti/store_test.go | 14 + sc/memiavl/store/setup.go | 62 ++ ss/rocksdb/README.md | 0 43 files changed, 9157 insertions(+) create mode 100644 benchmark/README.md create mode 100644 proto/memiavl/commit_info.proto create mode 100644 proto/memiavl/wal.proto create mode 100644 sc/memiavl/README.md create mode 100644 sc/memiavl/config/config.go create mode 100644 sc/memiavl/config/toml.go create mode 100644 sc/memiavl/db/benchmark_test.go create mode 100644 sc/memiavl/db/commit_info.pb.go create mode 100644 sc/memiavl/db/db.go create mode 100644 sc/memiavl/db/db_test.go create mode 100644 sc/memiavl/db/export.go create mode 100644 sc/memiavl/db/filelock.go create mode 100644 sc/memiavl/db/import.go create mode 100644 sc/memiavl/db/iterator.go create mode 100644 sc/memiavl/db/iterator_test.go create mode 100644 sc/memiavl/db/layout_little_endian.go create mode 100644 sc/memiavl/db/layout_native.go create mode 100644 sc/memiavl/db/mem_node.go create mode 100644 sc/memiavl/db/mmap.go create mode 100644 sc/memiavl/db/multitree.go create mode 100644 sc/memiavl/db/node.go create mode 100644 sc/memiavl/db/persisted_node.go create mode 100644 sc/memiavl/db/proof.go create mode 100644 sc/memiavl/db/proof_test.go create mode 100644 sc/memiavl/db/snapshot.go create mode 100644 sc/memiavl/db/snapshot_test.go create mode 100644 sc/memiavl/db/tree.go create mode 100644 sc/memiavl/db/tree_test.go create mode 100644 sc/memiavl/db/types.go create mode 100644 sc/memiavl/db/wal.go create mode 100644 sc/memiavl/db/wal.pb.go create mode 100644 sc/memiavl/db/wal_test.go create mode 100644 sc/memiavl/go.mod create mode 100644 sc/memiavl/go.sum create mode 100644 sc/memiavl/store/cachemulti/store.go create mode 100644 sc/memiavl/store/memiavlstore/store.go create mode 100644 sc/memiavl/store/rootmulti/export.go create mode 100644 sc/memiavl/store/rootmulti/import.go create mode 100644 sc/memiavl/store/rootmulti/store.go create mode 100644 sc/memiavl/store/rootmulti/store_test.go create mode 100644 sc/memiavl/store/setup.go create mode 100644 ss/rocksdb/README.md diff --git a/.gitignore b/.gitignore index 3b735ec..5f16b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,12 @@ # Go workspace file go.work +.DS_Store +*.swp +*.swo +*.swl +*.swm +*.swn +*.pyc +.dccache +.idea \ No newline at end of file diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..e69de29 diff --git a/proto/memiavl/commit_info.proto b/proto/memiavl/commit_info.proto new file mode 100644 index 0000000..2a0811b --- /dev/null +++ b/proto/memiavl/commit_info.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +option go_package = "github.com/sei-protocol/sei-db/memiavl"; + +import "gogoproto/gogo.proto"; + +// CommitInfo defines commit information used by the multi-store when committing +// a version/height. +message CommitInfo { + int64 version = 1; + repeated StoreInfo store_infos = 2 [(gogoproto.nullable) = false]; +} + +// StoreInfo defines store-specific commit information. It contains a reference +// between a store name and the commit ID. +message StoreInfo { + string name = 1; + CommitID commit_id = 2 [(gogoproto.nullable) = false]; +} + +// CommitID defines the committment information when a specific store is +// committed. +message CommitID { + option (gogoproto.goproto_stringer) = false; + + int64 version = 1; + bytes hash = 2; +} diff --git a/proto/memiavl/wal.proto b/proto/memiavl/wal.proto new file mode 100644 index 0000000..7243d23 --- /dev/null +++ b/proto/memiavl/wal.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; +package memiavl; + +option go_package = "github.com/sei-protocol/sei-db/memiavl"; + +import "gogoproto/gogo.proto"; +import "iavl/changeset.proto"; +import "memiavl/commit_info.proto"; + +// NamedChangeSet combine a tree name with the changeset +message NamedChangeSet { + iavl.ChangeSet changeset = 1 [(gogoproto.nullable) = false]; + string name = 2; +} + +// TreeNameUpgrade defines upgrade of tree names: +// - New tree: { name: "tree" } +// - Delete tree: { name: "tree", delete: true } +// - Rename tree: { name: "new-tree", rename_from: "old-tree" } +message TreeNameUpgrade { + string name = 1; + string rename_from = 2; + bool delete = 3; +} + +// WALEntry is a single Write-Ahead-Log entry +message WALEntry { + repeated NamedChangeSet changesets = 1; + repeated TreeNameUpgrade upgrades = 2; +} + +// MultiTreeMetadata stores the metadata for MultiTree +message MultiTreeMetadata { + CommitInfo commit_info = 1; + int64 initial_version = 2; +} diff --git a/sc/memiavl/README.md b/sc/memiavl/README.md new file mode 100644 index 0000000..e69de29 diff --git a/sc/memiavl/config/config.go b/sc/memiavl/config/config.go new file mode 100644 index 0000000..4c0fa6c --- /dev/null +++ b/sc/memiavl/config/config.go @@ -0,0 +1,33 @@ +package config + +const ( + DefaultCacheSize = 100000 + DefaultSnapshotInterval = 10000 +) + +type MemIAVLConfig struct { + // Enable defines if the memiavl should be enabled. + Enable bool `mapstructure:"enable"` + // ZeroCopy defines if the memiavl should return slices pointing to mmap-ed buffers directly (zero-copy), + // the zero-copied slices must not be retained beyond current block's execution. + // the sdk address cache will be disabled if zero-copy is enabled. + ZeroCopy bool `mapstructure:"zero-copy"` + // AsyncCommitBuffer defines the size of asynchronous commit queue, this greatly improve block catching-up + // performance, -1 means synchronous commit. + AsyncCommitBuffer int `mapstructure:"async-commit-buffer"` + // SnapshotKeepRecent defines what many old snapshots (excluding the latest one) to keep after new snapshots are + // taken, defaults to 1 to make sure ibc relayers work. + SnapshotKeepRecent uint32 `mapstructure:"snapshot-keep-recent"` + // SnapshotInterval defines the block interval the memiavl snapshot is taken, default to 1000. + SnapshotInterval uint32 `mapstructure:"snapshot-interval"` + // CacheSize defines the size of the cache for each memiavl store. + CacheSize int `mapstructure:"cache-size"` +} + +func DefaultMemIAVLConfig() MemIAVLConfig { + return MemIAVLConfig{ + CacheSize: DefaultCacheSize, + SnapshotInterval: DefaultSnapshotInterval, + SnapshotKeepRecent: 1, + } +} diff --git a/sc/memiavl/config/toml.go b/sc/memiavl/config/toml.go new file mode 100644 index 0000000..2848e22 --- /dev/null +++ b/sc/memiavl/config/toml.go @@ -0,0 +1,32 @@ +package config + +// DefaultConfigTemplate defines the configuration template for the memiavl configuration +const DefaultConfigTemplate = ` +############################################################################### +### MemIAVL Configuration ### +############################################################################### + +[memiavl] + +# Enable defines if the memiavl should be enabled. +enable = {{ .MemIAVL.Enable }} + +# ZeroCopy defines if the memiavl should return slices pointing to mmap-ed buffers directly (zero-copy), +# the zero-copied slices must not be retained beyond current block's execution. +# the sdk address cache will be disabled if zero-copy is enabled. +zero-copy = {{ .MemIAVL.ZeroCopy }} + +# AsyncCommitBuffer defines the size of asynchronous commit queue, this greatly improve block catching-up +# performance, -1 means synchronous commit. +async-commit-buffer = {{ .MemIAVL.AsyncCommitBuffer }} + +# SnapshotKeepRecent defines what many old snapshots (excluding the latest one) to keep after new snapshots are +# taken, defaults to 1 to make sure ibc relayers work. +snapshot-keep-recent = {{ .MemIAVL.SnapshotKeepRecent }} + +# SnapshotInterval defines the block interval the memiavl snapshot is taken, default to 1000. +snapshot-interval = {{ .MemIAVL.SnapshotInterval }} + +# CacheSize defines the size of the cache for each memiavl store, default to 1000. +cache-size = {{ .MemIAVL.CacheSize }} +` diff --git a/sc/memiavl/db/benchmark_test.go b/sc/memiavl/db/benchmark_test.go new file mode 100644 index 0000000..be76578 --- /dev/null +++ b/sc/memiavl/db/benchmark_test.go @@ -0,0 +1,239 @@ +package memiavl + +import ( + "bytes" + "encoding/binary" + "math/rand" + "sort" + "testing" + + iavlcache "github.com/cosmos/iavl/cache" + "github.com/stretchr/testify/require" + "github.com/tidwall/btree" +) + +func BenchmarkByteCompare(b *testing.B) { + var x, y [32]byte + for i := 0; i < b.N; i++ { + _ = bytes.Compare(x[:], y[:]) + } +} + +func BenchmarkRandomGet(b *testing.B) { + amount := 1000000 + items := genRandItems(amount) + targetKey := items[500].key + targetValue := items[500].value + targetItem := itemT{key: targetKey} + + tree := New(0) + for _, item := range items { + tree.set(item.key, item.value) + } + + snapshotDir := b.TempDir() + err := tree.WriteSnapshot(snapshotDir) + require.NoError(b, err) + snapshot, err := OpenSnapshot(snapshotDir) + require.NoError(b, err) + defer snapshot.Close() + + b.Run("memiavl", func(b *testing.B) { + require.Equal(b, targetValue, tree.Get(targetKey)) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = tree.Get(targetKey) + } + }) + b.Run("memiavl-disk", func(b *testing.B) { + diskTree := NewFromSnapshot(snapshot, true, 0) + require.Equal(b, targetValue, diskTree.Get(targetKey)) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = diskTree.Get(targetKey) + } + }) + b.Run("memiavl-disk-cache-hit", func(b *testing.B) { + diskTree := NewFromSnapshot(snapshot, true, 1) + require.Equal(b, targetValue, diskTree.Get(targetKey)) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = diskTree.Get(targetKey) + } + }) + b.Run("memiavl-disk-cache-miss", func(b *testing.B) { + diskTree := NewFromSnapshot(snapshot, true, 0) + // enforce an empty cache to emulate cache miss + diskTree.cache = iavlcache.New(0) + require.Equal(b, targetValue, diskTree.Get(targetKey)) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = diskTree.Get(targetKey) + } + }) + b.Run("btree-degree-2", func(b *testing.B) { + bt2 := btree.NewBTreeGOptions(lessG, btree.Options{ + NoLocks: true, + Degree: 2, + }) + for _, item := range items { + bt2.Set(item) + } + v, _ := bt2.Get(targetItem) + require.Equal(b, targetValue, v.value) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = bt2.Get(targetItem) + } + }) + b.Run("btree-degree-32", func(b *testing.B) { + bt32 := btree.NewBTreeGOptions(lessG, btree.Options{ + NoLocks: true, + Degree: 32, + }) + for _, item := range items { + bt32.Set(item) + } + v, _ := bt32.Get(targetItem) + require.Equal(b, targetValue, v.value) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = bt32.Get(targetItem) + } + }) + b.Run("iavl-lru", func(b *testing.B) { + cache := iavlcache.New(amount) + for _, item := range items { + cache.Add(NewIavlCacheNode(item.key, item.value)) + } + v := cache.Get(targetItem.key).(iavlCacheNode).value + require.Equal(b, targetValue, v) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = cache.Get(targetKey).(iavlCacheNode).value + } + }) + b.Run("go-map", func(b *testing.B) { + m := make(map[string][]byte, amount) + for _, item := range items { + m[string(item.key)] = item.value + } + v := m[string(targetItem.key)] + require.Equal(b, targetValue, v) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = m[string(targetKey)] + } + }) + + b.Run("binary-search", func(b *testing.B) { + // the last benchmark sort the items in place + sort.Slice(items, func(i, j int) bool { + return bytes.Compare(items[i].key, items[j].key) < 0 + }) + cmp := func(i int) bool { return bytes.Compare(items[i].key, targetKey) != -1 } + i := sort.Search(len(items), cmp) + require.Equal(b, targetValue, items[i].value) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + n := sort.Search(len(items), cmp) + _ = items[n].value + } + }) +} + +func BenchmarkRandomSet(b *testing.B) { + items := genRandItems(1000000) + b.ResetTimer() + b.Run("memiavl", func(b *testing.B) { + for i := 0; i < b.N; i++ { + tree := New(0) + for _, item := range items { + tree.set(item.key, item.value) + } + } + }) + b.Run("tree2", func(b *testing.B) { + for i := 0; i < b.N; i++ { + bt := btree.NewBTreeGOptions(lessG, btree.Options{ + NoLocks: true, + Degree: 2, + }) + for _, item := range items { + bt.Set(item) + } + } + }) + b.Run("tree32", func(b *testing.B) { + for i := 0; i < b.N; i++ { + bt := btree.NewBTreeGOptions(lessG, btree.Options{ + NoLocks: true, + Degree: 32, + }) + for _, item := range items { + bt.Set(item) + } + } + }) +} + +type itemT struct { + key, value []byte +} + +func lessG(a, b itemT) bool { + return bytes.Compare(a.key, b.key) == -1 +} + +func int64ToItemT(n uint64) itemT { + var key, value [8]byte + binary.BigEndian.PutUint64(key[:], n) + binary.LittleEndian.PutUint64(value[:], n) + return itemT{ + key: key[:], + value: value[:], + } +} + +func genRandItems(n int) []itemT { + r := rand.New(rand.NewSource(0)) + items := make([]itemT, n) + itemsM := make(map[uint64]bool) + for i := 0; i < n; i++ { + for { + key := uint64(r.Int63n(10000000000000000)) + if !itemsM[key] { + itemsM[key] = true + items[i] = int64ToItemT(key) + break + } + } + } + return items +} + +type iavlCacheNode struct { + key []byte + value []byte +} + +func NewIavlCacheNode(key, value []byte) iavlCacheNode { + return iavlCacheNode{key, value} +} + +func (n iavlCacheNode) GetKey() []byte { + return n.key +} + +func (n iavlCacheNode) GetCacheKey() []byte { + return n.key +} diff --git a/sc/memiavl/db/commit_info.pb.go b/sc/memiavl/db/commit_info.pb.go new file mode 100644 index 0000000..d18637a --- /dev/null +++ b/sc/memiavl/db/commit_info.pb.go @@ -0,0 +1,804 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: memiavl/commit_info.proto + +package memiavl + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// CommitInfo defines commit information used by the multi-store when committing +// a version/height. +type CommitInfo struct { + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + StoreInfos []StoreInfo `protobuf:"bytes,2,rep,name=store_infos,json=storeInfos,proto3" json:"store_infos"` +} + +func (m *CommitInfo) Reset() { *m = CommitInfo{} } +func (m *CommitInfo) String() string { return proto.CompactTextString(m) } +func (*CommitInfo) ProtoMessage() {} +func (*CommitInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_11dece027dbaaf68, []int{0} +} +func (m *CommitInfo) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CommitInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CommitInfo.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CommitInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_CommitInfo.Merge(m, src) +} +func (m *CommitInfo) XXX_Size() int { + return m.Size() +} +func (m *CommitInfo) XXX_DiscardUnknown() { + xxx_messageInfo_CommitInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_CommitInfo proto.InternalMessageInfo + +func (m *CommitInfo) GetVersion() int64 { + if m != nil { + return m.Version + } + return 0 +} + +func (m *CommitInfo) GetStoreInfos() []StoreInfo { + if m != nil { + return m.StoreInfos + } + return nil +} + +// StoreInfo defines store-specific commit information. It contains a reference +// between a store name and the commit ID. +type StoreInfo struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + CommitId CommitID `protobuf:"bytes,2,opt,name=commit_id,json=commitId,proto3" json:"commit_id"` +} + +func (m *StoreInfo) Reset() { *m = StoreInfo{} } +func (m *StoreInfo) String() string { return proto.CompactTextString(m) } +func (*StoreInfo) ProtoMessage() {} +func (*StoreInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_11dece027dbaaf68, []int{1} +} +func (m *StoreInfo) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *StoreInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_StoreInfo.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *StoreInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_StoreInfo.Merge(m, src) +} +func (m *StoreInfo) XXX_Size() int { + return m.Size() +} +func (m *StoreInfo) XXX_DiscardUnknown() { + xxx_messageInfo_StoreInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_StoreInfo proto.InternalMessageInfo + +func (m *StoreInfo) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *StoreInfo) GetCommitId() CommitID { + if m != nil { + return m.CommitId + } + return CommitID{} +} + +// CommitID defines the committment information when a specific store is +// committed. +type CommitID struct { + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Hash []byte `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` +} + +func (m *CommitID) Reset() { *m = CommitID{} } +func (*CommitID) ProtoMessage() {} +func (*CommitID) Descriptor() ([]byte, []int) { + return fileDescriptor_11dece027dbaaf68, []int{2} +} +func (m *CommitID) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CommitID) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CommitID.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CommitID) XXX_Merge(src proto.Message) { + xxx_messageInfo_CommitID.Merge(m, src) +} +func (m *CommitID) XXX_Size() int { + return m.Size() +} +func (m *CommitID) XXX_DiscardUnknown() { + xxx_messageInfo_CommitID.DiscardUnknown(m) +} + +var xxx_messageInfo_CommitID proto.InternalMessageInfo + +func (m *CommitID) GetVersion() int64 { + if m != nil { + return m.Version + } + return 0 +} + +func (m *CommitID) GetHash() []byte { + if m != nil { + return m.Hash + } + return nil +} + +func init() { + proto.RegisterType((*CommitInfo)(nil), "memiavl.CommitInfo") + proto.RegisterType((*StoreInfo)(nil), "memiavl.StoreInfo") + proto.RegisterType((*CommitID)(nil), "memiavl.CommitID") +} + +func init() { proto.RegisterFile("memiavl/commit_info.proto", fileDescriptor_11dece027dbaaf68) } + +var fileDescriptor_11dece027dbaaf68 = []byte{ + // 286 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x90, 0xb1, 0x4e, 0xc3, 0x30, + 0x14, 0x45, 0xe3, 0x36, 0xa2, 0xed, 0x0b, 0x0b, 0x16, 0x43, 0x60, 0x70, 0xa3, 0x4c, 0x11, 0x52, + 0x13, 0xa9, 0xb0, 0xc0, 0xc0, 0x50, 0xba, 0x74, 0x0d, 0x62, 0x61, 0x41, 0x69, 0x9a, 0x26, 0x96, + 0xb0, 0x5f, 0x65, 0x87, 0x4a, 0xfc, 0x05, 0x23, 0x23, 0x9f, 0xd3, 0xb1, 0x23, 0x13, 0x42, 0xc9, + 0x8f, 0xa0, 0xb8, 0x49, 0x47, 0xb6, 0xeb, 0x7b, 0xec, 0xa3, 0x2b, 0xc3, 0x85, 0xc8, 0x04, 0x4f, + 0xb6, 0xaf, 0x51, 0x8a, 0x42, 0xf0, 0xf2, 0x85, 0xcb, 0x35, 0x86, 0x1b, 0x85, 0x25, 0xd2, 0x41, + 0x8b, 0x2e, 0xcf, 0x73, 0xcc, 0xd1, 0x74, 0x51, 0x93, 0x0e, 0xd8, 0x4f, 0x00, 0x1e, 0xcc, 0x9b, + 0x85, 0x5c, 0x23, 0x75, 0x61, 0xb0, 0xcd, 0x94, 0xe6, 0x28, 0x5d, 0xe2, 0x91, 0xa0, 0x1f, 0x77, + 0x47, 0x7a, 0x0b, 0x8e, 0x2e, 0x51, 0x65, 0x46, 0xad, 0xdd, 0x9e, 0xd7, 0x0f, 0x9c, 0x29, 0x0d, + 0x5b, 0x79, 0xf8, 0xd8, 0xb0, 0x46, 0x31, 0xb3, 0x77, 0x3f, 0x63, 0x2b, 0x06, 0xdd, 0x15, 0xda, + 0x7f, 0x82, 0xd1, 0x11, 0x53, 0x0a, 0xb6, 0x4c, 0x44, 0x66, 0xf4, 0xa3, 0xd8, 0x64, 0x7a, 0x03, + 0xa3, 0x6e, 0xf7, 0xca, 0xed, 0x79, 0x24, 0x70, 0xa6, 0x67, 0x47, 0x73, 0xbb, 0x6e, 0xde, 0x8a, + 0x87, 0x87, 0x9b, 0x8b, 0x95, 0x7f, 0x0f, 0xc3, 0x8e, 0xfd, 0xb3, 0x9b, 0x82, 0x5d, 0x24, 0xba, + 0x30, 0xda, 0xd3, 0xd8, 0xe4, 0x3b, 0xfb, 0xf3, 0x6b, 0x6c, 0xcd, 0xe6, 0xbb, 0x8a, 0x91, 0x7d, + 0xc5, 0xc8, 0x6f, 0xc5, 0xc8, 0x47, 0xcd, 0xac, 0x7d, 0xcd, 0xac, 0xef, 0x9a, 0x59, 0xcf, 0x57, + 0x39, 0x2f, 0x8b, 0xb7, 0x65, 0x98, 0xa2, 0x88, 0x52, 0xf5, 0xbe, 0x29, 0x71, 0x82, 0x2a, 0x9f, + 0xa4, 0x45, 0xc2, 0x65, 0x94, 0x2a, 0x94, 0xa8, 0xa3, 0x76, 0xde, 0xf2, 0xc4, 0x7c, 0xe3, 0xf5, + 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x25, 0x25, 0xfb, 0x55, 0x82, 0x01, 0x00, 0x00, +} + +func (m *CommitInfo) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CommitInfo) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CommitInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.StoreInfos) > 0 { + for iNdEx := len(m.StoreInfos) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.StoreInfos[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintCommitInfo(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if m.Version != 0 { + i = encodeVarintCommitInfo(dAtA, i, uint64(m.Version)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *StoreInfo) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *StoreInfo) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *StoreInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.CommitId.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintCommitInfo(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintCommitInfo(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *CommitID) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CommitID) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CommitID) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Hash) > 0 { + i -= len(m.Hash) + copy(dAtA[i:], m.Hash) + i = encodeVarintCommitInfo(dAtA, i, uint64(len(m.Hash))) + i-- + dAtA[i] = 0x12 + } + if m.Version != 0 { + i = encodeVarintCommitInfo(dAtA, i, uint64(m.Version)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintCommitInfo(dAtA []byte, offset int, v uint64) int { + offset -= sovCommitInfo(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *CommitInfo) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Version != 0 { + n += 1 + sovCommitInfo(uint64(m.Version)) + } + if len(m.StoreInfos) > 0 { + for _, e := range m.StoreInfos { + l = e.Size() + n += 1 + l + sovCommitInfo(uint64(l)) + } + } + return n +} + +func (m *StoreInfo) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + sovCommitInfo(uint64(l)) + } + l = m.CommitId.Size() + n += 1 + l + sovCommitInfo(uint64(l)) + return n +} + +func (m *CommitID) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Version != 0 { + n += 1 + sovCommitInfo(uint64(m.Version)) + } + l = len(m.Hash) + if l > 0 { + n += 1 + l + sovCommitInfo(uint64(l)) + } + return n +} + +func sovCommitInfo(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozCommitInfo(x uint64) (n int) { + return sovCommitInfo(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *CommitInfo) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CommitInfo: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CommitInfo: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType) + } + m.Version = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Version |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StoreInfos", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthCommitInfo + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthCommitInfo + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.StoreInfos = append(m.StoreInfos, StoreInfo{}) + if err := m.StoreInfos[len(m.StoreInfos)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipCommitInfo(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthCommitInfo + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *StoreInfo) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: StoreInfo: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: StoreInfo: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthCommitInfo + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthCommitInfo + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CommitId", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthCommitInfo + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthCommitInfo + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.CommitId.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipCommitInfo(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthCommitInfo + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *CommitID) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CommitID: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CommitID: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType) + } + m.Version = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Version |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Hash", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthCommitInfo + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthCommitInfo + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Hash = append(m.Hash[:0], dAtA[iNdEx:postIndex]...) + if m.Hash == nil { + m.Hash = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipCommitInfo(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthCommitInfo + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipCommitInfo(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowCommitInfo + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthCommitInfo + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupCommitInfo + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthCommitInfo + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthCommitInfo = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowCommitInfo = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupCommitInfo = fmt.Errorf("proto: unexpected end of group") +) diff --git a/sc/memiavl/db/db.go b/sc/memiavl/db/db.go new file mode 100644 index 0000000..06e9dee --- /dev/null +++ b/sc/memiavl/db/db.go @@ -0,0 +1,1046 @@ +package memiavl + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/alitto/pond" + "github.com/cosmos/iavl" + "github.com/tidwall/wal" +) + +const ( + DefaultSnapshotInterval = 1000 + LockFileName = "LOCK" + DefaultSnapshotWriterLimit = 4 +) + +var errReadOnly = errors.New("db is read-only") + +// DB implements DB-like functionalities on top of MultiTree: +// - async snapshot rewriting +// - Write-ahead-log +// +// The memiavl.db directory looks like this: +// ``` +// > current -> snapshot-N +// > snapshot-N +// > bank +// > kvs +// > nodes +// > metadata +// > acc +// > ... other stores +// > wal +// ``` +type DB struct { + MultiTree + dir string + logger Logger + fileLock FileLock + readOnly bool + + // result channel of snapshot rewrite goroutine + snapshotRewriteChan chan snapshotResult + // the number of old snapshots to keep (excluding the latest one) + snapshotKeepRecent uint32 + // block interval to take a new snapshot + snapshotInterval uint32 + // make sure only one snapshot rewrite is running + pruneSnapshotLock sync.Mutex + triggerStateSyncExport func(height int64) + + // invariant: the LastIndex always match the current version of MultiTree + wal *wal.Log + walChanSize int + walChan chan *walEntry + walQuit chan error + + // pending changes, will be written into WAL in next Commit call + pendingLog WALEntry + + // The assumptions to concurrency: + // - The methods on DB are protected by a mutex + // - Each call of Load loads a separate instance, in query scenarios, + // it should be immutable, the cache stores will handle the temporary writes. + // - The DB for the state machine will handle writes through the Commit call, + // this method is the sole entry point for tree modifications, and there's no concurrency internally + // (the background snapshot rewrite is handled separately), so we don't need locks in the Tree. + mtx sync.Mutex + // worker goroutine IdleTimeout = 5s + snapshotWriterPool *pond.WorkerPool +} + +type Options struct { + Logger Logger + CreateIfMissing bool + InitialVersion uint32 + ReadOnly bool + // the initial stores when initialize the empty instance + InitialStores []string + SnapshotKeepRecent uint32 + SnapshotInterval uint32 + TriggerStateSyncExport func(height int64) + // load the target version instead of latest version + TargetVersion uint32 + // Buffer size for the asynchronous commit queue, -1 means synchronous commit, + // default to 0. + AsyncCommitBuffer int + // ZeroCopy if true, the get and iterator methods could return a slice pointing to mmaped blob files. + ZeroCopy bool + // CacheSize defines the cache's max entry size for each memiavl store. + CacheSize int + // LoadForOverwriting if true rollbacks the state, specifically the Load method will + // truncate the versions after the `TargetVersion`, the `TargetVersion` becomes the latest version. + // it do nothing if the target version is `0`. + LoadForOverwriting bool + + SnapshotWriterLimit int +} + +func (opts Options) Validate() error { + if opts.ReadOnly && opts.CreateIfMissing { + return errors.New("can't create db in read-only mode") + } + + if opts.ReadOnly && opts.LoadForOverwriting { + return errors.New("can't rollback db in read-only mode") + } + + return nil +} + +func (opts *Options) FillDefaults() { + if opts.Logger == nil { + opts.Logger = NewNopLogger() + } + + if opts.SnapshotInterval == 0 { + opts.SnapshotInterval = DefaultSnapshotInterval + } + + if opts.SnapshotWriterLimit <= 0 { + opts.SnapshotWriterLimit = DefaultSnapshotWriterLimit + } +} + +const ( + SnapshotPrefix = "snapshot-" + SnapshotDirLen = len(SnapshotPrefix) + 20 +) + +func Load(dir string, opts Options) (*DB, error) { + if err := opts.Validate(); err != nil { + return nil, fmt.Errorf("invalid options: %w", err) + } + opts.FillDefaults() + + if opts.CreateIfMissing { + if err := createDBIfNotExist(dir, opts.InitialVersion); err != nil { + return nil, fmt.Errorf("fail to load db: %w", err) + } + } + + var ( + err error + fileLock FileLock + ) + if !opts.ReadOnly { + fileLock, err = LockFile(filepath.Join(dir, LockFileName)) + if err != nil { + return nil, fmt.Errorf("fail to lock db: %w", err) + } + + // cleanup any temporary directories left by interrupted snapshot rewrite + if err := removeTmpDirs(dir); err != nil { + return nil, fmt.Errorf("fail to cleanup tmp directories: %w", err) + } + } + + snapshot := "current" + if opts.TargetVersion > 0 { + // find the biggest snapshot version that's less than or equal to the target version + snapshotVersion, err := seekSnapshot(dir, opts.TargetVersion) + if err != nil { + return nil, fmt.Errorf("fail to seek snapshot: %w", err) + } + snapshot = snapshotName(snapshotVersion) + } + + path := filepath.Join(dir, snapshot) + mtree, err := LoadMultiTree(path, opts.ZeroCopy, opts.CacheSize) + if err != nil { + return nil, err + } + + wal, err := OpenWAL(walPath(dir), &wal.Options{NoCopy: true, NoSync: true}) + if err != nil { + return nil, err + } + + if opts.TargetVersion == 0 || int64(opts.TargetVersion) > mtree.Version() { + if err := mtree.CatchupWAL(wal, int64(opts.TargetVersion)); err != nil { + return nil, errors.Join(err, wal.Close()) + } + } + + if opts.LoadForOverwriting && opts.TargetVersion > 0 { + currentSnapshot, err := os.Readlink(currentPath(dir)) + if err != nil { + return nil, fmt.Errorf("fail to read current version: %w", err) + } + + if snapshot != currentSnapshot { + // downgrade `"current"` link first + opts.Logger.Info("downgrade current link to %s", snapshot) + if err := updateCurrentSymlink(dir, snapshot); err != nil { + return nil, fmt.Errorf("fail to update current snapshot link: %w", err) + } + } + + // truncate the WAL + opts.Logger.Info("truncate WAL from back, version: %d", opts.TargetVersion) + if err := wal.TruncateBack(walIndex(int64(opts.TargetVersion), mtree.initialVersion)); err != nil { + return nil, fmt.Errorf("fail to truncate wal logs: %w", err) + } + + // prune snapshots that's larger than the target version + if err := traverseSnapshots(dir, false, func(version int64) (bool, error) { + if version <= int64(opts.TargetVersion) { + return true, nil + } + + if err := atomicRemoveDir(filepath.Join(dir, snapshotName(version))); err != nil { + opts.Logger.Error("fail to prune snapshot, version: %d", version) + } else { + opts.Logger.Info("prune snapshot, version: %d", version) + } + return false, nil + }); err != nil { + return nil, fmt.Errorf("fail to prune snapshots: %w", err) + } + } + // create worker pool. recv tasks to write snapshot + workerPool := pond.New(opts.SnapshotWriterLimit, opts.SnapshotWriterLimit*10) + + db := &DB{ + MultiTree: *mtree, + logger: opts.Logger, + dir: dir, + fileLock: fileLock, + readOnly: opts.ReadOnly, + wal: wal, + walChanSize: opts.AsyncCommitBuffer, + snapshotKeepRecent: opts.SnapshotKeepRecent, + snapshotInterval: opts.SnapshotInterval, + triggerStateSyncExport: opts.TriggerStateSyncExport, + snapshotWriterPool: workerPool, + } + + if !db.readOnly && db.Version() == 0 && len(opts.InitialStores) > 0 { + // do the initial upgrade with the `opts.InitialStores` + var upgrades []*TreeNameUpgrade + for _, name := range opts.InitialStores { + upgrades = append(upgrades, &TreeNameUpgrade{Name: name}) + } + if err := db.ApplyUpgrades(upgrades); err != nil { + return nil, errors.Join(err, db.Close()) + } + } + + return db, nil +} + +func removeTmpDirs(rootDir string) error { + entries, err := os.ReadDir(rootDir) + if err != nil { + return err + } + + for _, entry := range entries { + if !entry.IsDir() || !strings.HasSuffix(entry.Name(), "-tmp") { + continue + } + + if err := os.RemoveAll(filepath.Join(rootDir, entry.Name())); err != nil { + return err + } + } + + return nil +} + +// ReadOnly returns whether the DB is opened in read-only mode. +func (db *DB) ReadOnly() bool { + return db.readOnly +} + +// SetInitialVersion wraps `MultiTree.SetInitialVersion`. +// it do an immediate snapshot rewrite, because we can't use wal log to record this change, +// because we need it to convert versions to wal index in the first place. +func (db *DB) SetInitialVersion(initialVersion int64) error { + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + return errReadOnly + } + + if db.lastCommitInfo.Version > 0 { + return errors.New("initial version can only be set before any commit") + } + + if err := db.MultiTree.SetInitialVersion(initialVersion); err != nil { + return err + } + + return initEmptyDB(db.dir, db.initialVersion) +} + +// ApplyUpgrades wraps MultiTree.ApplyUpgrades, it also append the upgrades in a pending log, +// which will be persisted to the WAL in next Commit call. +func (db *DB) ApplyUpgrades(upgrades []*TreeNameUpgrade) error { + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + return errReadOnly + } + + if err := db.MultiTree.ApplyUpgrades(upgrades); err != nil { + return err + } + + db.pendingLog.Upgrades = append(db.pendingLog.Upgrades, upgrades...) + return nil +} + +// ApplyChangeSets wraps MultiTree.ApplyChangeSets, it also append the changesets in the pending log, +// which will be persisted to the WAL in next Commit call. +func (db *DB) ApplyChangeSets(changeSets []*NamedChangeSet) error { + if len(changeSets) == 0 { + return nil + } + + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + return errReadOnly + } + + if len(db.pendingLog.Changesets) > 0 { + return errors.New("don't support multiple ApplyChangeSets calls in the same version") + } + db.pendingLog.Changesets = changeSets + + return db.MultiTree.ApplyChangeSets(changeSets) +} + +// ApplyChangeSet wraps MultiTree.ApplyChangeSet, it also append the changesets in the pending log, +// which will be persisted to the WAL in next Commit call. +func (db *DB) ApplyChangeSet(name string, changeSet iavl.ChangeSet) error { + if len(changeSet.Pairs) == 0 { + return nil + } + + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + return errReadOnly + } + + for _, cs := range db.pendingLog.Changesets { + if cs.Name == name { + return errors.New("don't support multiple ApplyChangeSet calls with the same name in the same version") + } + } + + db.pendingLog.Changesets = append(db.pendingLog.Changesets, &NamedChangeSet{ + Name: name, + Changeset: changeSet, + }) + sort.SliceStable(db.pendingLog.Changesets, func(i, j int) bool { + return db.pendingLog.Changesets[i].Name < db.pendingLog.Changesets[j].Name + }) + + return db.MultiTree.ApplyChangeSet(name, changeSet) +} + +// checkAsyncTasks checks the status of background tasks non-blocking-ly and process the result +func (db *DB) checkAsyncTasks() error { + return errors.Join( + db.checkAsyncCommit(), + db.checkBackgroundSnapshotRewrite(), + ) +} + +// checkAsyncCommit check the quit signal of async wal writing +func (db *DB) checkAsyncCommit() error { + select { + case err := <-db.walQuit: + // async wal writing failed, we need to abort the state machine + return fmt.Errorf("async wal writing goroutine quit unexpectedly: %w", err) + default: + } + + return nil +} + +// CommittedVersion returns the latest version written in wal, or snapshot version if wal is empty. +func (db *DB) CommittedVersion() (int64, error) { + lastIndex, err := db.wal.LastIndex() + if err != nil { + return 0, err + } + if lastIndex == 0 { + return db.SnapshotVersion(), nil + } + return walVersion(lastIndex, db.initialVersion), nil +} + +// checkBackgroundSnapshotRewrite check the result of background snapshot rewrite, cleans up the old snapshots and switches to a new multitree +func (db *DB) checkBackgroundSnapshotRewrite() error { + // check the completeness of background snapshot rewriting + select { + case result := <-db.snapshotRewriteChan: + db.snapshotRewriteChan = nil + + if result.mtree == nil { + // background snapshot rewrite failed + return fmt.Errorf("background snapshot rewriting failed: %w", result.err) + } + + // wait for potential pending wal writings to finish, to make sure we catch up to latest state. + // in real world, block execution should be slower than wal writing, so this should not block for long. + for { + committedVersion, err := db.CommittedVersion() + if err != nil { + return fmt.Errorf("get wal version failed: %w", err) + } + if db.lastCommitInfo.Version == committedVersion { + break + } + time.Sleep(time.Nanosecond) + } + + // catchup the remaining wal + if err := result.mtree.CatchupWAL(db.wal, 0); err != nil { + return fmt.Errorf("catchup failed: %w", err) + } + + // do the switch + if err := db.reloadMultiTree(result.mtree); err != nil { + return fmt.Errorf("switch multitree failed: %w", err) + } + db.logger.Info("switched to new snapshot", "version", db.MultiTree.Version()) + + db.pruneSnapshots() + + // trigger state-sync snapshot export + if db.triggerStateSyncExport != nil { + db.triggerStateSyncExport(db.SnapshotVersion()) + } + default: + } + + return nil +} + +// pruneSnapshot prune the old snapshots +func (db *DB) pruneSnapshots() { + // wait until last prune finish + db.pruneSnapshotLock.Lock() + + go func() { + defer db.pruneSnapshotLock.Unlock() + + currentVersion, err := currentVersion(db.dir) + if err != nil { + db.logger.Error("failed to read current snapshot version", "err", err) + return + } + + counter := db.snapshotKeepRecent + if err := traverseSnapshots(db.dir, false, func(version int64) (bool, error) { + if version >= currentVersion { + // ignore any newer snapshot directories, there could be ongoning snapshot rewrite. + return false, nil + } + + if counter > 0 { + counter-- + return false, nil + } + + name := snapshotName(version) + db.logger.Info("prune snapshot", "name", name) + + if err := atomicRemoveDir(filepath.Join(db.dir, name)); err != nil { + db.logger.Error("failed to prune snapshot", "err", err) + } + + return false, nil + }); err != nil { + db.logger.Error("fail to prune snapshots", "err", err) + return + } + + // truncate WAL until the earliest remaining snapshot + earliestVersion, err := firstSnapshotVersion(db.dir) + if err != nil { + db.logger.Error("failed to find first snapshot", "err", err) + } + + if err := db.wal.TruncateFront(walIndex(earliestVersion+1, db.initialVersion)); err != nil { + db.logger.Error("failed to truncate wal", "err", err, "version", earliestVersion+1) + } + }() +} + +// Commit wraps SaveVersion to bump the version and writes the pending changes into log files to persist on disk +func (db *DB) Commit() (int64, error) { + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + return 0, errReadOnly + } + + v, err := db.MultiTree.SaveVersion(true) + if err != nil { + return 0, err + } + + // write logs if enabled + if db.wal != nil { + entry := walEntry{index: walIndex(v, db.initialVersion), data: db.pendingLog} + if db.walChanSize >= 0 { + if db.walChan == nil { + db.initAsyncCommit() + } + + // async wal writing + db.walChan <- &entry + } else { + bz, err := entry.data.Marshal() + if err != nil { + return 0, err + } + if err := db.wal.Write(entry.index, bz); err != nil { + return 0, err + } + } + } + + db.pendingLog = WALEntry{} + + if err := db.checkAsyncTasks(); err != nil { + return 0, err + } + db.rewriteIfApplicable(v) + + return v, nil +} + +func (db *DB) initAsyncCommit() { + walChan := make(chan *walEntry, db.walChanSize) + walQuit := make(chan error) + + go func() { + defer close(walQuit) + + batch := wal.Batch{} + for { + entries := channelBatchRecv(walChan) + if len(entries) == 0 { + // channel is closed + break + } + + for _, entry := range entries { + bz, err := entry.data.Marshal() + if err != nil { + walQuit <- err + return + } + batch.Write(entry.index, bz) + } + + if err := db.wal.WriteBatch(&batch); err != nil { + walQuit <- err + return + } + batch.Clear() + } + }() + + db.walChan = walChan + db.walQuit = walQuit +} + +// WaitAsyncCommit waits for the completion of async commit +func (db *DB) WaitAsyncCommit() error { + db.mtx.Lock() + defer db.mtx.Unlock() + + return db.waitAsyncCommit() +} + +func (db *DB) waitAsyncCommit() error { + if db.walChan == nil { + return nil + } + + close(db.walChan) + err := <-db.walQuit + + db.walChan = nil + db.walQuit = nil + return err +} + +func (db *DB) Copy() *DB { + db.mtx.Lock() + defer db.mtx.Unlock() + + return db.copy(db.cacheSize) +} + +func (db *DB) copy(cacheSize int) *DB { + mtree := db.MultiTree.Copy(cacheSize) + + return &DB{ + MultiTree: *mtree, + logger: db.logger, + dir: db.dir, + snapshotWriterPool: db.snapshotWriterPool, + } +} + +// RewriteSnapshot writes the current version of memiavl into a snapshot, and update the `current` symlink. +func (db *DB) RewriteSnapshot() error { + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + return errReadOnly + } + + snapshotDir := snapshotName(db.lastCommitInfo.Version) + tmpDir := snapshotDir + "-tmp" + path := filepath.Join(db.dir, tmpDir) + if err := db.MultiTree.WriteSnapshot(path, db.snapshotWriterPool); err != nil { + return errors.Join(err, os.RemoveAll(path)) + } + if err := os.Rename(path, filepath.Join(db.dir, snapshotDir)); err != nil { + return err + } + return updateCurrentSymlink(db.dir, snapshotDir) +} + +func (db *DB) Reload() error { + db.mtx.Lock() + defer db.mtx.Unlock() + + return db.reload() +} + +func (db *DB) reload() error { + mtree, err := LoadMultiTree(currentPath(db.dir), db.zeroCopy, db.cacheSize) + if err != nil { + return err + } + return db.reloadMultiTree(mtree) +} + +func (db *DB) reloadMultiTree(mtree *MultiTree) error { + if err := db.MultiTree.Close(); err != nil { + return err + } + + db.MultiTree = *mtree + // catch-up the pending changes + return db.MultiTree.applyWALEntry(db.pendingLog) +} + +// rewriteIfApplicable execute the snapshot rewrite strategy according to current height +func (db *DB) rewriteIfApplicable(height int64) { + if height%int64(db.snapshotInterval) != 0 { + return + } + + if err := db.rewriteSnapshotBackground(); err != nil { + db.logger.Error("failed to rewrite snapshot in background", "err", err) + } +} + +type snapshotResult struct { + mtree *MultiTree + err error +} + +// RewriteSnapshotBackground rewrite snapshot in a background goroutine, +// `Commit` will check the complete status, and switch to the new snapshot. +func (db *DB) RewriteSnapshotBackground() error { + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + return errReadOnly + } + + return db.rewriteSnapshotBackground() +} + +func (db *DB) rewriteSnapshotBackground() error { + if db.snapshotRewriteChan != nil { + return errors.New("there's another ongoing snapshot rewriting process") + } + + ch := make(chan snapshotResult) + db.snapshotRewriteChan = ch + + cloned := db.copy(0) + wal := db.wal + go func() { + defer close(ch) + + cloned.logger.Info("start rewriting snapshot", "version", cloned.Version()) + if err := cloned.RewriteSnapshot(); err != nil { + ch <- snapshotResult{err: err} + return + } + cloned.logger.Info("finished rewriting snapshot", "version", cloned.Version()) + mtree, err := LoadMultiTree(currentPath(cloned.dir), cloned.zeroCopy, 0) + if err != nil { + ch <- snapshotResult{err: err} + return + } + + // do a best effort catch-up, will do another final catch-up in main thread. + if err := mtree.CatchupWAL(wal, 0); err != nil { + ch <- snapshotResult{err: err} + return + } + + cloned.logger.Info("finished best-effort WAL catchup", "version", cloned.Version(), "latest", mtree.Version()) + + ch <- snapshotResult{mtree: mtree} + }() + + return nil +} + +func (db *DB) Close() error { + db.mtx.Lock() + defer db.mtx.Unlock() + + errs := []error{ + db.waitAsyncCommit(), db.MultiTree.Close(), db.wal.Close(), + } + db.wal = nil + + if db.fileLock != nil { + errs = append(errs, db.fileLock.Unlock()) + db.fileLock = nil + } + + return errors.Join(errs...) +} + +// TreeByName wraps MultiTree.TreeByName to add a lock. +func (db *DB) TreeByName(name string) *Tree { + db.mtx.Lock() + defer db.mtx.Unlock() + + return db.MultiTree.TreeByName(name) +} + +// Version wraps MultiTree.Version to add a lock. +func (db *DB) Version() int64 { + db.mtx.Lock() + defer db.mtx.Unlock() + + return db.MultiTree.Version() +} + +// LastCommitInfo returns the last commit info. +func (db *DB) LastCommitInfo() *CommitInfo { + db.mtx.Lock() + defer db.mtx.Unlock() + + return db.MultiTree.LastCommitInfo() +} + +func (db *DB) SaveVersion(updateCommitInfo bool) (int64, error) { + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + return 0, errReadOnly + } + + return db.MultiTree.SaveVersion(updateCommitInfo) +} + +func (db *DB) WorkingCommitInfo() *CommitInfo { + db.mtx.Lock() + defer db.mtx.Unlock() + + return db.MultiTree.WorkingCommitInfo() +} + +// UpdateCommitInfo wraps MultiTree.UpdateCommitInfo to add a lock. +func (db *DB) UpdateCommitInfo() { + db.mtx.Lock() + defer db.mtx.Unlock() + + if db.readOnly { + panic("can't update commit info in read-only mode") + } + + db.MultiTree.UpdateCommitInfo() +} + +// WriteSnapshot wraps MultiTree.WriteSnapshot to add a lock. +func (db *DB) WriteSnapshot(dir string) error { + db.mtx.Lock() + defer db.mtx.Unlock() + + return db.MultiTree.WriteSnapshot(dir, db.snapshotWriterPool) +} + +func snapshotName(version int64) string { + return fmt.Sprintf("%s%020d", SnapshotPrefix, version) +} + +func currentPath(root string) string { + return filepath.Join(root, "current") +} + +func currentTmpPath(root string) string { + return filepath.Join(root, "current-tmp") +} + +func currentVersion(root string) (int64, error) { + name, err := os.Readlink(currentPath(root)) + if err != nil { + return 0, err + } + + version, err := parseVersion(name) + if err != nil { + return 0, err + } + + return version, nil +} + +func parseVersion(name string) (int64, error) { + if !isSnapshotName(name) { + return 0, fmt.Errorf("invalid snapshot name %s", name) + } + + v, err := strconv.ParseInt(name[len(SnapshotPrefix):], 10, 32) + if err != nil { + return 0, fmt.Errorf("snapshot version overflows: %d", err) + } + + return v, nil +} + +// seekSnapshot find the biggest snapshot version that's smaller than or equal to the target version, +// returns 0 if not found. +func seekSnapshot(root string, targetVersion uint32) (int64, error) { + var ( + snapshotVersion int64 + found bool + ) + if err := traverseSnapshots(root, false, func(version int64) (bool, error) { + if version <= int64(targetVersion) { + found = true + snapshotVersion = version + return true, nil + } + return false, nil + }); err != nil { + return 0, err + } + + if !found { + return 0, fmt.Errorf("target version is pruned: %d", targetVersion) + } + + return snapshotVersion, nil +} + +// firstSnapshotVersion returns the earliest snapshot name in the db +func firstSnapshotVersion(root string) (int64, error) { + var found int64 + if err := traverseSnapshots(root, true, func(version int64) (bool, error) { + found = version + return true, nil + }); err != nil { + return 0, err + } + + if found == 0 { + return 0, errors.New("empty memiavl db") + } + + return found, nil +} + +func walPath(root string) string { + return filepath.Join(root, "wal") +} + +// init a empty memiavl db +// +// ``` +// snapshot-0 +// +// commit_info +// +// current -> snapshot-0 +// ``` +func initEmptyDB(dir string, initialVersion uint32) error { + tmp := NewEmptyMultiTree(initialVersion, 0) + snapshotDir := snapshotName(0) + // create tmp worker pool + pool := pond.New(DefaultSnapshotWriterLimit, DefaultSnapshotWriterLimit*10) + defer pool.Stop() + + if err := tmp.WriteSnapshot(filepath.Join(dir, snapshotDir), pool); err != nil { + return err + } + return updateCurrentSymlink(dir, snapshotDir) +} + +// updateCurrentSymlink creates or replace the current symblic link atomically. +// it could fail under concurrent usage for tmp file conflicts. +func updateCurrentSymlink(dir, snapshot string) error { + tmpPath := currentTmpPath(dir) + if err := os.Symlink(snapshot, tmpPath); err != nil { + return err + } + // assuming file renaming operation is atomic + return os.Rename(tmpPath, currentPath(dir)) +} + +// traverseSnapshots traverse the snapshot list in specified order. +func traverseSnapshots(dir string, ascending bool, callback func(int64) (bool, error)) error { + entries, err := os.ReadDir(dir) + if err != nil { + return err + } + + process := func(entry os.DirEntry) (bool, error) { + if !entry.IsDir() || !isSnapshotName(entry.Name()) { + return false, nil + } + + version, err := parseVersion(entry.Name()) + if err != nil { + return true, fmt.Errorf("invalid snapshot name: %w", err) + } + + return callback(version) + } + + if ascending { + for i := 0; i < len(entries); i++ { + stop, err := process(entries[i]) + if stop || err != nil { + return err + } + } + } else { + for i := len(entries) - 1; i >= 0; i-- { + stop, err := process(entries[i]) + if stop || err != nil { + return err + } + } + } + + return nil +} + +// atomicRemoveDir is equavalent to `mv snapshot snapshot-tmp && rm -r snapshot-tmp` +func atomicRemoveDir(path string) error { + tmpPath := path + "-tmp" + if err := os.Rename(path, tmpPath); err != nil { + return err + } + + return os.RemoveAll(tmpPath) +} + +// createDBIfNotExist detects if db does not exist and try to initialize an empty one. +func createDBIfNotExist(dir string, initialVersion uint32) error { + _, err := os.Stat(filepath.Join(dir, "current", MetadataFileName)) + if err != nil && os.IsNotExist(err) { + return initEmptyDB(dir, initialVersion) + } + return nil +} + +type walEntry struct { + index uint64 + data WALEntry +} + +func isSnapshotName(name string) bool { + return strings.HasPrefix(name, SnapshotPrefix) && len(name) == SnapshotDirLen +} + +// GetLatestVersion finds the latest version number without loading the whole db, +// it's needed for upgrade module to check store upgrades, +// it returns 0 if db don't exists or is empty. +func GetLatestVersion(dir string) (int64, error) { + metadata, err := readMetadata(dir) + if err != nil { + if os.IsNotExist(err) { + return 0, nil + } + return 0, err + } + + wal, err := OpenWAL(walPath(dir), &wal.Options{NoCopy: true}) + if err != nil { + return 0, err + } + lastIndex, err := wal.LastIndex() + if err != nil { + return 0, err + } + return walVersion(lastIndex, uint32(metadata.InitialVersion)), nil +} + +func channelBatchRecv[T any](ch <-chan *T) []*T { + // block if channel is empty + item := <-ch + if item == nil { + // channel is closed + return nil + } + + remaining := len(ch) + result := make([]*T, 0, remaining+1) + result = append(result, item) + for i := 0; i < remaining; i++ { + result = append(result, <-ch) + } + + return result +} diff --git a/sc/memiavl/db/db_test.go b/sc/memiavl/db/db_test.go new file mode 100644 index 0000000..fc56916 --- /dev/null +++ b/sc/memiavl/db/db_test.go @@ -0,0 +1,508 @@ +package memiavl + +import ( + "encoding/hex" + "errors" + "os" + "path/filepath" + "runtime/debug" + "strconv" + "testing" + "time" + + "github.com/cosmos/iavl" + "github.com/stretchr/testify/require" +) + +func TestRewriteSnapshot(t *testing.T) { + db, err := Load(t.TempDir(), Options{ + CreateIfMissing: true, + InitialStores: []string{"test"}, + }) + require.NoError(t, err) + + for i, changes := range ChangeSets { + cs := []*NamedChangeSet{ + { + Name: "test", + Changeset: changes, + }, + } + t.Run(strconv.Itoa(i), func(t *testing.T) { + require.NoError(t, db.ApplyChangeSets(cs)) + v, err := db.Commit() + require.NoError(t, err) + require.Equal(t, i+1, int(v)) + require.Equal(t, RefHashes[i], db.lastCommitInfo.StoreInfos[0].CommitId.Hash) + require.NoError(t, db.RewriteSnapshot()) + require.NoError(t, db.Reload()) + }) + } +} + +func TestRemoveSnapshotDir(t *testing.T) { + dbDir := t.TempDir() + defer os.RemoveAll(dbDir) + + snapshotDir := filepath.Join(dbDir, snapshotName(0)) + tmpDir := snapshotDir + "-tmp" + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + t.Fatalf("Failed to create dummy snapshot directory: %v", err) + } + db, err := Load(dbDir, Options{ + CreateIfMissing: true, + InitialStores: []string{"test"}, + SnapshotKeepRecent: 0, + }) + require.NoError(t, err) + require.NoError(t, db.Close()) + + _, err = os.Stat(tmpDir) + require.True(t, os.IsNotExist(err), "Expected temporary snapshot directory to be deleted, but it still exists") + + err = os.MkdirAll(tmpDir, os.ModePerm) + require.NoError(t, err) + + _, err = Load(dbDir, Options{ + ReadOnly: true, + }) + require.NoError(t, err) + + _, err = os.Stat(tmpDir) + require.False(t, os.IsNotExist(err)) + + db, err = Load(dbDir, Options{}) + require.NoError(t, err) + + _, err = os.Stat(tmpDir) + require.True(t, os.IsNotExist(err)) + + require.NoError(t, db.Close()) +} + +func TestRewriteSnapshotBackground(t *testing.T) { + db, err := Load(t.TempDir(), Options{ + CreateIfMissing: true, + InitialStores: []string{"test"}, + SnapshotKeepRecent: 0, // only a single snapshot is kept + }) + require.NoError(t, err) + + for i, changes := range ChangeSets { + cs := []*NamedChangeSet{ + { + Name: "test", + Changeset: changes, + }, + } + require.NoError(t, db.ApplyChangeSets(cs)) + v, err := db.Commit() + require.NoError(t, err) + require.Equal(t, i+1, int(v)) + require.Equal(t, RefHashes[i], db.lastCommitInfo.StoreInfos[0].CommitId.Hash) + + _ = db.RewriteSnapshotBackground() + time.Sleep(time.Millisecond * 20) + } + + for db.snapshotRewriteChan != nil { + require.NoError(t, db.checkAsyncTasks()) + } + + db.pruneSnapshotLock.Lock() + defer db.pruneSnapshotLock.Unlock() + + entries, err := os.ReadDir(db.dir) + require.NoError(t, err) + + // three files: snapshot, current link, wal, LOCK + require.Equal(t, 4, len(entries)) +} + +func TestWAL(t *testing.T) { + dir := t.TempDir() + db, err := Load(dir, Options{CreateIfMissing: true, InitialStores: []string{"test", "delete"}}) + require.NoError(t, err) + + for _, changes := range ChangeSets { + cs := []*NamedChangeSet{ + { + Name: "test", + Changeset: changes, + }, + } + require.NoError(t, db.ApplyChangeSets(cs)) + _, err := db.Commit() + require.NoError(t, err) + } + + require.Equal(t, 2, len(db.lastCommitInfo.StoreInfos)) + + require.NoError(t, db.ApplyUpgrades([]*TreeNameUpgrade{ + { + Name: "newtest", + RenameFrom: "test", + }, + { + Name: "delete", + Delete: true, + }, + })) + _, err = db.Commit() + require.NoError(t, err) + + require.NoError(t, db.Close()) + + db, err = Load(dir, Options{}) + require.NoError(t, err) + + require.Equal(t, "newtest", db.lastCommitInfo.StoreInfos[0].Name) + require.Equal(t, 1, len(db.lastCommitInfo.StoreInfos)) + require.Equal(t, RefHashes[len(RefHashes)-1], db.lastCommitInfo.StoreInfos[0].CommitId.Hash) +} + +func mockNameChangeSet(name, key, value string) []*NamedChangeSet { + return []*NamedChangeSet{ + { + Name: name, + Changeset: iavl.ChangeSet{ + Pairs: mockKVPairs(key, value), + }, + }, + } +} + +// 0/1 -> v :1 +// ... +// 100 -> v: 100 +func TestInitialVersion(t *testing.T) { + name := "test" + name1 := "new" + name2 := "new2" + key := "hello" + value := "world" + for _, initialVersion := range []int64{0, 1, 100} { + dir := t.TempDir() + db, err := Load(dir, Options{CreateIfMissing: true, InitialStores: []string{name}}) + require.NoError(t, err) + db.SetInitialVersion(initialVersion) + require.NoError(t, db.ApplyChangeSets(mockNameChangeSet(name, key, value))) + v, err := db.Commit() + require.NoError(t, err) + if initialVersion <= 1 { + require.Equal(t, int64(1), v) + } else { + require.Equal(t, initialVersion, v) + } + hash := db.LastCommitInfo().StoreInfos[0].CommitId.Hash + require.Equal(t, "6032661ab0d201132db7a8fa1da6a0afe427e6278bd122c301197680ab79ca02", hex.EncodeToString(hash)) + require.NoError(t, db.ApplyChangeSets(mockNameChangeSet(name, key, "world1"))) + v, err = db.Commit() + require.NoError(t, err) + hash = db.LastCommitInfo().StoreInfos[0].CommitId.Hash + if initialVersion <= 1 { + require.Equal(t, int64(2), v) + require.Equal(t, "ef0530f9bf1af56c19a3bac32a3ec4f76a6fefaacb2efd4027a2cf37240f60bb", hex.EncodeToString(hash)) + } else { + require.Equal(t, initialVersion+1, v) + require.Equal(t, "a719e7d699d42ea8e5637ec84675a2c28f14a00a71fb518f20aa2c395673a3b8", hex.EncodeToString(hash)) + } + require.NoError(t, db.Close()) + + db, err = Load(dir, Options{}) + require.NoError(t, err) + require.Equal(t, uint32(initialVersion), db.initialVersion) + require.Equal(t, v, db.Version()) + require.Equal(t, hex.EncodeToString(hash), hex.EncodeToString(db.LastCommitInfo().StoreInfos[0].CommitId.Hash)) + + db.ApplyUpgrades([]*TreeNameUpgrade{{Name: name1}}) + require.NoError(t, db.ApplyChangeSets(mockNameChangeSet(name1, key, value))) + v, err = db.Commit() + require.NoError(t, err) + if initialVersion <= 1 { + require.Equal(t, int64(3), v) + } else { + require.Equal(t, initialVersion+2, v) + } + require.Equal(t, 2, len(db.lastCommitInfo.StoreInfos)) + info := db.lastCommitInfo.StoreInfos[0] + require.Equal(t, name1, info.Name) + require.Equal(t, v, info.CommitId.Version) + require.Equal(t, "6032661ab0d201132db7a8fa1da6a0afe427e6278bd122c301197680ab79ca02", hex.EncodeToString(info.CommitId.Hash)) + // the nodes are created with version 1, which is compatible with iavl behavior: https://github.com/cosmos/iavl/pull/660 + require.Equal(t, info.CommitId.Hash, HashNode(newLeafNode([]byte(key), []byte(value), 1))) + + require.NoError(t, db.RewriteSnapshot()) + require.NoError(t, db.Reload()) + + db.ApplyUpgrades([]*TreeNameUpgrade{{Name: name2}}) + require.NoError(t, db.ApplyChangeSets(mockNameChangeSet(name2, key, value))) + v, err = db.Commit() + require.NoError(t, err) + if initialVersion <= 1 { + require.Equal(t, int64(4), v) + } else { + require.Equal(t, initialVersion+3, v) + } + require.Equal(t, 3, len(db.lastCommitInfo.StoreInfos)) + info2 := db.lastCommitInfo.StoreInfos[1] + require.Equal(t, name2, info2.Name) + require.Equal(t, v, info2.CommitId.Version) + require.Equal(t, hex.EncodeToString(info.CommitId.Hash), hex.EncodeToString(info2.CommitId.Hash)) + } +} + +func TestLoadVersion(t *testing.T) { + dir := t.TempDir() + db, err := Load(dir, Options{ + CreateIfMissing: true, + InitialStores: []string{"test"}, + }) + require.NoError(t, err) + + for i, changes := range ChangeSets { + cs := []*NamedChangeSet{ + { + Name: "test", + Changeset: changes, + }, + } + t.Run(strconv.Itoa(i), func(t *testing.T) { + require.NoError(t, db.ApplyChangeSets(cs)) + + // check the root hash + require.Equal(t, RefHashes[db.Version()], db.WorkingCommitInfo().StoreInfos[0].CommitId.Hash) + + _, err := db.Commit() + require.NoError(t, err) + }) + } + require.NoError(t, db.Close()) + + for v, expItems := range ExpectItems { + if v == 0 { + continue + } + tmp, err := Load(dir, Options{ + TargetVersion: uint32(v), + ReadOnly: true, + }) + require.NoError(t, err) + require.Equal(t, RefHashes[v-1], tmp.TreeByName("test").RootHash()) + require.Equal(t, expItems, collectIter(tmp.TreeByName("test").Iterator(nil, nil, true))) + } +} + +func TestZeroCopy(t *testing.T) { + db, err := Load(t.TempDir(), Options{InitialStores: []string{"test", "test2"}, CreateIfMissing: true, ZeroCopy: true}) + require.NoError(t, err) + require.NoError(t, db.ApplyChangeSets([]*NamedChangeSet{ + {Name: "test", Changeset: ChangeSets[0]}, + })) + _, err = db.Commit() + require.NoError(t, err) + require.NoError(t, errors.Join( + db.RewriteSnapshot(), + db.Reload(), + )) + + // the test tree's root hash will reference the zero-copy value + require.NoError(t, db.ApplyChangeSets([]*NamedChangeSet{ + {Name: "test2", Changeset: ChangeSets[0]}, + })) + _, err = db.Commit() + require.NoError(t, err) + + commitInfo := *db.LastCommitInfo() + + value := db.TreeByName("test").Get([]byte("hello")) + require.Equal(t, []byte("world"), value) + + db.SetZeroCopy(false) + valueCloned := db.TreeByName("test").Get([]byte("hello")) + require.Equal(t, []byte("world"), valueCloned) + + _ = commitInfo.StoreInfos[0].CommitId.Hash[0] + + require.NoError(t, db.Close()) + + require.Equal(t, []byte("world"), valueCloned) + + // accessing the zero-copy value after the db is closed triggers segment fault. + // reset global panic on fault setting after function finished + defer debug.SetPanicOnFault(debug.SetPanicOnFault(true)) + require.Panics(t, func() { + require.Equal(t, []byte("world"), value) + }) + + // it's ok to access after db closed + _ = commitInfo.StoreInfos[0].CommitId.Hash[0] +} + +func TestWalIndexConversion(t *testing.T) { + testCases := []struct { + index uint64 + version int64 + initialVersion uint32 + }{ + {1, 1, 0}, + {1, 1, 1}, + {1, 10, 10}, + {2, 11, 10}, + } + for _, tc := range testCases { + require.Equal(t, tc.index, walIndex(tc.version, tc.initialVersion)) + require.Equal(t, tc.version, walVersion(tc.index, tc.initialVersion)) + } +} + +func TestEmptyValue(t *testing.T) { + dir := t.TempDir() + db, err := Load(dir, Options{InitialStores: []string{"test"}, CreateIfMissing: true, ZeroCopy: true}) + require.NoError(t, err) + + require.NoError(t, db.ApplyChangeSets([]*NamedChangeSet{ + {Name: "test", Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello1"), Value: []byte("")}, + {Key: []byte("hello2"), Value: []byte("")}, + {Key: []byte("hello3"), Value: []byte("")}, + }, + }}, + })) + _, err = db.Commit() + require.NoError(t, err) + + require.NoError(t, db.ApplyChangeSets([]*NamedChangeSet{ + {Name: "test", Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{{Key: []byte("hello1"), Delete: true}}, + }}, + })) + version, err := db.Commit() + require.NoError(t, err) + + require.NoError(t, db.Close()) + + db, err = Load(dir, Options{ZeroCopy: true}) + require.NoError(t, err) + require.Equal(t, version, db.Version()) +} + +func TestInvalidOptions(t *testing.T) { + dir := t.TempDir() + + _, err := Load(dir, Options{ReadOnly: true}) + require.Error(t, err) + + _, err = Load(dir, Options{ReadOnly: true, CreateIfMissing: true}) + require.Error(t, err) + + db, err := Load(dir, Options{CreateIfMissing: true}) + require.NoError(t, err) + require.NoError(t, db.Close()) + + _, err = Load(dir, Options{LoadForOverwriting: true, ReadOnly: true}) + require.Error(t, err) + + _, err = Load(dir, Options{ReadOnly: true}) + require.NoError(t, err) +} + +func TestExclusiveLock(t *testing.T) { + dir := t.TempDir() + + db, err := Load(dir, Options{CreateIfMissing: true}) + require.NoError(t, err) + + _, err = Load(dir, Options{}) + require.Error(t, err) + + _, err = Load(dir, Options{ReadOnly: true}) + require.NoError(t, err) + + require.NoError(t, db.Close()) + + _, err = Load(dir, Options{}) + require.NoError(t, err) +} + +func TestFastCommit(t *testing.T) { + dir := t.TempDir() + + db, err := Load(dir, Options{CreateIfMissing: true, InitialStores: []string{"test"}, SnapshotInterval: 3, AsyncCommitBuffer: 10}) + require.NoError(t, err) + + cs := iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello1"), Value: make([]byte, 1024*1024)}, + }, + } + + // the bug reproduce when the wal writing is slower than commit, that happens when wal segment is full and create a new one, the wal writing will slow down a little bit, + // segment size is 20m, each change set is 1m, so we need a bit more than 20 commits to reproduce. + for i := 0; i < 30; i++ { + require.NoError(t, db.ApplyChangeSets([]*NamedChangeSet{{Name: "test", Changeset: cs}})) + _, err := db.Commit() + require.NoError(t, err) + } + + <-db.snapshotRewriteChan + require.NoError(t, db.Close()) +} + +func TestRepeatedApplyChangeSet(t *testing.T) { + db, err := Load(t.TempDir(), Options{CreateIfMissing: true, InitialStores: []string{"test1", "test2"}, SnapshotInterval: 3, AsyncCommitBuffer: 10}) + require.NoError(t, err) + + err = db.ApplyChangeSets([]*NamedChangeSet{ + {Name: "test1", Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello1"), Value: []byte("world1")}, + }, + }}, + {Name: "test2", Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello2"), Value: []byte("world2")}, + }, + }}, + }) + require.NoError(t, err) + + err = db.ApplyChangeSets([]*NamedChangeSet{{Name: "test1"}}) + require.Error(t, err) + err = db.ApplyChangeSet("test1", iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello2"), Value: []byte("world2")}, + }, + }) + require.Error(t, err) + + _, err = db.Commit() + require.NoError(t, err) + + err = db.ApplyChangeSet("test1", iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello2"), Value: []byte("world2")}, + }, + }) + require.NoError(t, err) + err = db.ApplyChangeSet("test2", iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello2"), Value: []byte("world2")}, + }, + }) + require.NoError(t, err) + + err = db.ApplyChangeSet("test1", iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello2"), Value: []byte("world2")}, + }, + }) + require.Error(t, err) + err = db.ApplyChangeSet("test2", iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("hello2"), Value: []byte("world2")}, + }, + }) + require.Error(t, err) +} diff --git a/sc/memiavl/db/export.go b/sc/memiavl/db/export.go new file mode 100644 index 0000000..ec54185 --- /dev/null +++ b/sc/memiavl/db/export.go @@ -0,0 +1,146 @@ +package memiavl + +import ( + "context" + "errors" + "fmt" + "path/filepath" +) + +// ErrorExportDone is returned by Exporter.Next() when all items have been exported. +var ErrorExportDone = errors.New("export is complete") + +// exportBufferSize is the number of nodes to buffer in the exporter. It improves throughput by +// processing multiple nodes per context switch, but take care to avoid excessive memory usage, +// especially since callers may export several IAVL stores in parallel (e.g. the Cosmos SDK). +const exportBufferSize = 32 + +type MultiTreeExporter struct { + // only one of them is non-nil + db *DB + mtree *MultiTree + + iTree int + exporter *Exporter +} + +func NewMultiTreeExporter(dir string, version uint32, supportExportNonSnapshotVersion bool) (exporter *MultiTreeExporter, err error) { + var ( + db *DB + mtree *MultiTree + ) + if supportExportNonSnapshotVersion { + db, err = Load(dir, Options{ + TargetVersion: version, + ZeroCopy: true, + ReadOnly: true, + SnapshotWriterLimit: DefaultSnapshotWriterLimit, + }) + if err != nil { + return nil, fmt.Errorf("invalid height: %d, %w", version, err) + } + } else { + curVersion, err := currentVersion(dir) + if err != nil { + return nil, fmt.Errorf("failed to load current version: %w", err) + } + if int64(version) > curVersion { + return nil, fmt.Errorf("snapshot is not created yet: height: %d", version) + } + mtree, err = LoadMultiTree(filepath.Join(dir, snapshotName(int64(version))), true, 0) + if err != nil { + return nil, fmt.Errorf("snapshot don't exists: height: %d, %w", version, err) + } + } + + return &MultiTreeExporter{ + db: db, + mtree: mtree, + }, nil +} + +func (mte *MultiTreeExporter) trees() []NamedTree { + if mte.db != nil { + return mte.db.trees + } + return mte.mtree.trees +} + +func (mte *MultiTreeExporter) Next() (interface{}, error) { + if mte.exporter != nil { + node, err := mte.exporter.Next() + if err != nil { + if err == ErrorExportDone { + mte.exporter.Close() + mte.exporter = nil + return mte.Next() + } + return nil, err + } + return node, nil + } + + trees := mte.trees() + if mte.iTree >= len(trees) { + return nil, ErrorExportDone + } + + tree := trees[mte.iTree] + mte.exporter = tree.Export() + mte.iTree++ + return tree.Name, nil +} + +func (mte *MultiTreeExporter) Close() error { + if mte.exporter != nil { + mte.exporter.Close() + mte.exporter = nil + } + + if mte.db != nil { + return mte.db.Close() + } + if mte.mtree != nil { + return mte.mtree.Close() + } + + return nil +} + +type exportWorker func(callback func(*ExportNode) bool) + +type Exporter struct { + ch <-chan *ExportNode + cancel context.CancelFunc +} + +func newExporter(worker exportWorker) *Exporter { + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan *ExportNode, exportBufferSize) + go func() { + defer close(ch) + worker(func(enode *ExportNode) bool { + select { + case ch <- enode: + case <-ctx.Done(): + return true + } + return false + }) + }() + return &Exporter{ch, cancel} +} + +func (e *Exporter) Next() (*ExportNode, error) { + if exportNode, ok := <-e.ch; ok { + return exportNode, nil + } + return nil, ErrorExportDone +} + +// Close closes the exporter. It is safe to call multiple times. +func (e *Exporter) Close() { + e.cancel() + for range e.ch { // drain channel + } +} diff --git a/sc/memiavl/db/filelock.go b/sc/memiavl/db/filelock.go new file mode 100644 index 0000000..2ee4065 --- /dev/null +++ b/sc/memiavl/db/filelock.go @@ -0,0 +1,27 @@ +package memiavl + +import ( + "path/filepath" + + "github.com/zbiljic/go-filelock" +) + +type FileLock interface { + Unlock() error +} + +func LockFile(fname string) (FileLock, error) { + path, err := filepath.Abs(fname) + if err != nil { + return nil, err + } + fl, err := filelock.New(path) + if err != nil { + return nil, err + } + if _, err := fl.TryLock(); err != nil { + return nil, err + } + + return fl, nil +} diff --git a/sc/memiavl/db/import.go b/sc/memiavl/db/import.go new file mode 100644 index 0000000..a64be6d --- /dev/null +++ b/sc/memiavl/db/import.go @@ -0,0 +1,260 @@ +package memiavl + +import ( + "errors" + "fmt" + "math" + "os" + "path/filepath" +) + +type MultiTreeImporter struct { + dir string + snapshotDir string + height int64 + importer *TreeImporter + fileLock FileLock +} + +func NewMultiTreeImporter(dir string, height uint64) (*MultiTreeImporter, error) { + if height > math.MaxUint32 { + return nil, fmt.Errorf("version overflows uint32: %d", height) + } + + var fileLock FileLock + fileLock, err := LockFile(filepath.Join(dir, LockFileName)) + if err != nil { + return nil, fmt.Errorf("fail to lock db: %w", err) + } + + return &MultiTreeImporter{ + dir: dir, + height: int64(height), + snapshotDir: snapshotName(int64(height)), + fileLock: fileLock, + }, nil +} + +func (mti *MultiTreeImporter) tmpDir() string { + return filepath.Join(mti.dir, mti.snapshotDir+"-tmp") +} + +func (mti *MultiTreeImporter) Add(item interface{}) error { + switch item := item.(type) { + case *ExportNode: + mti.AddNode(item) + return nil + case string: + return mti.AddTree(item) + default: + return fmt.Errorf("unknown item type: %T", item) + } +} + +func (mti *MultiTreeImporter) AddTree(name string) error { + if mti.importer != nil { + if err := mti.importer.Close(); err != nil { + return err + } + } + mti.importer = NewTreeImporter(filepath.Join(mti.tmpDir(), name), mti.height) + return nil +} + +func (mti *MultiTreeImporter) AddNode(node *ExportNode) { + mti.importer.Add(node) +} + +func (mti *MultiTreeImporter) Finalize() error { + if mti.importer != nil { + if err := mti.importer.Close(); err != nil { + return err + } + mti.importer = nil + } + + tmpDir := mti.tmpDir() + if err := updateMetadataFile(tmpDir, mti.height); err != nil { + return err + } + + if err := os.Rename(tmpDir, filepath.Join(mti.dir, mti.snapshotDir)); err != nil { + return err + } + + return updateCurrentSymlink(mti.dir, mti.snapshotDir) +} + +func (mti *MultiTreeImporter) Close() error { + var err error + if mti.importer != nil { + err = mti.importer.Close() + } + return errors.Join(err, mti.fileLock.Unlock()) +} + +// TreeImporter import a single memiavl tree from state-sync snapshot +type TreeImporter struct { + nodesChan chan *ExportNode + quitChan chan error +} + +func NewTreeImporter(dir string, version int64) *TreeImporter { + nodesChan := make(chan *ExportNode) + quitChan := make(chan error) + go func() { + defer close(quitChan) + quitChan <- doImport(dir, version, nodesChan) + }() + return &TreeImporter{nodesChan, quitChan} +} + +func (ai *TreeImporter) Add(node *ExportNode) { + ai.nodesChan <- node +} + +func (ai *TreeImporter) Close() error { + var err error + // tolerate double close + if ai.nodesChan != nil { + close(ai.nodesChan) + err = <-ai.quitChan + } + ai.nodesChan = nil + ai.quitChan = nil + return err +} + +// doImport a stream of `ExportNode`s into a new snapshot. +func doImport(dir string, version int64, nodes <-chan *ExportNode) (returnErr error) { + if version > int64(math.MaxUint32) { + return errors.New("version overflows uint32") + } + + return writeSnapshot(dir, uint32(version), func(w *snapshotWriter) (uint32, error) { + i := &importer{ + snapshotWriter: *w, + } + + for node := range nodes { + if err := i.Add(node); err != nil { + return 0, err + } + } + + switch len(i.leavesStack) { + case 0: + return 0, nil + case 1: + return i.leafCounter, nil + default: + return 0, fmt.Errorf("invalid node structure, found stack size %v after imported", len(i.leavesStack)) + } + }) +} + +type importer struct { + snapshotWriter + + // keep track of how many leaves has been written before the pending nodes + leavesStack []uint32 + // keep track of the pending nodes + nodeStack []*MemNode +} + +func (i *importer) Add(n *ExportNode) error { + if n.Version > int64(math.MaxUint32) { + return errors.New("version overflows uint32") + } + + if n.Height == 0 { + node := &MemNode{ + height: 0, + size: 1, + version: uint32(n.Version), + key: n.Key, + value: n.Value, + } + nodeHash := node.Hash() + if err := i.writeLeaf(node.version, node.key, node.value, nodeHash); err != nil { + return err + } + i.leavesStack = append(i.leavesStack, i.leafCounter) + i.nodeStack = append(i.nodeStack, node) + return nil + } + + // branch node + keyLeaf := i.leavesStack[len(i.leavesStack)-2] + leftNode := i.nodeStack[len(i.nodeStack)-2] + rightNode := i.nodeStack[len(i.nodeStack)-1] + + node := &MemNode{ + height: uint8(n.Height), + size: leftNode.size + rightNode.size, + version: uint32(n.Version), + key: n.Key, + left: leftNode, + right: rightNode, + } + nodeHash := node.Hash() + + // remove unnecessary reference to avoid memory leak + node.left = nil + node.right = nil + + preTrees := uint8(len(i.nodeStack) - 2) + if err := i.writeBranch(node.version, uint32(node.size), node.height, preTrees, keyLeaf, nodeHash); err != nil { + return err + } + + i.leavesStack = i.leavesStack[:len(i.leavesStack)-2] + i.leavesStack = append(i.leavesStack, i.leafCounter) + + i.nodeStack = i.nodeStack[:len(i.nodeStack)-2] + i.nodeStack = append(i.nodeStack, node) + return nil +} + +func updateMetadataFile(dir string, height int64) (returnErr error) { + entries, err := os.ReadDir(dir) + if err != nil { + return err + } + storeInfos := make([]StoreInfo, 0, len(entries)) + for _, e := range entries { + if !e.IsDir() { + continue + } + name := e.Name() + snapshot, err := OpenSnapshot(filepath.Join(dir, name)) + if err != nil { + return err + } + defer func() { + if err := snapshot.Close(); returnErr == nil { + returnErr = err + } + }() + storeInfos = append(storeInfos, StoreInfo{ + Name: name, + CommitId: CommitID{ + Version: height, + Hash: snapshot.RootHash(), + }, + }) + } + metadata := MultiTreeMetadata{ + CommitInfo: &CommitInfo{ + Version: height, + StoreInfos: storeInfos, + }, + // initial version should correspond to the first wal entry + InitialVersion: height + 1, + } + bz, err := metadata.Marshal() + if err != nil { + return err + } + return WriteFileSync(filepath.Join(dir, MetadataFileName), bz) +} diff --git a/sc/memiavl/db/iterator.go b/sc/memiavl/db/iterator.go new file mode 100644 index 0000000..d1aa0b0 --- /dev/null +++ b/sc/memiavl/db/iterator.go @@ -0,0 +1,114 @@ +package memiavl + +import "bytes" + +type Iterator struct { + // domain of iteration, end is exclusive + start, end []byte + ascending bool + zeroCopy bool + + // cache the next key-value pair + key, value []byte + + valid bool + + stack []Node +} + +func NewIterator(start, end []byte, ascending bool, root Node, zeroCopy bool) *Iterator { + iter := &Iterator{ + start: start, + end: end, + ascending: ascending, + valid: true, + zeroCopy: zeroCopy, + } + + if root != nil { + iter.stack = []Node{root} + } + + // cache the first key-value + iter.Next() + return iter +} + +func (iter *Iterator) Domain() ([]byte, []byte) { + return iter.start, iter.end +} + +// Valid implements dbm.Iterator. +func (iter *Iterator) Valid() bool { + return iter.valid +} + +// Error implements dbm.Iterator +func (iter *Iterator) Error() error { + return nil +} + +// Key implements dbm.Iterator +func (iter *Iterator) Key() []byte { + if !iter.zeroCopy { + return bytes.Clone(iter.key) + } + return iter.key +} + +// Value implements dbm.Iterator +func (iter *Iterator) Value() []byte { + if !iter.zeroCopy { + return bytes.Clone(iter.value) + } + return iter.value +} + +// Next implements dbm.Iterator +func (iter *Iterator) Next() { + for len(iter.stack) > 0 { + // pop node + node := iter.stack[len(iter.stack)-1] + iter.stack = iter.stack[:len(iter.stack)-1] + + key := node.Key() + startCmp := bytes.Compare(iter.start, key) + afterStart := iter.start == nil || startCmp < 0 + beforeEnd := iter.end == nil || bytes.Compare(key, iter.end) < 0 + + if node.IsLeaf() { + startOrAfter := afterStart || startCmp == 0 + if startOrAfter && beforeEnd { + iter.key = key + iter.value = node.Value() + return + } + } else { + // push children to stack + if iter.ascending { + if beforeEnd { + iter.stack = append(iter.stack, node.Right()) + } + if afterStart { + iter.stack = append(iter.stack, node.Left()) + } + } else { + if afterStart { + iter.stack = append(iter.stack, node.Left()) + } + if beforeEnd { + iter.stack = append(iter.stack, node.Right()) + } + } + } + } + + iter.valid = false +} + +// Close implements dbm.Iterator +func (iter *Iterator) Close() error { + iter.valid = false + iter.stack = nil + return nil +} diff --git a/sc/memiavl/db/iterator_test.go b/sc/memiavl/db/iterator_test.go new file mode 100644 index 0000000..4b20475 --- /dev/null +++ b/sc/memiavl/db/iterator_test.go @@ -0,0 +1,60 @@ +package memiavl + +import ( + "testing" + + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" +) + +func TestIterator(t *testing.T) { + tree := New(0) + require.Equal(t, ExpectItems[0], collectIter(tree.Iterator(nil, nil, true))) + + for _, changes := range ChangeSets { + tree.ApplyChangeSet(changes) + _, v, err := tree.SaveVersion(true) + require.NoError(t, err) + require.Equal(t, ExpectItems[v], collectIter(tree.Iterator(nil, nil, true))) + require.Equal(t, reverse(ExpectItems[v]), collectIter(tree.Iterator(nil, nil, false))) + } +} + +func TestIteratorRange(t *testing.T) { + tree := New(0) + for _, changes := range ChangeSets[:6] { + tree.ApplyChangeSet(changes) + _, _, err := tree.SaveVersion(true) + require.NoError(t, err) + } + + expItems := []pair{ + {[]byte("aello05"), []byte("world1")}, + {[]byte("aello06"), []byte("world1")}, + {[]byte("aello07"), []byte("world1")}, + {[]byte("aello08"), []byte("world1")}, + {[]byte("aello09"), []byte("world1")}, + } + require.Equal(t, expItems, collectIter(tree.Iterator([]byte("aello05"), []byte("aello10"), true))) + require.Equal(t, reverse(expItems), collectIter(tree.Iterator([]byte("aello05"), []byte("aello10"), false))) +} + +type pair struct { + key, value []byte +} + +func collectIter(iter dbm.Iterator) []pair { + result := []pair{} + for ; iter.Valid(); iter.Next() { + result = append(result, pair{key: iter.Key(), value: iter.Value()}) + } + return result +} + +func reverse[S ~[]E, E any](s S) S { + r := make(S, len(s)) + for i, j := 0, len(s)-1; i <= j; i, j = i+1, j-1 { + r[i], r[j] = s[j], s[i] + } + return r +} diff --git a/sc/memiavl/db/layout_little_endian.go b/sc/memiavl/db/layout_little_endian.go new file mode 100644 index 0000000..51c0f3e --- /dev/null +++ b/sc/memiavl/db/layout_little_endian.go @@ -0,0 +1,85 @@ +//go:build !nativebyteorder +// +build !nativebyteorder + +package memiavl + +import ( + "encoding/binary" +) + +// Nodes is a continuously stored IAVL nodes +type Nodes struct { + data []byte +} + +func NewNodes(data []byte) (Nodes, error) { + return Nodes{data}, nil +} + +func (nodes Nodes) Node(i uint32) NodeLayout { + offset := int(i) * SizeNode + return NodeLayout{data: (*[SizeNode]byte)(nodes.data[offset : offset+SizeNode])} +} + +// see comment of `PersistedNode` +type NodeLayout struct { + data *[SizeNode]byte +} + +func (node NodeLayout) Height() uint8 { + return node.data[OffsetHeight] +} + +func (node NodeLayout) PreTrees() uint8 { + return node.data[OffsetPreTrees] +} + +func (node NodeLayout) Version() uint32 { + return binary.LittleEndian.Uint32(node.data[OffsetVersion : OffsetVersion+4]) +} + +func (node NodeLayout) Size() uint32 { + return binary.LittleEndian.Uint32(node.data[OffsetSize : OffsetSize+4]) +} + +func (node NodeLayout) KeyLeaf() uint32 { + return binary.LittleEndian.Uint32(node.data[OffsetKeyLeaf : OffsetKeyLeaf+4]) +} + +func (node NodeLayout) Hash() []byte { + return node.data[OffsetHash : OffsetHash+SizeHash] +} + +// Leaves is a continuously stored IAVL nodes +type Leaves struct { + data []byte +} + +func NewLeaves(data []byte) (Leaves, error) { + return Leaves{data}, nil +} + +func (leaves Leaves) Leaf(i uint32) LeafLayout { + offset := int(i) * SizeLeaf + return LeafLayout{data: (*[SizeLeaf]byte)(leaves.data[offset : offset+SizeLeaf])} +} + +type LeafLayout struct { + data *[SizeLeaf]byte +} + +func (leaf LeafLayout) Version() uint32 { + return binary.LittleEndian.Uint32(leaf.data[OffsetLeafVersion : OffsetLeafVersion+4]) +} + +func (leaf LeafLayout) KeyLength() uint32 { + return binary.LittleEndian.Uint32(leaf.data[OffsetLeafKeyLen : OffsetLeafKeyLen+4]) +} + +func (leaf LeafLayout) KeyOffset() uint64 { + return binary.LittleEndian.Uint64(leaf.data[OffsetLeafKeyOffset : OffsetLeafKeyOffset+8]) +} + +func (leaf LeafLayout) Hash() []byte { + return leaf.data[OffsetLeafHash : OffsetLeafHash+32] +} diff --git a/sc/memiavl/db/layout_native.go b/sc/memiavl/db/layout_native.go new file mode 100644 index 0000000..34b6134 --- /dev/null +++ b/sc/memiavl/db/layout_native.go @@ -0,0 +1,125 @@ +//go:build nativebyteorder +// +build nativebyteorder + +package memiavl + +import ( + "errors" + "unsafe" +) + +func init() { + buf := [2]byte{} + *(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xABCD) + + if buf != [2]byte{0xCD, 0xAB} { + panic("native byte order is not little endian, please build without nativebyteorder") + } +} + +type NodeLayout = *nodeLayout + +// Nodes is a continuously stored IAVL nodes +type Nodes struct { + nodes []nodeLayout +} + +func NewNodes(buf []byte) (Nodes, error) { + // check alignment and size of the buffer + p := unsafe.Pointer(unsafe.SliceData(buf)) + if uintptr(p)%unsafe.Alignof(nodeLayout{}) != 0 { + return Nodes{}, errors.New("input buffer is not aligned") + } + size := int(unsafe.Sizeof(nodeLayout{})) + if len(buf)%size != 0 { + return Nodes{}, errors.New("input buffer length is not correct") + } + nodes := unsafe.Slice((*nodeLayout)(p), len(buf)/size) + return Nodes{nodes}, nil +} + +func (nodes Nodes) Node(i uint32) NodeLayout { + return &nodes.nodes[i] +} + +// see comment of `PersistedNode` +type nodeLayout struct { + data [4]uint32 + hash [32]byte +} + +func (node *nodeLayout) Height() uint8 { + return uint8(node.data[0]) +} + +func (node NodeLayout) PreTrees() uint8 { + return uint8(node.data[0] >> 8) +} + +func (node *nodeLayout) Version() uint32 { + return node.data[1] +} + +func (node *nodeLayout) Size() uint32 { + return node.data[2] +} + +func (node *nodeLayout) KeyLeaf() uint32 { + return node.data[3] +} + +func (node *nodeLayout) KeyOffset() uint64 { + return uint64(node.data[2]) | uint64(node.data[3])<<32 +} + +func (node *nodeLayout) Hash() []byte { + return node.hash[:] +} + +type LeafLayout = *leafLayout + +// Nodes is a continuously stored IAVL nodes +type Leaves struct { + leaves []leafLayout +} + +func NewLeaves(buf []byte) (Leaves, error) { + // check alignment and size of the buffer + p := unsafe.Pointer(unsafe.SliceData(buf)) + if uintptr(p)%unsafe.Alignof(leafLayout{}) != 0 { + return Leaves{}, errors.New("input buffer is not aligned") + } + size := int(unsafe.Sizeof(leafLayout{})) + if len(buf)%size != 0 { + return Leaves{}, errors.New("input buffer length is not correct") + } + leaves := unsafe.Slice((*leafLayout)(p), len(buf)/size) + return Leaves{leaves}, nil +} + +func (leaves Leaves) Leaf(i uint32) LeafLayout { + return &leaves.leaves[i] +} + +type leafLayout struct { + version uint32 + keyLen uint32 + keyOffset uint64 + hash [32]byte +} + +func (leaf *leafLayout) Version() uint32 { + return leaf.version +} + +func (leaf *leafLayout) KeyLength() uint32 { + return leaf.keyLen +} + +func (leaf *leafLayout) KeyOffset() uint64 { + return leaf.keyOffset +} + +func (leaf *leafLayout) Hash() []byte { + return leaf.hash[:] +} diff --git a/sc/memiavl/db/mem_node.go b/sc/memiavl/db/mem_node.go new file mode 100644 index 0000000..f56e345 --- /dev/null +++ b/sc/memiavl/db/mem_node.go @@ -0,0 +1,218 @@ +package memiavl + +import ( + "bytes" + "encoding/binary" + "io" +) + +type MemNode struct { + height uint8 + size int64 + version uint32 + key []byte + value []byte + left Node + right Node + + hash []byte +} + +var _ Node = (*MemNode)(nil) + +func newLeafNode(key, value []byte, version uint32) *MemNode { + return &MemNode{ + key: key, value: value, version: version, size: 1, + } +} + +func (node *MemNode) Height() uint8 { + return node.height +} + +func (node *MemNode) IsLeaf() bool { + return node.height == 0 +} + +func (node *MemNode) Size() int64 { + return node.size +} + +func (node *MemNode) Version() uint32 { + return node.version +} + +func (node *MemNode) Key() []byte { + return node.key +} + +func (node *MemNode) Value() []byte { + return node.value +} + +func (node *MemNode) Left() Node { + return node.left +} + +func (node *MemNode) Right() Node { + return node.right +} + +// Mutate clones the node if it's version is smaller than or equal to cowVersion, otherwise modify in-place +func (node *MemNode) Mutate(version, cowVersion uint32) *MemNode { + n := node + if node.version <= cowVersion { + cloned := *node + n = &cloned + } + n.version = version + n.hash = nil + return n +} + +func (node *MemNode) SafeHash() []byte { + return node.Hash() +} + +// Computes the hash of the node without computing its descendants. Must be +// called on nodes which have descendant node hashes already computed. +func (node *MemNode) Hash() []byte { + if node == nil { + return nil + } + if node.hash != nil { + return node.hash + } + node.hash = HashNode(node) + return node.hash +} + +func (node *MemNode) updateHeightSize() { + node.height = maxUInt8(node.left.Height(), node.right.Height()) + 1 + node.size = node.left.Size() + node.right.Size() +} + +func (node *MemNode) calcBalance() int { + return int(node.left.Height()) - int(node.right.Height()) +} + +func calcBalance(node Node) int { + return int(node.Left().Height()) - int(node.Right().Height()) +} + +// Invariant: node is returned by `Mutate(version)`. +// +// S L +// / \ => / \ +// L S +// / \ / \ +// LR LR +func (node *MemNode) rotateRight(version, cowVersion uint32) *MemNode { + newSelf := node.left.Mutate(version, cowVersion) + node.left = node.left.Right() + newSelf.right = node + node.updateHeightSize() + newSelf.updateHeightSize() + return newSelf +} + +// Invariant: node is returned by `Mutate(version, cowVersion)`. +// +// S R +// / \ => / \ +// R S +// / \ / \ +// RL RL +func (node *MemNode) rotateLeft(version, cowVersion uint32) *MemNode { + newSelf := node.right.Mutate(version, cowVersion) + node.right = node.right.Left() + newSelf.left = node + node.updateHeightSize() + newSelf.updateHeightSize() + return newSelf +} + +// Invariant: node is returned by `Mutate(version, cowVersion)`. +func (node *MemNode) reBalance(version, cowVersion uint32) *MemNode { + balance := node.calcBalance() + switch { + case balance > 1: + leftBalance := calcBalance(node.left) + if leftBalance >= 0 { + // left left + return node.rotateRight(version, cowVersion) + } + // left right + node.left = node.left.Mutate(version, cowVersion).rotateLeft(version, cowVersion) + return node.rotateRight(version, cowVersion) + case balance < -1: + rightBalance := calcBalance(node.right) + if rightBalance <= 0 { + // right right + return node.rotateLeft(version, cowVersion) + } + // right left + node.right = node.right.Mutate(version, cowVersion).rotateRight(version, cowVersion) + return node.rotateLeft(version, cowVersion) + default: + // nothing changed + return node + } +} + +func (node *MemNode) Get(key []byte) ([]byte, uint32) { + if node.IsLeaf() { + switch bytes.Compare(node.key, key) { + case -1: + return nil, 1 + case 1: + return nil, 0 + default: + return node.value, 0 + } + } + + if bytes.Compare(key, node.key) == -1 { + return node.Left().Get(key) + } + right := node.Right() + value, index := right.Get(key) + return value, index + uint32(node.Size()) - uint32(right.Size()) +} + +func (node *MemNode) GetByIndex(index uint32) ([]byte, []byte) { + if node.IsLeaf() { + if index == 0 { + return node.key, node.value + } + return nil, nil + } + + left := node.Left() + leftSize := uint32(left.Size()) + if index < leftSize { + return left.GetByIndex(index) + } + + right := node.Right() + return right.GetByIndex(index - leftSize) +} + +// EncodeBytes writes a varint length-prefixed byte slice to the writer, +// it's used for hash computation, must be compactible with the official IAVL implementation. +func EncodeBytes(w io.Writer, bz []byte) error { + var buf [binary.MaxVarintLen64]byte + n := binary.PutUvarint(buf[:], uint64(len(bz))) + if _, err := w.Write(buf[0:n]); err != nil { + return err + } + _, err := w.Write(bz) + return err +} + +func maxUInt8(a, b uint8) uint8 { + if a > b { + return a + } + return b +} diff --git a/sc/memiavl/db/mmap.go b/sc/memiavl/db/mmap.go new file mode 100644 index 0000000..402156a --- /dev/null +++ b/sc/memiavl/db/mmap.go @@ -0,0 +1,60 @@ +package memiavl + +import ( + "errors" + "os" + + "github.com/ledgerwatch/erigon-lib/mmap" +) + +// MmapFile manage the resources of a mmap-ed file +type MmapFile struct { + file *os.File + data []byte + // mmap handle for windows (this is used to close mmap) + handle *[mmap.MaxMapSize]byte +} + +// Open openes the file and create the mmap. +// the mmap is created with flags: PROT_READ, MAP_SHARED, MADV_RANDOM. +func NewMmap(path string) (*MmapFile, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + + data, handle, err := Mmap(file) + if err != nil { + _ = file.Close() + return nil, err + } + + return &MmapFile{ + file: file, + data: data, + handle: handle, + }, nil +} + +// Close closes the file and mmap handles +func (m *MmapFile) Close() error { + var err error + if m.handle != nil { + err = mmap.Munmap(m.data, m.handle) + } + return errors.Join(err, m.file.Close()) +} + +// Data returns the mmap-ed buffer +func (m *MmapFile) Data() []byte { + return m.data +} + +func Mmap(f *os.File) ([]byte, *[mmap.MaxMapSize]byte, error) { + fi, err := f.Stat() + if err != nil || fi.Size() == 0 { + return nil, nil, err + } + + return mmap.Mmap(f, int(fi.Size())) +} diff --git a/sc/memiavl/db/multitree.go b/sc/memiavl/db/multitree.go new file mode 100644 index 0000000..789f8d2 --- /dev/null +++ b/sc/memiavl/db/multitree.go @@ -0,0 +1,459 @@ +package memiavl + +import ( + "context" + "errors" + "fmt" + "math" + "os" + "path/filepath" + "sort" + + "github.com/alitto/pond" + "github.com/cosmos/iavl" + "github.com/tidwall/wal" + "golang.org/x/exp/slices" +) + +const MetadataFileName = "__metadata" + +type NamedTree struct { + *Tree + Name string +} + +// MultiTree manages multiple memiavl tree together, +// all the trees share the same latest version, the snapshots are always created at the same version. +// +// The snapshot structure is like this: +// ``` +// > snapshot-V +// > metadata +// > bank +// > kvs +// > nodes +// > metadata +// > acc +// > other stores... +// ``` +type MultiTree struct { + // if the tree is start from genesis, it's the initial version of the chain, + // if the tree is imported from snapshot, it's the imported version plus one, + // it always corresponds to the wal entry with index 1. + initialVersion uint32 + + zeroCopy bool + cacheSize int + + trees []NamedTree // always ordered by tree name + treesByName map[string]int // index of the trees by name + lastCommitInfo CommitInfo + + // the initial metadata loaded from disk snapshot + metadata MultiTreeMetadata +} + +func NewEmptyMultiTree(initialVersion uint32, cacheSize int) *MultiTree { + return &MultiTree{ + initialVersion: initialVersion, + treesByName: make(map[string]int), + zeroCopy: true, + cacheSize: cacheSize, + } +} + +func LoadMultiTree(dir string, zeroCopy bool, cacheSize int) (*MultiTree, error) { + metadata, err := readMetadata(dir) + if err != nil { + return nil, err + } + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + treeMap := make(map[string]*Tree, len(entries)) + treeNames := make([]string, 0, len(entries)) + for _, e := range entries { + if !e.IsDir() { + continue + } + name := e.Name() + treeNames = append(treeNames, name) + snapshot, err := OpenSnapshot(filepath.Join(dir, name)) + if err != nil { + return nil, err + } + treeMap[name] = NewFromSnapshot(snapshot, zeroCopy, cacheSize) + } + + slices.Sort(treeNames) + + trees := make([]NamedTree, len(treeNames)) + treesByName := make(map[string]int, len(trees)) + for i, name := range treeNames { + tree := treeMap[name] + trees[i] = NamedTree{Tree: tree, Name: name} + treesByName[name] = i + } + + mtree := &MultiTree{ + trees: trees, + treesByName: treesByName, + lastCommitInfo: *metadata.CommitInfo, + metadata: *metadata, + zeroCopy: zeroCopy, + cacheSize: cacheSize, + } + // initial version is nesserary for wal index conversion + mtree.setInitialVersion(metadata.InitialVersion) + return mtree, nil +} + +// TreeByName returns the tree by name, returns nil if not found +func (t *MultiTree) TreeByName(name string) *Tree { + if i, ok := t.treesByName[name]; ok { + return t.trees[i].Tree + } + return nil +} + +// Trees returns all the trees together with the name, ordered by name. +func (t *MultiTree) Trees() []NamedTree { + return t.trees +} + +func (t *MultiTree) SetInitialVersion(initialVersion int64) error { + if initialVersion >= math.MaxUint32 { + return fmt.Errorf("version overflows uint32: %d", initialVersion) + } + + if t.Version() != 0 { + return fmt.Errorf("multi tree is not empty: %d", t.Version()) + } + + for _, entry := range t.trees { + if !entry.Tree.IsEmpty() { + return fmt.Errorf("tree is not empty: %s", entry.Name) + } + } + + t.setInitialVersion(initialVersion) + return nil +} + +func (t *MultiTree) setInitialVersion(initialVersion int64) { + t.initialVersion = uint32(initialVersion) + for _, entry := range t.trees { + entry.Tree.initialVersion = t.initialVersion + } +} + +func (t *MultiTree) SetZeroCopy(zeroCopy bool) { + t.zeroCopy = zeroCopy + for _, entry := range t.trees { + entry.Tree.SetZeroCopy(zeroCopy) + } +} + +// Copy returns a snapshot of the tree which won't be corrupted by further modifications on the main tree. +func (t *MultiTree) Copy(cacheSize int) *MultiTree { + trees := make([]NamedTree, len(t.trees)) + treesByName := make(map[string]int, len(t.trees)) + for i, entry := range t.trees { + tree := entry.Tree.Copy(cacheSize) + trees[i] = NamedTree{Tree: tree, Name: entry.Name} + treesByName[entry.Name] = i + } + + clone := *t + clone.trees = trees + clone.treesByName = treesByName + return &clone +} + +func (t *MultiTree) Version() int64 { + return t.lastCommitInfo.Version +} + +func (t *MultiTree) SnapshotVersion() int64 { + return t.metadata.CommitInfo.Version +} + +func (t *MultiTree) LastCommitInfo() *CommitInfo { + return &t.lastCommitInfo +} + +func (t *MultiTree) applyWALEntry(entry WALEntry) error { + if err := t.ApplyUpgrades(entry.Upgrades); err != nil { + return err + } + return t.ApplyChangeSets(entry.Changesets) +} + +// ApplyUpgrades store name upgrades +func (t *MultiTree) ApplyUpgrades(upgrades []*TreeNameUpgrade) error { + if len(upgrades) == 0 { + return nil + } + + t.treesByName = nil // rebuild in the end + + for _, upgrade := range upgrades { + switch { + case upgrade.Delete: + i := slices.IndexFunc(t.trees, func(entry NamedTree) bool { + return entry.Name == upgrade.Name + }) + if i < 0 { + return fmt.Errorf("unknown tree name %s", upgrade.Name) + } + // swap deletion + t.trees[i], t.trees[len(t.trees)-1] = t.trees[len(t.trees)-1], t.trees[i] + t.trees = t.trees[:len(t.trees)-1] + case upgrade.RenameFrom != "": + // rename tree + i := slices.IndexFunc(t.trees, func(entry NamedTree) bool { + return entry.Name == upgrade.RenameFrom + }) + if i < 0 { + return fmt.Errorf("unknown tree name %s", upgrade.RenameFrom) + } + t.trees[i].Name = upgrade.Name + default: + // add tree + tree := NewWithInitialVersion(uint32(nextVersion(t.Version(), t.initialVersion)), t.cacheSize) + t.trees = append(t.trees, NamedTree{Tree: tree, Name: upgrade.Name}) + } + } + + sort.SliceStable(t.trees, func(i, j int) bool { + return t.trees[i].Name < t.trees[j].Name + }) + t.treesByName = make(map[string]int, len(t.trees)) + for i, tree := range t.trees { + if _, ok := t.treesByName[tree.Name]; ok { + return fmt.Errorf("memiavl tree name conflicts: %s", tree.Name) + } + t.treesByName[tree.Name] = i + } + + return nil +} + +// ApplyChangeSet applies change set for a single tree. +func (t *MultiTree) ApplyChangeSet(name string, changeSet iavl.ChangeSet) error { + i, found := t.treesByName[name] + if !found { + return fmt.Errorf("unknown tree name %s", name) + } + t.trees[i].Tree.ApplyChangeSet(changeSet) + return nil +} + +// ApplyChangeSets applies change sets for multiple trees. +func (t *MultiTree) ApplyChangeSets(changeSets []*NamedChangeSet) error { + for _, cs := range changeSets { + if err := t.ApplyChangeSet(cs.Name, cs.Changeset); err != nil { + return err + } + } + return nil +} + +// WorkingCommitInfo returns the commit info for the working tree +func (t *MultiTree) WorkingCommitInfo() *CommitInfo { + version := nextVersion(t.lastCommitInfo.Version, t.initialVersion) + return t.buildCommitInfo(version) +} + +// SaveVersion bumps the versions of all the stores and optionally returns the new app hash +func (t *MultiTree) SaveVersion(updateCommitInfo bool) (int64, error) { + t.lastCommitInfo.Version = nextVersion(t.lastCommitInfo.Version, t.initialVersion) + for _, entry := range t.trees { + if _, _, err := entry.Tree.SaveVersion(updateCommitInfo); err != nil { + return 0, err + } + } + + if updateCommitInfo { + t.UpdateCommitInfo() + } else { + // clear the dirty informaton + t.lastCommitInfo.StoreInfos = []StoreInfo{} + } + + return t.lastCommitInfo.Version, nil +} + +func (t *MultiTree) buildCommitInfo(version int64) *CommitInfo { + var infos []StoreInfo + for _, entry := range t.trees { + infos = append(infos, StoreInfo{ + Name: entry.Name, + CommitId: CommitID{ + Version: entry.Tree.Version(), + Hash: entry.Tree.RootHash(), + }, + }) + } + + return &CommitInfo{ + Version: version, + StoreInfos: infos, + } +} + +// UpdateCommitInfo update lastCommitInfo based on current status of trees. +// it's needed if `updateCommitInfo` is set to `false` in `ApplyChangeSet`. +func (t *MultiTree) UpdateCommitInfo() { + t.lastCommitInfo = *t.buildCommitInfo(t.lastCommitInfo.Version) +} + +// CatchupWAL replay the new entries in the WAL on the tree to catch-up to the target or latest version. +func (t *MultiTree) CatchupWAL(wal *wal.Log, endVersion int64) error { + lastIndex, err := wal.LastIndex() + if err != nil { + return fmt.Errorf("read wal last index failed, %w", err) + } + + firstIndex := walIndex(nextVersion(t.Version(), t.initialVersion), t.initialVersion) + if firstIndex > lastIndex { + // already up-to-date + return nil + } + + endIndex := lastIndex + if endVersion != 0 { + endIndex = walIndex(endVersion, t.initialVersion) + } + + if endIndex < firstIndex { + return fmt.Errorf("target index %d is pruned", endIndex) + } + + if endIndex > lastIndex { + return fmt.Errorf("target index %d is in the future, latest index: %d", endIndex, lastIndex) + } + + for i := firstIndex; i <= endIndex; i++ { + bz, err := wal.Read(i) + if err != nil { + return fmt.Errorf("read wal log failed, %w", err) + } + var entry WALEntry + if err := entry.Unmarshal(bz); err != nil { + return fmt.Errorf("unmarshal wal log failed, %w", err) + } + if err := t.applyWALEntry(entry); err != nil { + return fmt.Errorf("replay wal entry failed, %w", err) + } + if _, err := t.SaveVersion(false); err != nil { + return fmt.Errorf("replay change set failed, %w", err) + } + } + t.UpdateCommitInfo() + return nil +} + +func (t *MultiTree) WriteSnapshot(dir string, wp *pond.WorkerPool) error { + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + // write the snapshots in parallel and wait all jobs done + group, _ := wp.GroupContext(context.Background()) + + for _, entry := range t.trees { + tree, name := entry.Tree, entry.Name + group.Submit(func() error { + return tree.WriteSnapshot(filepath.Join(dir, name)) + }) + } + + if err := group.Wait(); err != nil { + return err + } + + // write commit info + metadata := MultiTreeMetadata{ + CommitInfo: &t.lastCommitInfo, + InitialVersion: int64(t.initialVersion), + } + bz, err := metadata.Marshal() + if err != nil { + return err + } + return WriteFileSync(filepath.Join(dir, MetadataFileName), bz) +} + +// WriteFileSync calls `f.Sync` after before closing the file +func WriteFileSync(name string, data []byte) error { + f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + _, err = f.Write(data) + if err == nil { + err = f.Sync() + } + if err1 := f.Close(); err1 != nil && err == nil { + err = err1 + } + return err +} + +func (t *MultiTree) Close() error { + errs := make([]error, 0, len(t.trees)) + for _, entry := range t.trees { + errs = append(errs, entry.Tree.Close()) + } + t.trees = nil + t.treesByName = nil + t.lastCommitInfo = CommitInfo{} + return errors.Join(errs...) +} + +func nextVersion(v int64, initialVersion uint32) int64 { + if v == 0 && initialVersion > 1 { + return int64(initialVersion) + } + return v + 1 +} + +// walIndex converts version to wal index based on initial version +func walIndex(v int64, initialVersion uint32) uint64 { + if initialVersion > 1 { + return uint64(v) - uint64(initialVersion) + 1 + } + return uint64(v) +} + +// walVersion converts wal index to version, reverse of walIndex +func walVersion(index uint64, initialVersion uint32) int64 { + if initialVersion > 1 { + return int64(index) + int64(initialVersion) - 1 + } + return int64(index) +} + +func readMetadata(dir string) (*MultiTreeMetadata, error) { + // load commit info + bz, err := os.ReadFile(filepath.Join(dir, MetadataFileName)) + if err != nil { + return nil, err + } + var metadata MultiTreeMetadata + if err := metadata.Unmarshal(bz); err != nil { + return nil, err + } + if metadata.CommitInfo.Version > math.MaxUint32 { + return nil, fmt.Errorf("commit info version overflows uint32: %d", metadata.CommitInfo.Version) + } + if metadata.InitialVersion > math.MaxUint32 { + return nil, fmt.Errorf("initial version overflows uint32: %d", metadata.InitialVersion) + } + + return &metadata, nil +} diff --git a/sc/memiavl/db/node.go b/sc/memiavl/db/node.go new file mode 100644 index 0000000..6f65931 --- /dev/null +++ b/sc/memiavl/db/node.go @@ -0,0 +1,201 @@ +package memiavl + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "fmt" + "io" +) + +// Node interface encapsulate the interface of both PersistedNode and MemNode. +type Node interface { + Height() uint8 + IsLeaf() bool + Size() int64 + Version() uint32 + Key() []byte + Value() []byte + Left() Node + Right() Node + Hash() []byte + + // SafeHash returns byte slice that's safe to retain + SafeHash() []byte + + // PersistedNode clone a new node, MemNode modify in place + Mutate(version, cowVersion uint32) *MemNode + + // Get query the value for a key, it's put into interface because a specialized implementation is more efficient. + Get(key []byte) ([]byte, uint32) + GetByIndex(uint32) ([]byte, []byte) +} + +// setRecursive do set operation. +// it always do modification and return new `MemNode`, even if the value is the same. +// also returns if it's an update or insertion, if update, the tree height and balance is not changed. +func setRecursive(node Node, key, value []byte, version, cowVersion uint32) (*MemNode, bool) { + if node == nil { + return newLeafNode(key, value, version), true + } + + nodeKey := node.Key() + if node.IsLeaf() { + switch bytes.Compare(key, nodeKey) { + case -1: + return &MemNode{ + height: 1, + size: 2, + version: version, + key: nodeKey, + left: newLeafNode(key, value, version), + right: node, + }, false + case 1: + return &MemNode{ + height: 1, + size: 2, + version: version, + key: key, + left: node, + right: newLeafNode(key, value, version), + }, false + default: + newNode := node.Mutate(version, cowVersion) + newNode.value = value + return newNode, true + } + } else { + var ( + newChild, newNode *MemNode + updated bool + ) + if bytes.Compare(key, nodeKey) == -1 { + newChild, updated = setRecursive(node.Left(), key, value, version, cowVersion) + newNode = node.Mutate(version, cowVersion) + newNode.left = newChild + } else { + newChild, updated = setRecursive(node.Right(), key, value, version, cowVersion) + newNode = node.Mutate(version, cowVersion) + newNode.right = newChild + } + + if !updated { + newNode.updateHeightSize() + newNode = newNode.reBalance(version, cowVersion) + } + + return newNode, updated + } +} + +// removeRecursive returns: +// - (nil, origNode, nil) -> nothing changed in subtree +// - (value, nil, newKey) -> leaf node is removed +// - (value, new node, newKey) -> subtree changed +func removeRecursive(node Node, key []byte, version, cowVersion uint32) ([]byte, Node, []byte) { + if node == nil { + return nil, nil, nil + } + + if node.IsLeaf() { + if bytes.Equal(node.Key(), key) { + return node.Value(), nil, nil + } + return nil, node, nil + } + + if bytes.Compare(key, node.Key()) == -1 { + value, newLeft, newKey := removeRecursive(node.Left(), key, version, cowVersion) + if value == nil { + return nil, node, nil + } + if newLeft == nil { + return value, node.Right(), node.Key() + } + newNode := node.Mutate(version, cowVersion) + newNode.left = newLeft + newNode.updateHeightSize() + return value, newNode.reBalance(version, cowVersion), newKey + } + + value, newRight, newKey := removeRecursive(node.Right(), key, version, cowVersion) + if value == nil { + return nil, node, nil + } + if newRight == nil { + return value, node.Left(), nil + } + + newNode := node.Mutate(version, cowVersion) + newNode.right = newRight + if newKey != nil { + newNode.key = newKey + } + newNode.updateHeightSize() + return value, newNode.reBalance(version, cowVersion), nil +} + +// Writes the node's hash to the given `io.Writer`. This function recursively calls +// children to update hashes. +func writeHashBytes(node Node, w io.Writer) error { + var ( + n int + buf [binary.MaxVarintLen64]byte + ) + + n = binary.PutVarint(buf[:], int64(node.Height())) + if _, err := w.Write(buf[0:n]); err != nil { + return fmt.Errorf("writing height, %w", err) + } + n = binary.PutVarint(buf[:], node.Size()) + if _, err := w.Write(buf[0:n]); err != nil { + return fmt.Errorf("writing size, %w", err) + } + n = binary.PutVarint(buf[:], int64(node.Version())) + if _, err := w.Write(buf[0:n]); err != nil { + return fmt.Errorf("writing version, %w", err) + } + + // Key is not written for inner nodes, unlike writeBytes. + + if node.IsLeaf() { + if err := EncodeBytes(w, node.Key()); err != nil { + return fmt.Errorf("writing key, %w", err) + } + + // Indirection needed to provide proofs without values. + // (e.g. ProofLeafNode.ValueHash) + valueHash := sha256.Sum256(node.Value()) + + if err := EncodeBytes(w, valueHash[:]); err != nil { + return fmt.Errorf("writing value, %w", err) + } + } else { + if err := EncodeBytes(w, node.Left().Hash()); err != nil { + return fmt.Errorf("writing left hash, %w", err) + } + if err := EncodeBytes(w, node.Right().Hash()); err != nil { + return fmt.Errorf("writing right hash, %w", err) + } + } + + return nil +} + +// HashNode computes the hash of the node. +func HashNode(node Node) []byte { + if node == nil { + return nil + } + h := sha256.New() + if err := writeHashBytes(node, h); err != nil { + panic(err) + } + return h.Sum(nil) +} + +// VerifyHash compare node's cached hash with computed one +func VerifyHash(node Node) bool { + return bytes.Equal(HashNode(node), node.Hash()) +} diff --git a/sc/memiavl/db/persisted_node.go b/sc/memiavl/db/persisted_node.go new file mode 100644 index 0000000..59f8c2a --- /dev/null +++ b/sc/memiavl/db/persisted_node.go @@ -0,0 +1,248 @@ +package memiavl + +import ( + "bytes" + "crypto/sha256" + "sort" +) + +const ( + OffsetHeight = 0 + OffsetPreTrees = OffsetHeight + 1 + OffsetVersion = OffsetHeight + 4 + OffsetSize = OffsetVersion + 4 + OffsetKeyLeaf = OffsetSize + 4 + + OffsetHash = OffsetKeyLeaf + 4 + SizeHash = sha256.Size + SizeNodeWithoutHash = OffsetHash + SizeNode = SizeNodeWithoutHash + SizeHash + + OffsetLeafVersion = 0 + OffsetLeafKeyLen = OffsetLeafVersion + 4 + OffsetLeafKeyOffset = OffsetLeafKeyLen + 4 + OffsetLeafHash = OffsetLeafKeyOffset + 8 + SizeLeafWithoutHash = OffsetLeafHash + SizeLeaf = SizeLeafWithoutHash + SizeHash +) + +// PersistedNode is backed by serialized byte array, usually mmap-ed from disk file. +// Encoding format (all integers are encoded in little endian): +// +// Branch node: +// - height : 1 +// - preTrees : 1 +// - _padding : 2 +// - version : 4 +// - size : 4 +// - key node : 4 // node index of the smallest leaf in right branch +// - hash : 32 +// Leaf node: +// - version : 4 +// - key len : 4 +// - key offset : 8 +// - hash : 32 +type PersistedNode struct { + snapshot *Snapshot + isLeaf bool + index uint32 +} + +var _ Node = PersistedNode{} + +func (node PersistedNode) branchNode() NodeLayout { + return node.snapshot.nodesLayout.Node(node.index) +} + +func (node PersistedNode) leafNode() LeafLayout { + return node.snapshot.leavesLayout.Leaf(node.index) +} + +func (node PersistedNode) Height() uint8 { + if node.isLeaf { + return 0 + } + return node.branchNode().Height() +} + +func (node PersistedNode) IsLeaf() bool { + return node.isLeaf +} + +func (node PersistedNode) Version() uint32 { + if node.isLeaf { + return node.leafNode().Version() + } + return node.branchNode().Version() +} + +func (node PersistedNode) Size() int64 { + if node.isLeaf { + return 1 + } + return int64(node.branchNode().Size()) +} + +func (node PersistedNode) Key() []byte { + if node.isLeaf { + return node.snapshot.LeafKey(node.index) + } + index := node.branchNode().KeyLeaf() + return node.snapshot.LeafKey(index) +} + +// Value returns nil for non-leaf node. +func (node PersistedNode) Value() []byte { + if !node.isLeaf { + return nil + } + _, value := node.snapshot.LeafKeyValue(node.index) + return value +} + +// Left result is not defined for leaf nodes. +func (node PersistedNode) Left() Node { + if node.isLeaf { + panic("can't call Left on leaf node") + } + + data := node.branchNode() + preTrees := uint32(data.PreTrees()) + startLeaf := getStartLeaf(node.index, data.Size(), preTrees) + keyLeaf := data.KeyLeaf() + if startLeaf+1 == keyLeaf { + return PersistedNode{snapshot: node.snapshot, index: startLeaf, isLeaf: true} + } + return PersistedNode{snapshot: node.snapshot, index: getLeftBranch(keyLeaf, preTrees)} +} + +// Right result is not defined for leaf nodes. +func (node PersistedNode) Right() Node { + if node.isLeaf { + panic("can't call Right on leaf node") + } + + data := node.branchNode() + keyLeaf := data.KeyLeaf() + preTrees := uint32(data.PreTrees()) + if keyLeaf == getEndLeaf(node.index, preTrees) { + return PersistedNode{snapshot: node.snapshot, index: keyLeaf, isLeaf: true} + } + return PersistedNode{snapshot: node.snapshot, index: node.index - 1} +} + +func (node PersistedNode) SafeHash() []byte { + return bytes.Clone(node.Hash()) +} + +func (node PersistedNode) Hash() []byte { + if node.isLeaf { + return node.leafNode().Hash() + } + return node.branchNode().Hash() +} + +func (node PersistedNode) Mutate(version, _ uint32) *MemNode { + if node.isLeaf { + key, value := node.snapshot.LeafKeyValue(node.index) + return &MemNode{ + height: 0, + size: 1, + version: version, + key: key, + value: value, + } + } + data := node.branchNode() + return &MemNode{ + height: data.Height(), + size: int64(data.Size()), + version: version, + key: node.Key(), + left: node.Left(), + right: node.Right(), + } +} + +func (node PersistedNode) Get(key []byte) ([]byte, uint32) { + var start, count uint32 + if node.isLeaf { + start = node.index + count = 1 + } else { + data := node.branchNode() + preTrees := uint32(data.PreTrees()) + count = data.Size() + start = getStartLeaf(node.index, count, preTrees) + } + + // binary search in the leaf node array + i := uint32(sort.Search(int(count), func(i int) bool { + leafKey := node.snapshot.LeafKey(start + uint32(i)) + return bytes.Compare(leafKey, key) >= 0 + })) + + leaf := i + start + if leaf >= start+count { + // return the next index if the key is greater than all keys in the node + return nil, i + } + + nodeKey, value := node.snapshot.LeafKeyValue(leaf) + if !bytes.Equal(nodeKey, key) { + return nil, i + } + + return value, i +} + +func (node PersistedNode) GetByIndex(leafIndex uint32) ([]byte, []byte) { + if node.isLeaf { + if leafIndex != 0 { + return nil, nil + } + return node.snapshot.LeafKeyValue(node.index) + } + data := node.branchNode() + preTrees := uint32(data.PreTrees()) + startLeaf := getStartLeaf(node.index, data.Size(), preTrees) + endLeaf := getEndLeaf(node.index, preTrees) + + i := startLeaf + leafIndex + if i > endLeaf { + return nil, nil + } + return node.snapshot.LeafKeyValue(i) +} + +// getStartLeaf returns the index of the first leaf in the node. +// +// > start leaf = pre leaves +// > = pre branches + pre trees +// > = total branches - sub branches + pre trees +// > = (index + 1) - (size - 1) + preTrees +// > = index + 2 - size + preTrees +func getStartLeaf(index, size, preTrees uint32) uint32 { + return index + 2 - size + preTrees +} + +// getEndLeaf returns the index of the last leaf in the node. +// +// > end leaf = start leaf + size - 1 +// > = (index + 2 - size + preTrees) + size - 1 +// > = index + 1 + preTrees +func getEndLeaf(index, preTrees uint32) uint32 { + return index + preTrees + 1 +} + +// getLeftBranch returns the index of the left branch of the node. +// +// > left branch = pre branches + left branches - 1 +// > = (total branches - sub branches) + (left leaves - 1) - 1 +// > = (total branches - sub branches) + (key leaf - start leaf - 1) - 1 +// > = (index+1 - (size-1)) + (key leaf - (index + 2 - size + preTrees) - 1) - 1 +// > = (index - size + 2) + key leaf - index - 2 + size - preTrees - 2 +// > = key leaf - preTrees - 2 +func getLeftBranch(keyLeaf, preTrees uint32) uint32 { + return keyLeaf - preTrees - 2 +} diff --git a/sc/memiavl/db/proof.go b/sc/memiavl/db/proof.go new file mode 100644 index 0000000..be94200 --- /dev/null +++ b/sc/memiavl/db/proof.go @@ -0,0 +1,194 @@ +package memiavl + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + + ics23 "github.com/confio/ics23/go" + "github.com/cosmos/iavl" +) + +/* +GetMembershipProof will produce a CommitmentProof that the given key (and queries value) exists in the iavl tree. +If the key doesn't exist in the tree, this will return an error. +*/ +func (t *Tree) GetMembershipProof(key []byte) (*ics23.CommitmentProof, error) { + exist, err := t.createExistenceProof(key) + if err != nil { + return nil, err + } + proof := &ics23.CommitmentProof{ + Proof: &ics23.CommitmentProof_Exist{ + Exist: exist, + }, + } + return proof, nil +} + +// VerifyMembership returns true iff proof is an ExistenceProof for the given key. +func (t *Tree) VerifyMembership(proof *ics23.CommitmentProof, key []byte) bool { + val := t.Get(key) + root := t.RootHash() + return ics23.VerifyMembership(ics23.IavlSpec, root, proof, key, val) +} + +/* +GetNonMembershipProof will produce a CommitmentProof that the given key doesn't exist in the iavl tree. +If the key exists in the tree, this will return an error. +*/ +func (t *Tree) GetNonMembershipProof(key []byte) (*ics23.CommitmentProof, error) { + // idx is one node right of what we want.... + var err error + idx, val := t.GetWithIndex(key) + if val != nil { + return nil, fmt.Errorf("cannot create NonExistanceProof when Key in State") + } + + nonexist := &ics23.NonExistenceProof{ + Key: key, + } + + if idx >= 1 { + leftkey, _ := t.GetByIndex(idx - 1) + nonexist.Left, err = t.createExistenceProof(leftkey) + if err != nil { + return nil, err + } + } + + // this will be nil if nothing right of the queried key + rightkey, _ := t.GetByIndex(idx) + if rightkey != nil { + nonexist.Right, err = t.createExistenceProof(rightkey) + if err != nil { + return nil, err + } + } + + proof := &ics23.CommitmentProof{ + Proof: &ics23.CommitmentProof_Nonexist{ + Nonexist: nonexist, + }, + } + return proof, nil +} + +// VerifyNonMembership returns true iff proof is a NonExistenceProof for the given key. +func (t *Tree) VerifyNonMembership(proof *ics23.CommitmentProof, key []byte) bool { + root := t.RootHash() + return ics23.VerifyNonMembership(ics23.IavlSpec, root, proof, key) +} + +// createExistenceProof will get the proof from the tree and convert the proof into a valid +// existence proof, if that's what it is. +func (t *Tree) createExistenceProof(key []byte) (*ics23.ExistenceProof, error) { + path, node, err := pathToLeaf(t.root, key) + return &ics23.ExistenceProof{ + Key: node.Key(), + Value: node.Value(), + Leaf: convertLeafOp(int64(node.Version())), + Path: convertInnerOps(path), + }, err +} + +func convertLeafOp(version int64) *ics23.LeafOp { + // this is adapted from iavl/proof.go:proofLeafNode.Hash() + prefix := convertVarIntToBytes(0) + prefix = append(prefix, convertVarIntToBytes(1)...) + prefix = append(prefix, convertVarIntToBytes(version)...) + + return &ics23.LeafOp{ + Hash: ics23.HashOp_SHA256, + PrehashValue: ics23.HashOp_SHA256, + Length: ics23.LengthOp_VAR_PROTO, + Prefix: prefix, + } +} + +// we cannot get the proofInnerNode type, so we need to do the whole path in one function +func convertInnerOps(path iavl.PathToLeaf) []*ics23.InnerOp { + steps := make([]*ics23.InnerOp, 0, len(path)) + + // lengthByte is the length prefix prepended to each of the sha256 sub-hashes + var lengthByte byte = 0x20 + + // we need to go in reverse order, iavl starts from root to leaf, + // we want to go up from the leaf to the root + for i := len(path) - 1; i >= 0; i-- { + // this is adapted from iavl/proof.go:proofInnerNode.Hash() + prefix := convertVarIntToBytes(int64(path[i].Height)) + prefix = append(prefix, convertVarIntToBytes(path[i].Size)...) + prefix = append(prefix, convertVarIntToBytes(path[i].Version)...) + + var suffix []byte + if len(path[i].Left) > 0 { + // length prefixed left side + prefix = append(prefix, lengthByte) + prefix = append(prefix, path[i].Left...) + // prepend the length prefix for child + prefix = append(prefix, lengthByte) + } else { + // prepend the length prefix for child + prefix = append(prefix, lengthByte) + // length-prefixed right side + suffix = []byte{lengthByte} + suffix = append(suffix, path[i].Right...) + } + + op := &ics23.InnerOp{ + Hash: ics23.HashOp_SHA256, + Prefix: prefix, + Suffix: suffix, + } + steps = append(steps, op) + } + return steps +} + +func convertVarIntToBytes(orig int64) []byte { + var buf [binary.MaxVarintLen64]byte + n := binary.PutVarint(buf[:], orig) + return buf[:n] +} + +func pathToLeaf(node Node, key []byte) (iavl.PathToLeaf, Node, error) { + var path iavl.PathToLeaf + + for { + height := node.Height() + if height == 0 { + if bytes.Equal(node.Key(), key) { + return path, node, nil + } + + return path, node, errors.New("key does not exist") + } + + if bytes.Compare(key, node.Key()) < 0 { + // left side + right := node.Right() + path = append(path, iavl.ProofInnerNode{ + Height: int8(height), + Size: node.Size(), + Version: int64(node.Version()), + Left: nil, + Right: right.Hash(), + }) + node = node.Left() + continue + } + + // right side + left := node.Left() + path = append(path, iavl.ProofInnerNode{ + Height: int8(height), + Size: node.Size(), + Version: int64(node.Version()), + Left: left.Hash(), + Right: nil, + }) + node = node.Right() + } +} diff --git a/sc/memiavl/db/proof_test.go b/sc/memiavl/db/proof_test.go new file mode 100644 index 0000000..18672c4 --- /dev/null +++ b/sc/memiavl/db/proof_test.go @@ -0,0 +1,59 @@ +package memiavl + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestProofs(t *testing.T) { + // do a round test for each version in ChangeSets + testCases := []struct { + existKey []byte + nonExistKey []byte + }{ + {[]byte("hello"), []byte("hello1")}, + {[]byte("hello1"), []byte("hello2")}, + {[]byte("hello2"), []byte("hell")}, + {[]byte("hello00"), []byte("hell")}, + {[]byte("hello00"), []byte("hello")}, + {[]byte("aello00"), []byte("hello")}, + {[]byte("hello1"), []byte("aello00")}, + } + + tmpDir := t.TempDir() + tree := New(0) + + for i, tc := range testCases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + changes := ChangeSets[i] + tree.ApplyChangeSet(changes) + _, _, err := tree.SaveVersion(true) + require.NoError(t, err) + + proof, err := tree.GetMembershipProof(tc.existKey) + require.NoError(t, err) + require.True(t, tree.VerifyMembership(proof, tc.existKey)) + + proof, err = tree.GetNonMembershipProof(tc.nonExistKey) + require.NoError(t, err) + require.True(t, tree.VerifyNonMembership(proof, tc.nonExistKey)) + + // test persisted tree + require.NoError(t, tree.WriteSnapshot(tmpDir)) + snapshot, err := OpenSnapshot(tmpDir) + require.NoError(t, err) + ptree := NewFromSnapshot(snapshot, true, 0) + defer ptree.Close() + + proof, err = ptree.GetMembershipProof(tc.existKey) + require.NoError(t, err) + require.True(t, ptree.VerifyMembership(proof, tc.existKey)) + + proof, err = ptree.GetNonMembershipProof(tc.nonExistKey) + require.NoError(t, err) + require.True(t, ptree.VerifyNonMembership(proof, tc.nonExistKey)) + }) + } +} diff --git a/sc/memiavl/db/snapshot.go b/sc/memiavl/db/snapshot.go new file mode 100644 index 0000000..12ca196 --- /dev/null +++ b/sc/memiavl/db/snapshot.go @@ -0,0 +1,568 @@ +package memiavl + +import ( + "bufio" + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "path/filepath" +) + +const ( + // SnapshotFileMagic is little endian encoded b"IAVL" + SnapshotFileMagic = 1280721225 + + // the initial snapshot format + SnapshotFormat = 0 + + // magic: uint32, format: uint32, version: uint32 + SizeMetadata = 12 + + FileNameNodes = "nodes" + FileNameLeaves = "leaves" + FileNameKVs = "kvs" + FileNameMetadata = "metadata" +) + +// Snapshot manage the lifecycle of mmap-ed files for the snapshot, +// it must out live the objects that derived from it. +type Snapshot struct { + nodesMap *MmapFile + leavesMap *MmapFile + kvsMap *MmapFile + + nodes []byte + leaves []byte + kvs []byte + + // parsed from metadata file + version uint32 + + // wrapping the raw nodes buffer + nodesLayout Nodes + leavesLayout Leaves + + // nil means empty snapshot + root *PersistedNode +} + +func NewEmptySnapshot(version uint32) *Snapshot { + return &Snapshot{ + version: version, + } +} + +// OpenSnapshot parse the version number and the root node index from metadata file, +// and mmap the other files. +func OpenSnapshot(snapshotDir string) (*Snapshot, error) { + // read metadata file + bz, err := os.ReadFile(filepath.Join(snapshotDir, FileNameMetadata)) + if err != nil { + return nil, err + } + if len(bz) != SizeMetadata { + return nil, fmt.Errorf("wrong metadata file size, expcted: %d, found: %d", SizeMetadata, len(bz)) + } + + magic := binary.LittleEndian.Uint32(bz) + if magic != SnapshotFileMagic { + return nil, fmt.Errorf("invalid metadata file magic: %d", magic) + } + format := binary.LittleEndian.Uint32(bz[4:]) + if format != SnapshotFormat { + return nil, fmt.Errorf("unknown snapshot format: %d", format) + } + version := binary.LittleEndian.Uint32(bz[8:]) + + var nodesMap, leavesMap, kvsMap *MmapFile + cleanupHandles := func(err error) error { + errs := []error{err} + if nodesMap != nil { + errs = append(errs, nodesMap.Close()) + } + if leavesMap != nil { + errs = append(errs, leavesMap.Close()) + } + if kvsMap != nil { + errs = append(errs, kvsMap.Close()) + } + return errors.Join(errs...) + } + + if nodesMap, err = NewMmap(filepath.Join(snapshotDir, FileNameNodes)); err != nil { + return nil, cleanupHandles(err) + } + if leavesMap, err = NewMmap(filepath.Join(snapshotDir, FileNameLeaves)); err != nil { + return nil, cleanupHandles(err) + } + if kvsMap, err = NewMmap(filepath.Join(snapshotDir, FileNameKVs)); err != nil { + return nil, cleanupHandles(err) + } + + nodes := nodesMap.Data() + leaves := leavesMap.Data() + kvs := kvsMap.Data() + + // validate nodes length + if len(nodes)%SizeNode != 0 { + return nil, cleanupHandles( + fmt.Errorf("corrupted snapshot, nodes file size %d is not a multiple of %d", len(nodes), SizeNode), + ) + } + if len(leaves)%SizeLeaf != 0 { + return nil, cleanupHandles( + fmt.Errorf("corrupted snapshot, leaves file size %d is not a multiple of %d", len(leaves), SizeLeaf), + ) + } + + nodesLen := len(nodes) / SizeNode + leavesLen := len(leaves) / SizeLeaf + if (leavesLen > 0 && nodesLen+1 != leavesLen) || (leavesLen == 0 && nodesLen != 0) { + return nil, cleanupHandles( + fmt.Errorf("corrupted snapshot, branch nodes size %d don't match leaves size %d", nodesLen, leavesLen), + ) + } + + nodesData, err := NewNodes(nodes) + if err != nil { + return nil, err + } + + leavesData, err := NewLeaves(leaves) + if err != nil { + return nil, err + } + + snapshot := &Snapshot{ + nodesMap: nodesMap, + leavesMap: leavesMap, + kvsMap: kvsMap, + + // cache the pointers + nodes: nodes, + leaves: leaves, + kvs: kvs, + + version: version, + + nodesLayout: nodesData, + leavesLayout: leavesData, + } + + if nodesLen > 0 { + snapshot.root = &PersistedNode{ + snapshot: snapshot, + isLeaf: false, + index: uint32(nodesLen - 1), + } + } else if leavesLen > 0 { + snapshot.root = &PersistedNode{ + snapshot: snapshot, + isLeaf: true, + index: 0, + } + } + + return snapshot, nil +} + +// Close closes the file and mmap handles, clears the buffers. +func (snapshot *Snapshot) Close() error { + var errs []error + + if snapshot.nodesMap != nil { + errs = append(errs, snapshot.nodesMap.Close()) + } + if snapshot.leavesMap != nil { + errs = append(errs, snapshot.leavesMap.Close()) + } + if snapshot.kvsMap != nil { + errs = append(errs, snapshot.kvsMap.Close()) + } + + // reset to an empty tree + *snapshot = *NewEmptySnapshot(snapshot.version) + return errors.Join(errs...) +} + +// IsEmpty returns if the snapshot is an empty tree. +func (snapshot *Snapshot) IsEmpty() bool { + return snapshot.root == nil +} + +// Node returns the branch node by index +func (snapshot *Snapshot) Node(index uint32) PersistedNode { + return PersistedNode{ + snapshot: snapshot, + index: index, + isLeaf: false, + } +} + +// Leaf returns the leaf node by index +func (snapshot *Snapshot) Leaf(index uint32) PersistedNode { + return PersistedNode{ + snapshot: snapshot, + index: index, + isLeaf: true, + } +} + +// Version returns the version of the snapshot +func (snapshot *Snapshot) Version() uint32 { + return snapshot.version +} + +// RootNode returns the root node +func (snapshot *Snapshot) RootNode() PersistedNode { + if snapshot.IsEmpty() { + panic("RootNode not supported on an empty snapshot") + } + return *snapshot.root +} + +func (snapshot *Snapshot) RootHash() []byte { + if snapshot.IsEmpty() { + return emptyHash + } + return snapshot.RootNode().Hash() +} + +// nodesLen returns the number of nodes in the snapshot +func (snapshot *Snapshot) nodesLen() int { + return len(snapshot.nodes) / SizeNode +} + +// leavesLen returns the number of nodes in the snapshot +func (snapshot *Snapshot) leavesLen() int { + return len(snapshot.leaves) / SizeLeaf +} + +// ScanNodes iterate over the nodes in the snapshot order (depth-first post-order, leaf nodes before branch nodes) +func (snapshot *Snapshot) ScanNodes(callback func(node PersistedNode) error) error { + for i := 0; i < snapshot.leavesLen(); i++ { + if err := callback(snapshot.Leaf(uint32(i))); err != nil { + return err + } + } + for i := 0; i < snapshot.nodesLen(); i++ { + if err := callback(snapshot.Node(uint32(i))); err != nil { + return err + } + } + return nil +} + +// Key returns a zero-copy slice of key by offset +func (snapshot *Snapshot) Key(offset uint64) []byte { + keyLen := binary.LittleEndian.Uint32(snapshot.kvs[offset:]) + offset += 4 + return snapshot.kvs[offset : offset+uint64(keyLen)] +} + +// KeyValue returns a zero-copy slice of key/value pair by offset +func (snapshot *Snapshot) KeyValue(offset uint64) ([]byte, []byte) { + len := uint64(binary.LittleEndian.Uint32(snapshot.kvs[offset:])) + offset += 4 + key := snapshot.kvs[offset : offset+len] + offset += len + len = uint64(binary.LittleEndian.Uint32(snapshot.kvs[offset:])) + offset += 4 + value := snapshot.kvs[offset : offset+len] + return key, value +} + +func (snapshot *Snapshot) LeafKey(index uint32) []byte { + leaf := snapshot.leavesLayout.Leaf(index) + offset := leaf.KeyOffset() + 4 + return snapshot.kvs[offset : offset+uint64(leaf.KeyLength())] +} + +func (snapshot *Snapshot) LeafKeyValue(index uint32) ([]byte, []byte) { + leaf := snapshot.leavesLayout.Leaf(index) + offset := leaf.KeyOffset() + 4 + length := uint64(leaf.KeyLength()) + key := snapshot.kvs[offset : offset+length] + offset += length + length = uint64(binary.LittleEndian.Uint32(snapshot.kvs[offset:])) + offset += 4 + return key, snapshot.kvs[offset : offset+length] +} + +// Export exports the nodes from snapshot file sequentially, more efficient than a post-order traversal. +func (snapshot *Snapshot) Export() *Exporter { + return newExporter(snapshot.export) +} + +func (snapshot *Snapshot) export(callback func(*ExportNode) bool) { + if snapshot.leavesLen() == 0 { + return + } + + if snapshot.leavesLen() == 1 { + leaf := snapshot.Leaf(0) + callback(&ExportNode{ + Height: 0, + Version: int64(leaf.Version()), + Key: leaf.Key(), + Value: leaf.Value(), + }) + return + } + + var pendingTrees int + var i, j uint32 + for ; i < uint32(snapshot.nodesLen()); i++ { + // pending branch node + node := snapshot.nodesLayout.Node(i) + for pendingTrees < int(node.PreTrees())+2 { + // add more leaf nodes + leaf := snapshot.leavesLayout.Leaf(j) + key, value := snapshot.KeyValue(leaf.KeyOffset()) + enode := &ExportNode{ + Height: 0, + Version: int64(leaf.Version()), + Key: key, + Value: value, + } + j++ + pendingTrees++ + + if callback(enode) { + return + } + } + enode := &ExportNode{ + Height: int8(node.Height()), + Version: int64(node.Version()), + Key: snapshot.LeafKey(node.KeyLeaf()), + } + pendingTrees-- + + if callback(enode) { + return + } + } +} + +// WriteSnapshot save the IAVL tree to a new snapshot directory. +func (t *Tree) WriteSnapshot(snapshotDir string) error { + return writeSnapshot(snapshotDir, t.version, func(w *snapshotWriter) (uint32, error) { + if t.root == nil { + return 0, nil + } else { + if err := w.writeRecursive(t.root); err != nil { + return 0, err + } + return w.leafCounter, nil + } + }) +} + +func writeSnapshot( + dir string, version uint32, + doWrite func(*snapshotWriter) (uint32, error), +) (returnErr error) { + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + nodesFile := filepath.Join(dir, FileNameNodes) + leavesFile := filepath.Join(dir, FileNameLeaves) + kvsFile := filepath.Join(dir, FileNameKVs) + + fpNodes, err := createFile(nodesFile) + if err != nil { + return err + } + defer func() { + if err := fpNodes.Close(); returnErr == nil { + returnErr = err + } + }() + + fpLeaves, err := createFile(leavesFile) + if err != nil { + return err + } + defer func() { + if err := fpLeaves.Close(); returnErr == nil { + returnErr = err + } + }() + + fpKVs, err := createFile(kvsFile) + if err != nil { + return err + } + defer func() { + if err := fpKVs.Close(); returnErr == nil { + returnErr = err + } + }() + + nodesWriter := bufio.NewWriter(fpNodes) + leavesWriter := bufio.NewWriter(fpLeaves) + kvsWriter := bufio.NewWriter(fpKVs) + + w := newSnapshotWriter(nodesWriter, leavesWriter, kvsWriter) + leaves, err := doWrite(w) + if err != nil { + return err + } + + if leaves > 0 { + if err := nodesWriter.Flush(); err != nil { + return err + } + if err := leavesWriter.Flush(); err != nil { + return err + } + if err := kvsWriter.Flush(); err != nil { + return err + } + + if err := fpKVs.Sync(); err != nil { + return err + } + if err := fpLeaves.Sync(); err != nil { + return err + } + if err := fpNodes.Sync(); err != nil { + return err + } + } + + // write metadata + var metadataBuf [SizeMetadata]byte + binary.LittleEndian.PutUint32(metadataBuf[:], SnapshotFileMagic) + binary.LittleEndian.PutUint32(metadataBuf[4:], SnapshotFormat) + binary.LittleEndian.PutUint32(metadataBuf[8:], version) + + metadataFile := filepath.Join(dir, FileNameMetadata) + fpMetadata, err := createFile(metadataFile) + if err != nil { + return err + } + defer func() { + if err := fpMetadata.Close(); returnErr == nil { + returnErr = err + } + }() + + if _, err := fpMetadata.Write(metadataBuf[:]); err != nil { + return err + } + + return fpMetadata.Sync() +} + +type snapshotWriter struct { + nodesWriter, leavesWriter, kvWriter io.Writer + + // count how many nodes have been written + branchCounter, leafCounter uint32 + + // record the current writing offset in kvs file + kvsOffset uint64 +} + +func newSnapshotWriter(nodesWriter, leavesWriter, kvsWriter io.Writer) *snapshotWriter { + return &snapshotWriter{ + nodesWriter: nodesWriter, + leavesWriter: leavesWriter, + kvWriter: kvsWriter, + } +} + +// writeKeyValue append key-value pair to kvs file and record the offset +func (w *snapshotWriter) writeKeyValue(key, value []byte) error { + var numBuf [4]byte + + binary.LittleEndian.PutUint32(numBuf[:], uint32(len(key))) + if _, err := w.kvWriter.Write(numBuf[:]); err != nil { + return err + } + if _, err := w.kvWriter.Write(key); err != nil { + return err + } + + binary.LittleEndian.PutUint32(numBuf[:], uint32(len(value))) + if _, err := w.kvWriter.Write(numBuf[:]); err != nil { + return err + } + if _, err := w.kvWriter.Write(value); err != nil { + return err + } + + w.kvsOffset += 4 + 4 + uint64(len(key)) + uint64(len(value)) + return nil +} + +func (w *snapshotWriter) writeLeaf(version uint32, key, value, hash []byte) error { + var buf [SizeLeafWithoutHash]byte + binary.LittleEndian.PutUint32(buf[OffsetLeafVersion:], version) + binary.LittleEndian.PutUint32(buf[OffsetLeafKeyLen:], uint32(len(key))) + binary.LittleEndian.PutUint64(buf[OffsetLeafKeyOffset:], w.kvsOffset) + + if err := w.writeKeyValue(key, value); err != nil { + return err + } + + if _, err := w.leavesWriter.Write(buf[:]); err != nil { + return err + } + if _, err := w.leavesWriter.Write(hash); err != nil { + return err + } + + w.leafCounter++ + return nil +} + +func (w *snapshotWriter) writeBranch(version, size uint32, height, preTrees uint8, keyLeaf uint32, hash []byte) error { + var buf [SizeNodeWithoutHash]byte + buf[OffsetHeight] = height + buf[OffsetPreTrees] = preTrees + binary.LittleEndian.PutUint32(buf[OffsetVersion:], version) + binary.LittleEndian.PutUint32(buf[OffsetSize:], size) + binary.LittleEndian.PutUint32(buf[OffsetKeyLeaf:], keyLeaf) + + if _, err := w.nodesWriter.Write(buf[:]); err != nil { + return err + } + if _, err := w.nodesWriter.Write(hash); err != nil { + return err + } + + w.branchCounter++ + return nil +} + +// writeRecursive write the node recursively in depth-first post-order, +// returns `(nodeIndex, err)`. +func (w *snapshotWriter) writeRecursive(node Node) error { + if node.IsLeaf() { + return w.writeLeaf(node.Version(), node.Key(), node.Value(), node.Hash()) + } + + // record the number of pending subtrees before the current one, + // it's always positive and won't exceed the tree height, so we can use an uint8 to store it. + preTrees := uint8(w.leafCounter - w.branchCounter) + + if err := w.writeRecursive(node.Left()); err != nil { + return err + } + keyLeaf := w.leafCounter + if err := w.writeRecursive(node.Right()); err != nil { + return err + } + + return w.writeBranch(node.Version(), uint32(node.Size()), node.Height(), preTrees, keyLeaf, node.Hash()) +} + +func createFile(name string) (*os.File, error) { + return os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) +} diff --git a/sc/memiavl/db/snapshot_test.go b/sc/memiavl/db/snapshot_test.go new file mode 100644 index 0000000..488a819 --- /dev/null +++ b/sc/memiavl/db/snapshot_test.go @@ -0,0 +1,193 @@ +package memiavl + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSnapshotEncodingRoundTrip(t *testing.T) { + // setup test tree + tree := New(0) + for _, changes := range ChangeSets[:len(ChangeSets)-1] { + tree.ApplyChangeSet(changes) + _, _, err := tree.SaveVersion(true) + require.NoError(t, err) + } + + snapshotDir := t.TempDir() + require.NoError(t, tree.WriteSnapshot(snapshotDir)) + + snapshot, err := OpenSnapshot(snapshotDir) + require.NoError(t, err) + + tree2 := NewFromSnapshot(snapshot, true, 0) + + require.Equal(t, tree.Version(), tree2.Version()) + require.Equal(t, tree.RootHash(), tree2.RootHash()) + + // verify all the node hashes in snapshot + for i := 0; i < snapshot.nodesLen(); i++ { + node := snapshot.Node(uint32(i)) + require.Equal(t, node.Hash(), HashNode(node)) + } + + require.NoError(t, snapshot.Close()) + + // test modify tree loaded from snapshot + snapshot, err = OpenSnapshot(snapshotDir) + require.NoError(t, err) + tree3 := NewFromSnapshot(snapshot, true, 0) + tree3.ApplyChangeSet(ChangeSets[len(ChangeSets)-1]) + hash, v, err := tree3.SaveVersion(true) + require.NoError(t, err) + require.Equal(t, RefHashes[len(ChangeSets)-1], hash) + require.Equal(t, len(ChangeSets), int(v)) + require.NoError(t, snapshot.Close()) +} + +func TestSnapshotExport(t *testing.T) { + expNodes := []*ExportNode{ + {Key: []byte("hello"), Value: []byte("world1"), Version: 2, Height: 0}, + {Key: []byte("hello1"), Value: []byte("world1"), Version: 2, Height: 0}, + {Key: []byte("hello1"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("hello2"), Value: []byte("world1"), Version: 3, Height: 0}, + {Key: []byte("hello3"), Value: []byte("world1"), Version: 3, Height: 0}, + {Key: []byte("hello3"), Value: nil, Version: 3, Height: 1}, + {Key: []byte("hello2"), Value: nil, Version: 3, Height: 2}, + } + + // setup test tree + tree := New(0) + for _, changes := range ChangeSets[:3] { + tree.ApplyChangeSet(changes) + _, _, err := tree.SaveVersion(true) + require.NoError(t, err) + } + + snapshotDir := t.TempDir() + require.NoError(t, tree.WriteSnapshot(snapshotDir)) + + snapshot, err := OpenSnapshot(snapshotDir) + require.NoError(t, err) + + var nodes []*ExportNode + exporter := snapshot.Export() + for { + node, err := exporter.Next() + if err == ErrorExportDone { + break + } + require.NoError(t, err) + nodes = append(nodes, node) + } + + require.Equal(t, expNodes, nodes) +} + +func TestSnapshotImportExport(t *testing.T) { + // setup test tree + tree := New(0) + for _, changes := range ChangeSets { + tree.ApplyChangeSet(changes) + _, _, err := tree.SaveVersion(true) + require.NoError(t, err) + } + + snapshotDir := t.TempDir() + require.NoError(t, tree.WriteSnapshot(snapshotDir)) + snapshot, err := OpenSnapshot(snapshotDir) + require.NoError(t, err) + + ch := make(chan *ExportNode) + + go func() { + defer close(ch) + + exporter := snapshot.Export() + for { + node, err := exporter.Next() + if err == ErrorExportDone { + break + } + require.NoError(t, err) + ch <- node + } + }() + + snapshotDir2 := t.TempDir() + err = doImport(snapshotDir2, tree.Version(), ch) + require.NoError(t, err) + + snapshot2, err := OpenSnapshot(snapshotDir2) + require.NoError(t, err) + require.Equal(t, snapshot.RootNode().Hash(), snapshot2.RootNode().Hash()) + + // verify all the node hashes in snapshot + for i := 0; i < snapshot2.nodesLen(); i++ { + node := snapshot2.Node(uint32(i)) + require.Equal(t, node.Hash(), HashNode(node)) + } +} + +func TestDBSnapshotRestore(t *testing.T) { + db, err := Load(t.TempDir(), Options{ + CreateIfMissing: true, + InitialStores: []string{"test", "test2"}, + AsyncCommitBuffer: -1, + }) + require.NoError(t, err) + + for _, changes := range ChangeSets { + cs := []*NamedChangeSet{ + { + Name: "test", + Changeset: changes, + }, + { + Name: "test2", + Changeset: changes, + }, + } + require.NoError(t, db.ApplyChangeSets(cs)) + _, err := db.Commit() + require.NoError(t, err) + + testSnapshotRoundTrip(t, db) + } + + require.NoError(t, db.RewriteSnapshot()) + require.NoError(t, db.Reload()) + require.Equal(t, len(ChangeSets), int(db.metadata.CommitInfo.Version)) + testSnapshotRoundTrip(t, db) +} + +func testSnapshotRoundTrip(t *testing.T, db *DB) { + exporter, err := NewMultiTreeExporter(db.dir, uint32(db.Version()), true) + require.NoError(t, err) + + restoreDir := t.TempDir() + importer, err := NewMultiTreeImporter(restoreDir, uint64(db.Version())) + require.NoError(t, err) + + for { + item, err := exporter.Next() + if err == ErrorExportDone { + break + } + require.NoError(t, err) + require.NoError(t, importer.Add(item)) + } + + require.NoError(t, exporter.Close()) + require.NoError(t, importer.Finalize()) + require.NoError(t, importer.Close()) + + db2, err := Load(restoreDir, Options{}) + require.NoError(t, err) + require.Equal(t, db.LastCommitInfo(), db2.LastCommitInfo()) + + // the imported db function normally + _, err = db2.Commit() + require.NoError(t, err) +} diff --git a/sc/memiavl/db/tree.go b/sc/memiavl/db/tree.go new file mode 100644 index 0000000..238d879 --- /dev/null +++ b/sc/memiavl/db/tree.go @@ -0,0 +1,300 @@ +package memiavl + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "math" + + "github.com/cosmos/iavl" + "github.com/cosmos/iavl/cache" +) + +var emptyHash = sha256.New().Sum(nil) + +func NewCache(cacheSize int) cache.Cache { + if cacheSize == 0 { + return nil + } + return cache.New(cacheSize) +} + +// verify change sets by replay them to rebuild iavl tree and verify the root hashes +type Tree struct { + version uint32 + // root node of empty tree is represented as `nil` + root Node + snapshot *Snapshot + + // simple lru cache provided by iavl library + cache cache.Cache + + initialVersion, cowVersion uint32 + + // when true, the get and iterator methods could return a slice pointing to mmaped blob files. + zeroCopy bool +} + +type cacheNode struct { + key, value []byte +} + +func (n *cacheNode) GetCacheKey() []byte { + return n.key +} + +func (n *cacheNode) GetKey() []byte { + return n.key +} + +// NewEmptyTree creates an empty tree at an arbitrary version. +func NewEmptyTree(version uint64, initialVersion uint32, cacheSize int) *Tree { + if version >= math.MaxUint32 { + panic("version overflows uint32") + } + + return &Tree{ + version: uint32(version), + initialVersion: initialVersion, + // no need to copy if the tree is not backed by snapshot + zeroCopy: true, + cache: NewCache(cacheSize), + } +} + +// New creates an empty tree at genesis version +func New(cacheSize int) *Tree { + return NewEmptyTree(0, 0, cacheSize) +} + +// New creates a empty tree with initial-version, +// it happens when a new store created at the middle of the chain. +func NewWithInitialVersion(initialVersion uint32, cacheSize int) *Tree { + return NewEmptyTree(0, initialVersion, cacheSize) +} + +// NewFromSnapshot mmap the blob files and create the root node. +func NewFromSnapshot(snapshot *Snapshot, zeroCopy bool, cacheSize int) *Tree { + tree := &Tree{ + version: snapshot.Version(), + snapshot: snapshot, + zeroCopy: zeroCopy, + cache: NewCache(cacheSize), + } + + if !snapshot.IsEmpty() { + tree.root = snapshot.RootNode() + } + + return tree +} + +func (t *Tree) SetZeroCopy(zeroCopy bool) { + t.zeroCopy = zeroCopy +} + +func (t *Tree) IsEmpty() bool { + return t.root == nil +} + +func (t *Tree) SetInitialVersion(initialVersion int64) error { + if initialVersion >= math.MaxUint32 { + return fmt.Errorf("version overflows uint32: %d", initialVersion) + } + t.initialVersion = uint32(initialVersion) + return nil +} + +// Copy returns a snapshot of the tree which won't be modified by further modifications on the main tree, +// the returned new tree can be accessed concurrently with the main tree. +func (t *Tree) Copy(cacheSize int) *Tree { + if _, ok := t.root.(*MemNode); ok { + // protect the existing `MemNode`s from get modified in-place + t.cowVersion = t.version + } + newTree := *t + // cache is not copied along because it's not thread-safe to access + newTree.cache = NewCache(cacheSize) + return &newTree +} + +// ApplyChangeSet apply the change set of a whole version, and update hashes. +func (t *Tree) ApplyChangeSet(changeSet iavl.ChangeSet) { + for _, pair := range changeSet.Pairs { + if pair.Delete { + t.remove(pair.Key) + } else { + t.set(pair.Key, pair.Value) + } + } +} + +func (t *Tree) set(key, value []byte) { + if value == nil { + // the value could be nil when replaying changes from write-ahead-log because of protobuf decoding + value = []byte{} + } + t.root, _ = setRecursive(t.root, key, value, t.version+1, t.cowVersion) + if t.cache != nil { + t.cache.Add(&cacheNode{key, value}) + } +} + +func (t *Tree) remove(key []byte) { + _, t.root, _ = removeRecursive(t.root, key, t.version+1, t.cowVersion) + if t.cache != nil { + t.cache.Remove(key) + } +} + +// SaveVersion increases the version number and optionally updates the hashes +func (t *Tree) SaveVersion(updateHash bool) ([]byte, int64, error) { + if t.version >= uint32(math.MaxUint32) { + return nil, 0, errors.New("version overflows uint32") + } + + var hash []byte + if updateHash { + hash = t.RootHash() + } + + t.version = nextVersionU32(t.version, t.initialVersion) + return hash, int64(t.version), nil +} + +// Version returns the current tree version +func (t *Tree) Version() int64 { + return int64(t.version) +} + +// RootHash updates the hashes and return the current root hash, +// it clones the persisted node's bytes, so the returned bytes is safe to retain. +func (t *Tree) RootHash() []byte { + if t.root == nil { + return emptyHash + } + return t.root.SafeHash() +} + +func (t *Tree) GetWithIndex(key []byte) (int64, []byte) { + if t.root == nil { + return 0, nil + } + + value, index := t.root.Get(key) + if !t.zeroCopy { + value = bytes.Clone(value) + } + return int64(index), value +} + +func (t *Tree) GetByIndex(index int64) ([]byte, []byte) { + if index > math.MaxUint32 { + return nil, nil + } + if t.root == nil { + return nil, nil + } + + key, value := t.root.GetByIndex(uint32(index)) + if !t.zeroCopy { + key = bytes.Clone(key) + value = bytes.Clone(value) + } + return key, value +} + +func (t *Tree) Get(key []byte) []byte { + if t.cache != nil { + if node := t.cache.Get(key); node != nil { + return node.(*cacheNode).value + } + } + + _, value := t.GetWithIndex(key) + if value == nil { + return nil + } + + if t.cache != nil { + t.cache.Add(&cacheNode{key, value}) + } + return value +} + +func (t *Tree) Has(key []byte) bool { + return t.Get(key) != nil +} + +func (t *Tree) Iterator(start, end []byte, ascending bool) *Iterator { + return NewIterator(start, end, ascending, t.root, t.zeroCopy) +} + +// ScanPostOrder scans the tree in post-order, and call the callback function on each node. +// If the callback function returns false, the scan will be stopped. +func (t *Tree) ScanPostOrder(callback func(node Node) bool) { + if t.root == nil { + return + } + + stack := []*stackEntry{{node: t.root}} + + for len(stack) > 0 { + entry := stack[len(stack)-1] + + if entry.node.IsLeaf() || entry.expanded { + callback(entry.node) + stack = stack[:len(stack)-1] + continue + } + + entry.expanded = true + stack = append(stack, &stackEntry{node: entry.node.Right()}) + stack = append(stack, &stackEntry{node: entry.node.Left()}) + } +} + +type stackEntry struct { + node Node + expanded bool +} + +// Export returns a snapshot of the tree which won't be corrupted by further modifications on the main tree. +func (t *Tree) Export() *Exporter { + if t.snapshot != nil && t.version == t.snapshot.Version() { + // snapshot export algorithm is more efficient + return t.snapshot.Export() + } + + // do normal post-order traversal export + return newExporter(func(callback func(node *ExportNode) bool) { + t.ScanPostOrder(func(node Node) bool { + return callback(&ExportNode{ + Key: node.Key(), + Value: node.Value(), + Version: int64(node.Version()), + Height: int8(node.Height()), + }) + }) + }) +} + +func (t *Tree) Close() error { + var err error + if t.snapshot != nil { + err = t.snapshot.Close() + t.snapshot = nil + } + t.root = nil + return err +} + +// nextVersionU32 is compatible with existing golang iavl implementation. +// see: https://github.com/cosmos/iavl/pull/660 +func nextVersionU32(v uint32, initialVersion uint32) uint32 { + if v == 0 && initialVersion > 1 { + return initialVersion + } + return v + 1 +} diff --git a/sc/memiavl/db/tree_test.go b/sc/memiavl/db/tree_test.go new file mode 100644 index 0000000..7900507 --- /dev/null +++ b/sc/memiavl/db/tree_test.go @@ -0,0 +1,272 @@ +package memiavl + +import ( + "fmt" + "strconv" + "testing" + + "github.com/cosmos/iavl" + "github.com/stretchr/testify/require" + db "github.com/tendermint/tm-db" +) + +var ( + ChangeSets []iavl.ChangeSet + RefHashes [][]byte + ExpectItems [][]pair +) + +func mockKVPairs(kvPairs ...string) []*iavl.KVPair { + result := make([]*iavl.KVPair, len(kvPairs)/2) + for i := 0; i < len(kvPairs); i += 2 { + result[i/2] = &iavl.KVPair{ + Key: []byte(kvPairs[i]), + Value: []byte(kvPairs[i+1]), + } + } + return result +} + +func init() { + ChangeSets = []iavl.ChangeSet{ + {Pairs: mockKVPairs("hello", "world")}, + {Pairs: mockKVPairs("hello", "world1", "hello1", "world1")}, + {Pairs: mockKVPairs("hello2", "world1", "hello3", "world1")}, + } + + changes := iavl.ChangeSet{} + for i := 0; i < 1; i++ { + changes.Pairs = append(changes.Pairs, &iavl.KVPair{Key: []byte(fmt.Sprintf("hello%02d", i)), Value: []byte("world1")}) + } + + ChangeSets = append(ChangeSets, changes) + ChangeSets = append(ChangeSets, iavl.ChangeSet{Pairs: []*iavl.KVPair{{Key: []byte("hello"), Delete: true}, {Key: []byte("hello19"), Delete: true}}}) + + changes = iavl.ChangeSet{} + for i := 0; i < 21; i++ { + changes.Pairs = append(changes.Pairs, &iavl.KVPair{Key: []byte(fmt.Sprintf("aello%02d", i)), Value: []byte("world1")}) + } + ChangeSets = append(ChangeSets, changes) + + changes = iavl.ChangeSet{} + for i := 0; i < 21; i++ { + changes.Pairs = append(changes.Pairs, &iavl.KVPair{Key: []byte(fmt.Sprintf("aello%02d", i)), Delete: true}) + } + for i := 0; i < 19; i++ { + changes.Pairs = append(changes.Pairs, &iavl.KVPair{Key: []byte(fmt.Sprintf("hello%02d", i)), Delete: true}) + } + ChangeSets = append(ChangeSets, changes) + + // generate ref hashes with ref impl + d := db.NewMemDB() + refTree, err := iavl.NewMutableTree(d, 0, true) + if err != nil { + panic(err) + } + for _, changes := range ChangeSets { + if err := applyChangeSetRef(refTree, changes); err != nil { + panic(err) + } + refHash, _, err := refTree.SaveVersion() + if err != nil { + panic(err) + } + RefHashes = append(RefHashes, refHash) + } + + ExpectItems = [][]pair{ + {}, + {{[]byte("hello"), []byte("world")}}, + { + {[]byte("hello"), []byte("world1")}, + {[]byte("hello1"), []byte("world1")}, + }, + { + {[]byte("hello"), []byte("world1")}, + {[]byte("hello1"), []byte("world1")}, + {[]byte("hello2"), []byte("world1")}, + {[]byte("hello3"), []byte("world1")}, + }, + { + {[]byte("hello"), []byte("world1")}, + {[]byte("hello00"), []byte("world1")}, + {[]byte("hello1"), []byte("world1")}, + {[]byte("hello2"), []byte("world1")}, + {[]byte("hello3"), []byte("world1")}, + }, + { + {[]byte("hello00"), []byte("world1")}, + {[]byte("hello1"), []byte("world1")}, + {[]byte("hello2"), []byte("world1")}, + {[]byte("hello3"), []byte("world1")}, + }, + { + {[]byte("aello00"), []byte("world1")}, + {[]byte("aello01"), []byte("world1")}, + {[]byte("aello02"), []byte("world1")}, + {[]byte("aello03"), []byte("world1")}, + {[]byte("aello04"), []byte("world1")}, + {[]byte("aello05"), []byte("world1")}, + {[]byte("aello06"), []byte("world1")}, + {[]byte("aello07"), []byte("world1")}, + {[]byte("aello08"), []byte("world1")}, + {[]byte("aello09"), []byte("world1")}, + {[]byte("aello10"), []byte("world1")}, + {[]byte("aello11"), []byte("world1")}, + {[]byte("aello12"), []byte("world1")}, + {[]byte("aello13"), []byte("world1")}, + {[]byte("aello14"), []byte("world1")}, + {[]byte("aello15"), []byte("world1")}, + {[]byte("aello16"), []byte("world1")}, + {[]byte("aello17"), []byte("world1")}, + {[]byte("aello18"), []byte("world1")}, + {[]byte("aello19"), []byte("world1")}, + {[]byte("aello20"), []byte("world1")}, + {[]byte("hello00"), []byte("world1")}, + {[]byte("hello1"), []byte("world1")}, + {[]byte("hello2"), []byte("world1")}, + {[]byte("hello3"), []byte("world1")}, + }, + { + {[]byte("hello1"), []byte("world1")}, + {[]byte("hello2"), []byte("world1")}, + {[]byte("hello3"), []byte("world1")}, + }, + } +} + +func applyChangeSetRef(t *iavl.MutableTree, changes iavl.ChangeSet) error { + for _, change := range changes.Pairs { + if change.Delete { + if _, _, err := t.Remove(change.Key); err != nil { + return err + } + } else { + if _, err := t.Set(change.Key, change.Value); err != nil { + return err + } + } + } + return nil +} + +func TestRootHashes(t *testing.T) { + tree := New(0) + + for i, changes := range ChangeSets { + tree.ApplyChangeSet(changes) + hash, v, err := tree.SaveVersion(true) + require.NoError(t, err) + require.Equal(t, i+1, int(v)) + require.Equal(t, RefHashes[i], hash) + } +} + +func TestNewKey(t *testing.T) { + tree := New(0) + + for i := 0; i < 4; i++ { + tree.set([]byte(fmt.Sprintf("key-%d", i)), []byte{1}) + } + _, _, err := tree.SaveVersion(true) + require.NoError(t, err) + + // the smallest key in the right half of the tree + require.Equal(t, tree.root.Key(), []byte("key-2")) + + // remove this key + tree.remove([]byte("key-2")) + + // check root node's key is changed + require.Equal(t, []byte("key-3"), tree.root.Key()) +} + +func TestEmptyTree(t *testing.T) { + tree := New(0) + require.Equal(t, emptyHash, tree.RootHash()) +} + +func TestTreeCopy(t *testing.T) { + tree := New(0) + + tree.ApplyChangeSet(iavl.ChangeSet{Pairs: []*iavl.KVPair{ + {Key: []byte("hello"), Value: []byte("world")}, + }}) + _, _, err := tree.SaveVersion(true) + require.NoError(t, err) + + snapshot := tree.Copy(0) + + tree.ApplyChangeSet(iavl.ChangeSet{Pairs: []*iavl.KVPair{ + {Key: []byte("hello"), Value: []byte("world1")}, + }}) + _, _, err = tree.SaveVersion(true) + require.NoError(t, err) + + require.Equal(t, []byte("world1"), tree.Get([]byte("hello"))) + require.Equal(t, []byte("world"), snapshot.Get([]byte("hello"))) + + // check that normal copy don't work + fakeSnapshot := *tree + + tree.ApplyChangeSet(iavl.ChangeSet{Pairs: []*iavl.KVPair{ + {Key: []byte("hello"), Value: []byte("world2")}, + }}) + _, _, err = tree.SaveVersion(true) + require.NoError(t, err) + + // get modified in-place + require.Equal(t, []byte("world2"), tree.Get([]byte("hello"))) + require.Equal(t, []byte("world2"), fakeSnapshot.Get([]byte("hello"))) +} + +func TestChangeSetMarshal(t *testing.T) { + for _, changes := range ChangeSets { + bz, err := changes.Marshal() + require.NoError(t, err) + + var cs iavl.ChangeSet + require.NoError(t, cs.Unmarshal(bz)) + require.Equal(t, changes, cs) + } +} + +func TestGetByIndex(t *testing.T) { + changes := iavl.ChangeSet{} + for i := 0; i < 20; i++ { + changes.Pairs = append(changes.Pairs, &iavl.KVPair{Key: []byte(fmt.Sprintf("hello%02d", i)), Value: []byte(strconv.Itoa(i))}) + } + + tree := New(0) + tree.ApplyChangeSet(changes) + _, _, err := tree.SaveVersion(true) + require.NoError(t, err) + + for i, pair := range changes.Pairs { + idx, v := tree.GetWithIndex(pair.Key) + require.Equal(t, pair.Value, v) + require.Equal(t, int64(i), idx) + + k, v := tree.GetByIndex(idx) + require.Equal(t, pair.Key, k) + require.Equal(t, pair.Value, v) + } + + // test persisted tree + dir := t.TempDir() + require.NoError(t, tree.WriteSnapshot(dir)) + snapshot, err := OpenSnapshot(dir) + require.NoError(t, err) + ptree := NewFromSnapshot(snapshot, true, 0) + defer ptree.Close() + + for i, pair := range changes.Pairs { + idx, v := ptree.GetWithIndex(pair.Key) + require.Equal(t, pair.Value, v) + require.Equal(t, int64(i), idx) + + k, v := ptree.GetByIndex(idx) + require.Equal(t, pair.Key, k) + require.Equal(t, pair.Value, v) + } +} diff --git a/sc/memiavl/db/types.go b/sc/memiavl/db/types.go new file mode 100644 index 0000000..16c82f1 --- /dev/null +++ b/sc/memiavl/db/types.go @@ -0,0 +1,34 @@ +package memiavl + +import fmt "fmt" + +// Logger is what any CometBFT library should take. +type Logger interface { + Debug(msg string, keyvals ...interface{}) + Info(msg string, keyvals ...interface{}) + Error(msg string, keyvals ...interface{}) +} + +type nopLogger struct{} + +// Interface assertions +var _ Logger = (*nopLogger)(nil) + +// NewNopLogger returns a logger that doesn't do anything. +func NewNopLogger() Logger { return &nopLogger{} } + +func (nopLogger) Info(string, ...interface{}) {} +func (nopLogger) Debug(string, ...interface{}) {} +func (nopLogger) Error(string, ...interface{}) {} + +// ExportNode contains exported node data. +type ExportNode struct { + Key []byte + Value []byte + Version int64 + Height int8 +} + +func (cid CommitID) String() string { + return fmt.Sprintf("CommitID{%v:%X}", cid.Hash, cid.Version) +} diff --git a/sc/memiavl/db/wal.go b/sc/memiavl/db/wal.go new file mode 100644 index 0000000..cb5b0d8 --- /dev/null +++ b/sc/memiavl/db/wal.go @@ -0,0 +1,100 @@ +package memiavl + +import ( + "bytes" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "unsafe" + + "github.com/tidwall/gjson" + "github.com/tidwall/wal" +) + +// OpenWAL opens the write ahead log, try to truncate the corrupted tail if there's any +// TODO fix in upstream: https://github.com/tidwall/wal/pull/22 +func OpenWAL(dir string, opts *wal.Options) (*wal.Log, error) { + log, err := wal.Open(dir, opts) + if err == wal.ErrCorrupt { + // try to truncate corrupted tail + var fis []os.DirEntry + fis, err = os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("read wal dir fail: %w", err) + } + var lastSeg string + for _, fi := range fis { + if fi.IsDir() || len(fi.Name()) < 20 { + continue + } + lastSeg = fi.Name() + } + + if len(lastSeg) == 0 { + return nil, err + } + if err = truncateCorruptedTail(filepath.Join(dir, lastSeg), opts.LogFormat); err != nil { + return nil, fmt.Errorf("truncate corrupted tail fail: %w", err) + } + + // try again + return wal.Open(dir, opts) + } + + return log, err +} + +func truncateCorruptedTail(path string, format wal.LogFormat) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + var pos int + for len(data) > 0 { + var n int + if format == wal.JSON { + n, err = loadNextJSONEntry(data) + } else { + n, err = loadNextBinaryEntry(data) + } + if err == wal.ErrCorrupt { + break + } + if err != nil { + return err + } + data = data[n:] + pos += n + } + if pos != len(data) { + return os.Truncate(path, int64(pos)) + } + return nil +} + +func loadNextJSONEntry(data []byte) (n int, err error) { + // {"index":number,"data":string} + idx := bytes.IndexByte(data, '\n') + if idx == -1 { + return 0, wal.ErrCorrupt + } + line := data[:idx] + dres := gjson.Get(*(*string)(unsafe.Pointer(&line)), "data") + if dres.Type != gjson.String { + return 0, wal.ErrCorrupt + } + return idx + 1, nil +} + +func loadNextBinaryEntry(data []byte) (n int, err error) { + // data_size + data + size, n := binary.Uvarint(data) + if n <= 0 { + return 0, wal.ErrCorrupt + } + if uint64(len(data)-n) < size { + return 0, wal.ErrCorrupt + } + return n + int(size), nil +} diff --git a/sc/memiavl/db/wal.pb.go b/sc/memiavl/db/wal.pb.go new file mode 100644 index 0000000..a8a1537 --- /dev/null +++ b/sc/memiavl/db/wal.pb.go @@ -0,0 +1,1110 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: memiavl/wal.proto + +package memiavl + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + proto1 "github.com/cosmos/iavl/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// NamedChangeSet combine a tree name with the changeset +type NamedChangeSet struct { + Changeset proto1.ChangeSet `protobuf:"bytes,1,opt,name=changeset,proto3" json:"changeset"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` +} + +func (m *NamedChangeSet) Reset() { *m = NamedChangeSet{} } +func (m *NamedChangeSet) String() string { return proto.CompactTextString(m) } +func (*NamedChangeSet) ProtoMessage() {} +func (*NamedChangeSet) Descriptor() ([]byte, []int) { + return fileDescriptor_3a36f610a0003eaf, []int{0} +} +func (m *NamedChangeSet) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *NamedChangeSet) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_NamedChangeSet.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *NamedChangeSet) XXX_Merge(src proto.Message) { + xxx_messageInfo_NamedChangeSet.Merge(m, src) +} +func (m *NamedChangeSet) XXX_Size() int { + return m.Size() +} +func (m *NamedChangeSet) XXX_DiscardUnknown() { + xxx_messageInfo_NamedChangeSet.DiscardUnknown(m) +} + +var xxx_messageInfo_NamedChangeSet proto.InternalMessageInfo + +func (m *NamedChangeSet) GetChangeset() proto1.ChangeSet { + if m != nil { + return m.Changeset + } + return proto1.ChangeSet{} +} + +func (m *NamedChangeSet) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +// TreeNameUpgrade defines upgrade of tree names: +// - New tree: { name: "tree" } +// - Delete tree: { name: "tree", delete: true } +// - Rename tree: { name: "new-tree", rename_from: "old-tree" } +type TreeNameUpgrade struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + RenameFrom string `protobuf:"bytes,2,opt,name=rename_from,json=renameFrom,proto3" json:"rename_from,omitempty"` + Delete bool `protobuf:"varint,3,opt,name=delete,proto3" json:"delete,omitempty"` +} + +func (m *TreeNameUpgrade) Reset() { *m = TreeNameUpgrade{} } +func (m *TreeNameUpgrade) String() string { return proto.CompactTextString(m) } +func (*TreeNameUpgrade) ProtoMessage() {} +func (*TreeNameUpgrade) Descriptor() ([]byte, []int) { + return fileDescriptor_3a36f610a0003eaf, []int{1} +} +func (m *TreeNameUpgrade) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *TreeNameUpgrade) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_TreeNameUpgrade.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *TreeNameUpgrade) XXX_Merge(src proto.Message) { + xxx_messageInfo_TreeNameUpgrade.Merge(m, src) +} +func (m *TreeNameUpgrade) XXX_Size() int { + return m.Size() +} +func (m *TreeNameUpgrade) XXX_DiscardUnknown() { + xxx_messageInfo_TreeNameUpgrade.DiscardUnknown(m) +} + +var xxx_messageInfo_TreeNameUpgrade proto.InternalMessageInfo + +func (m *TreeNameUpgrade) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *TreeNameUpgrade) GetRenameFrom() string { + if m != nil { + return m.RenameFrom + } + return "" +} + +func (m *TreeNameUpgrade) GetDelete() bool { + if m != nil { + return m.Delete + } + return false +} + +// WALEntry is a single Write-Ahead-Log entry +type WALEntry struct { + Changesets []*NamedChangeSet `protobuf:"bytes,1,rep,name=changesets,proto3" json:"changesets,omitempty"` + Upgrades []*TreeNameUpgrade `protobuf:"bytes,2,rep,name=upgrades,proto3" json:"upgrades,omitempty"` +} + +func (m *WALEntry) Reset() { *m = WALEntry{} } +func (m *WALEntry) String() string { return proto.CompactTextString(m) } +func (*WALEntry) ProtoMessage() {} +func (*WALEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_3a36f610a0003eaf, []int{2} +} +func (m *WALEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *WALEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_WALEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *WALEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_WALEntry.Merge(m, src) +} +func (m *WALEntry) XXX_Size() int { + return m.Size() +} +func (m *WALEntry) XXX_DiscardUnknown() { + xxx_messageInfo_WALEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_WALEntry proto.InternalMessageInfo + +func (m *WALEntry) GetChangesets() []*NamedChangeSet { + if m != nil { + return m.Changesets + } + return nil +} + +func (m *WALEntry) GetUpgrades() []*TreeNameUpgrade { + if m != nil { + return m.Upgrades + } + return nil +} + +// MultiTreeMetadata stores the metadata for MultiTree +type MultiTreeMetadata struct { + CommitInfo *CommitInfo `protobuf:"bytes,1,opt,name=commit_info,json=commitInfo,proto3" json:"commit_info,omitempty"` + InitialVersion int64 `protobuf:"varint,2,opt,name=initial_version,json=initialVersion,proto3" json:"initial_version,omitempty"` +} + +func (m *MultiTreeMetadata) Reset() { *m = MultiTreeMetadata{} } +func (m *MultiTreeMetadata) String() string { return proto.CompactTextString(m) } +func (*MultiTreeMetadata) ProtoMessage() {} +func (*MultiTreeMetadata) Descriptor() ([]byte, []int) { + return fileDescriptor_3a36f610a0003eaf, []int{3} +} +func (m *MultiTreeMetadata) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MultiTreeMetadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MultiTreeMetadata.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MultiTreeMetadata) XXX_Merge(src proto.Message) { + xxx_messageInfo_MultiTreeMetadata.Merge(m, src) +} +func (m *MultiTreeMetadata) XXX_Size() int { + return m.Size() +} +func (m *MultiTreeMetadata) XXX_DiscardUnknown() { + xxx_messageInfo_MultiTreeMetadata.DiscardUnknown(m) +} + +var xxx_messageInfo_MultiTreeMetadata proto.InternalMessageInfo + +func (m *MultiTreeMetadata) GetCommitInfo() *CommitInfo { + if m != nil { + return m.CommitInfo + } + return nil +} + +func (m *MultiTreeMetadata) GetInitialVersion() int64 { + if m != nil { + return m.InitialVersion + } + return 0 +} + +func init() { + proto.RegisterType((*NamedChangeSet)(nil), "memiavl.NamedChangeSet") + proto.RegisterType((*TreeNameUpgrade)(nil), "memiavl.TreeNameUpgrade") + proto.RegisterType((*WALEntry)(nil), "memiavl.WALEntry") + proto.RegisterType((*MultiTreeMetadata)(nil), "memiavl.MultiTreeMetadata") +} + +func init() { proto.RegisterFile("memiavl/wal.proto", fileDescriptor_3a36f610a0003eaf) } + +var fileDescriptor_3a36f610a0003eaf = []byte{ + // 397 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x91, 0x4f, 0x6f, 0xd3, 0x30, + 0x18, 0xc6, 0xe3, 0x75, 0x1a, 0xdd, 0x1b, 0x69, 0xd5, 0xcc, 0x04, 0x61, 0x87, 0x2c, 0xca, 0x85, + 0x08, 0x69, 0x89, 0xb4, 0x4d, 0xe2, 0xcc, 0x0a, 0x48, 0x48, 0x94, 0x43, 0xf8, 0x27, 0x38, 0x50, + 0xb9, 0x89, 0x9b, 0x5a, 0x8a, 0xed, 0xca, 0x71, 0x8b, 0xfa, 0x2d, 0xf8, 0x58, 0x3d, 0xf6, 0xc8, + 0x09, 0xa1, 0xf6, 0x8b, 0xa0, 0x38, 0xc6, 0x2d, 0xdc, 0xde, 0x3c, 0xfe, 0x3d, 0x8f, 0x9e, 0xf7, + 0x0d, 0x9c, 0x73, 0xca, 0x19, 0x59, 0xd6, 0xd9, 0x77, 0x52, 0xa7, 0x73, 0x25, 0xb5, 0xc4, 0x0f, + 0xac, 0x74, 0x79, 0x51, 0xc9, 0x4a, 0x1a, 0x2d, 0x6b, 0xa7, 0xee, 0xf9, 0xf2, 0xc2, 0xe0, 0xc5, + 0x8c, 0x88, 0x8a, 0x36, 0x54, 0x5b, 0xf5, 0xc9, 0xdf, 0x9c, 0x42, 0x72, 0xce, 0xf4, 0x98, 0x89, + 0xa9, 0x35, 0xc4, 0x5f, 0xe0, 0xec, 0x1d, 0xe1, 0xb4, 0x1c, 0x1a, 0xcb, 0x7b, 0xaa, 0xf1, 0x2d, + 0x9c, 0x3a, 0x7f, 0x80, 0x22, 0x94, 0xf8, 0x37, 0x83, 0xb4, 0x75, 0xa7, 0x8e, 0xb9, 0x3f, 0x5e, + 0xff, 0xba, 0xf2, 0xf2, 0x3d, 0x87, 0x31, 0x1c, 0x0b, 0xc2, 0x69, 0x70, 0x14, 0xa1, 0xe4, 0x34, + 0x37, 0x73, 0xfc, 0x0d, 0x06, 0x1f, 0x14, 0xa5, 0x6d, 0xfc, 0xc7, 0x79, 0xa5, 0x48, 0x49, 0x1d, + 0x86, 0xf6, 0x18, 0xbe, 0x02, 0x5f, 0xd1, 0x76, 0x1a, 0x4f, 0x95, 0xe4, 0x36, 0x01, 0x3a, 0xe9, + 0xb5, 0x92, 0x1c, 0x3f, 0x82, 0x93, 0x92, 0xd6, 0x54, 0xd3, 0xa0, 0x17, 0xa1, 0xa4, 0x9f, 0xdb, + 0xaf, 0x78, 0x05, 0xfd, 0xcf, 0x2f, 0xde, 0xbe, 0x12, 0x5a, 0xad, 0xf0, 0x73, 0x00, 0x57, 0xa6, + 0x09, 0x50, 0xd4, 0x4b, 0xfc, 0x9b, 0xc7, 0xa9, 0x5d, 0x3b, 0xfd, 0x77, 0xc3, 0xfc, 0x00, 0xc5, + 0x77, 0xd0, 0x5f, 0x74, 0xe5, 0x9a, 0xe0, 0xc8, 0xd8, 0x02, 0x67, 0xfb, 0xaf, 0x7d, 0xee, 0xc8, + 0x58, 0xc1, 0xf9, 0x68, 0x51, 0x6b, 0xd6, 0x12, 0x23, 0xaa, 0x49, 0x49, 0x34, 0xc1, 0x77, 0xe0, + 0x1f, 0xdc, 0xd7, 0x9e, 0xee, 0xa1, 0x4b, 0x1b, 0x9a, 0xb7, 0x37, 0x62, 0x2a, 0x73, 0x28, 0xdc, + 0x8c, 0x9f, 0xc2, 0x80, 0x09, 0xa6, 0x19, 0xa9, 0xc7, 0x4b, 0xaa, 0x1a, 0x26, 0x85, 0x39, 0x41, + 0x2f, 0x3f, 0xb3, 0xf2, 0xa7, 0x4e, 0xbd, 0x7f, 0xb9, 0xde, 0x86, 0x68, 0xb3, 0x0d, 0xd1, 0xef, + 0x6d, 0x88, 0x7e, 0xec, 0x42, 0x6f, 0xb3, 0x0b, 0xbd, 0x9f, 0xbb, 0xd0, 0xfb, 0xfa, 0xac, 0x62, + 0x7a, 0xb6, 0x98, 0xa4, 0x85, 0xe4, 0x59, 0xa1, 0x56, 0x73, 0x2d, 0xaf, 0xa5, 0xaa, 0xae, 0x8b, + 0x19, 0x61, 0x22, 0x2b, 0x94, 0x14, 0xb2, 0xc9, 0x6c, 0x8b, 0xc9, 0x89, 0xf9, 0xed, 0xb7, 0x7f, + 0x02, 0x00, 0x00, 0xff, 0xff, 0xc2, 0xf8, 0x73, 0x7d, 0x5b, 0x02, 0x00, 0x00, +} + +func (m *NamedChangeSet) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *NamedChangeSet) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *NamedChangeSet) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintWal(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0x12 + } + { + size, err := m.Changeset.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintWal(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *TreeNameUpgrade) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TreeNameUpgrade) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *TreeNameUpgrade) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Delete { + i-- + if m.Delete { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x18 + } + if len(m.RenameFrom) > 0 { + i -= len(m.RenameFrom) + copy(dAtA[i:], m.RenameFrom) + i = encodeVarintWal(dAtA, i, uint64(len(m.RenameFrom))) + i-- + dAtA[i] = 0x12 + } + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintWal(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *WALEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *WALEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *WALEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Upgrades) > 0 { + for iNdEx := len(m.Upgrades) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Upgrades[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintWal(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if len(m.Changesets) > 0 { + for iNdEx := len(m.Changesets) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Changesets[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintWal(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *MultiTreeMetadata) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MultiTreeMetadata) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MultiTreeMetadata) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.InitialVersion != 0 { + i = encodeVarintWal(dAtA, i, uint64(m.InitialVersion)) + i-- + dAtA[i] = 0x10 + } + if m.CommitInfo != nil { + { + size, err := m.CommitInfo.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintWal(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintWal(dAtA []byte, offset int, v uint64) int { + offset -= sovWal(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *NamedChangeSet) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.Changeset.Size() + n += 1 + l + sovWal(uint64(l)) + l = len(m.Name) + if l > 0 { + n += 1 + l + sovWal(uint64(l)) + } + return n +} + +func (m *TreeNameUpgrade) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + sovWal(uint64(l)) + } + l = len(m.RenameFrom) + if l > 0 { + n += 1 + l + sovWal(uint64(l)) + } + if m.Delete { + n += 2 + } + return n +} + +func (m *WALEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Changesets) > 0 { + for _, e := range m.Changesets { + l = e.Size() + n += 1 + l + sovWal(uint64(l)) + } + } + if len(m.Upgrades) > 0 { + for _, e := range m.Upgrades { + l = e.Size() + n += 1 + l + sovWal(uint64(l)) + } + } + return n +} + +func (m *MultiTreeMetadata) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.CommitInfo != nil { + l = m.CommitInfo.Size() + n += 1 + l + sovWal(uint64(l)) + } + if m.InitialVersion != 0 { + n += 1 + sovWal(uint64(m.InitialVersion)) + } + return n +} + +func sovWal(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozWal(x uint64) (n int) { + return sovWal(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *NamedChangeSet) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: NamedChangeSet: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: NamedChangeSet: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Changeset", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthWal + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthWal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Changeset.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthWal + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthWal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipWal(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *TreeNameUpgrade) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: TreeNameUpgrade: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: TreeNameUpgrade: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthWal + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthWal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RenameFrom", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthWal + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthWal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RenameFrom = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Delete", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Delete = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipWal(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *WALEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: WALEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: WALEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Changesets", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthWal + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthWal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Changesets = append(m.Changesets, &NamedChangeSet{}) + if err := m.Changesets[len(m.Changesets)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Upgrades", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthWal + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthWal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Upgrades = append(m.Upgrades, &TreeNameUpgrade{}) + if err := m.Upgrades[len(m.Upgrades)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipWal(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MultiTreeMetadata) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MultiTreeMetadata: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MultiTreeMetadata: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CommitInfo", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthWal + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthWal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.CommitInfo == nil { + m.CommitInfo = &CommitInfo{} + } + if err := m.CommitInfo.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field InitialVersion", wireType) + } + m.InitialVersion = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowWal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.InitialVersion |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipWal(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthWal + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipWal(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowWal + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowWal + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowWal + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthWal + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupWal + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthWal + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthWal = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowWal = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupWal = fmt.Errorf("proto: unexpected end of group") +) diff --git a/sc/memiavl/db/wal_test.go b/sc/memiavl/db/wal_test.go new file mode 100644 index 0000000..5c24d8d --- /dev/null +++ b/sc/memiavl/db/wal_test.go @@ -0,0 +1,45 @@ +package memiavl + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/wal" +) + +func TestCorruptedTail(t *testing.T) { + opts := &wal.Options{ + LogFormat: wal.JSON, + } + dir := t.TempDir() + + testCases := []struct { + name string + logs []byte + lastIndex uint64 + }{ + {"failure-1", []byte("\n"), 0}, + {"failure-2", []byte(`{}` + "\n"), 0}, + {"failure-3", []byte(`{"index":"1"}` + "\n"), 0}, + {"failure-4", []byte(`{"index":"1","data":"?"}`), 0}, + {"failure-5", []byte(`{"index":1,"data":"?"}` + "\n" + `{"index":"1","data":"?"}`), 1}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + os.WriteFile(filepath.Join(dir, "00000000000000000001"), tc.logs, 0o600) + + _, err := wal.Open(dir, opts) + require.Equal(t, wal.ErrCorrupt, err) + + log, err := OpenWAL(dir, opts) + require.NoError(t, err) + + lastIndex, err := log.LastIndex() + require.NoError(t, err) + require.Equal(t, tc.lastIndex, lastIndex) + }) + } +} diff --git a/sc/memiavl/go.mod b/sc/memiavl/go.mod new file mode 100644 index 0000000..77df2e8 --- /dev/null +++ b/sc/memiavl/go.mod @@ -0,0 +1,70 @@ +module github.com/sei-protocol/sei-db/memiavl + +go 1.19 + +require ( + cosmossdk.io/errors v1.0.0 + github.com/alitto/pond v1.8.3 + github.com/confio/ics23/go v0.9.0 + github.com/cosmos/cosmos-sdk v0.45.10 + github.com/cosmos/gogoproto v1.4.11 + github.com/cosmos/iavl v0.21.0-alpha.1.0.20230904092046-df3db2d96583 + github.com/ledgerwatch/erigon-lib v0.0.0-20230210071639-db0e7ed11263 + github.com/spf13/cast v1.5.0 + github.com/stretchr/testify v1.8.4 + github.com/tendermint/tm-db v0.6.8-0.20220519162814-e24b96538a12 + github.com/tidwall/btree v1.6.0 + github.com/tidwall/gjson v1.10.2 + github.com/tidwall/wal v1.1.7 + github.com/zbiljic/go-filelock v0.0.0-20170914061330-1dbf7103ab7d + golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb +) + +require ( + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cosmos/gorocksdb v1.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/badger/v3 v3.2103.2 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gogo/protobuf v1.3.3 // indirect + github.com/golang/glog v1.1.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/flatbuffers v1.12.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/linxGnu/grocksdb v1.8.4 // indirect + github.com/onsi/gomega v1.20.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect + github.com/tendermint/tendermint v0.37.0-dev // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/tinylru v1.1.0 // indirect + go.etcd.io/bbolt v1.3.7 // indirect + go.opencensus.io v0.23.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace ( + github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.2.61-0.20230902071955-518544a18bab + github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.1.8-0.20230726213826-031d03d26f2d + github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 + github.com/tendermint/tendermint => github.com/sei-protocol/sei-tendermint v0.2.28 + github.com/tendermint/tm-db => github.com/sei-protocol/tm-db v0.0.4 +) diff --git a/sc/memiavl/go.sum b/sc/memiavl/go.sum new file mode 100644 index 0000000..abee2ca --- /dev/null +++ b/sc/memiavl/go.sum @@ -0,0 +1,342 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cosmossdk.io/errors v1.0.0/go.mod h1:+hJZLuhdDE0pYN8HkOrVNwrIOYvUGnn6+4fjnJs/oV0= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs= +github.com/alitto/pond v1.8.3/go.mod h1:CmvIIGd5jKLasGI3D87qDkQxjzChdKMmnXMg3fG6M6Q= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cometbft/cometbft v0.37.3-0.20230920093934-46df7b597e3c/go.mod h1:gC4iJBO09opzl1pLiPl4fYWn8rRrfO713rRM5tZn5E4= +github.com/cometbft/cometbft-db v0.8.0 h1:vUMDaH3ApkX8m0KZvOFFy9b5DZHBAjsnEuo9AKVZpjo= +github.com/cometbft/cometbft-db v0.8.0/go.mod h1:6ASCP4pfhmrCBpfk01/9E1SI29nD3HfVHrY4PG8x5c0= +github.com/confio/ics23/go v0.9.0 h1:cWs+wdbS2KRPZezoaaj+qBleXgUk5WOQFMP3CQFGTr4= +github.com/confio/ics23/go v0.9.0/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cosmos/gogoproto v1.4.11 h1:LZcMHrx4FjUgrqQSWeaGC1v/TeuVFqSLa43CC6aWR2g= +github.com/cosmos/gogoproto v1.4.11/go.mod h1:/g39Mh8m17X8Q/GDEs5zYTSNaNnInBSohtaxzQnYq1Y= +github.com/cosmos/gorocksdb v1.2.0 h1:d0l3jJG8M4hBouIZq0mDUHZ+zjOx044J3nGRskwTb4Y= +github.com/cosmos/gorocksdb v1.2.0/go.mod h1:aaKvKItm514hKfNJpUJXnnOWeBnk2GL4+Qw9NHizILw= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crypto-org-chain/cronos/memiavl v0.0.4/go.mod h1:sCbJoEppeM3/7+Ox1heGlbVt+eWBcTmLb9UEjuotXIc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= +github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= +github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ledgerwatch/erigon-lib v0.0.0-20230210071639-db0e7ed11263 h1:LGEzZvf33Y1NhuP5+jI/ni9l1TFS6oYPDilgy74NusM= +github.com/ledgerwatch/erigon-lib v0.0.0-20230210071639-db0e7ed11263/go.mod h1:OXgMDuUo2lZ3NpH29ZvMYbk+LxFd5ffDl2Z2mGMuY/I= +github.com/linxGnu/grocksdb v1.8.4 h1:ZMsBpPpJNtRLHiKKp0mI7gW+NT4s7UgfD5xHxx1jVRo= +github.com/linxGnu/grocksdb v1.8.4/go.mod h1:xZCIb5Muw+nhbDK4Y5UJuOrin5MceOuiXkVUR7vp4WY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= +github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sei-protocol/sei-cosmos v0.2.61-0.20230902071955-518544a18bab/go.mod h1:fAI6mTwF3fDMJlMM6bkfV1AQXA073CvV83NhEaT7pyk= +github.com/sei-protocol/sei-iavl v0.1.8-0.20230726213826-031d03d26f2d h1:FWWK/dxqrurTRR3SBlpk0v/FgXef+Ev7/d3X1KBPrTo= +github.com/sei-protocol/sei-iavl v0.1.8-0.20230726213826-031d03d26f2d/go.mod h1:7PfkEVT5dcoQE+s/9KWdoXJ8VVVP1QpYYPLdxlkSXFk= +github.com/sei-protocol/sei-tendermint v0.2.28 h1:5PB1a/zu6H2iDbxIMnXgDtB4QwV5PZRikguX8gASLGI= +github.com/sei-protocol/sei-tendermint v0.2.28/go.mod h1:+BtDvAwTkE64BlxzpH9ZP7S6vUYT9wRXiZa/WW8/o4g= +github.com/sei-protocol/tm-db v0.0.4 h1:7Y4EU62Xzzg6wKAHEotm7SXQR0aPLcGhKHkh3qd0tnk= +github.com/sei-protocol/tm-db v0.0.4/go.mod h1:PWsIWOTwdwC7Ow/GUvx8HgUJTO691pBuorIQD8JvwAs= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= +github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo= +github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I= +github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8= +github.com/tidwall/wal v1.1.7 h1:emc1TRjIVsdKKSnpwGBAcsAGg0767SvUk8+ygx7Bb+4= +github.com/tidwall/wal v1.1.7/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zbiljic/go-filelock v0.0.0-20170914061330-1dbf7103ab7d h1:XQyeLr7N9iY9mi+TGgsBFkj54+j3fdoo8e2u6zrGP5A= +github.com/zbiljic/go-filelock v0.0.0-20170914061330-1dbf7103ab7d/go.mod h1:hoMeDjlNXTNqVwrCk8YDyaBS2g5vFfEX2ezMi4vb6CY= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA= +golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sc/memiavl/store/cachemulti/store.go b/sc/memiavl/store/cachemulti/store.go new file mode 100644 index 0000000..65d866a --- /dev/null +++ b/sc/memiavl/store/cachemulti/store.go @@ -0,0 +1,37 @@ +package cachemulti + +import ( + "io" + + "github.com/cosmos/cosmos-sdk/store/cachemulti" + "github.com/cosmos/cosmos-sdk/store/types" + dbm "github.com/tendermint/tm-db" +) + +var NoopCloser io.Closer = CloserFunc(func() error { return nil }) + +type CloserFunc func() error + +func (fn CloserFunc) Close() error { + return fn() +} + +// Store wraps sdk's cachemulti.Store to add io.Closer interface +type Store struct { + cachemulti.Store + io.Closer +} + +func NewStore( + db dbm.DB, stores map[types.StoreKey]types.CacheWrapper, keys map[string]types.StoreKey, + traceWriter io.Writer, traceContext types.TraceContext, listeners map[types.StoreKey][]types.WriteListener, + closer io.Closer, +) Store { + if closer == nil { + closer = NoopCloser + } + return Store{ + Store: cachemulti.NewStore(db, stores, keys, traceWriter, traceContext, listeners), + Closer: closer, + } +} diff --git a/sc/memiavl/store/memiavlstore/store.go b/sc/memiavl/store/memiavlstore/store.go new file mode 100644 index 0000000..4bd4466 --- /dev/null +++ b/sc/memiavl/store/memiavlstore/store.go @@ -0,0 +1,210 @@ +package memiavlstore + +import ( + "cosmossdk.io/errors" + "fmt" + ics23 "github.com/confio/ics23/go" + "github.com/cosmos/cosmos-sdk/store/listenkv" + "io" + + "github.com/cosmos/cosmos-sdk/store/tracekv" + "github.com/cosmos/iavl" + "github.com/sei-protocol/sei-db/memiavl/db" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + tmcrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" + + "github.com/cosmos/cosmos-sdk/store/cachekv" + "github.com/cosmos/cosmos-sdk/store/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/kv" +) + +var ( + _ types.KVStore = (*Store)(nil) + _ types.CommitStore = (*Store)(nil) + _ types.CommitKVStore = (*Store)(nil) + _ types.Queryable = (*Store)(nil) +) + +// Store Implements types.KVStore and CommitKVStore. +type Store struct { + tree *memiavl.Tree + logger log.Logger + + changeSet iavl.ChangeSet +} + +func New(tree *memiavl.Tree, logger log.Logger) *Store { + return &Store{tree: tree, logger: logger} +} + +func (st *Store) Commit(_ bool) types.CommitID { + panic("memiavl store is not supposed to be committed alone") +} + +func (st *Store) LastCommitID() types.CommitID { + hash := st.tree.RootHash() + return types.CommitID{ + Version: st.tree.Version(), + Hash: hash, + } +} + +// SetPruning panics as pruning options should be provided at initialization +// since IAVl accepts pruning options directly. +func (st *Store) SetPruning(_ types.PruningOptions) { + panic("cannot set pruning options on an initialized IAVL store") +} + +// SetPruning panics as pruning options should be provided at initialization +// since IAVl accepts pruning options directly. +func (st *Store) GetPruning() types.PruningOptions { + panic("cannot get pruning options on an initialized IAVL store") +} + +func (st *Store) GetWorkingHash() ([]byte, error) { + panic("not implemented") +} + +// Implements Store. +func (st *Store) GetStoreType() types.StoreType { + return types.StoreTypeIAVL +} + +func (st *Store) CacheWrap(k types.StoreKey) types.CacheWrap { + return cachekv.NewStore(st, k, types.DefaultCacheSizeLimit) +} + +// CacheWrapWithTrace implements the Store interface. +func (st *Store) CacheWrapWithTrace(k types.StoreKey, w io.Writer, tc types.TraceContext) types.CacheWrap { + return cachekv.NewStore(tracekv.NewStore(st, w, tc), k, types.DefaultCacheSizeLimit) +} + +func (st *Store) CacheWrapWithListeners(k types.StoreKey, listeners []types.WriteListener) types.CacheWrap { + return cachekv.NewStore(listenkv.NewStore(st, k, listeners), k, types.DefaultCacheSizeLimit) +} + +// Implements types.KVStore. +// +// we assume Set is only called in `Commit`, so the written state is only visible after commit. +func (st *Store) Set(key, value []byte) { + st.changeSet.Pairs = append(st.changeSet.Pairs, &iavl.KVPair{ + Key: key, Value: value, + }) +} + +// Implements types.KVStore. +func (st *Store) Get(key []byte) []byte { + return st.tree.Get(key) +} + +// Implements types.KVStore. +func (st *Store) Has(key []byte) bool { + return st.tree.Has(key) +} + +// Implements types.KVStore. +// +// we assume Delete is only called in `Commit`, so the written state is only visible after commit. +func (st *Store) Delete(key []byte) { + st.changeSet.Pairs = append(st.changeSet.Pairs, &iavl.KVPair{ + Key: key, Delete: true, + }) +} + +func (st *Store) Iterator(start, end []byte) types.Iterator { + return st.tree.Iterator(start, end, true) +} + +func (st *Store) ReverseIterator(start, end []byte) types.Iterator { + return st.tree.Iterator(start, end, false) +} + +// SetInitialVersion sets the initial version of the IAVL tree. It is used when +// starting a new chain at an arbitrary height. +// implements interface StoreWithInitialVersion +func (st *Store) SetInitialVersion(version int64) { + panic("memiavl store's SetInitialVersion is not supposed to be called directly") +} + +// PopChangeSet returns the change set and clear it +func (st *Store) PopChangeSet() iavl.ChangeSet { + cs := st.changeSet + st.changeSet = iavl.ChangeSet{} + return cs +} + +func (st *Store) Query(req abci.RequestQuery) (res abci.ResponseQuery) { + if req.Height > 0 && req.Height != st.tree.Version() { + return sdkerrors.QueryResult(errors.Wrap(sdkerrors.ErrInvalidHeight, "invalid height")) + } + + res.Height = st.tree.Version() + + switch req.Path { + case "/key": // get by key + res.Key = req.Data // data holds the key bytes + res.Value = st.tree.Get(res.Key) + + if !req.Prove { + break + } + + // get proof from tree and convert to merkle.Proof before adding to result + res.ProofOps = getProofFromTree(st.tree, req.Data, res.Value != nil) + case "/subspace": + pairs := kv.Pairs{ + Pairs: make([]kv.Pair, 0), + } + + subspace := req.Data + res.Key = subspace + + iterator := types.KVStorePrefixIterator(st, subspace) + for ; iterator.Valid(); iterator.Next() { + pairs.Pairs = append(pairs.Pairs, kv.Pair{Key: iterator.Key(), Value: iterator.Value()}) + } + iterator.Close() + + bz, err := pairs.Marshal() + if err != nil { + panic(fmt.Errorf("failed to marshal KV pairs: %w", err)) + } + + res.Value = bz + default: + return sdkerrors.QueryResult(errors.Wrapf(sdkerrors.ErrUnknownRequest, "unexpected query path: %v", req.Path)) + } + + return res +} + +// Takes a MutableTree, a key, and a flag for creating existence or absence proof and returns the +// appropriate merkle.Proof. Since this must be called after querying for the value, this function should never error +// Thus, it will panic on error rather than returning it +func getProofFromTree(tree *memiavl.Tree, key []byte, exists bool) *tmcrypto.ProofOps { + var ( + commitmentProof *ics23.CommitmentProof + err error + ) + + if exists { + // value was found + commitmentProof, err = tree.GetMembershipProof(key) + if err != nil { + // sanity check: If value was found, membership proof must be creatable + panic(fmt.Sprintf("unexpected value for empty proof: %s", err.Error())) + } + } else { + // value wasn't found + commitmentProof, err = tree.GetNonMembershipProof(key) + if err != nil { + // sanity check: If value wasn't found, nonmembership proof must be creatable + panic(fmt.Sprintf("unexpected error for nonexistence proof: %s", err.Error())) + } + } + + op := types.NewIavlCommitmentOp(key, commitmentProof) + return &tmcrypto.ProofOps{Ops: []tmcrypto.ProofOp{op.ProofOp()}} +} diff --git a/sc/memiavl/store/rootmulti/export.go b/sc/memiavl/store/rootmulti/export.go new file mode 100644 index 0000000..29fddd3 --- /dev/null +++ b/sc/memiavl/store/rootmulti/export.go @@ -0,0 +1,66 @@ +package rootmulti + +import ( + "fmt" + "math" + + snapshottypes "github.com/cosmos/cosmos-sdk/snapshots/types" + protoio "github.com/gogo/protobuf/io" + memiavl "github.com/sei-protocol/sei-db/memiavl/db" +) + +// Implements interface Snapshotter +func (rs *Store) Snapshot(height uint64, protoWriter protoio.Writer) error { + if height > math.MaxUint32 { + return fmt.Errorf("height overflows uint32: %d", height) + } + version := uint32(height) + + exporter, err := memiavl.NewMultiTreeExporter(rs.dir, version, rs.supportExportNonSnapshotVersion) + if err != nil { + return err + } + + defer exporter.Close() + + for { + item, err := exporter.Next() + if err != nil { + if err == memiavl.ErrorExportDone { + break + } + + return err + } + + switch item := item.(type) { + case *memiavl.ExportNode: + if err := protoWriter.WriteMsg(&snapshottypes.SnapshotItem{ + Item: &snapshottypes.SnapshotItem_IAVL{ + IAVL: &snapshottypes.SnapshotIAVLItem{ + Key: item.Key, + Value: item.Value, + Height: int32(item.Height), + Version: item.Version, + }, + }, + }); err != nil { + return err + } + case string: + if err := protoWriter.WriteMsg(&snapshottypes.SnapshotItem{ + Item: &snapshottypes.SnapshotItem_Store{ + Store: &snapshottypes.SnapshotStoreItem{ + Name: item, + }, + }, + }); err != nil { + return err + } + default: + return fmt.Errorf("unknown item type %T", item) + } + } + + return nil +} diff --git a/sc/memiavl/store/rootmulti/import.go b/sc/memiavl/store/rootmulti/import.go new file mode 100644 index 0000000..8ce453a --- /dev/null +++ b/sc/memiavl/store/rootmulti/import.go @@ -0,0 +1,91 @@ +package rootmulti + +import ( + "fmt" + "io" + "math" + + "cosmossdk.io/errors" + snapshottypes "github.com/cosmos/cosmos-sdk/snapshots/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + protoio "github.com/gogo/protobuf/io" + + "github.com/sei-protocol/sei-db/memiavl/db" +) + +// Implements interface Snapshotter +func (rs *Store) Restore( + height uint64, format uint32, protoReader protoio.Reader, +) (snapshottypes.SnapshotItem, error) { + if rs.db != nil { + if err := rs.db.Close(); err != nil { + return snapshottypes.SnapshotItem{}, fmt.Errorf("failed to close db: %w", err) + } + rs.db = nil + } + + item, err := rs.restore(height, format, protoReader) + if err != nil { + return snapshottypes.SnapshotItem{}, err + } + + return item, rs.LoadLatestVersion() +} + +func (rs *Store) restore( + height uint64, format uint32, protoReader protoio.Reader, +) (snapshottypes.SnapshotItem, error) { + importer, err := memiavl.NewMultiTreeImporter(rs.dir, height) + if err != nil { + return snapshottypes.SnapshotItem{}, err + } + defer importer.Close() + + var snapshotItem snapshottypes.SnapshotItem +loop: + for { + snapshotItem = snapshottypes.SnapshotItem{} + err := protoReader.ReadMsg(&snapshotItem) + if err == io.EOF { + break + } else if err != nil { + return snapshottypes.SnapshotItem{}, errors.Wrap(err, "invalid protobuf message") + } + + switch item := snapshotItem.Item.(type) { + case *snapshottypes.SnapshotItem_Store: + if err := importer.AddTree(item.Store.Name); err != nil { + return snapshottypes.SnapshotItem{}, err + } + case *snapshottypes.SnapshotItem_IAVL: + if item.IAVL.Height > math.MaxInt8 { + return snapshottypes.SnapshotItem{}, errors.Wrapf(sdkerrors.ErrLogic, "node height %v cannot exceed %v", + item.IAVL.Height, math.MaxInt8) + } + node := &memiavl.ExportNode{ + Key: item.IAVL.Key, + Value: item.IAVL.Value, + Height: int8(item.IAVL.Height), + Version: item.IAVL.Version, + } + // Protobuf does not differentiate between []byte{} as nil, but fortunately IAVL does + // not allow nil keys nor nil values for leaf nodes, so we can always set them to empty. + if node.Key == nil { + node.Key = []byte{} + } + if node.Height == 0 && node.Value == nil { + node.Value = []byte{} + } + importer.AddNode(node) + default: + // unknown element, could be an extension + break loop + } + } + + if err := importer.Finalize(); err != nil { + return snapshottypes.SnapshotItem{}, err + } + + return snapshotItem, nil +} diff --git a/sc/memiavl/store/rootmulti/store.go b/sc/memiavl/store/rootmulti/store.go new file mode 100644 index 0000000..0bef095 --- /dev/null +++ b/sc/memiavl/store/rootmulti/store.go @@ -0,0 +1,652 @@ +package rootmulti + +import ( + "fmt" + "io" + "math" + "sort" + "strings" + "sync" + + "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/store/listenkv" + "github.com/cosmos/cosmos-sdk/store/mem" + "github.com/cosmos/cosmos-sdk/store/rootmulti" + "github.com/cosmos/cosmos-sdk/store/transient" + "github.com/cosmos/cosmos-sdk/store/types" + pruningtypes "github.com/cosmos/cosmos-sdk/store/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/sei-protocol/sei-db/memiavl/db" + "github.com/sei-protocol/sei-db/memiavl/store/cachemulti" + "github.com/sei-protocol/sei-db/memiavl/store/memiavlstore" +) + +const CommitInfoFileName = "commit_infos" + +var ( + _ types.CommitMultiStore = (*Store)(nil) + _ types.Queryable = (*Store)(nil) +) + +type Store struct { + dir string + db *memiavl.DB + logger log.Logger + mtx sync.RWMutex + + // to keep it comptaible with cosmos-sdk 0.46, merge the memstores into commit info + lastCommitInfo *types.CommitInfo + + storesParams map[types.StoreKey]storeParams + keysByName map[string]types.StoreKey + stores map[types.StoreKey]types.CommitKVStore + listeners map[types.StoreKey][]types.WriteListener + + opts memiavl.Options + + // sdk46Compact defines if the root hash is compatible with cosmos-sdk 0.46 and before. + sdk46Compact bool + // it's more efficient to export snapshot versions, we can filter out the non-snapshot versions + supportExportNonSnapshotVersion bool +} + +func NewStore(dir string, logger log.Logger, sdk46Compact bool, supportExportNonSnapshotVersion bool) *Store { + return &Store{ + dir: dir, + logger: logger, + sdk46Compact: sdk46Compact, + supportExportNonSnapshotVersion: supportExportNonSnapshotVersion, + + storesParams: make(map[types.StoreKey]storeParams), + keysByName: make(map[string]types.StoreKey), + stores: make(map[types.StoreKey]types.CommitKVStore), + listeners: make(map[types.StoreKey][]types.WriteListener), + } +} + +// flush writes all the pending change sets to memiavl tree. +func (rs *Store) flush() error { + var changeSets []*memiavl.NamedChangeSet + for key := range rs.stores { + // it'll unwrap the inter-block cache + store := rs.GetCommitKVStore(key) + if memiavlStore, ok := store.(*memiavlstore.Store); ok { + cs := memiavlStore.PopChangeSet() + if len(cs.Pairs) > 0 { + changeSets = append(changeSets, &memiavl.NamedChangeSet{ + Name: key.Name(), + Changeset: cs, + }) + } + } + } + sort.SliceStable(changeSets, func(i, j int) bool { + return changeSets[i].Name < changeSets[j].Name + }) + + return rs.db.ApplyChangeSets(changeSets) +} + +// WorkingHash returns the app hash of the working tree, +// +// Implements interface Committer. +func (rs *Store) WorkingHash() []byte { + if err := rs.flush(); err != nil { + panic(err) + } + commitInfo := convertCommitInfo(rs.db.WorkingCommitInfo()) + if rs.sdk46Compact { + commitInfo = amendCommitInfo(commitInfo, rs.storesParams) + } + return commitInfo.Hash() +} + +// Implements interface Committer +func (rs *Store) Commit(bumpVersion bool) types.CommitID { + if !bumpVersion { + return rs.lastCommitInfo.CommitID() + } + if err := rs.flush(); err != nil { + panic(err) + } + + rs.mtx.Lock() + defer rs.mtx.Unlock() + for _, store := range rs.stores { + if store.GetStoreType() != types.StoreTypeIAVL { + _ = store.Commit(bumpVersion) + } + } + + _, err := rs.db.Commit() + if err != nil { + panic(err) + } + + // the underlying memiavl tree might be reloaded, reload the store as well. + for key := range rs.stores { + store := rs.stores[key] + if store.GetStoreType() == types.StoreTypeIAVL { + rs.stores[key], err = rs.loadCommitStoreFromParams(rs.db, key, rs.storesParams[key]) + if err != nil { + panic(fmt.Errorf("inconsistent store map, store %s not found", key.Name())) + } + } + } + + rs.lastCommitInfo = convertCommitInfo(rs.db.LastCommitInfo()) + if rs.sdk46Compact { + rs.lastCommitInfo = amendCommitInfo(rs.lastCommitInfo, rs.storesParams) + } + return rs.lastCommitInfo.CommitID() +} + +func (rs *Store) Close() error { + return rs.db.Close() +} + +// Implements interface Committer +func (rs *Store) LastCommitID() types.CommitID { + if rs.lastCommitInfo == nil { + v, err := memiavl.GetLatestVersion(rs.dir) + if err != nil { + panic(fmt.Errorf("failed to get latest version: %w", err)) + } + return types.CommitID{Version: v} + } + + return rs.lastCommitInfo.CommitID() +} + +// Implements interface Committer +func (rs *Store) SetPruning(pruningtypes.PruningOptions) { +} + +// Implements interface Committer +func (rs *Store) GetPruning() pruningtypes.PruningOptions { + return types.PruneDefault +} + +// Implements interface Store +func (rs *Store) GetStoreType() types.StoreType { + return types.StoreTypeMulti +} + +// Implements interface CacheWrapper +func (rs *Store) CacheWrap(storeKey types.StoreKey) types.CacheWrap { + return rs.CacheMultiStore().CacheWrap(storeKey).(types.CacheWrap) +} + +// Implements interface CacheWrapper +func (rs *Store) CacheWrapWithTrace(storeKey types.StoreKey, _ io.Writer, _ types.TraceContext) types.CacheWrap { + return rs.CacheWrap(storeKey) +} + +func (rs *Store) CacheWrapWithListeners(k types.StoreKey, listeners []types.WriteListener) types.CacheWrap { + return rs.CacheMultiStore().CacheWrapWithListeners(k, listeners).(types.CacheWrap) +} + +// Implements interface MultiStore +func (rs *Store) CacheMultiStore() types.CacheMultiStore { + rs.mtx.RLock() + defer rs.mtx.RUnlock() + stores := make(map[types.StoreKey]types.CacheWrapper) + for k, v := range rs.stores { + store := types.KVStore(v) + // Wire the listenkv.Store to allow listeners to observe the writes from the cache store, + // set same listeners on cache store will observe duplicated writes. + if rs.ListeningEnabled(k) { + store = listenkv.NewStore(store, k, rs.listeners[k]) + } + stores[k] = store + } + return cachemulti.NewStore(nil, stores, rs.keysByName, nil, nil, nil, nil) +} + +// Implements interface MultiStore +// used to createQueryContext, abci_query or grpc query service. +func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStore, error) { + if version == 0 || (rs.lastCommitInfo != nil && version == rs.lastCommitInfo.Version) { + return rs.CacheMultiStore(), nil + } + opts := rs.opts + opts.TargetVersion = uint32(version) + opts.ReadOnly = true + db, err := memiavl.Load(rs.dir, opts) + if err != nil { + return nil, err + } + + stores := make(map[types.StoreKey]types.CacheWrapper) + + // add the transient/mem stores registered in current app. + for k, store := range rs.stores { + if store.GetStoreType() != types.StoreTypeIAVL { + stores[k] = store + } + } + + // add all the iavl stores at the target version. + for _, tree := range db.Trees() { + stores[rs.keysByName[tree.Name]] = memiavlstore.New(tree.Tree, rs.logger) + } + + return cachemulti.NewStore(nil, stores, rs.keysByName, nil, nil, nil, db), nil +} + +// Implements interface MultiStore +func (rs *Store) GetStore(key types.StoreKey) types.Store { + return rs.CacheMultiStore().GetStore(key) +} + +// Implements interface MultiStore +func (rs *Store) GetKVStore(key types.StoreKey) types.KVStore { + return rs.CacheMultiStore().GetKVStore(key) +} + +// Implements interface MultiStore +func (rs *Store) TracingEnabled() bool { + return false +} + +// Implements interface MultiStore +func (rs *Store) SetTracer(w io.Writer) types.MultiStore { + return nil +} + +// Implements interface MultiStore +func (rs *Store) SetTracingContext(types.TraceContext) types.MultiStore { + return nil +} + +// Implements interface MultiStore +func (rs *Store) LatestVersion() int64 { + return rs.db.Version() +} + +// Implements interface Snapshotter +// not needed, memiavl manage its own snapshot/pruning strategy +func (rs *Store) PruneSnapshotHeight(height int64) { +} + +// Implements interface Snapshotter +// not needed, memiavl manage its own snapshot/pruning strategy +func (rs *Store) SetSnapshotInterval(snapshotInterval uint64) { +} + +// Implements interface CommitMultiStore +func (rs *Store) MountStoreWithDB(key types.StoreKey, typ types.StoreType, _ dbm.DB) { + if key == nil { + panic("MountIAVLStore() key cannot be nil") + } + if _, ok := rs.storesParams[key]; ok { + panic(fmt.Sprintf("store duplicate store key %v", key)) + } + if _, ok := rs.keysByName[key.Name()]; ok { + panic(fmt.Sprintf("store duplicate store key name %v", key)) + } + rs.storesParams[key] = newStoreParams(key, typ) + rs.keysByName[key.Name()] = key +} + +// Implements interface CommitMultiStore +func (rs *Store) GetCommitStore(key types.StoreKey) types.CommitStore { + return rs.GetCommitKVStore(key) +} + +// Implements interface CommitMultiStore +func (rs *Store) GetCommitKVStore(key types.StoreKey) types.CommitKVStore { + return rs.stores[key] +} + +// Implements interface CommitMultiStore +// used by normal node startup. +func (rs *Store) LoadLatestVersion() error { + return rs.LoadVersionAndUpgrade(0, nil) +} + +// Implements interface CommitMultiStore +func (rs *Store) LoadLatestVersionAndUpgrade(upgrades *types.StoreUpgrades) error { + return rs.LoadVersionAndUpgrade(0, upgrades) +} + +// Implements interface CommitMultiStore +// used by node startup with UpgradeStoreLoader +func (rs *Store) LoadVersionAndUpgrade(version int64, upgrades *types.StoreUpgrades) error { + if version > math.MaxUint32 { + return fmt.Errorf("version overflows uint32: %d", version) + } + + storesKeys := make([]types.StoreKey, 0, len(rs.storesParams)) + for key := range rs.storesParams { + storesKeys = append(storesKeys, key) + } + // deterministic iteration order for upgrades + sort.Slice(storesKeys, func(i, j int) bool { + return storesKeys[i].Name() < storesKeys[j].Name() + }) + + initialStores := make([]string, 0, len(storesKeys)) + for _, key := range storesKeys { + if rs.storesParams[key].typ == types.StoreTypeIAVL { + initialStores = append(initialStores, key.Name()) + } + } + + opts := rs.opts + opts.CreateIfMissing = true + opts.InitialStores = initialStores + opts.TargetVersion = uint32(version) + db, err := memiavl.Load(rs.dir, opts) + if err != nil { + return errors.Wrapf(err, "fail to load memiavl at %s", rs.dir) + } + + var treeUpgrades []*memiavl.TreeNameUpgrade + for _, key := range storesKeys { + switch { + case upgrades.IsDeleted(key.Name()): + treeUpgrades = append(treeUpgrades, &memiavl.TreeNameUpgrade{Name: key.Name(), Delete: true}) + case upgrades.IsAdded(key.Name()) || upgrades.RenamedFrom(key.Name()) != "": + treeUpgrades = append(treeUpgrades, &memiavl.TreeNameUpgrade{Name: key.Name(), RenameFrom: upgrades.RenamedFrom(key.Name())}) + } + } + + if len(treeUpgrades) > 0 { + if err := db.ApplyUpgrades(treeUpgrades); err != nil { + return err + } + } + + newStores := make(map[types.StoreKey]types.CommitKVStore, len(storesKeys)) + for _, key := range storesKeys { + newStores[key], err = rs.loadCommitStoreFromParams(db, key, rs.storesParams[key]) + if err != nil { + return err + } + } + + rs.db = db + rs.stores = newStores + // to keep the root hash compatible with cosmos-sdk 0.46 + if db.Version() != 0 { + rs.lastCommitInfo = convertCommitInfo(db.LastCommitInfo()) + if rs.sdk46Compact { + rs.lastCommitInfo = amendCommitInfo(rs.lastCommitInfo, rs.storesParams) + } + } else { + rs.lastCommitInfo = &types.CommitInfo{} + } + return nil +} + +func (rs *Store) loadCommitStoreFromParams(db *memiavl.DB, key types.StoreKey, params storeParams) (types.CommitKVStore, error) { + switch params.typ { + case types.StoreTypeMulti: + panic("recursive MultiStores not yet supported") + case types.StoreTypeIAVL: + tree := db.TreeByName(key.Name()) + if tree == nil { + return nil, fmt.Errorf("new store is not added in upgrades: %s", key.Name()) + } + return types.CommitKVStore(memiavlstore.New(tree, rs.logger)), nil + case types.StoreTypeDB: + panic("recursive MultiStores not yet supported") + case types.StoreTypeTransient: + _, ok := key.(*types.TransientStoreKey) + if !ok { + return nil, fmt.Errorf("invalid StoreKey for StoreTypeTransient: %s", key.String()) + } + + return transient.NewStore(), nil + + case types.StoreTypeMemory: + if _, ok := key.(*types.MemoryStoreKey); !ok { + return nil, fmt.Errorf("unexpected key type for a MemoryStoreKey; got: %s", key.String()) + } + + return mem.NewStore(), nil + + default: + panic(fmt.Sprintf("unrecognized store type %v", params.typ)) + } +} + +// Implements interface CommitMultiStore +// used by export cmd +func (rs *Store) LoadVersion(ver int64) error { + return rs.LoadVersionAndUpgrade(ver, nil) +} + +// SetInterBlockCache is a noop here because memiavl do caching on it's own, which works well with zero-copy. +func (rs *Store) SetInterBlockCache(c types.MultiStorePersistentCache) {} + +// Implements interface CommitMultiStore +// used by InitChain when the initial height is bigger than 1 +func (rs *Store) SetInitialVersion(version int64) error { + return rs.db.SetInitialVersion(version) +} + +// Implements interface CommitMultiStore +func (rs *Store) SetIAVLCacheSize(size int) { +} + +// Implements interface CommitMultiStore +func (rs *Store) SetIAVLDisableFastNode(disable bool) { +} + +// Implements interface CommitMultiStore +func (rs *Store) SetLazyLoading(lazyLoading bool) { +} + +func (rs *Store) SetMemIAVLOptions(opts memiavl.Options) { + if opts.Logger == nil { + opts.Logger = memiavl.Logger(rs.logger.With("module", "memiavl")) + } + rs.opts = opts +} + +// RollbackToVersion delete the versions after `target` and update the latest version. +// it should only be called in standalone cli commands. +func (rs *Store) RollbackToVersion(target int64) error { + if target <= 0 { + return fmt.Errorf("invalid rollback height target: %d", target) + } + + if target > math.MaxUint32 { + return fmt.Errorf("rollback height target %d exceeds max uint32", target) + } + + if rs.db != nil { + if err := rs.db.Close(); err != nil { + return err + } + } + + opts := rs.opts + opts.TargetVersion = uint32(target) + opts.LoadForOverwriting = true + + var err error + rs.db, err = memiavl.Load(rs.dir, opts) + + return err +} + +// Implements interface CommitMultiStore +func (rs *Store) ListeningEnabled(key types.StoreKey) bool { + if ls, ok := rs.listeners[key]; ok { + return len(ls) != 0 + } + return false +} + +// Implements interface CommitMultiStore +func (rs *Store) AddListeners(key types.StoreKey, listeners []types.WriteListener) { + if ls, ok := rs.listeners[key]; ok { + rs.listeners[key] = append(ls, listeners...) + } else { + rs.listeners[key] = listeners + } +} + +// getStoreByName performs a lookup of a StoreKey given a store name typically +// provided in a path. The StoreKey is then used to perform a lookup and return +// a Store. If the Store is wrapped in an inter-block cache, it will be unwrapped +// prior to being returned. If the StoreKey does not exist, nil is returned. +func (rs *Store) GetStoreByName(name string) types.Store { + key := rs.keysByName[name] + if key == nil { + return nil + } + + return rs.GetCommitKVStore(key) +} + +// Implements interface Queryable +func (rs *Store) Query(req abci.RequestQuery) abci.ResponseQuery { + version := req.Height + if version == 0 { + version = rs.db.Version() + } + + // If the request's height is the latest height we've committed, then utilize + // the store's lastCommitInfo as this commit info may not be flushed to disk. + // Otherwise, we query for the commit info from disk. + db := rs.db + if version != rs.lastCommitInfo.Version { + var err error + db, err = memiavl.Load(rs.dir, memiavl.Options{TargetVersion: uint32(version), ReadOnly: true}) + if err != nil { + return sdkerrors.QueryResult(err) + } + defer func(db *memiavl.DB) { + err := db.Close() + if err != nil { + rs.logger.Error("failed to close db", "error", err) + } + }(db) + } + + path := req.Path + storeName, subpath, err := parsePath(path) + if err != nil { + return sdkerrors.QueryResult(err) + } + + store := types.Queryable(memiavlstore.New(db.TreeByName(storeName), rs.logger)) + + // trim the path and make the query + req.Path = subpath + res := store.Query(req) + + if !req.Prove || !rootmulti.RequireProof(subpath) { + return res + } + + if res.ProofOps == nil || len(res.ProofOps.Ops) == 0 { + return sdkerrors.QueryResult(errors.Wrap(sdkerrors.ErrInvalidRequest, "proof is unexpectedly empty; ensure height has not been pruned")) + } + + commitInfo := convertCommitInfo(db.LastCommitInfo()) + if rs.sdk46Compact { + commitInfo = amendCommitInfo(commitInfo, rs.storesParams) + } + + // Restore origin path and append proof op. + res.ProofOps.Ops = append(res.ProofOps.Ops, commitInfo.ProofOp(storeName)) + + return res +} + +// parsePath expects a format like /[/] +// Must start with /, subpath may be empty +// Returns error if it doesn't start with / +func parsePath(path string) (storeName string, subpath string, err error) { + if !strings.HasPrefix(path, "/") { + return storeName, subpath, errors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid path: %s", path) + } + + paths := strings.SplitN(path[1:], "/", 2) + storeName = paths[0] + + if len(paths) == 2 { + subpath = "/" + paths[1] + } + + return storeName, subpath, nil +} + +type storeParams struct { + key types.StoreKey + typ types.StoreType +} + +func newStoreParams(key types.StoreKey, typ types.StoreType) storeParams { + return storeParams{ + key: key, + typ: typ, + } +} + +func mergeStoreInfos(commitInfo *types.CommitInfo, storeInfos []types.StoreInfo) *types.CommitInfo { + infos := make([]types.StoreInfo, 0, len(commitInfo.StoreInfos)+len(storeInfos)) + infos = append(infos, commitInfo.StoreInfos...) + infos = append(infos, storeInfos...) + sort.SliceStable(infos, func(i, j int) bool { + return infos[i].Name < infos[j].Name + }) + return &types.CommitInfo{ + Version: commitInfo.Version, + StoreInfos: infos, + } +} + +// amendCommitInfo add mem stores commit infos to keep it compatible with cosmos-sdk 0.46 +func amendCommitInfo(commitInfo *types.CommitInfo, storeParams map[types.StoreKey]storeParams) *types.CommitInfo { + var extraStoreInfos []types.StoreInfo + for key := range storeParams { + typ := storeParams[key].typ + if typ != types.StoreTypeIAVL && typ != types.StoreTypeTransient { + extraStoreInfos = append(extraStoreInfos, types.StoreInfo{ + Name: key.Name(), + CommitId: types.CommitID{}, + }) + } + } + return mergeStoreInfos(commitInfo, extraStoreInfos) +} + +func convertCommitInfo(commitInfo *memiavl.CommitInfo) *types.CommitInfo { + storeInfos := make([]types.StoreInfo, len(commitInfo.StoreInfos)) + for i, storeInfo := range commitInfo.StoreInfos { + storeInfos[i] = types.StoreInfo{ + Name: storeInfo.Name, + CommitId: types.CommitID{ + Version: storeInfo.CommitId.Version, + Hash: storeInfo.CommitId.Hash, + }, + } + } + return &types.CommitInfo{ + Version: commitInfo.Version, + StoreInfos: storeInfos, + } +} + +func (rs *Store) GetWorkingHash() ([]byte, error) { + hash := rs.WorkingHash() + return hash, nil +} + +func (rs *Store) GetEvents() []abci.Event { + panic("should never attempt to get events from commit multi store") +} + +func (rs *Store) ResetEvents() { + panic("should never attempt to reset events from commit multi store") +} diff --git a/sc/memiavl/store/rootmulti/store_test.go b/sc/memiavl/store/rootmulti/store_test.go new file mode 100644 index 0000000..895f498 --- /dev/null +++ b/sc/memiavl/store/rootmulti/store_test.go @@ -0,0 +1,14 @@ +package rootmulti + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/store/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" +) + +func TestLastCommitID(t *testing.T) { + store := NewStore(t.TempDir(), log.NewNopLogger(), false, false) + require.Equal(t, types.CommitID{}, store.LastCommitID()) +} diff --git a/sc/memiavl/store/setup.go b/sc/memiavl/store/setup.go new file mode 100644 index 0000000..4984f88 --- /dev/null +++ b/sc/memiavl/store/setup.go @@ -0,0 +1,62 @@ +package store + +import ( + "path/filepath" + + "github.com/cosmos/cosmos-sdk/baseapp" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/sei-protocol/sei-db/memiavl/db" + "github.com/sei-protocol/sei-db/memiavl/store/rootmulti" + "github.com/spf13/cast" + "github.com/tendermint/tendermint/libs/log" +) + +const ( + FlagMemIAVL = "memiavl.enable" + FlagAsyncCommitBuffer = "memiavl.async-commit-buffer" + FlagZeroCopy = "memiavl.zero-copy" + FlagSnapshotKeepRecent = "memiavl.snapshot-keep-recent" + FlagSnapshotInterval = "memiavl.snapshot-interval" + FlagCacheSize = "memiavl.cache-size" + FlagSnapshotWriterLimit = "memiavl.snapshot-writer-limit" +) + +// SetupMemIAVL insert the memiavl setter in front of baseapp options, so that +// the default rootmulti store is replaced by memiavl store, +func SetupMemIAVL(logger log.Logger, homePath string, appOpts servertypes.AppOptions, sdk46Compact bool, supportExportNonSnapshotVersion bool, baseAppOptions []func(*baseapp.BaseApp)) []func(*baseapp.BaseApp) { + if cast.ToBool(appOpts.Get(FlagMemIAVL)) { + opts := memiavl.Options{ + AsyncCommitBuffer: cast.ToInt(appOpts.Get(FlagAsyncCommitBuffer)), + ZeroCopy: cast.ToBool(appOpts.Get(FlagZeroCopy)), + SnapshotKeepRecent: cast.ToUint32(appOpts.Get(FlagSnapshotKeepRecent)), + SnapshotInterval: cast.ToUint32(appOpts.Get(FlagSnapshotInterval)), + CacheSize: cast.ToInt(appOpts.Get(FlagCacheSize)), + SnapshotWriterLimit: cast.ToInt(appOpts.Get(FlagSnapshotWriterLimit)), + } + + // cms must be overridden before the other options, because they may use the cms, + // make sure the cms aren't be overridden by the other options later on. + baseAppOptions = append([]func(*baseapp.BaseApp){setMemIAVL(homePath, logger, opts, sdk46Compact, supportExportNonSnapshotVersion)}, baseAppOptions...) + } + + return baseAppOptions +} + +func setMemIAVL( + homePath string, + logger log.Logger, + opts memiavl.Options, + sdk46Compact bool, + supportExportNonSnapshotVersion bool, +) func(*baseapp.BaseApp) { + return func(bapp *baseapp.BaseApp) { + // trigger state-sync snapshot creation by memiavl + opts.TriggerStateSyncExport = func(height int64) { + logger.Info("Triggering memIAVL state snapshot creation") + bapp.SnapshotIfApplicable(uint64(height)) + } + cms := rootmulti.NewStore(filepath.Join(homePath, "data", "memiavl.db"), logger, sdk46Compact, supportExportNonSnapshotVersion) + cms.SetMemIAVLOptions(opts) + bapp.SetCMS(cms) + } +} diff --git a/ss/rocksdb/README.md b/ss/rocksdb/README.md new file mode 100644 index 0000000..e69de29