Skip to content

Commit f2c79b7

Browse files
feat: saexec package (#20)
Introduces the `saexec.Executor`, which manages a FIFO queue of blocks for execution. Recommended review order: 1. `saexec/saexec.go` defines the `Executor` and getters. 2. `saexec/execution.go` implements the execution loop 3. `saexec/subscriptions.go` implements notification points for `eth_subscribe` 4. `saexec/saexec_test.go` testing of the above 5. All other files; those necessary for all of the above Closes #14 --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Stephen Buttolph <[email protected]>
1 parent 3973850 commit f2c79b7

File tree

21 files changed

+1561
-29
lines changed

21 files changed

+1561
-29
lines changed

.golangci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ linters-settings:
5151
- localmodule
5252
# The rest of these break developer expections, in increasing order of
5353
# divergence, so are at the end to increase the chance of being seen.
54-
- alias
5554
- dot
5655
- blank
5756
goheader:

blocks/blockstest/blocks.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,34 @@ func WithReceipts(rs types.Receipts) EthBlockOption {
6767
})
6868
}
6969

70+
// A BlockOption configures the default block properties created by [NewBlock].
71+
type BlockOption = options.Option[blockProperties]
72+
7073
// NewBlock constructs an SAE block, wrapping the raw Ethereum block.
71-
func NewBlock(tb testing.TB, eth *types.Block, parent, lastSettled *blocks.Block) *blocks.Block {
74+
func NewBlock(tb testing.TB, eth *types.Block, parent, lastSettled *blocks.Block, opts ...BlockOption) *blocks.Block {
7275
tb.Helper()
73-
b, err := blocks.New(eth, parent, lastSettled, saetest.NewTBLogger(tb, logging.Warn))
76+
77+
props := options.ApplyTo(&blockProperties{}, opts...)
78+
if props.logger == nil {
79+
props.logger = saetest.NewTBLogger(tb, logging.Warn)
80+
}
81+
82+
b, err := blocks.New(eth, parent, lastSettled, props.logger)
7483
require.NoError(tb, err, "blocks.New()")
7584
return b
7685
}
7786

87+
type blockProperties struct {
88+
logger logging.Logger
89+
}
90+
91+
// WithLogger overrides the logger passed to [blocks.New] by [NewBlock].
92+
func WithLogger(l logging.Logger) BlockOption {
93+
return options.Func[blockProperties](func(p *blockProperties) {
94+
p.logger = l
95+
})
96+
}
97+
7898
// NewGenesis constructs a new [core.Genesis], writes it to the database, and
7999
// returns wraps [core.Genesis.ToBlock] with [NewBlock]. It assumes a nil
80100
// [triedb.Config] unless overridden by a [WithTrieDBConfig]. The block is

blocks/blockstest/blocks_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,11 @@ func TestIntegration(t *testing.T) {
7474
txs = append(txs, tx)
7575
}
7676
}
77-
b := build.NewBlock(t, txs, ModifyHeader(func(h *types.Header) {
78-
h.GasLimit = 100e6
79-
}))
77+
b := build.NewBlock(t, txs, WithEthBlockOptions(
78+
ModifyHeader(func(h *types.Header) {
79+
h.GasLimit = 100e6
80+
})),
81+
)
8082

8183
receipts, _, _, err := stateProc.Process(b.EthBlock(), sdb, *bc.GetVMConfig())
8284
require.NoError(t, err, "%T.Process(%T.NewBlock().EthBlock()...)", stateProc, build)

blocks/blockstest/chain.go

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,23 @@
88
package blockstest
99

1010
import (
11+
"slices"
12+
"sync"
1113
"testing"
1214

15+
"github.com/ava-labs/libevm/common"
1316
"github.com/ava-labs/libevm/core/types"
17+
"github.com/ava-labs/libevm/libevm/options"
1418

1519
"github.com/ava-labs/strevm/blocks"
1620
)
1721

