Skip to content

Commit 1318968

Browse files
ARR4Nalarso16
andauthored
feat: blockstest and saetest.Wallet test helpers (#24)
Introduces test-only builders of blocks and transactions. Maintaining block lineage and per-key nonces is a common pattern that I noticed in all of my SAE development, so these abstract them. Testing is performed against a `libevm/core.BlockChain` as the gold standard, as these helpers will be key to testing the `saexec.Executor` to be introduced later. --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Austin Larson <[email protected]>
1 parent eb533e6 commit 1318968

File tree

6 files changed

+441
-32
lines changed

6 files changed

+441
-32
lines changed

blocks/blockstest/blocks.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
// Package blockstest provides test helpers for constructing [Streaming
5+
// Asynchronous Execution] (SAE) blocks.
6+
//
7+
// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
8+
package blockstest
9+
10+
import (
11+
"math/big"
12+
"slices"
13+
"testing"
14+
"time"
15+
16+
"github.com/ava-labs/avalanchego/utils/logging"
17+
"github.com/ava-labs/libevm/core"
18+
"github.com/ava-labs/libevm/core/state"
19+
"github.com/ava-labs/libevm/core/types"
20+
"github.com/ava-labs/libevm/ethdb"
21+
"github.com/ava-labs/libevm/libevm/options"
22+
"github.com/ava-labs/libevm/params"
23+
"github.com/ava-labs/libevm/triedb"
24+
"github.com/stretchr/testify/require"
25+
26+
"github.com/ava-labs/strevm/blocks"
27+
"github.com/ava-labs/strevm/gastime"
28+
"github.com/ava-labs/strevm/saetest"
29+
)
30+
31+
// An EthBlockOption configures the default block properties created by
32+
// [NewEthBlock].
33+
type EthBlockOption = options.Option[ethBlockProperties]
34+
35+
// NewEthBlock constructs a raw Ethereum block with the given arguments.
36+
func NewEthBlock(parent *types.Block, txs types.Transactions, opts ...EthBlockOption) *types.Block {
37+
props := &ethBlockProperties{
38+
header: &types.Header{
39+
Number: new(big.Int).Add(parent.Number(), big.NewInt(1)),
40+
ParentHash: parent.Hash(),
41+
BaseFee: big.NewInt(0),
42+
},
43+
}
44+
props = options.ApplyTo(props, opts...)
45+
return types.NewBlock(props.header, txs, nil, props.receipts, saetest.TrieHasher())
46+
}
47+
48+
type ethBlockProperties struct {
49+
header *types.Header
50+
receipts types.Receipts
51+
}
52+
53+
// ModifyHeader returns an option to modify the [types.Header] constructed by
54+
// [NewEthBlock]. It SHOULD NOT modify the `Number` and `ParentHash`, but MAY
55+
// modify any other field.
56+
func ModifyHeader(fn func(*types.Header)) EthBlockOption {
57+
return options.Func[ethBlockProperties](func(p *ethBlockProperties) {
58+
fn(p.header)
59+
})
60+
}
61+
62+
// WithReceipts returns an option to set the receipts of a block constructed by
63+
// [NewEthBlock].
64+
func WithReceipts(rs types.Receipts) EthBlockOption {
65+
return options.Func[ethBlockProperties](func(p *ethBlockProperties) {
66+
p.receipts = slices.Clone(rs)
67+
})
68+
}
69+
70+
// 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 {
72+
tb.Helper()
73+
b, err := blocks.New(eth, parent, lastSettled, saetest.NewTBLogger(tb, logging.Warn))
74+
require.NoError(tb, err, "blocks.New()")
75+
return b
76+
}
77+
78+
// NewGenesis constructs a new [core.Genesis], writes it to the database, and
79+
// returns wraps [core.Genesis.ToBlock] with [NewBlock]. It assumes a nil
80+
// [triedb.Config] unless overridden by a [WithTrieDBConfig]. The block is
81+
// marked as both executed and synchronous.
82+
func NewGenesis(tb testing.TB, db ethdb.Database, config *params.ChainConfig, alloc types.GenesisAlloc, opts ...GenesisOption) *blocks.Block {
83+
tb.Helper()
84+
conf := options.ApplyTo(&genesisConfig{}, opts...)
85+
86+
gen := &core.Genesis{
87+
Config: config,
88+
Alloc: alloc,
89+
}
90+
91+
tdb := state.NewDatabaseWithConfig(db, conf.tdbConfig).TrieDB()
92+
_, hash, err := core.SetupGenesisBlock(db, tdb, gen)
93+
require.NoError(tb, err, "core.SetupGenesisBlock()")
94+
require.NoErrorf(tb, tdb.Commit(hash, true), "%T.Commit(core.SetupGenesisBlock(...))", tdb)
95+
96+
b := NewBlock(tb, gen.ToBlock(), nil, nil)
97+
require.NoErrorf(tb, b.MarkExecuted(db, gastime.New(gen.Timestamp, 1, 0), time.Time{}, new(big.Int), nil, b.SettledStateRoot()), "%T.MarkExecuted()", b)
98+
require.NoErrorf(tb, b.MarkSynchronous(), "%T.MarkSynchronous()", b)
99+
return b
100+
}
101+
102+
type genesisConfig struct {
103+
tdbConfig *triedb.Config
104+
}
105+
106+
// A GenesisOption configures [NewGenesis].
107+
type GenesisOption = options.Option[genesisConfig]
108+
109+
// WithTrieDBConfig override the [triedb.Config] used by [NewGenesis].
110+
func WithTrieDBConfig(tc *triedb.Config) GenesisOption {
111+
return options.Func[genesisConfig](func(gc *genesisConfig) {
112+
gc.tdbConfig = tc
113+
})
114+
}

