Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (enterprise/poa) [#25838](https://github.com/cosmos/cosmos-sdk/pull/25838) Add the `poa` module under the `enterprise` directory.
* (grpc) [#25850](https://github.com/cosmos/cosmos-sdk/pull/25850) Add `GetBlockResults` and `GetLatestBlockResults` gRPC endpoints to expose CometBFT block results including `finalize_block_events`.
* (events) [#25877](https://github.com/cosmos/cosmos-sdk/pull/25877) Add `OverrideEvents` to `EventManagerI`.
* (blockstm) [#25777](https://github.com/cosmos/cosmos-sdk/issues/25777) Cache pre-state in MVMemory to support value-based validation.

### Improvements

Expand Down
196 changes: 176 additions & 20 deletions internal/blockstm/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package blockstm

import (
"context"
"fmt"
"strconv"
"sync/atomic"
"testing"
Expand All @@ -11,57 +12,212 @@ import (
storetypes "cosmossdk.io/store/types"
)

func executeBlock(stores map[storetypes.StoreKey]int, storage MultiStore, worker int, block *MockBlock) error {
incarnationCache := make([]atomic.Pointer[map[string]any], block.Size())
for i := 0; i < block.Size(); i++ {
m := make(map[string]any)
incarnationCache[i].Store(&m)
}
return ExecuteBlock(context.Background(), block.Size(), stores, storage, worker, func(txn TxnIndex, store MultiStore) {
cache := incarnationCache[txn].Swap(nil)
block.ExecuteTx(txn, store, *cache)
incarnationCache[txn].Store(cache)
})
}

func BenchmarkBlockSTM(b *testing.B) {
stores := map[storetypes.StoreKey]int{StoreKeyAuth: 0, StoreKeyBank: 1}
for i := 0; i < 26; i++ {
key := storetypes.NewKVStoreKey(strconv.FormatInt(int64(i), 10))
stores[key] = i + 2
}
storage := NewMultiMemDB(stores)
abasamevalueKeys := abaSameValueKeys(10000)
hasKeys := hasKeys(10000)
abaBigValue := make([]byte, 64<<10) // 64KiB
iterateAccounts := 100
testCases := []struct {
name string
block *MockBlock
setup func(MultiStore)
}{
{"random-10000/100", testBlock(10000, 100)},
{"no-conflict-10000", noConflictBlock(10000)},
{"worst-case-10000", worstCaseBlock(10000)},
{"iterate-10000/100", iterateBlock(10000, 100)},
{"random-10000/100", testBlock(10000, 100), nil},
{"has-hit-10000/100", hasBlock(10000, 100, hasKeys), func(storage MultiStore) { prepopulateHasKeys(storage, hasKeys) }},
{"has-miss-10000/100", hasBlock(10000, 100, hasKeys), nil},
{"no-conflict-10000", noConflictBlock(10000), nil},
{"worst-case-10000", worstCaseBlock(10000), nil},
{"iterate-10000/100", iterateBlock(10000, iterateAccounts), nil},
{
"iterate-10000/100-prepop",
iterateBlock(10000, iterateAccounts),
func(storage MultiStore) { prepopulateIterateAccounts(storage, iterateAccounts) },
},
{"iterate-newkeys-2000", iterateNewKeysBlock(2000), nil},
{
"aba-samevalue-10000",
abaSameValueBlock(abasamevalueKeys),
func(storage MultiStore) { prepopulateABASameValue(storage, abasamevalueKeys) },
},
{
"aba-samevalue-bigvalue-10000",
abaSameValueBlockWithValue(abasamevalueKeys, abaBigValue),
func(storage MultiStore) { prepopulateABASameValueWithValue(storage, abasamevalueKeys, abaBigValue) },
},
}
for _, tc := range testCases {
b.Run(tc.name+"-sequential", func(b *testing.B) {
storage := NewMultiMemDB(stores)
if tc.setup != nil {
tc.setup(storage)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
runSequential(storage, tc.block)
}
b.ReportMetric(1, "exec/txn")
b.ReportMetric(0, "val/txn")
})
for _, worker := range []int{1, 5, 10, 15, 20} {
b.Run(tc.name+"-worker-"+strconv.Itoa(worker), func(b *testing.B) {
storage := NewMultiMemDB(stores)
if tc.setup != nil {
tc.setup(storage)
}

incarnationCache := make([]atomic.Pointer[map[string]any], tc.block.Size())
for i := 0; i < tc.block.Size(); i++ {
m := make(map[string]any)
incarnationCache[i].Store(&m)
}

b.ResetTimer()
var executedTotal, validatedTotal uint64
for i := 0; i < b.N; i++ {
require.NoError(
b, executeBlock(stores, storage, worker, tc.block),
executed, validated, err := executeBlockWithEstimatesImpl(
context.Background(),
tc.block.Size(),
stores,
storage,
worker,
nil,
func(txn TxnIndex, store MultiStore) {
cache := incarnationCache[txn].Swap(nil)
tc.block.ExecuteTx(txn, store, *cache)
incarnationCache[txn].Store(cache)
},
false,
)
require.NoError(b, err)
executedTotal += executed
validatedTotal += validated
}
denom := float64(b.N * tc.block.Size())
b.ReportMetric(float64(executedTotal)/denom, "exec/txn")
b.ReportMetric(float64(validatedTotal)/denom, "val/txn")
})
}
}
}

func prepopulateIterateAccounts(storage MultiStore, accounts int) {
auth := storage.GetKVStore(StoreKeyAuth)
bank := storage.GetKVStore(StoreKeyBank)
zero := make([]byte, 8)
for i := 0; i < accounts; i++ {
acc := accountName(int64(i))
auth.Set([]byte("nonce"+acc), zero)
bank.Set([]byte("balance"+acc), zero)
}
}

// iterateNewKeysBlock stresses unordered index iteration costs by inserting a new key in each
// transaction and immediately iterating the store, forcing frequent key snapshot rebuilds.
func iterateNewKeysBlock(size int) *MockBlock {
txs := make([]Tx, size)
for i := 0; i < size; i++ {
idx := i
txs[i] = func(store MultiStore, _ Cache) error {
kv := store.GetKVStore(StoreKeyAuth)
kv.Set([]byte(fmt.Sprintf("iter-newkeys/%08d", idx)), []byte{1})

it := kv.Iterator(nil, nil)
defer it.Close()
for j := 0; it.Valid() && j < 10; j++ {
it.Next()
}
return nil
}
}
return NewMockBlock(txs)
}

func runSequential(storage MultiStore, block *MockBlock) {
for i, tx := range block.Txs {
block.Results[i] = tx(storage, nil)
}
}

func abaSameValueKeys(n int) [][]byte {
keys := make([][]byte, n)
for i := 0; i < n; i++ {
keys[i] = []byte(fmt.Sprintf("aba-samevalue/%08d", i))
}
return keys
}

// prepopulateABASameValue seeds storage with values matching the transaction writes.
// This creates a scenario where value-based validation prevents unnecessary re-execution.
func prepopulateABASameValue(storage MultiStore, keys [][]byte) {
kv := storage.GetKVStore(StoreKeyAuth)
value := []byte{1}
for _, key := range keys {
kv.Set(key, value)
}
}

func prepopulateABASameValueWithValue(storage MultiStore, keys [][]byte, value []byte) {
kv := storage.GetKVStore(StoreKeyAuth)
for _, key := range keys {
kv.Set(key, value)
}
}

func hasKeys(n int) [][]byte {
keys := make([][]byte, n)
for i := 0; i < n; i++ {
keys[i] = []byte(fmt.Sprintf("has/%08d", i))
}
return keys
}

func prepopulateHasKeys(storage MultiStore, keys [][]byte) {
kv := storage.GetKVStore(StoreKeyAuth)
value := []byte{1}
for _, key := range keys {
kv.Set(key, value)
}
}

func hasBlock(size, readsPerTx int, keys [][]byte) *MockBlock {
txs := make([]Tx, size)
for i := 0; i < size; i++ {
idx := i
txs[i] = func(store MultiStore, _ Cache) error {
kv := store.GetKVStore(StoreKeyAuth)
for j := 0; j < readsPerTx; j++ {
_ = kv.Has(keys[(idx+j)%len(keys)])
}
return nil
}
}
return NewMockBlock(txs)
}

func abaSameValueBlock(keys [][]byte) *MockBlock {
value := []byte{1}
return abaSameValueBlockWithValue(keys, value)
}

func abaSameValueBlockWithValue(keys [][]byte, value []byte) *MockBlock {
txs := make([]Tx, len(keys))
for i := range keys {
idx := i
txs[i] = func(store MultiStore, _ Cache) error {
kv := store.GetKVStore(StoreKeyAuth)
// Read dependency on previous key.
if idx > 0 {
_ = kv.Get(keys[idx-1])
}
// Write same value as pre-state.
kv.Set(keys[idx], value)
return nil
}
}
return NewMockBlock(txs)
}
133 changes: 133 additions & 0 deletions internal/blockstm/keycursor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package blockstm

import (
"bytes"

"github.com/tidwall/btree"

"github.com/cosmos/cosmos-sdk/internal/blockstm/tree"
)

type keyCursor[V any] interface {
Valid() bool
Key() Key
// Tree returns the per-key version tree, when available.
Tree() *tree.SmallBTree[secondaryDataItem[V]]
Next()
// Seek positions the cursor on the given key (if present).
Seek(Key) bool
Close()
}

type noopKeyCursor[V any] struct{}

func (noopKeyCursor[V]) Valid() bool { return false }
func (noopKeyCursor[V]) Key() Key { return nil }
func (noopKeyCursor[V]) Tree() *tree.SmallBTree[secondaryDataItem[V]] {
return nil
}
func (noopKeyCursor[V]) Next() {}
func (noopKeyCursor[V]) Seek(Key) bool { return false }
func (noopKeyCursor[V]) Close() {}

// btreeKeyCursor iterates the ordered key set using a tidwall/btree iterator.
//
// Depending on the underlying tree options, the iterator may hold a read lock
// until Close(). Callers should not block while the cursor is open.
type btreeKeyCursor[V any] struct {
iter btree.IterG[mvIndexKeyEntry[V]]

start []byte
end []byte
ascending bool
valid bool
}

func newBTreeKeyCursor[V any](keys interface {
Iter() btree.IterG[mvIndexKeyEntry[V]]
}, start, end []byte, ascending bool,
) *btreeKeyCursor[V] {
if keys == nil {
return &btreeKeyCursor[V]{start: start, end: end, ascending: ascending, valid: false}
}

it := keys.Iter()
c := &btreeKeyCursor[V]{iter: it, start: start, end: end, ascending: ascending}
c.valid = c.seekInitial()
return c
}

func (c *btreeKeyCursor[V]) seekInitial() bool {
var ok bool
if c.ascending {
if c.start != nil {
search := mvIndexKeyEntry[V]{Key: Key(c.start)}
ok = c.iter.Seek(search)
} else {
ok = c.iter.First()
}
} else {
if c.end != nil {
search := mvIndexKeyEntry[V]{Key: Key(c.end)}
ok = c.iter.Seek(search)
if !ok {
ok = c.iter.Last()
} else {
// end is exclusive
ok = c.iter.Prev()
}
} else {
ok = c.iter.Last()
}
}
if !ok {
return false
}
return c.keyInRange(c.Key())
}

func (c *btreeKeyCursor[V]) Close() {
c.iter.Release()
}

func (c *btreeKeyCursor[V]) Valid() bool {
return c.valid
}

func (c *btreeKeyCursor[V]) Key() Key {
return c.iter.Item().Key
}

func (c *btreeKeyCursor[V]) Tree() *tree.SmallBTree[secondaryDataItem[V]] {
return c.iter.Item().Tree
}

func (c *btreeKeyCursor[V]) Next() {
if !c.valid {
return
}
if c.ascending {
c.valid = c.iter.Next()
} else {
c.valid = c.iter.Prev()
}
if c.valid {
c.valid = c.keyInRange(c.Key())
}
}

func (c *btreeKeyCursor[V]) Seek(key Key) bool {
search := mvIndexKeyEntry[V]{Key: key}
c.valid = c.iter.Seek(search)
if c.valid {
c.valid = c.keyInRange(c.Key())
}
return c.valid
}

func (c *btreeKeyCursor[V]) keyInRange(key []byte) bool {
if c.ascending {
return c.end == nil || bytes.Compare(key, c.end) < 0
}
return c.start == nil || bytes.Compare(key, c.start) >= 0
}
Loading
Loading