Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
171cd6f
feat: `saexec` package
ARR4N Sep 26, 2025
5eca897
Merge branch 'main' into arr4n/saexec
ARR4N Nov 17, 2025
1fd6ecf
refactor!: use MIT contract for testing
ARR4N Nov 17, 2025
bd0a0c3
fix: log errored tx execution and continue
ARR4N Nov 17, 2025
f3ca5e7
doc: fix misspelling
ARR4N Nov 17, 2025
d04027f
refactor: `escrow` package provides contract helpers
ARR4N Nov 17, 2025
f442e76
fix: remove duplicated log fields
ARR4N Nov 17, 2025
d74bccc
refactor: `newSUT()` constructs memory database
ARR4N Nov 17, 2025
90a49eb
feat: `saetest.TBLogger` errors cancel context
ARR4N Nov 17, 2025
0245a1b
feat: user-defined before- and after-block hooks
ARR4N Nov 17, 2025
fc2f6fc
Merge branch 'main' into arr4n/saexec
ARR4N Nov 17, 2025
0183d17
feat: `hook.Points` supports sub-second block times
ARR4N Nov 17, 2025
e7772ce
chore: placate the linter
ARR4N Nov 17, 2025
03aa639
chore: if `golangci-lint` just said what it wanted then this would al…
ARR4N Nov 17, 2025
472f9fb
chore: Linty McLintface
ARR4N Nov 18, 2025
433494f
fix: contextual opcodes, particularly `BLOCKHASH`
ARR4N Nov 18, 2025
d56614d
refactor: `defer` unlocking `CondVar.L`
ARR4N Nov 18, 2025
f7bcfb1
chore: no points for guessing
ARR4N Nov 18, 2025
476e73c
refactor: use `zap.Stringer` for block hash
ARR4N Nov 18, 2025
18ee4b5
refactor: move `TimeNotThreadsafe` to test file and un-export
ARR4N Nov 18, 2025
6feac2a
refactor: simplify `ChainBuilder.NewBlock` options handling
ARR4N Nov 18, 2025
eb40cdd
test: `BASEFEE` op code
ARR4N Nov 18, 2025
9e314c4
test: additional contextual op codes
ARR4N Nov 18, 2025
e042828
refactor: exponential backoff for `Enqueue()` wait warning
ARR4N Nov 18, 2025
894b190
doc: permalink to `Escrow.sol`
ARR4N Nov 18, 2025
88d0728
refactor: return `consensus.Engine(nil)`
ARR4N Nov 18, 2025
f55b3b9
refactor: correct parenthesis matching
ARR4N Nov 18, 2025
00e5417
refactor: stop embedding `BlockSource` in `chainContext`
ARR4N Nov 18, 2025
c1f12b8
fix: `ChainBuilder` uses `sync.Map` for blocks stored by hash
ARR4N Nov 18, 2025
4fe05c1
refactor: remove specific type from unindexed `for range`
ARR4N Nov 18, 2025
0fafec4
refactor: determine contract address from `Nonce()` value of tx
ARR4N Nov 18, 2025
e64de9c
refactor: use `math.Pow()` for calculating gas price in test
ARR4N Nov 18, 2025
beed9ca
refactor: remove `executionScratchSpace` and `Executor.init()`
ARR4N Nov 18, 2025
c6d1ef2
refactor: make gas clock internal variable in `Executor.execute()`
ARR4N Nov 18, 2025
48342cd
fix: `ApplyTransaction` errors are `FATAL`
ARR4N Nov 19, 2025
caf3242
feat: `snapshot.Tree.Cap(..., 0)` on shutdown to persist top layer
ARR4N Nov 19, 2025
0e3a778
refactor: review suggestions
ARR4N Nov 19, 2025
06256a9
refactor!: don't disable snapshot generation on shutdown
ARR4N Nov 19, 2025
f41f1a7
refactor: base block-ordering assertion on parent hash
ARR4N Nov 19, 2025
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: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ linters-settings:
- localmodule
# The rest of these break developer expections, in increasing order of
# divergence, so are at the end to increase the chance of being seen.
- alias
- dot
- blank
goheader:
Expand Down
24 changes: 22 additions & 2 deletions blocks/blockstest/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,34 @@ func WithReceipts(rs types.Receipts) EthBlockOption {
})
}

// A BlockOption configures the default block properties created by [NewBlock].
type BlockOption = options.Option[blockProperties]