1822
// A ChainBuilder builds a chain of blocks, maintaining necessary invariants.
1923
type ChainBuilder struct {
20-
chain []*blocks.Block
24+
chain []*blocks.Block
25+
blocksByHash sync.Map
26+
27+
defaultOpts []ChainOption
2128
}
2229

2330
// NewChainBuilder returns a new ChainBuilder starting from the provided block,
@@ -28,17 +35,78 @@ func NewChainBuilder(genesis *blocks.Block) *ChainBuilder {
2835
}
2936
}
3037

38+
// A ChainOption configures [ChainBuilder.NewBlock].
39+
type ChainOption = options.Option[chainOptions]
40+
41+
// SetDefaultOptions sets the default options upon which all
42+
// additional options passed to [ChainBuilder.NewBlock] are appended.
43+
func (cb *ChainBuilder) SetDefaultOptions(opts ...ChainOption) {
44+
cb.defaultOpts = opts
45+
}
46+
47+
type chainOptions struct {
48+
eth []EthBlockOption
49+
sae []BlockOption
50+
}
51+
52+
// WithEthBlockOptions wraps the options that [ChainBuilder.NewBlock] propagates
53+
// to [NewEthBlock].
54+
func WithEthBlockOptions(opts ...EthBlockOption) ChainOption {
55+
return options.Func[chainOptions](func(co *chainOptions) {
56+
co.eth = append(co.eth, opts...)
57+
})
58+
}
59+
60+
// WithBlockOptions wraps the options that [ChainBuilder.NewBlock] propagates to
61+
// [NewBlock].
62+
func WithBlockOptions(opts ...BlockOption) ChainOption {
63+
return options.Func[chainOptions](func(co *chainOptions) {
64+
co.sae = append(co.sae, opts...)
65+
})
66+
}
67+
3168
// NewBlock constructs and returns a new block in the chain.
32-
func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts ...EthBlockOption) *blocks.Block {
69+
func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts ...ChainOption) *blocks.Block {
3370
tb.Helper()
71+
72+
allOpts := new(chainOptions)
73+
options.ApplyTo(allOpts, cb.defaultOpts...)
74+
options.ApplyTo(allOpts, opts...)
75+
3476
last := cb.Last()
35-
eth := NewEthBlock(last.EthBlock(), txs, opts...)
36-
cb.chain = append(cb.chain, NewBlock(tb, eth, last, nil)) // TODO(arr4n) support last-settled blocks
37-
return cb.Last()
77+
eth := NewEthBlock(last.EthBlock(), txs, allOpts.eth...)
78+
b := NewBlock(tb, eth, last, nil, allOpts.sae...) // TODO(arr4n) support last-settled blocks
79+
cb.chain = append(cb.chain, b)
80+
cb.blocksByHash.Store(b.Hash(), b)
81+
82+
return b
3883
}
3984

4085
// Last returns the last block to be built by the builder, which MAY be the
4186
// genesis block passed to the constructor.
4287
func (cb *ChainBuilder) Last() *blocks.Block {
4388
return cb.chain[len(cb.chain)-1]
4489
}
90+
91+
// AllBlocks returns all blocks, including the genesis passed to
92+
// [NewChainBuilder].
93+
func (cb *ChainBuilder) AllBlocks() []*blocks.Block {
94+
return slices.Clone(cb.chain)
95+
}
96+
97+
// AllExceptGenesis returns all blocks created with [ChainBuilder.NewBlock].
98+
func (cb *ChainBuilder) AllExceptGenesis() []*blocks.Block {
99+
return slices.Clone(cb.chain[1:])
100+
}
101+
102+
// GetBlock returns the block with specified hash and height, and a flag
103+
// indicating if it was found. If either argument does not match, it returns
104+
// `nil, false`.
105+
func (cb *ChainBuilder) GetBlock(h common.Hash, num uint64) (*blocks.Block, bool) {
106+
ifc, _ := cb.blocksByHash.Load(h)
107+
b, ok := ifc.(*blocks.Block)
108+
if !ok || b.NumberU64() != num {
109+
return nil, false
110+
}
111+
return b, true
112+
}