blocks/blockstest/blocks_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package blockstest
5+
6+
import (
7+
"math/big"
8+
"testing"
9+
10+
"github.com/ava-labs/libevm/common"
11+
"github.com/ava-labs/libevm/consensus"
12+
"github.com/ava-labs/libevm/core"
13+
"github.com/ava-labs/libevm/core/rawdb"
14+
"github.com/ava-labs/libevm/core/state"
15+
"github.com/ava-labs/libevm/core/types"
16+
"github.com/ava-labs/libevm/core/vm"
17+
"github.com/ava-labs/libevm/params"
18+
"github.com/holiman/uint256"
19+
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
22+
"github.com/ava-labs/strevm/saetest"
23+
)
24+
25+
func TestIntegration(t *testing.T) {
26+
const (
27+
numAccounts = 2
28+
numBlocks = 3
29+
txsPerAccountPerBlock = 3
30+
)
31+
32+
config := params.AllDevChainProtocolChanges
33+
wallet := saetest.NewUNSAFEWallet(t, numAccounts, types.LatestSigner(config))
34+
alloc := saetest.MaxAllocFor(wallet.Addresses()...)
35+
36+
db := rawdb.NewMemoryDatabase()
37+
38+
// Although the point of SAE is to replace [core.BlockChain], it remains the
39+
// ground truth for correct block and transaction processing. If we were to
40+
// test [ChainBuilder] with an SAE executor, which is itself going to be
41+
// tested with the builder, then we would have a circular argument for
42+
// correctness.
43+
bc, err := core.NewBlockChain(
44+
db, nil,
45+
&core.Genesis{
46+
Config: config,
47+
Alloc: alloc,
48+
},
49+
nil, engine{}, vm.Config{},
50+
func(*types.Header) bool { return true },
51+
nil,
52+
)
53+
require.NoError(t, err, "core.NewBlockChain()")
54+
stateProc := core.NewStateProcessor(config, bc, engine{})
55+
56+
sdb, err := state.New(bc.Genesis().Root(), state.NewDatabase(db), nil)
57+
require.NoError(t, err, "state.New(%T.Genesis().Root())", bc)
58+
59+
build := NewChainBuilder(NewBlock(t, bc.Genesis(), nil, nil))
60+
dest := common.Address{'d', 'e', 's', 't'}
61+
for i := range numBlocks {
62+
// Genesis is block 0
63+
blockNum := uint64(i + 1) //nolint:gosec // Known to not overflow
64+
65+
var txs types.Transactions
66+
for range txsPerAccountPerBlock {
67+
for i := range numAccounts {
68+
tx := wallet.SetNonceAndSign(t, i, &types.LegacyTx{
69+
To: &dest,
70+
Value: big.NewInt(1),
71+
Gas: params.TxGas,
72+
GasPrice: big.NewInt(1),
73+
})
74+
txs = append(txs, tx)
75+
}
76+
}
77+
b := build.NewBlock(t, txs, ModifyHeader(func(h *types.Header) {
78+
h.GasLimit = 100e6
79+
}))
80+
81+
receipts, _, _, err := stateProc.Process(b.EthBlock(), sdb, *bc.GetVMConfig())
82+
require.NoError(t, err, "%T.Process(%T.NewBlock().EthBlock()...)", stateProc, build)
83+
for _, r := range receipts {
84+
assert.Equal(t, types.ReceiptStatusSuccessful, r.Status, "%T.Status", r)
85+
assert.Equal(t, blockNum, r.BlockNumber.Uint64(), "%T.BlockNumber", r)
86+
}
87+
}
88+
89+
t.Run("balance_of_recipient", func(t *testing.T) {
90+
bal := sdb.GetBalance(dest)
91+
require.True(t, bal.IsUint64(), "%T.GetBalance(...).IsUint64()", sdb)
92+
require.Equal(t, uint64(numAccounts*numBlocks*txsPerAccountPerBlock), bal.Uint64())
93+
})
94+
}
95+
96+
// engine is a fake [consensus.Engine], implementing the minimum number of
97+
// methods to avoid a panic.
98+
type engine struct {
99+
consensus.Engine
100+
}
101+
102+
func (engine) VerifyHeader(consensus.ChainHeaderReader, *types.Header) error {
103+
return nil
104+
}
105+
106+
func (engine) Author(*types.Header) (common.Address, error) {
107+
return common.Address{'a', 'u', 't', 'h'}, nil
108+
}
109+
110+
func (engine) Finalize(consensus.ChainHeaderReader, *types.Header, *state.StateDB, []*types.Transaction, []*types.Header, []*types.Withdrawal) {
111+
}
112+
113+
func TestNewGenesis(t *testing.T) {
114+
config := params.AllDevChainProtocolChanges
115+
signer := types.LatestSigner(config)
116+
wallet := saetest.NewUNSAFEWallet(t, 10, signer)
117+
alloc := saetest.MaxAllocFor(wallet.Addresses()...)
118+
119+
db := rawdb.NewMemoryDatabase()
120+
gen := NewGenesis(t, db, config, alloc)
121+
122+
assert.True(t, gen.Executed(), "genesis.Executed()")
123+
assert.NoError(t, gen.WaitUntilSettled(t.Context()), "genesis.WaitUntilSettled()")
124+
assert.Equal(t, gen.Hash(), gen.LastSettled().Hash(), "genesis.LastSettled().Hash() is self")
125+
126+
t.Run("alloc", func(t *testing.T) {
127+
sdb, err := state.New(gen.SettledStateRoot(), state.NewDatabase(db), nil)
128+
require.NoError(t, err, "state.New(genesis.SettledStateRoot())")
129+
for i, addr := range wallet.Addresses() {
130+
want := new(uint256.Int).SetAllOne()
131+
assert.Truef(t, sdb.GetBalance(addr).Eq(want), "%T.GetBalance(%T.Addresses()[%d]) is max uint256", sdb, wallet, i)
132+
}
133+
})
134+
}