// NewBlock constructs an SAE block, wrapping the raw Ethereum block.
func NewBlock(tb testing.TB, eth *types.Block, parent, lastSettled *blocks.Block) *blocks.Block {
func NewBlock(tb testing.TB, eth *types.Block, parent, lastSettled *blocks.Block, opts ...BlockOption) *blocks.Block {
tb.Helper()
b, err := blocks.New(eth, parent, lastSettled, saetest.NewTBLogger(tb, logging.Warn))

props := options.ApplyTo(&blockProperties{}, opts...)
if props.logger == nil {
props.logger = saetest.NewTBLogger(tb, logging.Warn)
}

b, err := blocks.New(eth, parent, lastSettled, props.logger)
require.NoError(tb, err, "blocks.New()")
return b
}

type blockProperties struct {
logger logging.Logger
}

// WithLogger overrides the logger passed to [blocks.New] by [NewBlock].
func WithLogger(l logging.Logger) BlockOption {
return options.Func[blockProperties](func(p *blockProperties) {
p.logger = l
})
}

// NewGenesis constructs a new [core.Genesis], writes it to the database, and
// returns wraps [core.Genesis.ToBlock] with [NewBlock]. It assumes a nil
// [triedb.Config] unless overridden by a [WithTrieDBConfig]. The block is
Expand Down
8 changes: 5 additions & 3 deletions blocks/blockstest/blocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ func TestIntegration(t *testing.T) {
txs = append(txs, tx)
}
}
b := build.NewBlock(t, txs, ModifyHeader(func(h *types.Header) {
h.GasLimit = 100e6
}))
b := build.NewBlock(t, txs, WithEthBlockOptions(
ModifyHeader(func(h *types.Header) {
h.GasLimit = 100e6
})),
)

receipts, _, _, err := stateProc.Process(b.EthBlock(), sdb, *bc.GetVMConfig())
require.NoError(t, err, "%T.Process(%T.NewBlock().EthBlock()...)", stateProc, build)
Expand Down
78 changes: 73 additions & 5 deletions blocks/blockstest/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@
package blockstest

import (
"slices"
"sync"
"testing"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/libevm/options"

"github.com/ava-labs/strevm/blocks"
)

// A ChainBuilder builds a chain of blocks, maintaining necessary invariants.
type ChainBuilder struct {
chain []*blocks.Block
chain []*blocks.Block
blocksByHash sync.Map

defaultOpts []ChainOption
}

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

// A ChainOption configures [ChainBuilder.NewBlock].
type ChainOption = options.Option[chainOptions]

// SetDefaultOptions sets the default options upon which all
// additional options passed to [ChainBuilder.NewBlock] are appended.
func (cb *ChainBuilder) SetDefaultOptions(opts ...ChainOption) {
cb.defaultOpts = opts
}

type chainOptions struct {
eth []EthBlockOption
sae []BlockOption
}

// WithEthBlockOptions wraps the options that [ChainBuilder.NewBlock] propagates
// to [NewEthBlock].
func WithEthBlockOptions(opts ...EthBlockOption) ChainOption {
return options.Func[chainOptions](func(co *chainOptions) {
co.eth = append(co.eth, opts...)
})
}

// WithBlockOptions wraps the options that [ChainBuilder.NewBlock] propagates to
// [NewBlock].
func WithBlockOptions(opts ...BlockOption) ChainOption {
return options.Func[chainOptions](func(co *chainOptions) {
co.sae = append(co.sae, opts...)
})
}

// NewBlock constructs and returns a new block in the chain.
func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts ...EthBlockOption) *blocks.Block {
func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts ...ChainOption) *blocks.Block {
tb.Helper()

allOpts := new(chainOptions)
options.ApplyTo(allOpts, cb.defaultOpts...)
options.ApplyTo(allOpts, opts...)

last := cb.Last()
eth := NewEthBlock(last.EthBlock(), txs, opts...)
cb.chain = append(cb.chain, NewBlock(tb, eth, last, nil)) // TODO(arr4n) support last-settled blocks
return cb.Last()
eth := NewEthBlock(last.EthBlock(), txs, allOpts.eth...)
b := NewBlock(tb, eth, last, nil, allOpts.sae...) // TODO(arr4n) support last-settled blocks
cb.chain = append(cb.chain, b)
cb.blocksByHash.Store(b.Hash(), b)

return b
}