blocks/export.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ func (b *Block) BuildTime() uint64 { return b.b.Time() }
3737
// Hash returns [types.Block.Hash] from the wrapped [types.Block].
3838
func (b *Block) Hash() common.Hash { return b.b.Hash() }
3939

40+
// Header returns [types.Block.Header] from the wrapped [types.Block].
41+
func (b *Block) Header() *types.Header { return b.b.Header() }
42+
4043
// ParentHash returns [types.Block.ParentHash] from the wrapped [types.Block].
4144
func (b *Block) ParentHash() common.Hash { return b.b.ParentHash() }
4245

@@ -45,3 +48,6 @@ func (b *Block) NumberU64() uint64 { return b.b.NumberU64() }
4548

4649
// Number returns [types.Block.Number] from the wrapped [types.Block].
4750
func (b *Block) Number() *big.Int { return b.b.Number() }
51+
52+
// Transactions returns [types.Block.Transactions] from the wrapped [types.Block].
53+
func (b *Block) Transactions() types.Transactions { return b.b.Transactions() }

cmputils/types.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package cmputils
5+
6+
import (
7+
"math/big"
8+
9+
"github.com/ava-labs/libevm/core/types"
10+
"github.com/google/go-cmp/cmp"
11+
)
12+
13+
// BigInts returns a [cmp.Comparer] for [big.Int] pointers. A nil pointer is not
14+
// equal to zero.
15+
func BigInts() cmp.Option {
16+
return ComparerWithNilCheck(func(a, b *big.Int) bool {
17+
return a.Cmp(b) == 0
18+
})
19+
}
20+
21+
// BlocksByHash returns a [cmp.Comparer] for [types.Block] pointers, equating
22+
// them by hash alone.
23+
func BlocksByHash() cmp.Option {
24+
return ComparerWithNilCheck(func(b, c *types.Block) bool {
25+
return b.Hash() == c.Hash()
26+
})
27+
}
28+
29+
// ReceiptsByTxHash returns a [cmp.Comparer] for [types.Receipt] pointers,
30+
// equating them by transaction hash alone.
31+
func ReceiptsByTxHash() cmp.Option {
32+
return ComparerWithNilCheck(func(r, s *types.Receipt) bool {
33+
return r.TxHash == s.TxHash
34+
})
35+
}