blocks/blockstest/chain.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
// Package blockstest provides test helpers for constructing [Streaming
5+
// Asynchronous Execution] (SAE) blocks.
6+
//
7+
// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
8+
package blockstest
9+
10+
import (
11+
"testing"
12+
13+
"github.com/ava-labs/libevm/core/types"
14+
15+
"github.com/ava-labs/strevm/blocks"
16+
)
17+
18+
// A ChainBuilder builds a chain of blocks, maintaining necessary invariants.
19+
type ChainBuilder struct {
20+
chain []*blocks.Block
21+
}
22+
23+
// NewChainBuilder returns a new ChainBuilder starting from the provided block,
24+
// which MUST NOT be nil.
25+
func NewChainBuilder(genesis *blocks.Block) *ChainBuilder {
26+
return &ChainBuilder{
27+
chain: []*blocks.Block{genesis},
28+
}
29+
}
30+
31+
// 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 {
33+
tb.Helper()
34+
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()
38+
}
39+
40+
// Last returns the last block to be built by the builder, which MAY be the
41+
// genesis block passed to the constructor.
42+
func (cb *ChainBuilder) Last() *blocks.Block {
43+
return cb.chain[len(cb.chain)-1]
44+
}

go.mod

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
module github.com/ava-labs/strevm
22

