package lite import ( "encoding/hex" "sort" "sync" liteErr "github.com/tendermint/tendermint/lite/errors" ) type memStoreProvider struct { mtx sync.RWMutex // byHeight is always sorted by Height... need to support range search (nil, h] // btree would be more efficient for larger sets byHeight fullCommits byHash map[string]FullCommit sorted bool } // fullCommits just exists to allow easy sorting type fullCommits []FullCommit func (s fullCommits) Len() int { return len(s) } func (s fullCommits) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s fullCommits) Less(i, j int) bool { return s[i].Height() < s[j].Height() } // NewMemStoreProvider returns a new in-memory provider. func NewMemStoreProvider() Provider { return &memStoreProvider{ byHeight: fullCommits{}, byHash: map[string]FullCommit{}, } } func (m *memStoreProvider) encodeHash(hash []byte) string { return hex.EncodeToString(hash) } // StoreCommit stores a FullCommit after verifying it. func (m *memStoreProvider) StoreCommit(fc FullCommit) error { // make sure the fc is self-consistent before saving err := fc.ValidateBasic(fc.Commit.Header.ChainID) if err != nil { return err } // store the valid fc key := m.encodeHash(fc.ValidatorsHash()) m.mtx.Lock() defer m.mtx.Unlock() m.byHash[key] = fc m.byHeight = append(m.byHeight, fc) m.sorted = false return nil } // GetByHeight returns the FullCommit for height h or an error if the commit is not found. func (m *memStoreProvider) GetByHeight(h int64) (FullCommit, error) { // By heuristics, GetByHeight with linearsearch is fast enough // for about 50 keys but after that, it needs binary search. // See https://github.com/tendermint/tendermint/pull/1043#issue-285188242 m.mtx.RLock() n := len(m.byHeight) m.mtx.RUnlock() if n <= 50 { return m.getByHeightLinearSearch(h) } return m.getByHeightBinarySearch(h) } func (m *memStoreProvider) sortByHeightIfNecessaryLocked() { if !m.sorted { sort.Sort(m.byHeight) m.sorted = true } } func (m *memStoreProvider) getByHeightLinearSearch(h int64) (FullCommit, error) { m.mtx.Lock() defer m.mtx.Unlock() m.sortByHeightIfNecessaryLocked() // search from highest to lowest for i := len(m.byHeight) - 1; i >= 0; i-- { if fc := m.byHeight[i]; fc.Height() <= h { return fc, nil } } return FullCommit{}, liteErr.ErrCommitNotFound() } func (m *memStoreProvider) getByHeightBinarySearch(h int64) (FullCommit, error) { m.mtx.Lock() defer m.mtx.Unlock() m.sortByHeightIfNecessaryLocked() low, high := 0, len(m.byHeight)-1 var mid int var hmid int64 var midFC FullCommit // Our goal is to either find: // * item ByHeight with the query // * greatest height with a height <= query for low <= high { mid = int(uint(low+high) >> 1) // Avoid an overflow midFC = m.byHeight[mid] hmid = midFC.Height() switch { case hmid == h: return midFC, nil case hmid < h: low = mid + 1 case hmid > h: high = mid - 1 } } if high >= 0 { if highFC := m.byHeight[high]; highFC.Height() < h { return highFC, nil } } return FullCommit{}, liteErr.ErrCommitNotFound() } // GetByHash returns the FullCommit for the hash or an error if the commit is not found. func (m *memStoreProvider) GetByHash(hash []byte) (FullCommit, error) { m.mtx.RLock() defer m.mtx.RUnlock() fc, ok := m.byHash[m.encodeHash(hash)] if !ok { return fc, liteErr.ErrCommitNotFound() } return fc, nil } // LatestCommit returns the latest FullCommit or an error if no commits exist. func (m *memStoreProvider) LatestCommit() (FullCommit, error) { m.mtx.Lock() defer m.mtx.Unlock() l := len(m.byHeight) if l == 0 { return FullCommit{}, liteErr.ErrCommitNotFound() } m.sortByHeightIfNecessaryLocked() return m.byHeight[l-1], nil }