gastime/gastime.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ func New(unixSeconds uint64, target, startingExcess gas.Gas) *Time {
5858
// TargetToRate is the ratio between [Time.Target] and [proxytime.Time.Rate].
5959
const TargetToRate = 2
6060

61+
// TargetToExcessScaling is the ratio between [Time.Target] and the reciprocal
62+
// of the [Time.Excess] coefficient used in calculating [Time.Price]. In
63+
// [ACP-176] this is the K variable.
64+
//
65+
// [ACP-176]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/176-dynamic-evm-gas-limit-and-price-discovery-updates
66+
const TargetToExcessScaling = 87
67+
6168
// MaxTarget is the maximum allowable [Time.Target] to avoid overflows of the
6269
// associated [proxytime.Time.Rate]. Values above this are silently clamped.
6370
const MaxTarget = gas.Gas(math.MaxUint64 / TargetToRate)
@@ -91,14 +98,11 @@ func (tm *Time) Price() gas.Price {
9198
// excessScalingFactor returns the K variable of ACP-103/176, i.e. 87*T, capped
9299
// at [math.MaxUint64].
93100
func (tm *Time) excessScalingFactor() gas.Gas {
94-
const (
95-
targetToK = 87
96-
overflowThreshold = math.MaxUint64 / targetToK
97-
)
101+
const overflowThreshold = math.MaxUint64 / TargetToExcessScaling
98102
if tm.target > overflowThreshold {
99103
return math.MaxUint64
100104
}
101-
return targetToK * tm.target
105+
return TargetToExcessScaling * tm.target
102106
}
103107

104108
// BaseFee is equivalent to [Time.Price], returning the result as a uint256 for

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ go 1.24.8
55
require (
66
github.com/ava-labs/avalanchego v1.13.2
77
github.com/ava-labs/libevm v1.13.15-0.20251112182915-1ec8741af98f
8-
github.com/google/go-cmp v0.6.0
8+
github.com/google/go-cmp v0.7.0
99
github.com/holiman/uint256 v1.2.4
1010
github.com/stretchr/testify v1.10.0
11+
go.uber.org/goleak v1.3.0
1112
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
1213
)
1314

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
166166
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
167167
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
168168
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
169-
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
170-
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
169+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
170+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
171171
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
172172
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
173173
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=

hook/hook.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
// Package hook defines points in an SAE block's lifecycle at which common or
5+
// user-injected behaviour needs to be performed. Functions in this package
6+
// SHOULD be called by all code dealing with a block at the respective point in
7+
// its lifecycle, be that during validation, execution, or otherwise.
8+
package hook
9+
10+
import (
11+
"fmt"
12+
13+
"github.com/ava-labs/avalanchego/vms/components/gas"
14+
"github.com/ava-labs/libevm/core/state"
15+
"github.com/ava-labs/libevm/core/types"
16+
"github.com/ava-labs/libevm/params"
17+
18+
"github.com/ava-labs/strevm/blocks"
19+
"github.com/ava-labs/strevm/gastime"
20+
"github.com/ava-labs/strevm/intmath"
21+
saeparams "github.com/ava-labs/strevm/params"
22+
)
23+
24+
// Points define user-injected hook points.
25+
type Points interface {
26+
GasTarget(parent *types.Block) gas.Gas
27+
SubSecondBlockTime(*types.Block) gas.Gas
28+
BeforeBlock(params.Rules, *state.StateDB, *types.Block) error
29+
AfterBlock(*state.StateDB, *types.Block, types.Receipts)
30+
}
31+
32+
// BeforeBlock is intended to be called before processing a block, with the gas
33+
// target sourced from [Points].
34+
func BeforeBlock(pts Points, rules params.Rules, sdb *state.StateDB, b *blocks.Block, clock *gastime.Time) error {
35+
clock.FastForwardTo(
36+
b.BuildTime(),
37+
pts.SubSecondBlockTime(b.EthBlock()),
38+
)
39+
target := pts.GasTarget(b.ParentBlock().EthBlock())
40+
if err := clock.SetTarget(target); err != nil {
41+
return fmt.Errorf("%T.SetTarget() before block: %w", clock, err)
42+
}
43+
return pts.BeforeBlock(rules, sdb, b.EthBlock())
44+
}
45+
46+
// AfterBlock is intended to be called after processing a block, with the gas
47+
// sourced from [types.Block.GasUsed] or equivalent.
48+
func AfterBlock(pts Points, sdb *state.StateDB, b *types.Block, clock *gastime.Time, used gas.Gas, rs types.Receipts) {
49+
clock.Tick(used)
50+
pts.AfterBlock(sdb, b, rs)
51+
}
52+
53+
// MinimumGasConsumption MUST be used as the implementation for the respective
54+
// method on [params.RulesHooks]. The concrete type implementing the hooks MUST
55+
// propagate incoming and return arguments unchanged.
56+
func MinimumGasConsumption(txLimit uint64) uint64 {
57+
_ = (params.RulesHooks)(nil) // keep the import to allow [] doc links
58+
return intmath.CeilDiv(txLimit, saeparams.Lambda)
59+
}

0 commit comments

Comments
 (0)