3-
go 1.24.7
3+
go 1.24.8
44

55
require (
66
github.com/ava-labs/avalanchego v1.13.2
7-
github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1
7+
github.com/ava-labs/libevm v1.13.15-0.20251112182915-1ec8741af98f
88
github.com/google/go-cmp v0.6.0
99
github.com/holiman/uint256 v1.2.4
1010
github.com/stretchr/testify v1.10.0
1111
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
1212
)
1313

1414
require (
15+
github.com/Microsoft/go-winio v0.6.1 // indirect
16+
github.com/VictoriaMetrics/fastcache v1.12.1 // indirect
1517
github.com/bits-and-blooms/bitset v1.10.0 // indirect
1618
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
1719
github.com/cockroachdb/errors v1.9.1 // indirect
@@ -22,15 +24,18 @@ require (
2224
github.com/consensys/bavard v0.1.13 // indirect
2325
github.com/consensys/gnark-crypto v0.12.1 // indirect
2426
github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect
25-
github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect
27+
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
28+
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
2629
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
27-
github.com/ethereum/c-kzg-4844 v0.4.0 // indirect
30+
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
2831
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect
2932
github.com/getsentry/sentry-go v0.18.0 // indirect
3033
github.com/go-ole/go-ole v1.3.0 // indirect
3134
github.com/gofrs/flock v0.8.1 // indirect
3235
github.com/gogo/protobuf v1.3.2 // indirect
3336
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
37+
github.com/gorilla/websocket v1.5.0 // indirect
38+
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
3439
github.com/klauspost/compress v1.15.15 // indirect
3540
github.com/kr/pretty v0.3.1 // indirect
3641
github.com/kr/text v0.2.0 // indirect
@@ -45,7 +50,9 @@ require (
4550
github.com/tklauser/go-sysconf v0.3.12 // indirect
4651
github.com/tklauser/numcpus v0.6.1 // indirect
4752
github.com/yusufpapurcu/wmi v1.2.2 // indirect
48-
golang.org/x/sync v0.15.0 // indirect
53+
golang.org/x/mod v0.29.0 // indirect
54+
golang.org/x/sync v0.17.0 // indirect
55+
golang.org/x/tools v0.38.0 // indirect
4956
rsc.io/tmplfunc v0.0.3 // indirect
5057
)
5158

@@ -85,11 +92,11 @@ require (
8592
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
8693
go.uber.org/multierr v1.11.0 // indirect
8794
go.uber.org/zap v1.26.0
88-
golang.org/x/crypto v0.39.0 // indirect
89-
golang.org/x/net v0.41.0 // indirect
90-
golang.org/x/sys v0.33.0 // indirect
91-
golang.org/x/term v0.32.0 // indirect
92-
golang.org/x/text v0.26.0 // indirect
95+
golang.org/x/crypto v0.43.0 // indirect
96+
golang.org/x/net v0.46.0 // indirect
97+
golang.org/x/sys v0.37.0 // indirect
98+
golang.org/x/term v0.36.0 // indirect
99+
golang.org/x/text v0.30.0 // indirect
93100
gonum.org/v1/gonum v0.11.0 // indirect
94101
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect
95102
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect

0 commit comments

Comments
 (0)