Skip to content

Commit 825fd62

Browse files
Eric-Warehimeyihuangmmsqetechnicallyty
authored
feat: Upstream BlockSTM Fork (#25483)
Co-authored-by: yihuang <[email protected]> Co-authored-by: mmsqe <[email protected]> Co-authored-by: mmsqe <[email protected]> Co-authored-by: Tyler <[email protected]>
1 parent d790942 commit 825fd62

33 files changed

+4343
-4
lines changed

.github/workflows/test.yml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ jobs:
234234
with:
235235
name: "${{ github.sha }}-store-coverage"
236236
continue-on-error: true
237+
- uses: actions/download-artifact@v4
238+
if: env.GIT_DIFF
239+
with:
240+
name: "${{ github.sha }}-blockstm-coverage"
241+
continue-on-error: true
237242
- uses: actions/download-artifact@v4
238243
if: env.GIT_DIFF
239244
with:
@@ -253,7 +258,7 @@ jobs:
253258
if: env.GIT_DIFF
254259
uses: codecov/codecov-action@v5
255260
with:
256-
files: ./00profile.out,./01profile.out,./02profile.out,./03profile.out,./integration-profile.out,./e2e-profile.out,./client/v2/coverage.out,./core/coverage.out,./depinject/coverage.out,./errors/coverage.out,./math/coverage.out,./schema/coverage.out,./collections/coverage.out,./tools/cosmovisor/coverage.out,./tools/confix/coverage.out,./store/coverage.out,./log/coverage.out,./x/tx/coverage.out,./tools/benchmark/coverage.out
261+
files: ./00profile.out,./01profile.out,./02profile.out,./03profile.out,./integration-profile.out,./e2e-profile.out,./client/v2/coverage.out,./core/coverage.out,./depinject/coverage.out,./errors/coverage.out,./math/coverage.out,./schema/coverage.out,./collections/coverage.out,./tools/cosmovisor/coverage.out,./tools/confix/coverage.out,./store/coverage.out,./log/coverage.out,./x/tx/coverage.out,./tools/benchmark/coverage.out,./blockstm/coverage.out
257262
fail_ci_if_error: false
258263
verbose: true
259264
token: ${{ secrets.CODECOV_TOKEN }}
@@ -570,6 +575,33 @@ jobs:
570575
with:
571576
name: "${{ github.sha }}-store-coverage"
572577
path: ./store/coverage.out
578+
test-blockstm:
579+
runs-on: depot-ubuntu-22.04-4
580+
steps:
581+
- uses: actions/checkout@v5
582+
- uses: actions/setup-go@v6
583+
with:
584+
go-version: "1.25"
585+
check-latest: true
586+
cache: true
587+
cache-dependency-path: store/go.sum
588+
- uses: technote-space/[email protected]
589+
id: git_diff
590+
with:
591+
PATTERNS: |
592+
blockstm/**/*.go
593+
blockstm/go.mod
594+
blockstm/go.sum
595+
- name: tests
596+
if: env.GIT_DIFF
597+
run: |
598+
cd blockstm
599+
go test -mod=readonly -timeout 30m -coverprofile=coverage.out -covermode=atomic -coverpkg=./,./tree -tags='norace ledger test_ledger_mock' ./...
600+
- uses: actions/upload-artifact@v4
601+
if: env.GIT_DIFF
602+
with:
603+
name: "${{ github.sha }}-blockstm-coverage"
604+
path: ./blockstm/coverage.out
573605

574606
test-log:
575607
runs-on: depot-ubuntu-22.04-4

blockstm/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
`blockstm` implements the [block-stm algorithm](https://arxiv.org/abs/2203.06871), it follows the paper pseudocode pretty closely.
2+
3+
The main API is a simple function call `ExecuteBlock`:
4+
5+
```golang
6+
type ExecuteFn func(TxnIndex, MultiStore)
7+
func ExecuteBlock(
8+
ctx context.Context, // context for cancellation
9+
blockSize int, // the number of the transactions to be executed
10+
stores []storetypes.StoreKey, // the list of store keys to support
11+
storage MultiStore, // the parent storage, after all transactions are executed, the whole change sets are written into parent storage at once
12+
executors int, // how many concurrent executors to spawn
13+
executeFn ExecuteFn, // callback function to actually execute a transaction with a wrapped `MultiStore`.
14+
) error
15+
```
16+
17+
The main deviations from the paper are:
18+
19+
### Optimisation
20+
21+
We applied the optimization described in section 4 of the paper:
22+
23+
```
24+
Block-STM calls add_dependency from the VM itself, and can thus re-read and continue execution when false is returned.
25+
```
26+
27+
When the VM execution reads an `ESTIMATE` mark, it'll hang on a `CondVar`, so it can resume execution after the dependency is resolved,
28+
much more efficient than abortion and rerun.
29+
30+
### Support Deletion, Iteration, and MultiStore
31+
32+
These features are necessary for integration with cosmos-sdk.
33+
34+
The multi-version data structure is implemented with nested btree for easier iteration support,
35+
the `WriteSet` is also implemented with a btree, and it takes advantage of ordered property to optimize some logic.
36+
37+
The internal data structures are also adapted with multiple stores in mind.
38+
39+
### Attribution
40+
41+
This package was originally authored in [go-block-stm](https://github.com/crypto-org-chain/go-block-stm). We have brought the full source tree into the SDK so that we can natively incorporate the library and required changes into the SDK. Over time we expect to incoporate optimizations and deviations from the upstream implementation.

blockstm/bench_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package blockstm
2+
3+
import (
4+
"context"
5+
"strconv"
6+
"testing"
7+
8+
"github.com/test-go/testify/require"
9+
10+
storetypes "cosmossdk.io/store/types"
11+
)
12+
13+
func BenchmarkBlockSTM(b *testing.B) {
14+
stores := map[storetypes.StoreKey]int{StoreKeyAuth: 0, StoreKeyBank: 1}
15+
for i := 0; i < 26; i++ {
16+
key := storetypes.NewKVStoreKey(strconv.FormatInt(int64(i), 10))
17+
stores[key] = i + 2
18+
}
19+
storage := NewMultiMemDB(stores)
20+
testCases := []struct {
21+
name string
22+
block *MockBlock
23+
}{
24+
{"random-10000/100", testBlock(10000, 100)},
25+
{"no-conflict-10000", noConflictBlock(10000)},
26+
{"worst-case-10000", worstCaseBlock(10000)},
27+
{"iterate-10000/100", iterateBlock(10000, 100)},
28+
}
29+
for _, tc := range testCases {
30+
b.Run(tc.name+"-sequential", func(b *testing.B) {
31+
b.ResetTimer()
32+
for i := 0; i < b.N; i++ {
33+
runSequential(storage, tc.block)
34+
}
35+
})
36+
for _, worker := range []int{1, 5, 10, 15, 20} {
37+
b.Run(tc.name+"-worker-"+strconv.Itoa(worker), func(b *testing.B) {
38+
b.ResetTimer()
39+
for i := 0; i < b.N; i++ {
40+
require.NoError(
41+
b,
42+
ExecuteBlock(context.Background(), tc.block.Size(), stores, storage, worker, tc.block.ExecuteTx),
43+
)
44+
}
45+
})
46+
}
47+
}
48+
}
49+
50+
func runSequential(storage MultiStore, block *MockBlock) {
51+
for i, tx := range block.Txs {
52+
block.Results[i] = tx(storage)
53+
}
54+
}

blockstm/condvar.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package blockstm
2+
3+
import "sync"
4+
5+
type Condvar struct {
6+
sync.Mutex
7+
notified bool
8+
cond sync.Cond
9+
}
10+
11+
func NewCondvar() *Condvar {
12+
c := &Condvar{}
13+
c.cond = *sync.NewCond(c)
14+
return c
15+
}
16+
17+
func (cv *Condvar) Wait() {
18+
cv.Lock()
19+
for !cv.notified {
20+
cv.cond.Wait()
21+
}
22+
cv.Unlock()
23+
}
24+
25+
func (cv *Condvar) Notify() {
26+
cv.Lock()
27+
cv.notified = true
28+
cv.Unlock()
29+
cv.cond.Signal()
30+
}

blockstm/executor.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package blockstm
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
// Executor fields are not mutated during execution.
9+
type Executor struct {
10+
ctx context.Context // context for cancellation
11+
scheduler *Scheduler // scheduler for task management
12+
txExecutor TxExecutor // callback to actually execute a transaction
13+
mvMemory *MVMemory // multi-version memory for the executor
14+
15+
// index of the executor, used for debugging output
16+
i int
17+
}
18+
19+
func NewExecutor(
20+
ctx context.Context,
21+
scheduler *Scheduler,
22+
txExecutor TxExecutor,
23+
mvMemory *MVMemory,
24+
i int,
25+
) *Executor {
26+
return &Executor{
27+
ctx: ctx,
28+
scheduler: scheduler,
29+
txExecutor: txExecutor,
30+
mvMemory: mvMemory,
31+
i: i,
32+
}
33+
}
34+
35+
// Run executes all tasks until completion
36+
// Invariant `num_active_tasks`:
37+
// - `NextTask` increases it if returns a valid task.
38+
// - `TryExecute` and `NeedsReexecution` don't change it if it returns a new valid task to run,
39+
// otherwise it decreases it.
40+
func (e *Executor) Run() error {
41+
var kind TaskKind
42+
version := InvalidTxnVersion
43+
for !e.scheduler.Done() {
44+
if !version.Valid() {
45+
// check for cancellation
46+
select {
47+
case <-e.ctx.Done():
48+
return nil
49+
default:
50+
}
51+
52+
version, kind = e.scheduler.NextTask()
53+
continue
54+
}
55+
56+
switch kind {
57+
case TaskKindExecution:
58+
version, kind = e.TryExecute(version)
59+
case TaskKindValidation:
60+
version, kind = e.NeedsReexecution(version)
61+
default:
62+
return fmt.Errorf("unknown task kind %v", kind)
63+
}
64+
}
65+
return nil
66+
}
67+
68+
func (e *Executor) TryExecute(version TxnVersion) (TxnVersion, TaskKind) {
69+
e.scheduler.executedTxns.Add(1)
70+
view := e.execute(version.Index)
71+
wroteNewLocation := e.mvMemory.Record(version, view)
72+
return e.scheduler.FinishExecution(version, wroteNewLocation)
73+
}
74+
75+
func (e *Executor) NeedsReexecution(version TxnVersion) (TxnVersion, TaskKind) {
76+
e.scheduler.validatedTxns.Add(1)
77+
valid := e.mvMemory.ValidateReadSet(version.Index)
78+
aborted := !valid && e.scheduler.TryValidationAbort(version)
79+
if aborted {
80+
e.mvMemory.ConvertWritesToEstimates(version.Index)
81+
}
82+
return e.scheduler.FinishValidation(version.Index, aborted)
83+
}
84+
85+
func (e *Executor) execute(txn TxnIndex) *MultiMVMemoryView {
86+
view := e.mvMemory.View(txn)
87+
e.txExecutor(txn, view)
88+
return view
89+
}

0 commit comments

Comments
 (0)