Skip to content

Commit 5ca8376

Browse files
committed
feat: read-only state access
1 parent 031d4ff commit 5ca8376

File tree

3 files changed

+93
-25
lines changed

3 files changed

+93
-25
lines changed

libevm/ethtest/evm.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,36 @@ import (
2323

2424
"github.com/stretchr/testify/require"
2525

26-
"github.com/ava-labs/libevm/common"
2726
"github.com/ava-labs/libevm/core"
2827
"github.com/ava-labs/libevm/core/rawdb"
2928
"github.com/ava-labs/libevm/core/state"
29+
"github.com/ava-labs/libevm/core/types"
3030
"github.com/ava-labs/libevm/core/vm"
31+
"github.com/ava-labs/libevm/ethdb"
3132
"github.com/ava-labs/libevm/params"
3233
)
3334

35+
// NewEmptyStateDB returns a fresh database from [rawdb.NewMemoryDatabase], a
36+
// [state.Database] wrapping it, and a [state.StateDB] wrapping that, opened to
37+
// [types.EmptyRootHash].
38+
func NewEmptyStateDB(tb testing.TB) (ethdb.Database, state.Database, *state.StateDB) {
39+
tb.Helper()
40+
41+
db := rawdb.NewMemoryDatabase()
42+
cache := state.NewDatabase(db)
43+
sdb, err := state.New(types.EmptyRootHash, cache, nil)
44+
require.NoError(tb, err, "state.New()")
45+
return db, cache, sdb
46+
}
47+
3448
// NewZeroEVM returns a new EVM backed by a [rawdb.NewMemoryDatabase]; all other
3549
// arguments to [vm.NewEVM] are the zero values of their respective types,
3650
// except for the use of [core.CanTransfer] and [core.Transfer] instead of nil
3751
// functions.
3852
func NewZeroEVM(tb testing.TB, opts ...EVMOption) (*state.StateDB, *vm.EVM) {
3953
tb.Helper()
4054

41-
sdb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
42-
require.NoError(tb, err, "state.New()")
55+
_, _, sdb := NewEmptyStateDB(tb)
4356

4457
args := &evmConstructorArgs{
4558
vm.BlockContext{

libevm/precompiles/parallel/parallel.go

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import (
2525

2626
"github.com/ava-labs/libevm/common"
2727
"github.com/ava-labs/libevm/core"
28+
"github.com/ava-labs/libevm/core/state"
2829
"github.com/ava-labs/libevm/core/types"
2930
"github.com/ava-labs/libevm/core/vm"
31+
"github.com/ava-labs/libevm/libevm"
3032
"github.com/ava-labs/libevm/params"
3133
)
3234

@@ -44,16 +46,17 @@ import (
4446
type Handler[Result any] interface {
4547
BeforeBlock(*types.Header)
4648
Gas(*types.Transaction) (gas uint64, process bool)
47-
Process(index int, tx *types.Transaction) Result
49+
Process(index int, tx *types.Transaction, sdb libevm.StateReader) Result
4850
}
4951

5052
// A Processor orchestrates dispatch and collection of results from a [Handler].
5153
type Processor[R any] struct {
52-
handler Handler[R]
53-
workers sync.WaitGroup
54-
work chan *job
55-
results [](chan result[R])
56-
txGas map[common.Hash]uint64
54+
handler Handler[R]
55+
workers sync.WaitGroup
56+
work chan *job
57+
results [](chan result[R])
58+
txGas map[common.Hash]uint64
59+
stateShare stateDBSharer
5760
}
5861

5962
type job struct {
@@ -66,36 +69,75 @@ type result[T any] struct {
6669
val *T
6770
}
6871

72+
// A stateDBSharer allows concurrent workers to make copies of a primary
73+
// database. When the `nextAvailable` channel is closed, all workers call
74+
// [state.StateDB.Copy] then signal completion on the [sync.WaitGroup]. The
75+
// channel is replaced for each round of distribution.
76+
type stateDBSharer struct {
77+
nextAvailable chan struct{}
78+
primary *state.StateDB
79+
mu sync.Mutex
80+
workers int
81+
wg sync.WaitGroup
82+
}
83+
6984
// New constructs a new [Processor] with the specified number of concurrent
7085
// workers. [Processor.Close] must be called after the final call to
7186
// [Processor.FinishBlock] to avoid leaking goroutines.
7287
func New[R any](h Handler[R], workers int) *Processor[R] {
88+
workers = max(workers, 1)
89+
7390
p := &Processor[R]{
7491
handler: h,
7592
work: make(chan *job),
7693
txGas: make(map[common.Hash]uint64),
94+
stateShare: stateDBSharer{
95+
workers: workers,
96+
nextAvailable: make(chan struct{}),
97+
},
7798
}
7899

79-
workers = max(workers, 1)
80-
p.workers.Add(workers)
100+
p.workers.Add(workers) // for shutdown via [Processor.Close]
101+
p.stateShare.wg.Add(workers) // for readiness of [Processor.worker] loops
81102
for range workers {
82103
go p.worker()
83104
}
105+
p.stateShare.wg.Wait()
106+
84107
return p
85108
}
86109

87110
func (p *Processor[R]) worker() {
88111
defer p.workers.Done()
112+
113+
var sdb *state.StateDB
114+
share := &p.stateShare
115+
stateAvailable := share.nextAvailable
116+
// Without this signal of readiness, a premature call to
117+
// [Processor.StartBlock] could replace `share.nextAvailable` before we've
118+
// copied it.
119+
share.wg.Done()
120+
89121
for {
90-
w, ok := <-p.work
91-
if !ok {
92-
return
93-
}
122+
select {
123+
case <-stateAvailable: // guaranteed at the beginning of each block
124+
share.mu.Lock()
125+
sdb = share.primary.Copy()
126+
share.mu.Unlock()
94127

95-
r := p.handler.Process(w.index, w.tx)
96-
p.results[w.index] <- result[R]{
97-
tx: w.tx.Hash(),
98-
val: &r,
128+
stateAvailable = share.nextAvailable
129+
share.wg.Done()
130+
131+
case w, ok := <-p.work:
132+
if !ok {
133+
return
134+
}
135+
136+
r := p.handler.Process(w.index, w.tx, sdb)
137+
p.results[w.index] <- result[R]{
138+
tx: w.tx.Hash(),
139+
val: &r,
140+
}
99141
}
100142
}
101143
}
@@ -109,7 +151,8 @@ func (p *Processor[R]) Close() {
109151
// StartBlock dispatches transactions to the [Handler] and returns immediately.
110152
// It MUST be paired with a call to [Processor.FinishBlock], without overlap of
111153
// blocks.
112-
func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules) error {
154+
func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules, sdb *state.StateDB) error {
155+
p.stateShare.distribute(sdb)
113156
p.handler.BeforeBlock(types.CopyHeader(b.Header()))
114157
txs := b.Transactions()
115158
jobs := make([]*job, 0, len(txs))
@@ -149,6 +192,17 @@ func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules) error {
149192
return nil
150193
}
151194

195+
func (s *stateDBSharer) distribute(sdb *state.StateDB) {
196+
s.primary = sdb // no need to Copy() as each worker does it
197+
198+
ch := s.nextAvailable
199+
s.nextAvailable = make(chan struct{}) // already copied by each worker
200+
201+
s.wg.Add(s.workers)
202+
close(ch)
203+
s.wg.Wait()
204+
}
205+
152206
// FinishBlock returns the [Processor] to a state ready for the next block. A
153207
// return from FinishBlock guarantees that all dispatched work from the
154208
// respective call to [Processor.StartBlock] has been completed.

libevm/precompiles/parallel/parallel_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func reverserOutput(data, extra []byte) []byte {
7070
return out
7171
}
7272

73-
func (r *reverser) Process(i int, tx *types.Transaction) []byte {
73+
func (r *reverser) Process(i int, tx *types.Transaction, _ libevm.StateReader) []byte {
7474
return reverserOutput(tx.Data(), r.extra)
7575
}
7676

@@ -127,6 +127,8 @@ func TestProcessor(t *testing.T) {
127127
})
128128
}
129129

130+
_, _, sdb := ethtest.NewEmptyStateDB(t)
131+
130132
for _, tt := range tests {
131133
t.Run("", func(t *testing.T) {
132134
t.Logf("%+v", tt)
@@ -165,7 +167,7 @@ func TestProcessor(t *testing.T) {
165167

166168
extra := []byte("extra")
167169
block := types.NewBlock(&types.Header{Extra: extra}, txs, nil, nil, trie.NewStackTrie(nil))
168-
require.NoError(t, p.StartBlock(block, rules), "StartBlock()")
170+
require.NoError(t, p.StartBlock(block, rules, sdb), "StartBlock()")
169171
defer p.FinishBlock(block)
170172

171173
for i, tx := range txs {
@@ -287,18 +289,17 @@ func TestIntegration(t *testing.T) {
287289
TransactionIndex: ui,
288290
}
289291
if addr == handler.addr {
290-
res := handler.Process(i, tx)
291292
wantR.Logs = []*types.Log{{
292293
TxHash: tx.Hash(),
293294
TxIndex: ui,
294-
Data: res[:],
295+
Data: reverserOutput(data, nil),
295296
}}
296297
}
297298
want = append(want, wantR)
298299
}
299300

300301
block := types.NewBlock(header, txs, nil, nil, trie.NewStackTrie(nil))
301-
require.NoError(t, sut.StartBlock(block, rules), "StartBlock()")
302+
require.NoError(t, sut.StartBlock(block, rules, state), "StartBlock()")
302303
defer sut.FinishBlock(block)
303304

304305
pool := core.GasPool(math.MaxUint64)

0 commit comments

Comments
 (0)