// Last returns the last block to be built by the builder, which MAY be the
// genesis block passed to the constructor.
func (cb *ChainBuilder) Last() *blocks.Block {
return cb.chain[len(cb.chain)-1]
}

// AllBlocks returns all blocks, including the genesis passed to
// [NewChainBuilder].
func (cb *ChainBuilder) AllBlocks() []*blocks.Block {
return slices.Clone(cb.chain)
}

// AllExceptGenesis returns all blocks created with [ChainBuilder.NewBlock].
func (cb *ChainBuilder) AllExceptGenesis() []*blocks.Block {
return slices.Clone(cb.chain[1:])
}

// GetBlock returns the block with specified hash and height, and a flag
// indicating if it was found. If either argument does not match, it returns
// `nil, false`.
func (cb *ChainBuilder) GetBlock(h common.Hash, num uint64) (*blocks.Block, bool) {
ifc, _ := cb.blocksByHash.Load(h)
b, ok := ifc.(*blocks.Block)
if !ok || b.NumberU64() != num {
return nil, false
}
return b, true
}
6 changes: 6 additions & 0 deletions blocks/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ func (b *Block) BuildTime() uint64 { return b.b.Time() }
// Hash returns [types.Block.Hash] from the wrapped [types.Block].
func (b *Block) Hash() common.Hash { return b.b.Hash() }

// Header returns [types.Block.Header] from the wrapped [types.Block].
func (b *Block) Header() *types.Header { return b.b.Header() }

// ParentHash returns [types.Block.ParentHash] from the wrapped [types.Block].
func (b *Block) ParentHash() common.Hash { return b.b.ParentHash() }

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

// Number returns [types.Block.Number] from the wrapped [types.Block].
func (b *Block) Number() *big.Int { return b.b.Number() }

// Transactions returns [types.Block.Transactions] from the wrapped [types.Block].
func (b *Block) Transactions() types.Transactions { return b.b.Transactions() }
35 changes: 35 additions & 0 deletions cmputils/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package cmputils

import (
"math/big"

"github.com/ava-labs/libevm/core/types"
"github.com/google/go-cmp/cmp"
)

// BigInts returns a [cmp.Comparer] for [big.Int] pointers. A nil pointer is not
// equal to zero.
func BigInts() cmp.Option {
return ComparerWithNilCheck(func(a, b *big.Int) bool {
return a.Cmp(b) == 0
})
}

// BlocksByHash returns a [cmp.Comparer] for [types.Block] pointers, equating
// them by hash alone.
func BlocksByHash() cmp.Option {
return ComparerWithNilCheck(func(b, c *types.Block) bool {
return b.Hash() == c.Hash()
})
}

