mirror of
https://github.com/fluencelabs/tendermint
synced 2025-04-25 14:52:17 +00:00
Updates #1021 * Implement a GetHeightBinarySearch method that looks for the height using the binary search algorithm guaranteeing worst case iteration time of O(log2(n)) whereas worst case iteration time of O(n) for the current linear search So if n we had 500 commits stored by height and sorted, to trigger the worst case scenario for each, pass in the most negative height you can find e.g. -1 Linear search: 500 iterations Binary search: 9 iterations with n=1000, qHeight = -1 Linear search: 1000 iterations Binary search: 10 iterations with n=1e6, qHeight = -1 Linear search: 1e6 iterations Binary search: 20 iterations Of course there are realistic expectations e.g. a max of commits that may be saved so linear search might be useful for very small size set because it has less preparing overhead and only ~2 types of comparisons, but nonetheless binary search shines as soon as we start to hit say 50 commits to search from as you can see below: ```shell $ go test -v -run=^$ -bench=MemStore goos: darwin goarch: amd64 pkg: github.com/tendermint/tendermint/lite BenchmarkMemStoreProviderGetByHeightLinearSearch5-4 300000 6491 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightLinearSearch50-4 200000 12064 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightLinearSearch100-4 50000 32987 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightLinearSearch500-4 5000 395521 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightLinearSearch1000-4 500 2940724 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch5-4 300000 6281 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch50-4 200000 10117 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch100-4 100000 18447 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch500-4 20000 89029 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch1000-4 5000 265719 ns/op 1600 B/op 15 allocs/op PASS ok github.com/tendermint/tendermint/lite 86.614s $ go test -v -run=^$ -bench=MemStore goos: darwin goarch: amd64 pkg: github.com/tendermint/tendermint/lite BenchmarkMemStoreProviderGetByHeightLinearSearch5-4 300000 6779 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightLinearSearch50-4 100000 12980 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightLinearSearch100-4 30000 43598 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightLinearSearch500-4 5000 377462 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightLinearSearch1000-4 500 3278122 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch5-4 300000 7084 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch50-4 200000 9852 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch100-4 100000 19020 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch500-4 20000 99463 ns/op 1600 B/op 15 allocs/op BenchmarkMemStoreProviderGetByHeightBinarySearch1000-4 5000 259293 ns/op 1600 B/op 15 allocs/op PASS ok github.com/tendermint/tendermint/lite 86.204s ``` which gives ```shell $ benchstat old.txt new.txt name old time/op new time/op delta MemStoreProviderGetByHeight5-4 6.63µs ± 2% 6.68µs ± 6% ~ (p=1.000 n=2+2) MemStoreProviderGetByHeight50-4 12.5µs ± 4% 10.0µs ± 1% ~ (p=0.333 n=2+2) MemStoreProviderGetByHeight100-4 38.3µs ±14% 18.7µs ± 2% ~ (p=0.333 n=2+2) MemStoreProviderGetByHeight500-4 386µs ± 2% 94µs ± 6% ~ (p=0.333 n=2+2) MemStoreProviderGetByHeight1000-4 3.11ms ± 5% 0.26ms ± 1% ~ (p=0.333 n=2+2) ``` If need be we can make a hybrid algorithm that switches between the linear and binary search depending on the number of items. This is reminiscent of Python's TimSort algorithm.
157 lines
4.4 KiB
Go
157 lines
4.4 KiB
Go
package lite
|
|
|
|
import (
|
|
"time"
|
|
|
|
crypto "github.com/tendermint/go-crypto"
|
|
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
// ValKeys is a helper for testing.
|
|
//
|
|
// It lets us simulate signing with many keys, either ed25519 or secp256k1.
|
|
// The main use case is to create a set, and call GenCommit
|
|
// to get properly signed header for testing.
|
|
//
|
|
// You can set different weights of validators each time you call
|
|
// ToValidators, and can optionally extend the validator set later
|
|
// with Extend or ExtendSecp
|
|
type ValKeys []crypto.PrivKey
|
|
|
|
// GenValKeys produces an array of private keys to generate commits.
|
|
func GenValKeys(n int) ValKeys {
|
|
res := make(ValKeys, n)
|
|
for i := range res {
|
|
res[i] = crypto.GenPrivKeyEd25519().Wrap()
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Change replaces the key at index i.
|
|
func (v ValKeys) Change(i int) ValKeys {
|
|
res := make(ValKeys, len(v))
|
|
copy(res, v)
|
|
res[i] = crypto.GenPrivKeyEd25519().Wrap()
|
|
return res
|
|
}
|
|
|
|
// Extend adds n more keys (to remove, just take a slice).
|
|
func (v ValKeys) Extend(n int) ValKeys {
|
|
extra := GenValKeys(n)
|
|
return append(v, extra...)
|
|
}
|
|
|
|
// GenSecpValKeys produces an array of secp256k1 private keys to generate commits.
|
|
func GenSecpValKeys(n int) ValKeys {
|
|
res := make(ValKeys, n)
|
|
for i := range res {
|
|
res[i] = crypto.GenPrivKeySecp256k1().Wrap()
|
|
}
|
|
return res
|
|
}
|
|
|
|
// ExtendSecp adds n more secp256k1 keys (to remove, just take a slice).
|
|
func (v ValKeys) ExtendSecp(n int) ValKeys {
|
|
extra := GenSecpValKeys(n)
|
|
return append(v, extra...)
|
|
}
|
|
|
|
// ToValidators produces a list of validators from the set of keys
|
|
// The first key has weight `init` and it increases by `inc` every step
|
|
// so we can have all the same weight, or a simple linear distribution
|
|
// (should be enough for testing).
|
|
func (v ValKeys) ToValidators(init, inc int64) *types.ValidatorSet {
|
|
res := make([]*types.Validator, len(v))
|
|
for i, k := range v {
|
|
res[i] = types.NewValidator(k.PubKey(), init+int64(i)*inc)
|
|
}
|
|
return types.NewValidatorSet(res)
|
|
}
|
|
|
|
// signHeader properly signs the header with all keys from first to last exclusive.
|
|
func (v ValKeys) signHeader(header *types.Header, first, last int) *types.Commit {
|
|
votes := make([]*types.Vote, len(v))
|
|
|
|
// we need this list to keep the ordering...
|
|
vset := v.ToValidators(1, 0)
|
|
|
|
// fill in the votes we want
|
|
for i := first; i < last; i++ {
|
|
if i >= len(v) {
|
|
break
|
|
}
|
|
vote := makeVote(header, vset, v[i])
|
|
votes[vote.ValidatorIndex] = vote
|
|
}
|
|
|
|
res := &types.Commit{
|
|
BlockID: types.BlockID{Hash: header.Hash()},
|
|
Precommits: votes,
|
|
}
|
|
return res
|
|
}
|
|
|
|
func makeVote(header *types.Header, vals *types.ValidatorSet, key crypto.PrivKey) *types.Vote {
|
|
addr := key.PubKey().Address()
|
|
idx, _ := vals.GetByAddress(addr)
|
|
vote := &types.Vote{
|
|
ValidatorAddress: addr,
|
|
ValidatorIndex: idx,
|
|
Height: header.Height,
|
|
Round: 1,
|
|
Timestamp: time.Now().UTC(),
|
|
Type: types.VoteTypePrecommit,
|
|
BlockID: types.BlockID{Hash: header.Hash()},
|
|
}
|
|
// Sign it
|
|
signBytes := types.SignBytes(header.ChainID, vote)
|
|
vote.Signature = key.Sign(signBytes)
|
|
return vote
|
|
}
|
|
|
|
// Silences warning that vals can also be merkle.Hashable
|
|
// nolint: interfacer
|
|
func genHeader(chainID string, height int64, txs types.Txs,
|
|
vals *types.ValidatorSet, appHash, consHash, resHash []byte) *types.Header {
|
|
|
|
return &types.Header{
|
|
ChainID: chainID,
|
|
Height: height,
|
|
Time: time.Now(),
|
|
NumTxs: int64(len(txs)),
|
|
TotalTxs: int64(len(txs)),
|
|
// LastBlockID
|
|
// LastCommitHash
|
|
ValidatorsHash: vals.Hash(),
|
|
DataHash: txs.Hash(),
|
|
AppHash: appHash,
|
|
ConsensusHash: consHash,
|
|
LastResultsHash: resHash,
|
|
}
|
|
}
|
|
|
|
// GenCommit calls genHeader and signHeader and combines them into a Commit.
|
|
func (v ValKeys) GenCommit(chainID string, height int64, txs types.Txs,
|
|
vals *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) Commit {
|
|
|
|
header := genHeader(chainID, height, txs, vals, appHash, consHash, resHash)
|
|
check := Commit{
|
|
Header: header,
|
|
Commit: v.signHeader(header, first, last),
|
|
}
|
|
return check
|
|
}
|
|
|
|
// GenFullCommit calls genHeader and signHeader and combines them into a Commit.
|
|
func (v ValKeys) GenFullCommit(chainID string, height int64, txs types.Txs,
|
|
vals *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) FullCommit {
|
|
|
|
header := genHeader(chainID, height, txs, vals, appHash, consHash, resHash)
|
|
commit := Commit{
|
|
Header: header,
|
|
Commit: v.signHeader(header, first, last),
|
|
}
|
|
return NewFullCommit(commit, vals)
|
|
}
|