Skip to content
Merged
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
114 changes: 114 additions & 0 deletions blocks/blockstest/blocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// Package blockstest provides test helpers for constructing [Streaming
// Asynchronous Execution] (SAE) blocks.
//
// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
package blockstest

import (
"math/big"
"slices"
"testing"
"time"

"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/libevm/core"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/ethdb"
"github.com/ava-labs/libevm/libevm/options"
"github.com/ava-labs/libevm/params"
"github.com/ava-labs/libevm/triedb"
"github.com/stretchr/testify/require"

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

// An EthBlockOption configures the default block properties created by
// [NewEthBlock].
type EthBlockOption = options.Option[ethBlockProperties]

// NewEthBlock constructs a raw Ethereum block with the given arguments.
func NewEthBlock(parent *types.Block, txs types.Transactions, opts ...EthBlockOption) *types.Block {
props := &ethBlockProperties{
header: &types.Header{
Number: new(big.Int).Add(parent.Number(), big.NewInt(1)),
ParentHash: parent.Hash(),
BaseFee: big.NewInt(0),
},
}
props = options.ApplyTo(props, opts...)
return types.NewBlock(props.header, txs, nil, props.receipts, saetest.TrieHasher())
}

type ethBlockProperties struct {
header *types.Header
receipts types.Receipts
}

// ModifyHeader returns an option to modify the [types.Header] constructed by
// [NewEthBlock]. It SHOULD NOT modify the `Number` and `ParentHash`, but MAY
// modify any other field.
func ModifyHeader(fn func(*types.Header)) EthBlockOption {
return options.Func[ethBlockProperties](func(p *ethBlockProperties) {
fn(p.header)
})
}

// WithReceipts returns an option to set the receipts of a block constructed by
// [NewEthBlock].
func WithReceipts(rs types.Receipts) EthBlockOption {
return options.Func[ethBlockProperties](func(p *ethBlockProperties) {
p.receipts = slices.Clone(rs)
})
}

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

// 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
// marked as both executed and synchronous.
func NewGenesis(tb testing.TB, db ethdb.Database, config *params.ChainConfig, alloc types.GenesisAlloc, opts ...GenesisOption) *blocks.Block {
tb.Helper()
conf := options.ApplyTo(&genesisConfig{}, opts...)

gen := &core.Genesis{
Config: config,
Alloc: alloc,
}

tdb := state.NewDatabaseWithConfig(db, conf.tdbConfig).TrieDB()
_, hash, err := core.SetupGenesisBlock(db, tdb, gen)
require.NoError(tb, err, "core.SetupGenesisBlock()")
require.NoErrorf(tb, tdb.Commit(hash, true), "%T.Commit(core.SetupGenesisBlock(...))", tdb)

b := NewBlock(tb, gen.ToBlock(), nil, nil)
require.NoErrorf(tb, b.MarkExecuted(db, gastime.New(gen.Timestamp, 1, 0), time.Time{}, new(big.Int), nil, b.SettledStateRoot()), "%T.MarkExecuted()", b)
require.NoErrorf(tb, b.MarkSynchronous(), "%T.MarkSynchronous()", b)
return b
}

type genesisConfig struct {
tdbConfig *triedb.Config
}

// A GenesisOption configures [NewGenesis].
type GenesisOption = options.Option[genesisConfig]

// WithTrieDBConfig override the [triedb.Config] used by [NewGenesis].
func WithTrieDBConfig(tc *triedb.Config) GenesisOption {
return options.Func[genesisConfig](func(gc *genesisConfig) {
gc.tdbConfig = tc
})
}
134 changes: 134 additions & 0 deletions blocks/blockstest/blocks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package blockstest

import (
"math/big"
"testing"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/consensus"
"github.com/ava-labs/libevm/core"
"github.com/ava-labs/libevm/core/rawdb"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/core/vm"
"github.com/ava-labs/libevm/params"
"github.com/holiman/uint256"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

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

func TestIntegration(t *testing.T) {
const (
numAccounts = 2
numBlocks = 3
txsPerAccountPerBlock = 3
)

config := params.AllDevChainProtocolChanges
wallet := saetest.NewUNSAFEWallet(t, numAccounts, types.LatestSigner(config))
alloc := saetest.MaxAllocFor(wallet.Addresses()...)

db := rawdb.NewMemoryDatabase()

// Although the point of SAE is to replace [core.BlockChain], it remains the
// ground truth for correct block and transaction processing. If we were to
// test [ChainBuilder] with an SAE executor, which is itself going to be
// tested with the builder, then we would have a circular argument for
// correctness.
bc, err := core.NewBlockChain(
db, nil,
&core.Genesis{
Config: config,
Alloc: alloc,
},
nil, engine{}, vm.Config{},
func(*types.Header) bool { return true },
nil,
)
require.NoError(t, err, "core.NewBlockChain()")
stateProc := core.NewStateProcessor(config, bc, engine{})

sdb, err := state.New(bc.Genesis().Root(), state.NewDatabase(db), nil)
require.NoError(t, err, "state.New(%T.Genesis().Root())", bc)

build := NewChainBuilder(NewBlock(t, bc.Genesis(), nil, nil))
dest := common.Address{'d', 'e', 's', 't'}
for i := range numBlocks {
// Genesis is block 0
blockNum := uint64(i + 1) //nolint:gosec // Known to not overflow

var txs types.Transactions
for range txsPerAccountPerBlock {
for i := range numAccounts {
tx := wallet.SetNonceAndSign(t, i, &types.LegacyTx{
To: &dest,
Value: big.NewInt(1),
Gas: params.TxGas,
GasPrice: big.NewInt(1),
})
txs = append(txs, tx)
}
}
b := build.NewBlock(t, txs, ModifyHeader(func(h *types.Header) {
h.GasLimit = 100e6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gas limit is an annoying thing to have to specify every time, what if you just made it "sufficiently large" so that it won't cause problems for you rest?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only necessary when using a core.StateProcessor(), which will never be the case outside of this one single test as everything else will use the SAE executor.

}))

receipts, _, _, err := stateProc.Process(b.EthBlock(), sdb, *bc.GetVMConfig())
require.NoError(t, err, "%T.Process(%T.NewBlock().EthBlock()...)", stateProc, build)
for _, r := range receipts {
assert.Equal(t, types.ReceiptStatusSuccessful, r.Status, "%T.Status", r)
assert.Equal(t, blockNum, r.BlockNumber.Uint64(), "%T.BlockNumber", r)
}
}

t.Run("balance_of_recipient", func(t *testing.T) {
bal := sdb.GetBalance(dest)
require.True(t, bal.IsUint64(), "%T.GetBalance(...).IsUint64()", sdb)
require.Equal(t, uint64(numAccounts*numBlocks*txsPerAccountPerBlock), bal.Uint64())
})
}

// engine is a fake [consensus.Engine], implementing the minimum number of
// methods to avoid a panic.
type engine struct {
consensus.Engine
}

func (engine) VerifyHeader(consensus.ChainHeaderReader, *types.Header) error {
return nil
}

func (engine) Author(*types.Header) (common.Address, error) {
return common.Address{'a', 'u', 't', 'h'}, nil
}

func (engine) Finalize(consensus.ChainHeaderReader, *types.Header, *state.StateDB, []*types.Transaction, []*types.Header, []*types.Withdrawal) {
}

func TestNewGenesis(t *testing.T) {
config := params.AllDevChainProtocolChanges
signer := types.LatestSigner(config)
wallet := saetest.NewUNSAFEWallet(t, 10, signer)
alloc := saetest.MaxAllocFor(wallet.Addresses()...)

db := rawdb.NewMemoryDatabase()
gen := NewGenesis(t, db, config, alloc)

assert.True(t, gen.Executed(), "genesis.Executed()")
assert.NoError(t, gen.WaitUntilSettled(t.Context()), "genesis.WaitUntilSettled()")
assert.Equal(t, gen.Hash(), gen.LastSettled().Hash(), "genesis.LastSettled().Hash() is self")

t.Run("alloc", func(t *testing.T) {
sdb, err := state.New(gen.SettledStateRoot(), state.NewDatabase(db), nil)
require.NoError(t, err, "state.New(genesis.SettledStateRoot())")
for i, addr := range wallet.Addresses() {
want := new(uint256.Int).SetAllOne()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could move this outside the loop, since it's shared

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered that, and in production I would, but I prioritised reducing cognitive load by co-locating the expected value with the assertion.

assert.Truef(t, sdb.GetBalance(addr).Eq(want), "%T.GetBalance(%T.Addresses()[%d]) is max uint256", sdb, wallet, i)
}
})
}
44 changes: 44 additions & 0 deletions blocks/blockstest/chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// Package blockstest provides test helpers for constructing [Streaming
// Asynchronous Execution] (SAE) blocks.
//
// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
package blockstest

import (
"testing"

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

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

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

// NewChainBuilder returns a new ChainBuilder starting from the provided block,
// which MUST NOT be nil.
func NewChainBuilder(genesis *blocks.Block) *ChainBuilder {
return &ChainBuilder{
chain: []*blocks.Block{genesis},
}
}

// NewBlock constructs and returns a new block in the chain.
func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts ...EthBlockOption) *blocks.Block {
tb.Helper()
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()
}

// 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]
}
27 changes: 17 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
module github.com/ava-labs/strevm

go 1.24.7
go 1.24.8

require (
github.com/ava-labs/avalanchego v1.13.2
github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1
github.com/ava-labs/libevm v1.13.15-0.20251112182915-1ec8741af98f
github.com/google/go-cmp v0.6.0
github.com/holiman/uint256 v1.2.4
github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
)

require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/VictoriaMetrics/fastcache v1.12.1 // indirect
github.com/bits-and-blooms/bitset v1.10.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/cockroachdb/errors v1.9.1 // indirect
Expand All @@ -22,15 +24,18 @@ require (
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect
github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/ethereum/c-kzg-4844 v0.4.0 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect
github.com/getsentry/sentry-go v0.18.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
Expand All @@ -45,7 +50,9 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/tools v0.38.0 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

Expand Down Expand Up @@ -85,11 +92,11 @@ require (
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.30.0 // indirect
gonum.org/v1/gonum v0.11.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
Expand Down
Loading
Loading