// ReceiptsByTxHash returns a [cmp.Comparer] for [types.Receipt] pointers,
// equating them by transaction hash alone.
func ReceiptsByTxHash() cmp.Option {
return ComparerWithNilCheck(func(r, s *types.Receipt) bool {
return r.TxHash == s.TxHash
})
}
14 changes: 9 additions & 5 deletions gastime/gastime.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ func New(unixSeconds uint64, target, startingExcess gas.Gas) *Time {
// TargetToRate is the ratio between [Time.Target] and [proxytime.Time.Rate].
const TargetToRate = 2

// TargetToExcessScaling is the ratio between [Time.Target] and the reciprocal
// of the [Time.Excess] coefficient used in calculating [Time.Price]. In
// [ACP-176] this is the K variable.
//
// [ACP-176]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/176-dynamic-evm-gas-limit-and-price-discovery-updates
const TargetToExcessScaling = 87

// MaxTarget is the maximum allowable [Time.Target] to avoid overflows of the
// associated [proxytime.Time.Rate]. Values above this are silently clamped.
const MaxTarget = gas.Gas(math.MaxUint64 / TargetToRate)
Expand Down Expand Up @@ -91,14 +98,11 @@ func (tm *Time) Price() gas.Price {
// excessScalingFactor returns the K variable of ACP-103/176, i.e. 87*T, capped
// at [math.MaxUint64].
func (tm *Time) excessScalingFactor() gas.Gas {
const (
targetToK = 87
overflowThreshold = math.MaxUint64 / targetToK
)
const overflowThreshold = math.MaxUint64 / TargetToExcessScaling
if tm.target > overflowThreshold {
return math.MaxUint64
}
return targetToK * tm.target
return TargetToExcessScaling * tm.target
}

// BaseFee is equivalent to [Time.Price], returning the result as a uint256 for
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ go 1.24.8
require (
github.com/ava-labs/avalanchego v1.13.2
github.com/ava-labs/libevm v1.13.15-0.20251112182915-1ec8741af98f
github.com/google/go-cmp v0.6.0
github.com/google/go-cmp v0.7.0
github.com/holiman/uint256 v1.2.4
github.com/stretchr/testify v1.10.0
go.uber.org/goleak v1.3.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
Expand Down
59 changes: 59 additions & 0 deletions hook/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// Package hook defines points in an SAE block's lifecycle at which common or
// user-injected behaviour needs to be performed. Functions in this package
// SHOULD be called by all code dealing with a block at the respective point in
// its lifecycle, be that during validation, execution, or otherwise.
package hook

import (
"fmt"

"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/params"

"github.com/ava-labs/strevm/blocks"
"github.com/ava-labs/strevm/gastime"
"github.com/ava-labs/strevm/intmath"
saeparams "github.com/ava-labs/strevm/params"
)

// Points define user-injected hook points.
type Points interface {
GasTarget(parent *types.Block) gas.Gas
SubSecondBlockTime(*types.Block) gas.Gas
BeforeBlock(params.Rules, *state.StateDB, *types.Block) error
AfterBlock(*state.StateDB, *types.Block, types.Receipts)
}

// BeforeBlock is intended to be called before processing a block, with the gas
// target sourced from [Points].
func BeforeBlock(pts Points, rules params.Rules, sdb *state.StateDB, b *blocks.Block, clock *gastime.Time) error {
clock.FastForwardTo(
b.BuildTime(),
pts.SubSecondBlockTime(b.EthBlock()),
)
target := pts.GasTarget(b.ParentBlock().EthBlock())
if err := clock.SetTarget(target); err != nil {
return fmt.Errorf("%T.SetTarget() before block: %w", clock, err)
}
return pts.BeforeBlock(rules, sdb, b.EthBlock())
}

// AfterBlock is intended to be called after processing a block, with the gas
// sourced from [types.Block.GasUsed] or equivalent.
func AfterBlock(pts Points, sdb *state.StateDB, b *types.Block, clock *gastime.Time, used gas.Gas, rs types.Receipts) {
clock.Tick(used)
pts.AfterBlock(sdb, b, rs)
}

// MinimumGasConsumption MUST be used as the implementation for the respective
// method on [params.RulesHooks]. The concrete type implementing the hooks MUST
// propagate incoming and return arguments unchanged.
func MinimumGasConsumption(txLimit uint64) uint64 {
_ = (params.RulesHooks)(nil) // keep the import to allow [] doc links
return intmath.CeilDiv(txLimit, saeparams.Lambda)
}
39 changes: 39 additions & 0 deletions hook/hookstest/stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// Package hookstest provides a test double for SAE's [hook] package.
package hookstest

import (
"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/params"

"github.com/ava-labs/strevm/hook"
)

// Stub implements [hook.Points].
type Stub struct {
Target gas.Gas
}

var _ hook.Points = (*Stub)(nil)

// GasTarget ignores its argument and always returns [Stub.Target].
func (s *Stub) GasTarget(parent *types.Block) gas.Gas {
return s.Target
}

// SubSecondBlockTime time ignores its argument and always returns 0.
func (*Stub) SubSecondBlockTime(*types.Block) gas.Gas {
return 0
}

// BeforeBlock is a no-op that always returns nil.
func (*Stub) BeforeBlock(params.Rules, *state.StateDB, *types.Block) error {
return nil
}

// AfterBlock is a no-op.
func (*Stub) AfterBlock(*state.StateDB, *types.Block, types.Receipts) {}
12 changes: 12 additions & 0 deletions params/params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// Package params declares [Streaming Asynchronous Execution] (SAE) parameters.
//
// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
package params

// Lambda is the denominator for computing the minimum gas consumed per
// transaction. For a transaction with gas limit `g`, the minimum consumption is
// ceil(g/Lambda).
const Lambda = 2
3 changes: 1 addition & 2 deletions proxytime/proxytime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import (
"testing"
"time"

gocmp "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

gocmp "github.com/google/go-cmp/cmp"
)

func frac(num, den uint64) FractionalSecond[uint64] {
Expand Down
Loading
Loading